Merge branch 'misyaguziya:develop' into develop

This commit is contained in:
超级有节操的逆袭
2025-12-01 15:33:12 +08:00
committed by GitHub
34 changed files with 854 additions and 116 deletions

2
bat/build.bat Normal file
View File

@@ -0,0 +1,2 @@
call .venv/Scripts/activate
pyinstaller spec/backend.spec --distpath src-tauri/bin --clean --noconfirm --log-level ERROR

2
bat/build_cuda.bat Normal file
View File

@@ -0,0 +1,2 @@
call .venv_cuda/Scripts/activate
pyinstaller spec/backend_cuda.spec --distpath src-tauri/bin --clean --noconfirm --log-level ERROR

View File

@@ -1,2 +0,0 @@
call .venv/Scripts/activate
pyinstaller backend.spec --distpath src-tauri/bin --clean --noconfirm --log-level ERROR

View File

@@ -1,2 +0,0 @@
call .venv_cuda/Scripts/activate
pyinstaller backend_cuda.spec --distpath src-tauri/bin --clean --noconfirm --log-level ERROR

View File

@@ -1,6 +0,0 @@
import shutil
shutil.rmtree('build', ignore_errors=True)
shutil.rmtree('dist', ignore_errors=True)
shutil.rmtree('src-tauri\\bin', ignore_errors=True)
shutil.rmtree('src-tauri\\target', ignore_errors=True)

422
docs/readme_build.md Normal file
View File

@@ -0,0 +1,422 @@
# VRCTビルドガイド
このドキュメントでは、VRCTプロジェクトのビルド方法について説明します。
## 目次
- [必要な環境](#必要な環境)
- [初回セットアップ](#初回セットアップ)
- [ビルドの種類](#ビルドの種類)
- [開発ビルド](#開発ビルド)
- [リリースビルド](#リリースビルド)
- [ビルドプロセスの詳細](#ビルドプロセスの詳細)
- [トラブルシューティング](#トラブルシューティング)
## 必要な環境
### 必須ソフトウェア
- **Node.js** (npm含む)
- **Python 3.x**
- **Rust** (Tauri用)
- **Git**
### 推奨環境
- Windows 10/11
- メモリ: 8GB以上
- ストレージ: 5GB以上の空き容量
## 初回セットアップ
### 1. リポジトリのクローン
```bash
git clone <repository-url>
cd VRCT
```
### 2. Node.js依存関係のインストール
```bash
npm install
```
### 3. Python環境のセットアップ
以下のコマンドで、CPU版とCUDA版の両方の仮想環境を作成します:
```bash
npm run setup-python
```
このコマンドは以下の処理を実行します:
- `.venv` (CPU版) の作成と依存関係のインストール
- `.venv_cuda` (CUDA版) の作成と依存関係のインストール
> **注意**: CUDA版を使用する場合は、NVIDIAのGPUとCUDA Toolkit 12.8が必要です。
## ビルドの種類
VRCTでは、以下の2種類のビルドが可能です:
### CPU版
標準的なCPUで動作するバージョン。GPUは不要。
### CUDA版
NVIDIA GPUを活用した高速処理版。CUDA対応GPUが必要。
## 開発ビルド
開発中にアプリケーションを実行・テストするためのビルドです。
### CPU版の開発ビルド
```bash
npm run dev
```
このコマンドは以下を実行します:
1. 実行中のプロセスを終了 (`task-kill`)
2. ビルドファイルのクリーンアップ (`clean`)
3. バージョン情報の更新 (`update-version`)
4. Pythonバックエンドのビルド (`build-python`)
5. ViteとTauriの開発サーバー起動
### CUDA版の開発ビルド
```bash
npm run dev-cuda
```
CPU版と同様ですが、CUDA対応のPythonバックエンドをビルドします。
### UIのみの開発
バックエンドのビルドをスキップして、UIのみを開発する場合:
```bash
npm run dev-ui
```
## リリースビルド
配布用のインストーラーを作成するビルドです。
### CPU版のリリースビルド
```bash
npm run build
```
または、ZIP形式でパッケージング:
```bash
npm run release
```
生成されるファイル:
- インストーラー: `src-tauri/target/release/bundle/nsis/`
- ZIPファイル: `VRCT.zip` (releaseコマンド使用時)
### CUDA版のリリースビルド
```bash
npm run build-cuda
```
または、ZIP形式でパッケージング:
```bash
npm run release-cuda
```
生成されるファイル:
- インストーラー: `src-tauri/target/release/bundle/nsis/`
- ZIPファイル: `VRCT_cuda.zip` (release-cudaコマンド使用時)
### 両バージョンの同時ビルド
CPU版とCUDA版の両方をビルドする場合:
```bash
npm run release-all
```
## ビルドプロセスの詳細
### バージョン管理
バージョンは `package.json` で一元管理され、以下のファイルに自動で同期されます:
```bash
npm run update-version
```
更新されるファイル:
- `src-tauri/tauri.conf.json`
- `src-python/config.py`
### どこにバージョンを設定すればReleaseに反映されるか
- **設定箇所**: `package.json``version` が唯一のソース・オブ・トゥルース。
- **反映方法**: `npm run update-version``build`/`build-cuda`/`release`コマンド内でも自動実行)により、
- `src-tauri/tauri.conf.json``version` に同期Tauri/NSISインストーラーの表示・メタデータに使用
- `src-python/config.py``self._VERSION` に同期(ランタイム表示等に使用)
- **成果物への影響**:
- インストーラーNSIS`tauri.conf.json``version` を取り込み、プロダクトバージョンとして反映。
- ZIPパッケージ名はスクリプト既定では固定`VRCT.zip`/`VRCT_cuda.zip`)。ファイル名にバージョンを含めたい場合は、`package.json``release` スクリプトを調整してください。
### Pythonバックエンドのビルド
#### CPU版
```bash
npm run build-python
```
実行内容:
- `.venv` 環境をアクティベート
- PyInstallerで `spec/backend.spec` を使用してビルド
- 出力先: `src-tauri/bin/`
#### CUDA版
```bash
npm run build-python-cuda
```
実行内容:
- `.venv_cuda` 環境をアクティベート
- PyInstallerで `spec/backend_cuda.spec` を使用してビルド
- 出力先: `src-tauri/bin/`
### フロントエンドのビルド
```bash
npm run vite-build
```
Viteを使用してフロントエンドReactをビルドし、`dist/` ディレクトリに出力します。
### Tauriアプリケーションのビルド
```bash
npm run tauri build
```
Tauriを使用して最終的なデスクトップアプリケーションをビルドします。
## GitHub ActionsでのRelease自動化
Windows用のReleaseをGitHub Actionsで自動生成・公開する例です。`package.json` のバージョンをタグ・リリース名に使い、TauriのNSISインストーラーとZIPを添付します。
### 推奨トリガー
- タグプッシュ(例: `v*`)または手動実行(`workflow_dispatch`
### サンプルワークフローWindows
```yaml
name: Release (Windows)
on:
workflow_dispatch: {}
push:
tags:
- 'v*'
jobs:
build-release-windows:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
run: |
npm ci
- name: Setup Python envs (.venv/.venv_cuda)
run: |
npm run setup-python
- name: Sync versions from package.json
run: |
npm run update-version
- name: Build (CPU)
run: |
npm run build
- name: Package ZIP (CPU)
run: |
python utils/zip.py --zip_name VRCT.zip
- name: Read version from package.json
id: pkg
shell: pwsh
run: |
$version = (Get-Content package.json | ConvertFrom-Json).version
echo "version=$version" >> $env:GITHUB_OUTPUT
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: VRCT-windows-${{ steps.pkg.outputs.version }}
path: |
src-tauri/target/release/bundle/nsis/**/*
VRCT.zip
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.pkg.outputs.version }}
name: VRCT v${{ steps.pkg.outputs.version }}
files: |
src-tauri/target/release/bundle/nsis/**/*
VRCT.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
### ポイント
- ビルド前に必ず `npm run update-version` を実行して、`tauri.conf.json``config.py` にバージョンを同期します。
- アーティファクトのパスは既定構成に合わせています:
- インストーラー: `src-tauri/target/release/bundle/nsis/`
- ZIP: ルート直下の `VRCT.zip`
- CUDA版も同様にビルドする場合は、`npm run build-cuda``python utils/zip.py --zip_name VRCT_cuda.zip` を追加して、別アーティファクト名でアップロード・添付してください。
## ユーティリティコマンド
### クリーンアップ
```bash
npm run clean
```
以下のディレクトリを削除します:
- `build/`
- `dist/`
- `src-tauri/bin/`
- `src-tauri/target/`
### プロセスの強制終了
```bash
npm run task-kill
```
VRCTに関連する実行中のプロセスを終了します。
## ディレクトリ構成
```
VRCT/
├── bat/ # バッチスクリプト
│ ├── build.bat # CPU版Pythonビルド
│ ├── build_cuda.bat # CUDA版Pythonビルド
│ └── install.bat # Python環境セットアップ
├── spec/ # PyInstallerスペックファイル
│ ├── backend.spec # CPU版ビルド設定
│ └── backend_cuda.spec # CUDA版ビルド設定
├── src-python/ # Pythonバックエンドソースコード
├── src-tauri/ # Tauriアプリケーション設定
│ ├── bin/ # ビルド済みPythonバイナリ生成
│ └── target/ # Tauriビルド出力生成
├── src-ui/ # Reactフロントエンドソースコード
├── utils/ # ユーティリティスクリプト
│ ├── clean.py # クリーンアップスクリプト
│ ├── task_kill.py # プロセス終了スクリプト
│ ├── update_version.py # バージョン更新スクリプト
│ └── zip.py # ZIPパッケージング
├── package.json # Node.js設定とバージョン管理
├── requirements.txt # Python依存関係CPU版
└── requirements_cuda.txt # Python依存関係CUDA版
```
## トラブルシューティング
### Python環境のエラー
仮想環境を再作成してください:
```bash
npm run setup-python
```
### ビルドが失敗する
1. クリーンアップを実行:
```bash
npm run clean
```
2. Node.js依存関係を再インストール:
```bash
npm install
```
3. 再度ビルド:
```bash
npm run build
```
### CUDA版が動作しない
- CUDA Toolkit 12.8がインストールされているか確認
- NVIDIA GPUドライバーが最新か確認
- `requirements_cuda.txt` の依存関係が正しくインストールされているか確認
### プロセスが残っている
```bash
npm run task-kill
```
を実行して、すべてのVRCTプロセスを終了してください。
## 参考情報
### PyInstallerスペックファイル
- `spec/backend.spec` - CPU版の設定
- `spec/backend_cuda.spec` - CUDA版の設定
これらのファイルでは、以下を設定しています:
- エントリーポイント: `src-python/mainloop.py`
- データファイル(フォント、プロンプト、言語ファイル等)のパス
- 依存ライブラリのパス
### バージョン管理フロー
1. `package.json` のバージョンを更新
2. `npm run update-version` を実行
3. 自動的に `tauri.conf.json``config.py` が更新される
### リリースパッケージの内容
ZIPファイルには以下が含まれます:
- `VRCT.exe` - メインアプリケーション
- `VRCT-sidecar.exe` - Pythonバックエンド
- `_internal/` - 必要な依存ファイル
## ライセンス
プロジェクトのライセンスについては、`LICENSE` ファイルを参照してください。

View File

@@ -1,27 +1,28 @@
{ {
"name": "vrct", "name": "vrct",
"private": true, "private": true,
"version": "0.0.0", "version": "3.3.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"setup-python": "install.bat", "setup-python": "bat\\install.bat",
"build-python": "build.bat", "build-python": "bat\\build.bat",
"build-python-cuda": "build_cuda.bat", "build-python-cuda": "bat\\build_cuda.bat",
"vite": "vite", "vite": "vite",
"vite-build": "vite build", "vite-build": "vite build",
"vite-preview": "vite preview", "vite-preview": "vite preview",
"tauri": "tauri", "tauri": "tauri",
"tauri-dev": "tauri dev", "tauri-dev": "tauri dev",
"task-kill": "python task_kill.py", "task-kill": "python utils\\task_kill.py",
"clean": "python clean.py", "clean": "python utils\\clean.py",
"dev": "npm run task-kill && npm run build-python && npm run dev-ui", "update-version": "python utils\\update_version.py",
"dev-cuda": "npm run task-kill && npm run build-python-cuda && npm run dev-ui", "dev": "npm run task-kill && npm run clean && npm run update-version && npm run build-python && npm run dev-ui",
"dev-ui": "npm-run-all --parallel vite tauri-dev", "dev-cuda": "npm run task-kill && npm run clean && npm run update-version && npm run build-python-cuda && npm run dev-ui",
"build": "npm run clean && npm run build-python && npm run vite-build && npm run tauri build", "dev-ui": "npm run task-kill && npm-run-all --parallel vite tauri-dev",
"build-cuda": "npm run clean && npm run build-python-cuda && npm run vite-build && npm run tauri build", "build": "npm run task-kill && npm run clean && npm run update-version && npm run build-python && npm run vite-build && npm run tauri build",
"release": "npm run build && python zip.py --zip_name VRCT.zip", "build-cuda": "npm run task-kill && npm run clean && npm run update-version && npm run build-python-cuda && npm run vite-build && npm run tauri build",
"release-cuda": "npm run build-cuda && python zip.py --zip_name VRCT_cuda.zip", "release": "npm run update-version && npm run build && python utils\\zip.py --zip_name VRCT.zip",
"release-all": "npm run release && npm run release-cuda" "release-cuda": "npm run update-version && npm run build-cuda && python utils\\zip.py --zip_name VRCT_cuda.zip",
"release-all": "npm run update-version && npm run release && npm run release-cuda"
}, },
"dependencies": { "dependencies": {
"@babel/standalone": "7.27.0", "@babel/standalone": "7.27.0",

View File

@@ -2,17 +2,17 @@
a = Analysis( a = Analysis(
['src-python\\mainloop.py'], ['..\\src-python\\mainloop.py'],
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[ datas=[
('./src-python/models/overlay/fonts', 'fonts/'), ('./../src-python/models/overlay/fonts', 'fonts/'),
('./src-python/models/translation/prompt', 'prompt/'), ('./../src-python/models/translation/prompt', 'prompt/'),
('./src-python/models/translation/languages', 'languages/'), ('./../src-python/models/translation/languages', 'languages/'),
('.venv_cuda/Lib/site-packages/zeroconf', 'zeroconf/'), ('./../.venv/Lib/site-packages/zeroconf', 'zeroconf/'),
('.venv_cuda/Lib/site-packages/openvr', 'openvr/'), ('./../.venv/Lib/site-packages/openvr', 'openvr/'),
('.venv_cuda/Lib/site-packages/faster_whisper', 'faster_whisper/'), ('./../.venv/Lib/site-packages/faster_whisper', 'faster_whisper/'),
('.venv/Lib/site-packages/hf_xet', 'hf_xet/') ('./../.venv/Lib/site-packages/hf_xet', 'hf_xet/')
], ],
hiddenimports=[], hiddenimports=[],
hookspath=[], hookspath=[],

View File

@@ -2,17 +2,17 @@
a = Analysis( a = Analysis(
['src-python\\mainloop.py'], ['..\\src-python\\mainloop.py'],
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[ datas=[
('./src-python/models/overlay/fonts', 'fonts/'), ('./../src-python/models/overlay/fonts', 'fonts/'),
('./src-python/models/translation/prompt', 'prompt/'), ('./../src-python/models/translation/prompt', 'prompt/'),
('./src-python/models/translation/languages', 'languages/'), ('./../src-python/models/translation/languages', 'languages/'),
('.venv/Lib/site-packages/zeroconf', 'zeroconf/'), ('./../.venv_cuda/Lib/site-packages/zeroconf', 'zeroconf/'),
('.venv/Lib/site-packages/openvr', 'openvr/'), ('./../.venv_cuda/Lib/site-packages/openvr', 'openvr/'),
('.venv/Lib/site-packages/faster_whisper', 'faster_whisper/'), ('./../.venv_cuda/Lib/site-packages/faster_whisper', 'faster_whisper/'),
('.venv/Lib/site-packages/hf_xet', 'hf_xet/') ('./../.venv/Lib/site-packages/hf_xet', 'hf_xet/')
], ],
hiddenimports=[], hiddenimports=[],
hookspath=[], hookspath=[],

View File

@@ -527,6 +527,19 @@ def _compute_device_validator(val, inst):
return copy.deepcopy(val) return copy.deepcopy(val)
return None return None
def _allowed_in_populated(list_attr_name: str):
def _inner(value, inst):
try:
lst = getattr(inst, list_attr_name)
except Exception:
return True # インスタンス状態取得失敗時も弾かない
if not lst: # 空/未初期化
return True
if value is None:
return True
return value in lst
return _inner
class Config: class Config:
"""Application configuration singleton. """Application configuration singleton.
@@ -558,8 +571,16 @@ class Config:
return cls._instance return cls._instance
def saveConfigToFile(self) -> None: def saveConfigToFile(self) -> None:
# 永続化対象を descriptor 情報 (json_serializable_vars) から再構成
filtered = {}
for var_name, var_func in json_serializable_vars.items():
try:
filtered[var_name] = var_func(self)
except Exception:
pass
self._config_data = filtered
with open(self.PATH_CONFIG, "w", encoding="utf-8") as fp: with open(self.PATH_CONFIG, "w", encoding="utf-8") as fp:
json_dump(self._config_data, fp, indent=4, ensure_ascii=False) json_dump(filtered, fp, indent=4, ensure_ascii=False)
def saveConfig(self, key: str, value: Any, immediate_save: bool = False) -> None: def saveConfig(self, key: str, value: Any, immediate_save: bool = False) -> None:
self._config_data[key] = value self._config_data[key] = value
@@ -601,12 +622,12 @@ class Config:
# Read Write # Read Write
# --- Simple boolean flags (managed by descriptor) --- # --- Simple boolean flags (managed by descriptor) ---
ENABLE_TRANSLATION = ManagedProperty('ENABLE_TRANSLATION', type_=bool) ENABLE_TRANSLATION = ManagedProperty('ENABLE_TRANSLATION', type_=bool, serialize=False)
ENABLE_TRANSCRIPTION_SEND = ManagedProperty('ENABLE_TRANSCRIPTION_SEND', type_=bool) ENABLE_TRANSCRIPTION_SEND = ManagedProperty('ENABLE_TRANSCRIPTION_SEND', type_=bool, serialize=False)
ENABLE_TRANSCRIPTION_RECEIVE = ManagedProperty('ENABLE_TRANSCRIPTION_RECEIVE', type_=bool) ENABLE_TRANSCRIPTION_RECEIVE = ManagedProperty('ENABLE_TRANSCRIPTION_RECEIVE', type_=bool, serialize=False)
ENABLE_FOREGROUND = ManagedProperty('ENABLE_FOREGROUND', type_=bool) ENABLE_FOREGROUND = ManagedProperty('ENABLE_FOREGROUND', type_=bool, serialize=False)
ENABLE_CHECK_ENERGY_SEND = ManagedProperty('ENABLE_CHECK_ENERGY_SEND', type_=bool) ENABLE_CHECK_ENERGY_SEND = ManagedProperty('ENABLE_CHECK_ENERGY_SEND', type_=bool, serialize=False)
ENABLE_CHECK_ENERGY_RECEIVE = ManagedProperty('ENABLE_CHECK_ENERGY_RECEIVE', type_=bool) ENABLE_CHECK_ENERGY_RECEIVE = ManagedProperty('ENABLE_CHECK_ENERGY_RECEIVE', type_=bool, serialize=False)
# --- Selectable dict/list properties (managed by descriptor, not serialized) --- # --- Selectable dict/list properties (managed by descriptor, not serialized) ---
# These are dynamically generated in init_config() based on installed packages/APIs # These are dynamically generated in init_config() based on installed packages/APIs
@@ -711,11 +732,11 @@ class Config:
USE_EXCLUDE_WORDS = ManagedProperty('USE_EXCLUDE_WORDS', type_=bool) USE_EXCLUDE_WORDS = ManagedProperty('USE_EXCLUDE_WORDS', type_=bool)
CTRANSLATE2_WEIGHT_TYPE = ManagedProperty('CTRANSLATE2_WEIGHT_TYPE', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST) CTRANSLATE2_WEIGHT_TYPE = ManagedProperty('CTRANSLATE2_WEIGHT_TYPE', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST)
WHISPER_WEIGHT_TYPE = ManagedProperty('WHISPER_WEIGHT_TYPE', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_WHISPER_WEIGHT_TYPE_LIST) WHISPER_WEIGHT_TYPE = ManagedProperty('WHISPER_WEIGHT_TYPE', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_WHISPER_WEIGHT_TYPE_LIST)
SELECTED_PLAMO_MODEL = ManagedProperty('SELECTED_PLAMO_MODEL', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_PLAMO_MODEL_LIST) SELECTED_PLAMO_MODEL = ManagedProperty('SELECTED_PLAMO_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_PLAMO_MODEL_LIST'))
SELECTED_GEMINI_MODEL = ManagedProperty('SELECTED_GEMINI_MODEL', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_GEMINI_MODEL_LIST) SELECTED_GEMINI_MODEL = ManagedProperty('SELECTED_GEMINI_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_GEMINI_MODEL_LIST'))
SELECTED_OPENAI_MODEL = ManagedProperty('SELECTED_OPENAI_MODEL', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_OPENAI_MODEL_LIST) SELECTED_OPENAI_MODEL = ManagedProperty('SELECTED_OPENAI_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_OPENAI_MODEL_LIST'))
SELECTED_LMSTUDIO_MODEL = ManagedProperty('SELECTED_LMSTUDIO_MODEL', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_LMSTUDIO_MODEL_LIST) SELECTED_LMSTUDIO_MODEL = ManagedProperty('SELECTED_LMSTUDIO_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_LMSTUDIO_MODEL_LIST'))
SELECTED_OLLAMA_MODEL = ManagedProperty('SELECTED_OLLAMA_MODEL', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_OLLAMA_MODEL_LIST) SELECTED_OLLAMA_MODEL = ManagedProperty('SELECTED_OLLAMA_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_OLLAMA_MODEL_LIST'))
# --- Translation and language settings --- # --- Translation and language settings ---
MIC_WORD_FILTER = ValidatedProperty('MIC_WORD_FILTER', _mic_word_filter_validator) MIC_WORD_FILTER = ValidatedProperty('MIC_WORD_FILTER', _mic_word_filter_validator)
@@ -736,7 +757,7 @@ class Config:
def init_config(self): def init_config(self):
# Read Only # Read Only
self._VERSION = "3.3.1" self._VERSION = "3.3.2"
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
self._PATH_LOCAL = os_path.dirname(sys.executable) self._PATH_LOCAL = os_path.dirname(sys.executable)
else: else:
@@ -1006,15 +1027,44 @@ class Config:
self._config_data = json_load(fp) self._config_data = json_load(fp)
for key, value in self._config_data.items(): for key, value in self._config_data.items():
# 読み込み時: serialize=True かつ readonlyでない Descriptor のみ反映。
# 未知キーDescriptorなしは無視して注入を防止。
try: try:
descriptor = getattr(type(self), key, None)
if isinstance(descriptor, ManagedProperty):
if descriptor.readonly or not descriptor.serialize:
continue
setattr(self, key, value) setattr(self, key, value)
elif isinstance(descriptor, ValidatedProperty):
if not descriptor.serialize:
continue
setattr(self, key, value)
else:
# 不明キーは破棄(古い/不要/改竄の可能性)
continue
except Exception: except Exception:
errorLogging() errorLogging()
self.saveConfigToFile()
with open(self.PATH_CONFIG, 'w', encoding="utf-8") as fp: def revalidate_selected_models(self):
for var_name, var_func in json_serializable_vars.items(): pairs = [
self._config_data[var_name] = var_func(self) ('SELECTED_PLAMO_MODEL', 'SELECTABLE_PLAMO_MODEL_LIST'),
json_dump(self._config_data, fp, indent=4, ensure_ascii=False) ('SELECTED_GEMINI_MODEL', 'SELECTABLE_GEMINI_MODEL_LIST'),
('SELECTED_OPENAI_MODEL', 'SELECTABLE_OPENAI_MODEL_LIST'),
('SELECTED_LMSTUDIO_MODEL', 'SELECTABLE_LMSTUDIO_MODEL_LIST'),
('SELECTED_OLLAMA_MODEL', 'SELECTABLE_OLLAMA_MODEL_LIST'),
]
for sel_attr, list_attr in pairs:
try:
current = getattr(self, sel_attr)
lst = getattr(self, list_attr)
if lst and current is not None and current not in lst:
if len(lst) > 0:
setattr(self, sel_attr, lst[0])
else:
setattr(self, sel_attr, None)
except Exception:
errorLogging()
# Auto-register all descriptors after Config class definition # Auto-register all descriptors after Config class definition
_auto_register_descriptors() _auto_register_descriptors()

View File

@@ -1900,6 +1900,9 @@ class Controller:
} }
return response return response
def getTranslatorLMStudioConnection(self, *args, **kwargs) -> dict:
return {"status":200, "result":model.getTranslatorLMStudioConnected()}
def checkTranslatorLMStudioConnection(self, *args, **kwargs) -> dict: def checkTranslatorLMStudioConnection(self, *args, **kwargs) -> dict:
printLog("Check Translator LMStudio Connection") printLog("Check Translator LMStudio Connection")
translator_name = "LMStudio" translator_name = "LMStudio"
@@ -1909,6 +1912,8 @@ class Controller:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True
config.SELECTABLE_LMSTUDIO_MODEL_LIST = model.getTranslatorLMStudioModelList() config.SELECTABLE_LMSTUDIO_MODEL_LIST = model.getTranslatorLMStudioModelList()
self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST) self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST)
if len(config.SELECTABLE_LMSTUDIO_MODEL_LIST) == 0:
raise Exception("No LMStudio models available")
if config.SELECTED_LMSTUDIO_MODEL not in config.SELECTABLE_LMSTUDIO_MODEL_LIST: if config.SELECTED_LMSTUDIO_MODEL not in config.SELECTABLE_LMSTUDIO_MODEL_LIST:
config.SELECTED_LMSTUDIO_MODEL = config.SELECTABLE_LMSTUDIO_MODEL_LIST[0] config.SELECTED_LMSTUDIO_MODEL = config.SELECTABLE_LMSTUDIO_MODEL_LIST[0]
model.setTranslatorLMStudioModel(model=config.SELECTED_LMSTUDIO_MODEL) model.setTranslatorLMStudioModel(model=config.SELECTED_LMSTUDIO_MODEL)
@@ -1935,6 +1940,10 @@ class Controller:
} }
return response return response
def getConnectedLMStudio(self, *args, **kwargs) -> dict:
is_connected = model.getTranslatorLMStudioConnectedStatus()
return {"status":200, "result": is_connected}
def getTranslatorLMStudioURL(self, *args, **kwargs) -> dict: def getTranslatorLMStudioURL(self, *args, **kwargs) -> dict:
return {"status":200, "result":config.LMSTUDIO_URL} return {"status":200, "result":config.LMSTUDIO_URL}
@@ -1949,6 +1958,8 @@ class Controller:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True
config.SELECTABLE_LMSTUDIO_MODEL_LIST = model.getTranslatorLMStudioModelList() config.SELECTABLE_LMSTUDIO_MODEL_LIST = model.getTranslatorLMStudioModelList()
self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST) self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST)
if len(config.SELECTABLE_LMSTUDIO_MODEL_LIST) == 0:
raise Exception("No LMStudio models available")
if config.SELECTED_LMSTUDIO_MODEL not in config.SELECTABLE_LMSTUDIO_MODEL_LIST: if config.SELECTED_LMSTUDIO_MODEL not in config.SELECTABLE_LMSTUDIO_MODEL_LIST:
config.SELECTED_LMSTUDIO_MODEL = config.SELECTABLE_LMSTUDIO_MODEL_LIST[0] config.SELECTED_LMSTUDIO_MODEL = config.SELECTABLE_LMSTUDIO_MODEL_LIST[0]
model.setTranslatorLMStudioModel(model=config.SELECTED_LMSTUDIO_MODEL) model.setTranslatorLMStudioModel(model=config.SELECTED_LMSTUDIO_MODEL)
@@ -2011,6 +2022,9 @@ class Controller:
} }
return response return response
def getTranslatorOllamaConnection(self, *args, **kwargs) -> dict:
return {"status":200, "result":model.getTranslatorOllamaConnected()}
def checkTranslatorOllamaConnection(self, *args, **kwargs) -> dict: def checkTranslatorOllamaConnection(self, *args, **kwargs) -> dict:
printLog("Check Translator Ollama Connection") printLog("Check Translator Ollama Connection")
translator_name = "Ollama" translator_name = "Ollama"
@@ -2020,6 +2034,8 @@ class Controller:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True
config.SELECTABLE_OLLAMA_MODEL_LIST = model.getTranslatorOllamaModelList() config.SELECTABLE_OLLAMA_MODEL_LIST = model.getTranslatorOllamaModelList()
self.run(200, self.run_mapping["selectable_ollama_model_list"], config.SELECTABLE_OLLAMA_MODEL_LIST) self.run(200, self.run_mapping["selectable_ollama_model_list"], config.SELECTABLE_OLLAMA_MODEL_LIST)
if len(config.SELECTABLE_OLLAMA_MODEL_LIST) == 0:
raise Exception("No Ollama models available")
if config.SELECTED_OLLAMA_MODEL not in config.SELECTABLE_OLLAMA_MODEL_LIST: if config.SELECTED_OLLAMA_MODEL not in config.SELECTABLE_OLLAMA_MODEL_LIST:
config.SELECTED_OLLAMA_MODEL = config.SELECTABLE_OLLAMA_MODEL_LIST[0] config.SELECTED_OLLAMA_MODEL = config.SELECTABLE_OLLAMA_MODEL_LIST[0]
model.setTranslatorOllamaModel(model=config.SELECTED_OLLAMA_MODEL) model.setTranslatorOllamaModel(model=config.SELECTED_OLLAMA_MODEL)
@@ -2957,13 +2973,13 @@ class Controller:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
if config.AUTH_KEYS[engine] is not None: if config.AUTH_KEYS[engine] is not None:
if model.authenticationTranslatorPlamoAuthKey(auth_key=config.AUTH_KEYS[engine]) is True: if model.authenticationTranslatorPlamoAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("Plamo API Key is valid")
config.SELECTABLE_PLAMO_MODEL_LIST = model.getTranslatorPlamoModelList() config.SELECTABLE_PLAMO_MODEL_LIST = model.getTranslatorPlamoModelList()
if config.SELECTED_PLAMO_MODEL not in config.SELECTABLE_PLAMO_MODEL_LIST: if config.SELECTED_PLAMO_MODEL not in config.SELECTABLE_PLAMO_MODEL_LIST:
config.SELECTED_PLAMO_MODEL = config.SELECTABLE_PLAMO_MODEL_LIST[0] config.SELECTED_PLAMO_MODEL = config.SELECTABLE_PLAMO_MODEL_LIST[0]
model.setTranslatorPlamoModel(config.SELECTED_PLAMO_MODEL) model.setTranslatorPlamoModel(config.SELECTED_PLAMO_MODEL)
model.updateTranslatorPlamoClient() model.updateTranslatorPlamoClient()
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("Plamo API Key is valid")
else: else:
# error update Auth key # error update Auth key
auth_keys = config.AUTH_KEYS auth_keys = config.AUTH_KEYS
@@ -2975,13 +2991,13 @@ class Controller:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
if config.AUTH_KEYS[engine] is not None: if config.AUTH_KEYS[engine] is not None:
if model.authenticationTranslatorGeminiAuthKey(auth_key=config.AUTH_KEYS[engine]) is True: if model.authenticationTranslatorGeminiAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("Gemini API Key is valid")
config.SELECTABLE_GEMINI_MODEL_LIST = model.getTranslatorGeminiModelList() config.SELECTABLE_GEMINI_MODEL_LIST = model.getTranslatorGeminiModelList()
if config.SELECTED_GEMINI_MODEL not in config.SELECTABLE_GEMINI_MODEL_LIST: if config.SELECTED_GEMINI_MODEL not in config.SELECTABLE_GEMINI_MODEL_LIST:
config.SELECTED_GEMINI_MODEL = config.SELECTABLE_GEMINI_MODEL_LIST[0] config.SELECTED_GEMINI_MODEL = config.SELECTABLE_GEMINI_MODEL_LIST[0]
model.setTranslatorGeminiModel(config.SELECTED_GEMINI_MODEL) model.setTranslatorGeminiModel(config.SELECTED_GEMINI_MODEL)
model.updateTranslatorGeminiClient() model.updateTranslatorGeminiClient()
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("Gemini API Key is valid")
else: else:
# error update Auth key # error update Auth key
auth_keys = config.AUTH_KEYS auth_keys = config.AUTH_KEYS
@@ -2993,13 +3009,13 @@ class Controller:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
if config.AUTH_KEYS[engine] is not None: if config.AUTH_KEYS[engine] is not None:
if model.authenticationTranslatorOpenAIAuthKey(auth_key=config.AUTH_KEYS[engine]) is True: if model.authenticationTranslatorOpenAIAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("OpenAI API Key is valid")
config.SELECTABLE_OPENAI_MODEL_LIST = model.getTranslatorOpenAIModelList() config.SELECTABLE_OPENAI_MODEL_LIST = model.getTranslatorOpenAIModelList()
if config.SELECTED_OPENAI_MODEL not in config.SELECTABLE_OPENAI_MODEL_LIST: if config.SELECTED_OPENAI_MODEL not in config.SELECTABLE_OPENAI_MODEL_LIST:
config.SELECTED_OPENAI_MODEL = config.SELECTABLE_OPENAI_MODEL_LIST[0] config.SELECTED_OPENAI_MODEL = config.SELECTABLE_OPENAI_MODEL_LIST[0]
model.setTranslatorOpenAIModel(config.SELECTED_OPENAI_MODEL) model.setTranslatorOpenAIModel(config.SELECTED_OPENAI_MODEL)
model.updateTranslatorOpenAIClient() model.updateTranslatorOpenAIClient()
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("OpenAI API Key is valid")
else: else:
# error update Auth key # error update Auth key
auth_keys = config.AUTH_KEYS auth_keys = config.AUTH_KEYS
@@ -3007,30 +3023,36 @@ class Controller:
config.AUTH_KEYS = auth_keys config.AUTH_KEYS = auth_keys
printLog("OpenAI API Key is invalid") printLog("OpenAI API Key is invalid")
case "LMStudio": case "LMStudio":
printLog("Start check LMStudio API Key") printLog("Start check LMStudio Server")
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
if config.LMSTUDIO_URL is not None: if config.LMSTUDIO_URL is not None:
if model.authenticationTranslatorLMStudio(base_url=config.LMSTUDIO_URL) is True: if model.authenticationTranslatorLMStudio(base_url=config.LMSTUDIO_URL) is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("LMStudio URL is valid")
config.SELECTABLE_LMSTUDIO_MODEL_LIST = model.getTranslatorLMStudioModelList() config.SELECTABLE_LMSTUDIO_MODEL_LIST = model.getTranslatorLMStudioModelList()
if len(config.SELECTABLE_LMSTUDIO_MODEL_LIST) == 0:
printLog("LMStudio model list is empty")
break
if config.SELECTED_LMSTUDIO_MODEL not in config.SELECTABLE_LMSTUDIO_MODEL_LIST: if config.SELECTED_LMSTUDIO_MODEL not in config.SELECTABLE_LMSTUDIO_MODEL_LIST:
config.SELECTED_LMSTUDIO_MODEL = config.SELECTABLE_LMSTUDIO_MODEL_LIST[0] config.SELECTED_LMSTUDIO_MODEL = config.SELECTABLE_LMSTUDIO_MODEL_LIST[0]
model.setTranslatorLMStudioModel(config.SELECTED_LMSTUDIO_MODEL) model.setTranslatorLMStudioModel(config.SELECTED_LMSTUDIO_MODEL)
model.updateTranslatorLMStudioClient() model.updateTranslatorLMStudioClient()
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("LMStudio is available")
else: else:
printLog("LMStudio is not available") printLog("LMStudio is not available")
case "Ollama": case "Ollama":
printLog("Start check Ollama API Key") printLog("Start check Ollama Server")
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
if model.authenticationTranslatorOllama() is True: if model.authenticationTranslatorOllama() is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("Ollama is available")
config.SELECTABLE_OLLAMA_MODEL_LIST = model.getTranslatorOllamaModelList() config.SELECTABLE_OLLAMA_MODEL_LIST = model.getTranslatorOllamaModelList()
if len(config.SELECTABLE_OLLAMA_MODEL_LIST) == 0:
printLog("Ollama model list is empty")
break
if config.SELECTED_OLLAMA_MODEL not in config.SELECTABLE_OLLAMA_MODEL_LIST: if config.SELECTED_OLLAMA_MODEL not in config.SELECTABLE_OLLAMA_MODEL_LIST:
config.SELECTED_OLLAMA_MODEL = config.SELECTABLE_OLLAMA_MODEL_LIST[0] config.SELECTED_OLLAMA_MODEL = config.SELECTABLE_OLLAMA_MODEL_LIST[0]
model.setTranslatorOllamaModel(config.SELECTED_OLLAMA_MODEL) model.setTranslatorOllamaModel(config.SELECTED_OLLAMA_MODEL)
model.updateTranslatorOllamaClient() model.updateTranslatorOllamaClient()
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("Ollama is available")
else: else:
printLog("Ollama is not available") printLog("Ollama is not available")
case _: case _:
@@ -3126,6 +3148,9 @@ class Controller:
model.stopWebSocketServer() model.stopWebSocketServer()
printLog("WebSocket server host or port is not available") printLog("WebSocket server host or port is not available")
printLog("Revalidate Selected Models")
config.revalidate_selected_models()
printLog("Update settings") printLog("Update settings")
self.updateConfigSettings() self.updateConfigSettings()

View File

@@ -207,6 +207,7 @@ mapping = {
"/set/data/openai_auth_key": {"status": True, "variable":controller.setOpenAIAuthKey}, "/set/data/openai_auth_key": {"status": True, "variable":controller.setOpenAIAuthKey},
"/delete/data/openai_auth_key": {"status": True, "variable":controller.delOpenAIAuthKey}, "/delete/data/openai_auth_key": {"status": True, "variable":controller.delOpenAIAuthKey},
"/get/data/connected_lmstudio": {"status": True, "variable":controller.getTranslatorLMStudioConnection},
"/run/lmstudio_connection": {"status": True, "variable":controller.checkTranslatorLMStudioConnection}, "/run/lmstudio_connection": {"status": True, "variable":controller.checkTranslatorLMStudioConnection},
"/get/data/selectable_lmstudio_model_list": {"status": True, "variable":controller.getTranslatorLStudioModelList}, "/get/data/selectable_lmstudio_model_list": {"status": True, "variable":controller.getTranslatorLStudioModelList},
"/get/data/selected_lmstudio_model": {"status": True, "variable":controller.getTranslatorLMStudioModel}, "/get/data/selected_lmstudio_model": {"status": True, "variable":controller.getTranslatorLMStudioModel},
@@ -214,6 +215,7 @@ mapping = {
"/get/data/lmstudio_url": {"status": True, "variable":controller.getTranslatorLMStudioURL}, "/get/data/lmstudio_url": {"status": True, "variable":controller.getTranslatorLMStudioURL},
"/set/data/lmstudio_url": {"status": True, "variable":controller.setTranslatorLMStudioURL}, "/set/data/lmstudio_url": {"status": True, "variable":controller.setTranslatorLMStudioURL},
"/get/data/connected_ollama": {"status": True, "variable":controller.getTranslatorOllamaConnection},
"/run/ollama_connection": {"status": True, "variable":controller.checkTranslatorOllamaConnection}, "/run/ollama_connection": {"status": True, "variable":controller.checkTranslatorOllamaConnection},
"/get/data/selectable_ollama_model_list": {"status": True, "variable":controller.getTranslatorOllamaModelList}, "/get/data/selectable_ollama_model_list": {"status": True, "variable":controller.getTranslatorOllamaModelList},
"/get/data/selected_ollama_model": {"status": True, "variable":controller.getTranslatorOllamaModel}, "/get/data/selected_ollama_model": {"status": True, "variable":controller.getTranslatorOllamaModel},

View File

@@ -249,6 +249,10 @@ class Model:
self.ensure_initialized() self.ensure_initialized()
self.translator.updateOpenAIClient() self.translator.updateOpenAIClient()
def getTranslatorLMStudioConnected(self) -> bool:
self.ensure_initialized()
return self.translator.getLMStudioConnected()
def authenticationTranslatorLMStudio(self, base_url: str) -> bool: def authenticationTranslatorLMStudio(self, base_url: str) -> bool:
result = self.translator.setLMStudioClientURL(base_url=base_url, root_path=config.PATH_LOCAL) result = self.translator.setLMStudioClientURL(base_url=base_url, root_path=config.PATH_LOCAL)
return result return result
@@ -265,6 +269,10 @@ class Model:
self.ensure_initialized() self.ensure_initialized()
self.translator.updateLMStudioClient() self.translator.updateLMStudioClient()
def getTranslatorOllamaConnected(self) -> bool:
self.ensure_initialized()
return self.translator.getOllamaConnected()
def authenticationTranslatorOllama(self) -> bool: def authenticationTranslatorOllama(self) -> bool:
result = self.translator.checkOllamaClient(root_path=config.PATH_LOCAL) result = self.translator.checkOllamaClient(root_path=config.PATH_LOCAL)
return result return result

View File

@@ -1,6 +1,6 @@
from openai import OpenAI
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from pydantic import SecretStr from pydantic import SecretStr
import requests
try: try:
from .translation_languages import translation_lang from .translation_languages import translation_lang
@@ -8,33 +8,35 @@ try:
except Exception: except Exception:
import sys import sys
from os import path as os_path from os import path as os_path
sys.path.append(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__))))) sys.path.append(os_path.dirname(os_path.abspath(__file__)))
from translation_languages import translation_lang from translation_languages import translation_lang, loadTranslationLanguages
from translation_utils import loadPromptConfig from translation_utils import loadPromptConfig
loadTranslationLanguages(path=".", force=True)
def _authentication_check(api_key: str, base_url: str | None = None) -> bool: def _authentication_check(base_url: str | None = None) -> bool:
"""Check if the provided API key is valid by attempting to list models. """Check if the provided API key is valid by attempting to list models.
""" """
try: try:
client = OpenAI(api_key=api_key, base_url=base_url) response = requests.get(f"{base_url}/models", timeout=0.2)
client.models.list() if response.status_code == 200:
return True return True
else:
return False
except Exception: except Exception:
return False return False
def _get_available_text_models(api_key: str, base_url: str | None = None) -> list[str]: def _get_available_text_models(base_url: str | None = None) -> list[str]:
"""Extract the list of available text models from the LM Studio. """Extract the list of available text models from the LM Studio.
""" """
try: try:
client = OpenAI(api_key=api_key, base_url=base_url) response = requests.get(f"{base_url}/models", timeout=0.2)
res = client.models.list() models = response.json()["data"]
models = res.data
except Exception: except Exception:
models = [] models = []
allowed_models = [] allowed_models = []
for model in models: for model in models:
allowed_models.append(model.id) allowed_models.append(model["id"])
allowed_models.sort() allowed_models.sort()
return allowed_models return allowed_models
@@ -58,13 +60,13 @@ class LMStudioClient:
return self.base_url return self.base_url
def setBaseURL(self, base_url: str | None) -> None: def setBaseURL(self, base_url: str | None) -> None:
result = _authentication_check(api_key=self.api_key, base_url=base_url) result = _authentication_check(base_url=base_url)
if result: if result:
self.base_url = base_url self.base_url = base_url
return result return result
def getModelList(self) -> list[str]: def getModelList(self) -> list[str]:
return _get_available_text_models(api_key=self.api_key, base_url=self.base_url) if self.base_url else [] return _get_available_text_models(base_url=self.base_url) if self.base_url else []
def getModel(self) -> str: def getModel(self) -> str:
return self.model return self.model
@@ -108,7 +110,7 @@ class LMStudioClient:
return content.strip() return content.strip()
if __name__ == "__main__": if __name__ == "__main__":
client = LMStudioClient(base_url="http://192.168.68.110:1234/v1") client = LMStudioClient(base_url="http://127.0.0.1:1234/v1")
models = client.getModelList() models = client.getModelList()
if models: if models:
print("Available models:", models) print("Available models:", models)

View File

@@ -7,15 +7,16 @@ try:
except Exception: except Exception:
import sys import sys
from os import path as os_path from os import path as os_path
sys.path.append(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__))))) sys.path.append(os_path.dirname(os_path.abspath(__file__)))
from translation_languages import translation_lang from translation_languages import translation_lang, loadTranslationLanguages
from translation_utils import loadPromptConfig from translation_utils import loadPromptConfig
loadTranslationLanguages(path=".", force=True)
def _authentication_check(base_url: str | None = None) -> bool: def _authentication_check(base_url: str | None = None) -> bool:
"""Check authentication for Ollama API. """Check authentication for Ollama API.
""" """
try: try:
response = requests.get(f"{base_url}") response = requests.get(f"{base_url}", timeout=0.2)
if response.status_code == 200: if response.status_code == 200:
return True return True
else: else:

View File

@@ -176,6 +176,16 @@ class Translator:
"""Update the OpenAI client (fetch available models).""" """Update the OpenAI client (fetch available models)."""
self.openai_client.updateClient() self.openai_client.updateClient()
def getLMStudioConnected(self) -> bool:
"""Get LM Studio connection status.
Returns True if connected, False otherwise.
"""
if self.lmstudio_client is None:
return False
else:
return True
def setLMStudioClientURL(self, base_url: str | None = None, root_path: str = None) -> bool: def setLMStudioClientURL(self, base_url: str | None = None, root_path: str = None) -> bool:
"""Authenticate LM Studio with the provided base URL. """Authenticate LM Studio with the provided base URL.
@@ -207,13 +217,26 @@ class Translator:
"""Update the LM Studio client (fetch available models).""" """Update the LM Studio client (fetch available models)."""
self.lmstudio_client.updateClient() self.lmstudio_client.updateClient()
def getOllamaConnected(self) -> bool:
"""Get Ollama connection status.
Returns True if connected, False otherwise.
"""
if self.ollama_client is None:
return False
else:
return True
def checkOllamaClient(self, root_path: str = None) -> bool: def checkOllamaClient(self, root_path: str = None) -> bool:
"""Check if Ollama client is available. """Check if Ollama client is available.
Returns True if Ollama is reachable, False otherwise. Returns True if Ollama is reachable, False otherwise.
""" """
self.ollama_client = OllamaClient(root_path=root_path) self.ollama_client = OllamaClient(root_path=root_path)
return self.ollama_client.authenticationCheck() result = self.ollama_client.authenticationCheck()
if result is False:
self.ollama_client = None
return result
def getOllamaModelList(self, root_path: str = None) -> bool: def getOllamaModelList(self, root_path: str = None) -> bool:
"""Initialize Ollama client and fetch available models. """Initialize Ollama client and fetch available models.

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "VRCT", "productName": "VRCT",
"version": "3.3.1", "version": "3.3.2",
"identifier": "com.vrct.app", "identifier": "com.vrct.app",
"build": { "build": {
"beforeDevCommand": "", "beforeDevCommand": "",
@@ -11,7 +11,8 @@
}, },
"app": { "app": {
"enableGTKAppId": false, "enableGTKAppId": false,
"windows": [{ "windows": [
{
"title": "VRCT", "title": "VRCT",
"center": true, "center": true,
"width": 450, "width": 450,
@@ -21,10 +22,14 @@
"transparent": true, "transparent": true,
"decorations": false, "decorations": false,
"shadow": false "shadow": false
}], }
],
"security": { "security": {
"csp": null, "csp": null,
"capabilities": ["default", "vrct-capability"] "capabilities": [
"default",
"vrct-capability"
]
} }
}, },
"bundle": { "bundle": {

View File

@@ -2,6 +2,7 @@ import { useI18n } from "@useI18n";
import { import {
useNotificationStatus, useNotificationStatus,
useLLMConnection,
} from "@logics_common"; } from "@logics_common";
import { import {
@@ -46,6 +47,11 @@ export const _useBackendErrorHandling = () => {
updateWebsocketPort, updateWebsocketPort,
} = useAdvancedSettings(); } = useAdvancedSettings();
const {
updateIsOllamaConnected,
updateIsLMStudioConnected,
} = useLLMConnection();
const errorHandling_Backend = ({message, data, endpoint, result}) => { const errorHandling_Backend = ({message, data, endpoint, result}) => {
switch (endpoint) { switch (endpoint) {
case "/run/error_device": case "/run/error_device":
@@ -221,6 +227,18 @@ export const _useBackendErrorHandling = () => {
} }
return; return;
case "/run/lmstudio_connection":
updateIsLMStudioConnected(data);
showNotification_Error(message);
console.error(message);
return;
case "/run/ollama_connection":
updateIsOllamaConnected(data);
showNotification_Error(message);
console.error(message);
return;
default: default:
console.error(`Invalid endpoint or message: ${endpoint}\nmessage: ${message}\nresult: ${JSON.stringify(result)}`); console.error(`Invalid endpoint or message: ${endpoint}\nmessage: ${message}\nresult: ${JSON.stringify(result)}`);
return; return;

View File

@@ -15,3 +15,4 @@ export { useHandleOscQuery } from "./useHandleOscQuery";
export { useIsOscAvailable } from "./useIsOscAvailable"; export { useIsOscAvailable } from "./useIsOscAvailable";
export { useIsVrctAvailable } from "./useIsVrctAvailable"; export { useIsVrctAvailable } from "./useIsVrctAvailable";
export { useFetch } from "./useFetch"; export { useFetch } from "./useFetch";
export { useLLMConnection } from "./useLLMConnection";

View File

@@ -0,0 +1,47 @@
import { useStdoutToPython } from "@useStdoutToPython";
import {
useStore_IsLMStudioConnected,
useStore_IsOllamaConnected,
} from "@store";
export const useLLMConnection = () => {
const { asyncStdoutToPython } = useStdoutToPython();
const {
currentIsLMStudioConnected,
updateIsLMStudioConnected,
pendingIsLMStudioConnected,
} = useStore_IsLMStudioConnected();
const {
currentIsOllamaConnected,
updateIsOllamaConnected,
pendingIsOllamaConnected,
} = useStore_IsOllamaConnected();
const checkConnection_LMStudio = () => {
pendingIsLMStudioConnected();
asyncStdoutToPython("/run/lmstudio_connection");
};
const setConnectionStatus_LMStudio = (is_connected) => {
updateIsLMStudioConnected(is_connected);
};
const checkConnection_Ollama = () => {
pendingIsOllamaConnected();
asyncStdoutToPython("/run/ollama_connection");
};
const setConnectionStatus_Ollama = (is_connected) => {
updateIsOllamaConnected(is_connected);
};
return {
currentIsLMStudioConnected,
updateIsLMStudioConnected,
setConnectionStatus_LMStudio,
checkConnection_LMStudio,
currentIsOllamaConnected,
updateIsOllamaConnected,
setConnectionStatus_Ollama,
checkConnection_Ollama,
};
};

View File

@@ -167,6 +167,8 @@ export const { atomInstance: Atom_NotificationStatus, useHook: useStore_Notifica
key: 0, key: 0,
message: "", message: "",
}, "NotificationStatus"); }, "NotificationStatus");
export const { atomInstance: Atom_IsLMStudioConnected, useHook: useStore_IsLMStudioConnected } = createAtomWithHook(false, "IsLMStudioConnected");
export const { atomInstance: Atom_IsOllamaConnected, useHook: useStore_IsOllamaConnected } = createAtomWithHook(false, "IsOllamaConnected");
// Main Page // Main Page
// Common // Common

View File

@@ -101,12 +101,17 @@ export const getPluginsList = () => {
if (IS_PLUGIN_PATH_DEV_MODE || IS_PLUGIN_LIST_URL_DEV_MODE) console.warn("ui_configs IS_PLUGIN_PATH_DEV_MODE or IS_PLUGIN_LIST_URL_DEV_MODE is true. Turn to 'false' when it's production environment."); if (IS_PLUGIN_PATH_DEV_MODE || IS_PLUGIN_LIST_URL_DEV_MODE) console.warn("ui_configs IS_PLUGIN_PATH_DEV_MODE or IS_PLUGIN_LIST_URL_DEV_MODE is true. Turn to 'false' when it's production environment.");
export const translator_status = [ export const translator_status = [
{ id: "DeepL", label: "DeepL", is_available: false }, { id: "CTranslate2", label: `AI\nCTranslate2`, is_available: false, is_default: true },
{ id: "DeepL_API", label: `DeepL API`, is_available: false },
{ id: "Google", label: "Google", is_available: false }, { id: "Google", label: "Google", is_available: false },
{ id: "Bing", label: "Bing", is_available: false }, { id: "Bing", label: "Bing", is_available: false },
{ id: "Papago", label: "Papago", is_available: false }, { id: "Papago", label: "Papago", is_available: false },
{ id: "CTranslate2", label: `AI\nCTranslate2`, is_available: false, is_default: true }, { id: "DeepL", label: "DeepL", is_available: false },
{ id: "DeepL_API", label: `DeepL API`, is_available: false },
{ id: "Plamo_API", label: `Plamo API`, is_available: false },
{ id: "Gemini_API", label: `Gemini API`, is_available: false },
{ id: "OpenAI_API", label: `OpenAI API`, is_available: false },
{ id: "LMStudio", label: `LMStudio`, is_available: false },
{ id: "Ollama", label: `Ollama`, is_available: false },
]; ];
export const ctranslate2_weight_type_status = [ export const ctranslate2_weight_type_status = [

View File

@@ -20,6 +20,12 @@ export const STATIC_ROUTE_META_LIST = [
{ endpoint: "/run/open_filepath_logs", ns: common, hook_name: "useOpenFolder", method_name: "openedFolder_MessageLogs" }, { endpoint: "/run/open_filepath_logs", ns: common, hook_name: "useOpenFolder", method_name: "openedFolder_MessageLogs" },
{ endpoint: "/run/open_filepath_config_file", ns: common, hook_name: "useOpenFolder", method_name: "openedFolder_ConfigFile" }, { endpoint: "/run/open_filepath_config_file", ns: common, hook_name: "useOpenFolder", method_name: "openedFolder_ConfigFile" },
{ endpoint: "/get/data/connected_lmstudio", ns: common, hook_name: "useLLMConnection", method_name: "setConnectionStatus_LMStudio" },
{ endpoint: "/run/lmstudio_connection", ns: common, hook_name: "useLLMConnection", method_name: "setConnectionStatus_LMStudio" },
{ endpoint: "/get/data/connected_ollama", ns: common, hook_name: "useLLMConnection", method_name: "setConnectionStatus_Ollama" },
{ endpoint: "/run/ollama_connection", ns: common, hook_name: "useLLMConnection", method_name: "setConnectionStatus_Ollama" },
// Software Version // Software Version
{ endpoint: "/get/data/version", ns: common, hook_name: "useSoftwareVersion", method_name: "updateSoftwareVersion" }, { endpoint: "/get/data/version", ns: common, hook_name: "useSoftwareVersion", method_name: "updateSoftwareVersion" },
// Latest Software Version Info // Latest Software Version Info

View File

@@ -0,0 +1,19 @@
import styles from "./ConnectionCheckButton.module.scss";
export const ConnectionCheckButton = (props) => {
const label = props.state === "pending"
? "Checking... 🌀"
: props.variable === true
? "Connected ✅"
: "Disconnected ❌";
return (
<div className={styles.container}>
<p>{label}</p>
<p>{`UI Status: ${props.state}`}</p>
<button className={styles.button_wrapper} onClick={props.checkFunction}>
<p className={styles.button_label}>Connection Check</p>
</button>
</div>
);
};

View File

@@ -0,0 +1,15 @@
.button_wrapper {
padding: 1.6rem;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.button_svg {
width: 2.4rem;
color: var(--dark_400_color);
}

View File

@@ -14,3 +14,4 @@ export { ThresholdComponent } from "./threshold_component/ThresholdComponent";
export { WordFilter, WordFilterListToggleComponent } from "./word_filter/WordFilter"; export { WordFilter, WordFilterListToggleComponent } from "./word_filter/WordFilter";
export { DownloadModels } from "./download_models/DownloadModels"; export { DownloadModels } from "./download_models/DownloadModels";
export { MessageFormat } from "./message_format/MessageFormat"; export { MessageFormat } from "./message_format/MessageFormat";
export { ConnectionCheckButton } from "./connection_check_button/ConnectionCheckButton";

View File

@@ -19,6 +19,7 @@ import {
WordFilterListToggleComponent, WordFilterListToggleComponent,
DownloadModels, DownloadModels,
MessageFormat, MessageFormat,
ConnectionCheckButton,
} from "../_components"; } from "../_components";
import { Checkbox } from "@common_components"; import { Checkbox } from "@common_components";
@@ -181,6 +182,10 @@ export const DownloadModelsContainer = (props) => (
<CommonContainer Component={DownloadModels} {...props} /> <CommonContainer Component={DownloadModels} {...props} />
); );
export const ConnectionCheckButtonContainer = (props) => (
<CommonContainer Component={ConnectionCheckButton} {...props} />
);
export const MessageFormatContainer = (props) => { export const MessageFormatContainer = (props) => {
return ( return (
<> <>

View File

@@ -17,6 +17,7 @@ import {
EntryWithSaveButtonContainer, EntryWithSaveButtonContainer,
RadioButtonContainer, RadioButtonContainer,
DropdownMenuContainer, DropdownMenuContainer,
ConnectionCheckButtonContainer,
useOnMouseLeaveDropdownMenu, useOnMouseLeaveDropdownMenu,
} from "../_templates/Templates"; } from "../_templates/Templates";
@@ -25,9 +26,11 @@ import {
DropdownMenu, DropdownMenu,
MultiDropdownMenu, MultiDropdownMenu,
LabelComponent, LabelComponent,
ConnectionCheckButton,
} from "../_components"; } from "../_components";
import { deepl_auth_key_url } from "@ui_configs"; import { deepl_auth_key_url } from "@ui_configs";
import { useLLMConnection } from "@logics_common";
export const Translation = () => { export const Translation = () => {
return ( return (
@@ -46,9 +49,11 @@ export const Translation = () => {
<OpenAIAuthKey_Box /> <OpenAIAuthKey_Box />
<OpenAIModelContainer /> <OpenAIModelContainer />
<LMStudioConnectionCheck_Box />
<LMStudioURL_Box /> <LMStudioURL_Box />
<LMStudioModelContainer /> <LMStudioModelContainer />
<OllamaConnectionCheck_Box />
<OllamaModelContainer /> <OllamaModelContainer />
</> </>
); );
@@ -422,6 +427,23 @@ const OpenAIModelContainer = () => {
const LMStudioConnectionCheck_Box = () => {
const { t } = useI18n();
const { currentIsLMStudioConnected, checkConnection_LMStudio } = useLLMConnection();
return (
<>
<ConnectionCheckButtonContainer
label="Check LM Studio Connection"
variable={currentIsLMStudioConnected.data}
state={currentIsLMStudioConnected.state}
checkFunction={checkConnection_LMStudio}
remove_border_bottom={true}
// width="10rem"
/>
</>
);
};
const LMStudioURL_Box = () => { const LMStudioURL_Box = () => {
const { t } = useI18n(); const { t } = useI18n();
const { currentLMStudioURL, setLMStudioURL, deleteLMStudioURL } = useTranslation(); const { currentLMStudioURL, setLMStudioURL, deleteLMStudioURL } = useTranslation();
@@ -475,6 +497,23 @@ const LMStudioModelContainer = () => {
); );
}; };
const OllamaConnectionCheck_Box = () => {
const { t } = useI18n();
const { currentIsOllamaConnected, checkConnection_Ollama } = useLLMConnection();
return (
<>
<ConnectionCheckButtonContainer
label="Check Ollama Connection"
variable={currentIsOllamaConnected.data}
state={currentIsOllamaConnected.state}
checkFunction={checkConnection_Ollama}
remove_border_bottom={true}
// width="10rem"
/>
</>
);
};
const OllamaModelContainer = () => { const OllamaModelContainer = () => {
const { t } = useI18n(); const { t } = useI18n();
const { const {

View File

@@ -8,6 +8,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
overflow-y: auto;
} }
.relative_container { .relative_container {
@@ -17,8 +18,7 @@
} }
.wrapper { .wrapper {
width: 100%; padding: 2rem 0;
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;

8
utils/clean.py Normal file
View File

@@ -0,0 +1,8 @@
import os
import shutil
root = os.path.dirname(os.path.dirname(__file__))
shutil.rmtree(os.path.join(root, 'build'), ignore_errors=True)
shutil.rmtree(os.path.join(root, 'dist'), ignore_errors=True)
shutil.rmtree(os.path.join(root, 'src-tauri', 'bin'), ignore_errors=True)
shutil.rmtree(os.path.join(root, 'src-tauri', 'target'), ignore_errors=True)

39
utils/update_version.py Normal file
View File

@@ -0,0 +1,39 @@
import os
import json
def update_versions():
root = os.path.join(os.path.dirname(os.path.dirname(__file__)))
# package.jsonからバージョンを読み取る
with open(os.path.join(root, "package.json"), "r", encoding="utf-8") as f:
package_json = json.load(f)
version = package_json["version"]
# tauri.conf.jsonを更新
tauri_conf_path = os.path.join(root, "src-tauri", "tauri.conf.json")
with open(tauri_conf_path, "r", encoding="utf-8") as f:
tauri_conf = json.load(f)
tauri_conf["version"] = version
with open(tauri_conf_path, "w", encoding="utf-8") as f:
json.dump(tauri_conf, f, indent=4, ensure_ascii=False)
# config.pyを更新
config_path = os.path.join(root, "src-python", "config.py")
with open(config_path, "r", encoding="utf-8") as f:
content = f.read()
# VERSION行を置換
import re
pattern = r'(self\._VERSION = ")[^"]+(")'
replacement = rf'\g<1>{version}\g<2>'
new_content = re.sub(pattern, replacement, content)
with open(config_path, "w", encoding="utf-8") as f:
f.write(new_content)
print(f"updated to version {version}")
if __name__ == "__main__":
update_versions()