From fcb12953026b854241809be75682e4b3afdea1dd Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:55:48 +0900 Subject: [PATCH] Add documentation and coding guidelines for VRCT backend - Introduced a comprehensive coding rules document outlining naming conventions, module structure, import order, type annotations, error handling, and testing practices. - Created a specification document detailing project goals, target users, and functional/non-functional requirements for the VRCT project. - Added a design document describing the application's architecture, initialization policies, concurrency models, and error handling strategies. - Included a detailed design document specifying major classes, functions, data structures, and exception handling. - Removed outdated mypy configuration and several unused scripts related to documentation verification and cleanup. - Deleted test files for OSC and overlay imports as part of the cleanup process. --- src-python/docs/CHANGELOG.md | 27 - src-python/docs/README.md | 18 - src-python/docs/api.md | 701 -------- src-python/docs/architecture.md | 21 - src-python/docs/config.md | 433 +++++ src-python/docs/controller.md | 1225 ++++++++++++++ src-python/docs/device_manager.md | 1427 +++++++++++++++++ src-python/docs/diagrams.md | 51 - src-python/docs/mainloop.md | 346 ++++ src-python/docs/model.md | 1277 +++++++++++++++ src-python/docs/modules/config.md | 212 --- src-python/docs/modules/config_ref.md | 39 - src-python/docs/modules/controller.md | 162 -- src-python/docs/modules/controller_ref.md | 25 - src-python/docs/modules/device_manager.md | 84 - src-python/docs/modules/device_manager_ref.md | 93 -- src-python/docs/modules/mainloop.md | 50 - src-python/docs/modules/model.md | 118 -- src-python/docs/modules/model_extra.md | 60 - src-python/docs/modules/osc.md | 47 - src-python/docs/modules/overlay.md | 59 - src-python/docs/modules/overlay_image.md | 115 -- src-python/docs/modules/transcription.md | 126 -- src-python/docs/modules/translation.md | 113 -- src-python/docs/modules/transliteration.md | 17 - src-python/docs/modules/utils.md | 132 -- src-python/docs/modules/watchdog.md | 80 - src-python/docs/modules/websocket.md | 18 - src-python/docs/run_events_payloads.md | 125 -- src-python/docs/runtime.md | 43 - src-python/docs/utils.md | 940 +++++++++++ ...{CODING_RULES.md => コーディングルール.md} | 0 src-python/docs/仕様書.md | 58 + src-python/docs/設計書.md | 57 + src-python/docs/詳細設計書.md | 66 + src-python/mypy.ini | 32 - .../scripts/cleanup_docs_placeholders.py | 16 - src-python/scripts/find_doc_tokens.py | 21 - src-python/scripts/print_mapping.py | 28 - src-python/scripts/verify_docs_vs_code.py | 161 -- .../scripts/verify_docs_vs_code_runtime.py | 126 -- src-python/tests/test_osc_imports.py | 6 - src-python/tests/test_overlay_imports.py | 30 - 43 files changed, 5829 insertions(+), 2956 deletions(-) delete mode 100644 src-python/docs/CHANGELOG.md delete mode 100644 src-python/docs/README.md delete mode 100644 src-python/docs/api.md delete mode 100644 src-python/docs/architecture.md create mode 100644 src-python/docs/config.md create mode 100644 src-python/docs/controller.md create mode 100644 src-python/docs/device_manager.md delete mode 100644 src-python/docs/diagrams.md create mode 100644 src-python/docs/mainloop.md create mode 100644 src-python/docs/model.md delete mode 100644 src-python/docs/modules/config.md delete mode 100644 src-python/docs/modules/config_ref.md delete mode 100644 src-python/docs/modules/controller.md delete mode 100644 src-python/docs/modules/controller_ref.md delete mode 100644 src-python/docs/modules/device_manager.md delete mode 100644 src-python/docs/modules/device_manager_ref.md delete mode 100644 src-python/docs/modules/mainloop.md delete mode 100644 src-python/docs/modules/model.md delete mode 100644 src-python/docs/modules/model_extra.md delete mode 100644 src-python/docs/modules/osc.md delete mode 100644 src-python/docs/modules/overlay.md delete mode 100644 src-python/docs/modules/overlay_image.md delete mode 100644 src-python/docs/modules/transcription.md delete mode 100644 src-python/docs/modules/translation.md delete mode 100644 src-python/docs/modules/transliteration.md delete mode 100644 src-python/docs/modules/utils.md delete mode 100644 src-python/docs/modules/watchdog.md delete mode 100644 src-python/docs/modules/websocket.md delete mode 100644 src-python/docs/run_events_payloads.md delete mode 100644 src-python/docs/runtime.md create mode 100644 src-python/docs/utils.md rename src-python/docs/{CODING_RULES.md => コーディングルール.md} (100%) create mode 100644 src-python/docs/仕様書.md create mode 100644 src-python/docs/設計書.md create mode 100644 src-python/docs/詳細設計書.md delete mode 100644 src-python/mypy.ini delete mode 100644 src-python/scripts/cleanup_docs_placeholders.py delete mode 100644 src-python/scripts/find_doc_tokens.py delete mode 100644 src-python/scripts/print_mapping.py delete mode 100644 src-python/scripts/verify_docs_vs_code.py delete mode 100644 src-python/scripts/verify_docs_vs_code_runtime.py delete mode 100644 src-python/tests/test_osc_imports.py delete mode 100644 src-python/tests/test_overlay_imports.py diff --git a/src-python/docs/CHANGELOG.md b/src-python/docs/CHANGELOG.md deleted file mode 100644 index 723f3942..00000000 --- a/src-python/docs/CHANGELOG.md +++ /dev/null @@ -1,27 +0,0 @@ -# CHANGELOG - -## 2025-10-09 — 型チェック整備と安全性向上 - -- 修正: `controller.py` - - `Controller.chatMessage` の戻り値注釈を `dict` に明示(関数は JSON 系の応答オブジェクトを返します)。 - - `Controller.checkSoftwareUpdated` が実際に応答を返すように `return` を追加。 - -- 修正: `model.py` - - `startCheckMicEnergy` / `startCheckSpeakerEnergy` のコールバック引数を Optional に変更し、呼び出し前に `callable` チェックを追加。これにより None を渡しても安全に扱えるようになりました。 - - `convertMessageToTransliteration` の返り値を常に list に統一。hiragana/romaji が False の場合は空リストを返します。 - - `createOverlayImageLargeLog` 等の Overlay 作成関数で `target_language` を dict で受けた場合に内部で言語リストへ正規化する挙動を明確化。 - -- 目的: mypy の型チェックの警告/エラーを削減し、ランタイムでの None 呼び出しによるクラッシュを防止するための低リスクな変更です。 - -- 注記: - - 追加で `types-requests` をプロジェクト仮想環境にインストールし、mypy の外部型スタブ不足を解消しました。 - - 本チェンジは内部の型注釈とガードを中心としており、動作ロジックの大きな変更は行っていません。動作確認は mypy(型チェック)と ruff(lint)を通過したことをもって行っています。 - -## 1.0.0 (initial) -- 初回ドキュメント作成: ソースコードに基づく仕様書 / 詳細設計書を docs 配下に追加。 -- 対象: utils, model, controller, device_manager, config, translation, transcription, overlay, websocket, osc, transliteration, watchdog - -今後の作業候補: -- requirements.txt の自動生成とテストスイート追加 -- ドキュメントの API サンプル(リクエスト/レスポンス)追加 -- UML 図/シーケンス図の画像化 diff --git a/src-python/docs/README.md b/src-python/docs/README.md deleted file mode 100644 index be03a5d0..00000000 --- a/src-python/docs/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# VRCT — ドキュメント - -このドキュメントセットは、VRCT プロジェクト(`src-python`)に含まれる実装の仕様書 / 設計書 / 詳細設計書です。 - -目的 -- ソースコード構造、モジュール間データフロー、API エンドポイント、設定、実行手順、トラブルシュートを網羅して開発・運用の参照を容易にする。 - -対象 -- `utils.py`, `model.py`, `controller.py`, `mainloop.py`, `device_manager.py`, `config.py` および `models/` 以下の全モジュール。 - -ドキュメント構成(主要ファイル) -- `architecture.md` — アーキテクチャ概観 -- `modules/` — 各モジュールごとの詳細設計(個別ファイル) -- `api.md` — 外部/内部向け API エンドポイント マッピング(`mainloop.py` の `mapping` / `run_mapping` に準拠) -- `runtime.md` — 実行/セットアップ手順、依存関係 -- `diagrams.md` — システム図(Mermaid とテキスト両方) -- `CODING_RULES.md` — プロジェクト固有のコーディング規約(命名・型方針・lint/mypy 方針 等) -- `CHANGELOG.md` — 変更履歴 \ No newline at end of file diff --git a/src-python/docs/api.md b/src-python/docs/api.md deleted file mode 100644 index f512a191..00000000 --- a/src-python/docs/api.md +++ /dev/null @@ -1,701 +0,0 @@ ---- - - - -## API エンドポイント仕様 - -概要 -- このドキュメントは `mainloop.py` の `mapping` と `run_mapping` に定義された全エンドポイントを列挙します。 -- すべてのリクエストは標準入力経由で JSON を一行送る形で受信され、標準出力へ JSON 応答を出力します。 - -共通リクエスト形式 -- JSON オブジェクトを 1 行で標準入力に流します。 -- フィールド: - - `endpoint`: エンドポイント文字列 (例: `/get/data/version`) - - `data`: 任意(多くの GET 系は null、SET 系は新しい値やオブジェクト) - -例 -```json -{"endpoint":"/get/data/version","data":null} -``` - -共通レスポンス形式 -- mainloop は各リクエストの処理結果を次の形式で標準出力に出します(内部 util の `printResponse` を経由): - -成功例: -```json -{"status":200,"endpoint":"/get/data/version","result":"3.2.2"} -``` - -エラー例: -```json -{"status":400,"endpoint":"/set/data/osc_ip_address","result":{"message":"Invalid IP address","data":"127.0.0.1"}} -``` - -ロック状態と再試行 -- `mapping` にある各ハンドラは `"status": True|False` を持ちます。 - - False の場合、`handleRequest` は 423 (Locked endpoint) を返し、メインのハンドラはその要求をキューに戻して待機します(遅延再実行のため)。 - -run イベント -- `controller` は UI 更新などの非同期通知を行うために `run(status, endpoint, payload)` を呼び出します。これらは `run_mapping` にマップされ、外部 UI には `/run/...` 形式のエンドポイントで配信されます。 - -以下は `controller.py` から抽出した run イベントと、実際に送られるペイロードの具体例です。UI 側はこれらの JSON 形状を期待することで正しく動作します。 - -`/run/connected_network` (200) - - payload: true | false - -`/run/enable_ai_models` (200) - - payload: true | false - -`/run/mic_host_list` (200) - - payload: ["Host 1", "Host 2"] - -`/run/mic_device_list` (200) - - payload: ["Microphone (Realtek)", "Headset Microphone"] - -`/run/speaker_device_list` (200) - - payload: ["Speakers (Realtek)", "Headset"] - -`/run/initialization_complete` (200) - - payload: dict mapping endpoint -> current value (constructed from init_mapping) - - 例: {"/get/data/version":"3.2.2","/get/data/selected_tab_no":0} - -`/run/selected_mic_device` (200) - - payload: {"host": , "device": } - -`/run/selected_speaker_device` (200) - - payload: string (device name) - -`/run/error_device` (400) - - payload: {"message":"No mic device detected","data": null} - -`/run/check_mic_volume` (200) - - payload: numeric energy value (float) - -`/run/check_speaker_volume` (200) - - payload: numeric energy value (float) - -`/run/download_progress_ctranslate2_weight` (200) - - payload: {"weight_type":"m2m100_418m","progress":0.42} - -`/run/downloaded_ctranslate2_weight` (200) - - payload: "m2m100_418m" - -`/run/error_ctranslate2_weight` (400) - - payload: {"message":"CTranslate2 weight download error","data": null} - -`/run/download_progress_whisper_weight` (200) - - payload: {"weight_type":"base","progress":0.78} - -`/run/downloaded_whisper_weight` (200) - - payload: "base" - -`/run/error_whisper_weight` (400) - - payload: {"message":"Whisper weight download error","data": null} - -`/run/word_filter` (200) - - payload: {"message":"Detected by word filter: "} - -`/run/error_translation_engine` (400) - - payload: {"message":"Translation engine limit error","data": null} - -`/run/error_translation_mic_vram_overflow` (400) - - payload: {"message":"VRAM out of memory during translation of mic","data":""} - -`/run/error_translation_speaker_vram_overflow` (400) - - payload: {"message":"VRAM out of memory during translation of speaker","data":""} - -`/run/error_translation_chat_vram_overflow` (400) - - payload: {"message":"VRAM out of memory during translation of chat","data":""} - -`/run/enable_translation` (200/400) - - payload: on OOM: {"message":"Translation disabled due to VRAM overflow","data": false} - -`/run/transcription_send_mic_message` (200) - - payload: - { - "original": {"message": "Hello", "transliteration": []}, - "translations": [ {"message":"こんにちは","transliteration":[]}, ... ] - } - -`/run/transcription_receive_speaker_message` (200) - - payload: same shape as `/run/transcription_send_mic_message` - -`/run/software_update_info` (200) - - payload: e.g. {"has_update": true, "latest_version": "3.3.0"} - -`/run/selected_translation_compute_type` (200) - - payload: string ("auto"|"cpu"|"cuda:0") - -`/run/selected_transcription_compute_type` (200) - - payload: string - -`/run/selected_translation_engines` (200) - - payload: config.SELECTED_TRANSLATION_ENGINES (list/dict per tab) - -`/run/translation_engines` (200) - - payload: ["CTranslate2"] - -`/run/initialization_progress` (200) - - payload: integer (1..4) - -`/run/enable_osc_query` (200) - - payload: {"data": true|false, "disabled_functions": ["vrc_mic_mute_sync"]} - - -エンドポイント一覧(mapping にある全エンドポイント) - -注: 各行の説明では、`method` 的な概念はありません。すべてのエンドポイントは JSON リクエストで同様に呼び出します。`data` の期待値は説明に記載しています。 - -1) メイン操作 - -- /set/enable/translation — data: null — 翻訳を有効にします。 - - 成功応答例: - ```json - {"status":200, "endpoint":"/set/enable/translation", "result": true} - ``` - - 失敗例(VRAM OOM を検出して無効化されたケースは run イベントで通知されます): - ```json - {"status":400, "endpoint":"/set/enable/translation", "result":{"message":"Translation disabled due to VRAM overflow","data":false}} - ``` - -- /set/disable/translation — data: null — 翻訳を無効にします。 - - 成功応答例: - ```json - {"status":200, "endpoint":"/set/disable/translation", "result": false} - ``` - -- /set/enable/transcription_send — data: null — マイク転写(送信)を有効化します。 - - 実行はスレッドで開始される場合がある。成功例: - ```json - {"status":200, "endpoint":"/set/enable/transcription_send", "result": true} - ``` - -- /set/disable/transcription_send — data: null — 停止要求。成功例: - ```json - {"status":200, "endpoint":"/set/disable/transcription_send", "result": false} - ``` - -- /set/enable/transcription_receive — data: null — スピーカー側の転写を有効化します。 -- /set/disable/transcription_receive — data: null — 無効化します。 - -- /set/enable/foreground — data: null — フォアグラウンド表示を有効化します。 - - 成功例: {"status":200, "endpoint":"/set/enable/foreground", "result": true} - -- /get/data/selected_tab_no — data: null — 現在のタブ番号を返します。 - - 例: {"status":200, "endpoint":"/get/data/selected_tab_no", "result": 0} - -- /get/data/main_window_sidebar_compact_mode — data: null — サイドバーのコンパクト表示の現在値を返します。 - - 例: {"status":200, "endpoint":"/get/data/main_window_sidebar_compact_mode","result": false} - - -- /set/data/selected_tab_no — data: int — タブ番号を設定します。 - - リクエスト例: {"endpoint":"/set/data/selected_tab_no","data":1} - - 成功応答例: {"status":200, "endpoint":"/set/data/selected_tab_no","result":1} - -- /get/data/translation_engines — data: null — 利用可能な翻訳エンジン一覧を返します。 - - 例: {"status":200, "endpoint":"/get/data/translation_engines","result":["CTranslate2"]} - -- /get/data/selectable_language_list — data: null — 選択可能な言語一覧(言語コード, country 等を含むデータ構造) - - 例: {"status":200, "endpoint":"/get/data/selectable_language_list","result":[{"language":"English","country":"US"},{"language":"Japanese","country":"JP"}]} - -- /get/data/transcription_engines — data: null — 利用可能な転写エンジン一覧 - - 例: {"status":200, "endpoint":"/get/data/transcription_engines","result":["Google","Whisper"]} - - -- /run/send_message_box — data: {"id": <任意>, "message": "..."} - - 内部で `Controller.chatMessage` を呼び出します。戻りは変換済メッセージ構造体。 - - リクエスト例: - ```json - {"endpoint":"/run/send_message_box","data":{"id":123,"message":"Hello"}} - ``` - - 成功応答例: - ```json - {"status":200,"endpoint":"/run/send_message_box","result":{"id":123,"original":{"message":"Hello","transliteration":[]},"translations":[{"message":"","transliteration":[]}]}} - ``` - -- /run/typing_message_box — data: null — OSC でタイピング状態を伝える場合に使用。成功例: {"status":200,...} -- /run/stop_typing_message_box — data: null — 停止。 - -- /run/send_text_overlay — data: object — オーバーレイに表示するテキストを更新します。例: {"text":"Hello","lang":"English"} - - 成功応答は送信した data をそのまま返すことが多い。 - -- /run/swap_your_language_and_target_language — data: null — 選択中の入出力言語を入れ替えます。成功例: {"status":200, ...} - - -/run/update_software — data: null — 非同期でアップデート処理を開始します。成功応答: {"status":200, "result": true} -/run/update_cuda_software — data: null — CUDA アップデートを開始します。 - - -/set/enable/transcription_receive — data: null — スピーカー側の転写(受信)を有効化 -/set/disable/transcription_receive — data: null — 無効化 - - -/set/enable/foreground — data: null — フォアグラウンド表示を有効化 -/set/disable/foreground — data: null — 無効化 - -- /get/data/selected_tab_no — data: null — 現在のタブ番号を返す -- /set/data/selected_tab_no — data: int — タブ番号を設定 - -- /get/data/translation_engines — data: null — 使える翻訳エンジン一覧を返す - -- /get/data/selected_translation_engines — data: null — 各タブで選択されている翻訳エンジン(タブ別辞書) - - 例: {"status":200, "endpoint":"/get/data/selected_translation_engines","result":{"0":["CTranslate2"],"1":["CTranslate2"]}} - -- /get/data/selected_your_languages — data: null — 各タブの入力言語設定 - - 例: {"status":200, "endpoint":"/get/data/selected_your_languages","result":{"0":{"language":"English","enable":true}}} - -- /get/data/selected_target_languages — data: null — 各タブの出力言語設定 - - 例: {"status":200, "endpoint":"/get/data/selected_target_languages","result":{"0":{"1":{"language":"Japanese","enable":true}}}} - -- /get/data/selected_transcription_engine — data: null — 現在選択されている転写エンジン - - 例: {"status":200, "endpoint":"/get/data/selected_transcription_engine","result":"Whisper"} - -- /run/send_message_box — data: {"id":..., "message": "..."} — チャット送信を実行(chatMessage を内部呼び出し) -- /run/typing_message_box — data: null — タイピング開始通知(OSC 経由で送信される場合あり) -- /run/stop_typing_message_box — data: null — タイピング停止 - -- /run/send_text_overlay — data: {text settings...} — オーバーレイ用のテキスト表示を更新 - -- /run/swap_your_language_and_target_language — data: null — 入出力言語を入れ替え - -- /run/update_software — data: null — ソフト更新処理をスレッドで開始 -- /run/update_cuda_software — data: null — CUDA 関連更新を開始 - -2) 表示・外観設定 -- /get/data/version — data: null — アプリ版を返す -- /get/data/transparency — data: null — 透過率 -- /set/data/transparency — data: int — 透過率を設定 -- /get/data/ui_scaling — data: null — UI スケール -- /set/data/ui_scaling — data: int -- /get/data/textbox_ui_scaling, /set/data/textbox_ui_scaling -- /get/data/message_box_ratio, /set/data/message_box_ratio -- /get/data/send_message_button_type, /set/data/send_message_button_type -- /get/data/show_resend_button, /set/enable/show_resend_button, /set/disable/show_resend_button -- /get/data/font_family, /set/data/font_family -- /get/data/ui_language, /set/data/ui_language -- /get/data/main_window_geometry, /set/data/main_window_geometry - -3) 計算デバイス関連 -- /get/data/compute_mode — data: null — compute mode -- /get/data/translation_compute_device_list — data: null — 選択可能な翻訳デバイス一覧 -- /get/data/selected_translation_compute_device — data: null -- /set/data/selected_translation_compute_device — data: device descriptor — 選択 -- /get/data/transcription_compute_device_list — same as translation -- /get/data/selected_transcription_compute_device, /set/data/selected_transcription_compute_device - -4) 翻訳設定 -- /get/data/selectable_ctranslate2_weight_type_dict — data: null — 利用可能な ctranslate2 重みの辞書 -- /get/data/ctranslate2_weight_type, /set/data/ctranslate2_weight_type -- /get/data/selected_translation_compute_type, /set/data/selected_translation_compute_type -- /run/download_ctranslate2_weight — data: "weight_type" — 指定した重みをダウンロード(非同期可) -- /get/data/deepl_auth_key — data: null — DeepL API キー(存在すれば返却、セキュリティ上の注意あり) -- /set/data/deepl_auth_key — data: "" — DeepL キーを設定(キー検証あり) -- /delete/data/deepl_auth_key — data: null — DeepL キーを削除 - -- /set/data/selected_translation_engines — data: dict/list — 各タブの翻訳エンジン選択を設定します。 - - 例: {"endpoint":"/set/data/selected_translation_engines","data":{"0":["CTranslate2"]}} - -- /set/data/selected_transcription_engine — data: string — 現在の転写エンジンを設定します。 - - 例: {"endpoint":"/set/data/selected_transcription_engine","data":"Whisper"} - -- /set/enable/main_window_sidebar_compact_mode — data: null — サイドバーをコンパクト表示に設定 - - 例: {"status":200,"endpoint":"/set/enable/main_window_sidebar_compact_mode","result": true} - -- /set/disable/main_window_sidebar_compact_mode — data: null — サイドバーのコンパクト表示を解除 - - 例: {"status":200,"endpoint":"/set/disable/main_window_sidebar_compact_mode","result": false} -- /get/data/convert_message_to_romaji, /set/enable/convert_message_to_romaji, /set/disable/convert_message_to_romaji -- /get/data/convert_message_to_hiragana, /set/enable/convert_message_to_hiragana, /set/disable/convert_message_to_hiragana - -5) トランスクリプション / デバイス -- /get/data/mic_host_list, /get/data/mic_device_list, /get/data/speaker_device_list -- /get/data/auto_mic_select, /set/enable/auto_mic_select, /set/disable/auto_mic_select -- /get/data/selected_mic_host, /set/data/selected_mic_host -- /get/data/selected_mic_device, /set/data/selected_mic_device -- /get/data/mic_threshold, /set/data/mic_threshold -- /get/data/mic_automatic_threshold, /set/enable/mic_automatic_threshold, /set/disable/mic_automatic_threshold -- /get/data/mic_record_timeout, /set/data/mic_record_timeout -- /get/data/mic_phrase_timeout, /set/data/mic_phrase_timeout -- /get/data/mic_max_phrases, /set/data/mic_max_phrases -- /get/data/hotkeys, /set/data/hotkeys -- /get/data/plugins_status, /set/data/plugins_status -- /get/data/mic_avg_logprob, /set/data/mic_avg_logprob -- /get/data/mic_no_speech_prob, /set/data/mic_no_speech_prob -- /set/enable/check_mic_threshold, /set/disable/check_mic_threshold -- /get/data/mic_word_filter, /set/data/mic_word_filter - -6) スピーカー側設定 -- /get/data/auto_speaker_select, /set/enable/auto_speaker_select, /set/disable/auto_speaker_select -- /get/data/selected_speaker_device, /set/data/selected_speaker_device -- /get/data/speaker_threshold, /set/data/speaker_threshold -- /get/data/speaker_automatic_threshold, /set/enable/speaker_automatic_threshold, /set/disable/speaker_automatic_threshold -- /get/data/speaker_record_timeout, /set/data/speaker_record_timeout -- /get/data/speaker_phrase_timeout, /set/data/speaker_phrase_timeout -- /get/data/speaker_max_phrases, /set/data/speaker_max_phrases -- /get/data/speaker_avg_logprob, /set/data/speaker_avg_logprob -- /get/data/speaker_no_speech_prob, /set/data/speaker_no_speech_prob -- /set/enable/check_speaker_threshold, /set/disable/check_speaker_threshold - -7) Whisper / トランスクリプション重み -- /get/data/selectable_whisper_weight_type_dict -- /get/data/whisper_weight_type, /set/data/whisper_weight_type -- /get/data/selected_transcription_compute_type, /set/data/selected_transcription_compute_type -- /run/download_whisper_weight — data: "weight_type" - -8) VR / オーバーレイ -- /get/data/overlay_small_log, /set/enable/overlay_small_log, /set/disable/overlay_small_log -- /get/data/overlay_small_log_settings, /set/data/overlay_small_log_settings -- /get/data/overlay_large_log, /set/enable/overlay_large_log, /set/disable/overlay_large_log -- /get/data/overlay_large_log_settings, /set/data/overlay_large_log_settings -- /get/data/overlay_show_only_translated_messages, /set/enable/overlay_show_only_translated_messages, /set/disable/overlay_show_only_translated_messages - -9) その他設定 -- /get/data/send_message_format_parts, /set/data/send_message_format_parts -- /get/data/received_message_format_parts, /set/data/received_message_format_parts -- /get/data/auto_clear_message_box, /set/enable/auto_clear_message_box, /set/disable/auto_clear_message_box -- /get/data/send_only_translated_messages, /set/enable/send_only_translated_messages, /set/disable/send_only_translated_messages -- /get/data/logger_feature, /set/enable/logger_feature, /set/disable/logger_feature -- /run/open_filepath_logs -- /get/data/vrc_mic_mute_sync, /set/enable/vrc_mic_mute_sync, /set/disable/vrc_mic_mute_sync -- /get/data/send_message_to_vrc, /set/enable/send_message_to_vrc, /set/disable/send_message_to_vrc -- /get/data/send_received_message_to_vrc, /set/enable/send_received_message_to_vrc, /set/disable/send_received_message_to_vrc - -10) WebSocket -- /get/data/websocket_host, /set/data/websocket_host -- /get/data/websocket_port, /set/data/websocket_port -- /get/data/websocket_server, /set/enable/websocket_server, /set/disable/websocket_server - -11) OSC / 高度設定 -- /get/data/osc_ip_address, /set/data/osc_ip_address -- /get/data/osc_port, /set/data/osc_port -- /get/data/notification_vrc_sfx, /set/enable/notification_vrc_sfx, /set/disable/notification_vrc_sfx -- /run/open_filepath_config_file -- /run/feed_watchdog - -挙動メモ / 注意点 -- `data` は受信時に `encodeBase64` が適用される場合があります(バイナリや特殊文字対策)。 -- いくつかのエンドポイントは内部的にバックグラウンドスレッドを立ち上げます(ダウンロード・更新処理・transliteration 等)。 -- 翻訳・転写関連は VRAM OOM を検知すると自動的に関連機能を無効化し、UI に 400 系の run イベントを送信します。API 消費者はこれらの run イベントを監視する必要があります。 - -次の作業 -- `docs/modules/controller.md` に記載した Controller のメソッド詳細と紐付けて、各エンドポイントごとに具体的な request/response のサンプル(body の構造)を追加します。 -### API / メッセージマッピング(詳細) - -このアプリは stdin/stdout を通じた 1 行 JSON メッセージで制御します。内部では `mainloop.py` の `mapping` が受信 endpoint を Controller のメソッドに結び付け、`run_mapping` が非同期通知のエンドポイントを定義します。 - -受信メッセージ(stdin) -```json -{ "endpoint": "/set/data/selected_tab_no", "data": 0 } -``` - -送信メッセージ(stdout) -- 成功: printResponse が次を出力します。 -```json -{ "status": 200, "endpoint": "/get/data/version", "result": "3.2.2" } -``` -- エラー: -```json -{ "status": 400, "endpoint": "/set/data/osc_ip_address", "result": {"message":"Invalid IP address","data":"127.0.0.1"} } -``` - -動作原則 -- `/get/data/*` : Controller の getter を呼び、設定やリストを返す。 -- `/set/data/*` : Controller の setter を呼び、設定を変更して新値を返す。 -- `/run/*` : 非同期アクションや UI ボタンが実行する処理(ダウンロード、更新、送信など)。 -- `mapping` の `"status": False` はロック(423 を返し、要求はキューに戻され再試行される)。 - -表記ルール -- Controller メソッドは `Controller.` の形式で明記。 -- `run events` は Controller が UI に通知する `run_mapping` の `/run/...` エンドポイント名を列挙します。 - -以下は `mainloop.py` の `mapping` に基づいた、主要エンドポイントの詳細(カテゴリ順)。 - -1) メイン操作(チャット/翻訳/転写) - -- Endpoint: `/set/enable/translation` - - Controller: `Controller.setEnableTranslation` - - data: null - - success: {status:200, result: true} - - error example: {status:400, result:{message:"Translation disabled due to VRAM overflow", data: False}} - - run events: `/run/enable_translation` を発行して UI に状態を通知する。 - -- Endpoint: `/set/disable/translation` - - Controller: `Controller.setDisableTranslation` - - data: null - - success: {status:200, result: false} - - run events: `/run/enable_translation` - -- Endpoint: `/set/enable/transcription_send` - - Controller: `Controller.setEnableTranscriptionSend` - - data: null - - success: {status:200, result: true} - - side-effect: `Controller.startThreadingTranscriptionSendMessage` を呼びバックグラウンドで音声転写を開始する。 - - run events: `/run/enable_transcription_send` - -- Endpoint: `/set/disable/transcription_send` - - Controller: `Controller.setDisableTranscriptionSend` - - data: null - - success: {status:200, result: false} - -- Endpoint: `/run/send_message_box` - - Controller: `Controller.sendMessageBox` -> 内部で `Controller.chatMessage` - - data: {"id": <任意>, "message": "..."} - - success example: {status:200, result: {"id":123, "original":{...}, "translations":[...]}} - - run events: 転送先言語や翻訳結果があれば `/run/transcription_send_mic_message` などが発行される。 - -- Endpoint: `/run/send_text_overlay` - - Controller: `Controller.sendTextOverlay` - - data: object (例: {"text":"Hello","lang":"English"}) - - success: echo back the data - - side-effect: オーバーレイ更新(small/large に応じた出力) - -2) 表示 / 外観設定 -- Endpoint: `/get/data/version` - - Controller: `Controller.getVersion` - - data: null - - success: {status:200, result: config.VERSION} - -- Endpoint: `/get/data/transparency` / `/set/data/transparency` - - Controller: `Controller.getTransparency` / `Controller.setTransparency` - - data for set: integer (0-255 等、設定側で検証) - - success example: {status:200, result: } - -(UI スケーリング、textbox スケーリング、font_family, ui_language 等の /get と /set は同様のパターン: Controller の getXXX / setXXX を呼ぶ) - -3) 計算デバイス関連 -- Endpoint: `/get/data/translation_compute_device_list` -> `Controller.getComputeDeviceList` - - data: null - - result: list of device descriptors (構造は `config.SELECTABLE_COMPUTE_DEVICE_LIST` に従う) - -- Endpoint: `/set/data/selected_translation_compute_device` - - Controller: `Controller.setSelectedTranslationComputeDevice` - - data: device descriptor (例: {"name":"cuda:0","type":"gpu"}) - - side-effects: `model.setChangedTranslatorParameters(True)` が呼ばれ、実行時にモデル再ロードが必要な場合がある。 - - success: {status:200, result: selected_device} - -4) 翻訳/重み管理 -- Endpoint: `/get/data/selectable_ctranslate2_weight_type_dict` - - Controller: `Controller.getSelectableCtranslate2WeightTypeDict` - - result: dict mapping weight_type -> bool - -- Endpoint: `/run/download_ctranslate2_weight` - - Controller: `Controller.downloadCtranslate2Weight` - - data: "weight_type" (例: "m2m100_418m") - - behavior: 非同期フラグでスレッド起動可能。進捗は run events `/run/download_progress_ctranslate2_weight` を発行。完了時に `/run/downloaded_ctranslate2_weight`。 - -- Endpoint: `/set/data/deepl_auth_key` - - Controller: `Controller.setDeeplAuthKey` - - data: string (API key) - - behavior: 内部で `model.authenticationTranslatorDeepLAuthKey` を実行して検証。失敗時は 400 を返す。 - -5) トランスクリプション / デバイス -- Endpoint: `/get/data/mic_host_list` -> `Controller.getMicHostList` - - data: null - - result: dict/list of hosts - -- Endpoint: `/set/data/selected_mic_host` -> `Controller.setSelectedMicHost` - - data: host identifier (string) - - side-effects: デフォルトデバイスを `model.getMicDefaultDevice()` で選択し、エネルギーチェックや転写スレッドの再起動が発生する場合がある。 - -- Endpoint: `/set/data/mic_threshold` -> `Controller.setMicThreshold` - - data: integer - - validation: 0 <= value <= config.MAX_MIC_THRESHOLD - - success: {status:200, result: new_value} error: 400 with message and old value - -6) スピーカー関連(受信) -- Endpoint: `/set/data/selected_speaker_device` -> `Controller.setSelectedSpeakerDevice` - - data: device descriptor - - side-effects: スピーカー転写スレッド(ENABLE_CHECK_ENERGY_RECEIVE)を再起動する可能性あり - -7) Whisper / トランスクリプション重み -- Endpoint: `/run/download_whisper_weight` - - Controller: `Controller.downloadWhisperWeight` - - data: "weight_type" - - run events: `/run/download_progress_whisper_weight`, `/run/downloaded_whisper_weight` - -8) オーバーレイ / VR -- Endpoint: `/set/enable/overlay_small_log` -> `Controller.setEnableOverlaySmallLog` - - side-effect: `model.startOverlay()` を呼び、`model.updateOverlaySmallLog` で描画が更新される - -9) WebSocket / OSC / Watchdog -- Endpoint: `/set/data/websocket_host` -> `Controller.setWebSocketHost` - - validation: IP 形式チェック (`isValidIpAddress`) - - if WebSocket server running: attempts to restart server on new host/port (checks availability via `isAvailableWebSocketServer`) - -- Endpoint: `/set/data/osc_ip_address` -> `Controller.setOscIpAddress` - - validation: IP 形式。失敗時は 400 を返す。 - -- Endpoint: `/run/feed_watchdog` -> `Controller.feedWatchdog` - - Controller: `Controller.feedWatchdog` ➜ `model.feedWatchdog()` - -共通的な失敗モード(クライアント実装者向けメモ) -- 無効なパラメータ: 400 と {message,data} を返す。 -- ロック: 423 (Locked endpoint) — UI 側はリトライまたはキュー内での再試行を待つ。 -- 内部エラー: 500 とエラーメッセージ(詳細はログ)を返す。 -- VRAM OOM / モデルエラー: Controller は `model.detectVRAMError` を使い、必要に応じて機能無効化と run イベントで通知する。 - -付録: すぐ使える呼び出し例 -- バージョン取得 -```json -{ "endpoint": "/get/data/version", "data": null } -``` - -- タブ切替 -```json -{ "endpoint": "/set/data/selected_tab_no", "data": 1 } -``` - -- メッセージ送信(チャット) -```json -{ "endpoint": "/run/send_message_box", "data": {"id": 555, "message": "Hello world"} } -``` - -次の作業 -- ① `docs/modules/controller.md` の各メソッドとこの `docs/api.md` を突き合わせ、未記載の `run_mapping` イベントのペイロード例を追加します。 -- ② 軽い品質ゲート(README と runtime 注意の草案作成)を実行します。 - -## エンドポイント別 JSON スキーマ(補完) - -このセクションでは `mainloop.py` の `mapping` に定義された全エンドポイントをパターンごとに整理し、クライアントが送信すべき `request` と期待される `response` の JSON スキーマを明示します。多数のエンドポイントは共通パターンに従うため、パターン定義と代表例でほとんどのケースをカバーしています。 - -共通ルール -- リクエストは必ず 1 行 JSON: {"endpoint": "", "data": }。 -- レスポンスは {"status": , "endpoint": "", "result": } の形式(内部の `printResponse` により出力)。 - -1) /get/data/* パターン(読み取り) -- request.data: null -- response.result: 直ちに返せる JSON 値(数値/文字列/配列/辞書) -- schema(JSON Schema 風の簡易表記): - - request: - { - "endpoint": "/get/data/", - "data": null - } - - response: - { - "status": 200, - "endpoint": "/get/data/", - "result": - } - - 代表例: - - `/get/data/version` → result: string - {"status":200,"endpoint":"/get/data/version","result":"3.2.2"} - - `/get/data/mic_device_list` → result: ["Device 1", "Device 2"] - -2) /set/data/* パターン(書き込み) -- request.data: セッタが期待する型(下に代表的な型を列挙) -- response.result: 新しい値または検証済の値(成功時) -- error: バリデーション失敗時は status 400 と {message,data} - - 共通 request/response: - - request: - { - "endpoint": "/set/data/", - "data": - } - - response (success): - { - "status":200, - "endpoint":"/set/data/", - "result": - } - - response (validation error): - { - "status":400, - "endpoint":"/set/data/", - "result": {"message": "", "data": } - } - - 代表的リクエスト型一覧(多くはこの型いずれか): - - int: `/set/data/selected_tab_no`, `/set/data/transparency`, `/set/data/mic_threshold` など - - string: `/set/data/selected_mic_host`, `/set/data/selected_speaker_device`, `/set/data/deepl_auth_key` など - - dict/object: `/set/data/selected_your_languages`, `/set/data/selected_target_languages`, `/set/data/send_message_format_parts` など - - list: `/set/data/mic_word_filter` など - -3) フラグ切替(enable / disable) - -- 概要: 機能の有効化/無効化を行うエンドポイント群は、実装で定義された具体的なエンドポイント名(例: `/set/enable/translation`, `/set/disable/translation`, `/set/enable/foreground` など)で提供されています。本ドキュメントでは umbrella 的な汎用トークン(`/set/enable` や `/set/disable` 単体)は記載せず、実際に実装で定義されている concrete エンドポイントのみを列挙しています。 - -- 振る舞いの要点: - - リクエストの `data` は通常 `null` です。 - - 成功応答は多くの場合 boolean を返します(例: `{ "status":200, "endpoint":"/set/enable/foreground", "result": true }`)。 - - 条件により有効化/無効化ができない場合は 400 を返し、`{ "message": "...", "data": }` の形で詳細が返されます。 - -具体的なフラグ切替エンドポイントはドキュメント本文の各該当箇所で個別に列挙しています(例: `/set/enable/translation`, `/set/disable/translation`, `/set/enable/transcription_send`, `/set/disable/transcription_send`, `/set/enable/main_window_sidebar_compact_mode`, など)。 - -4) /run/*(アクション・実行系) -- request.data: アクションに依存(例: `/run/send_message_box` は {id, message}) -- response.result: 多くは action の結果(True/False, object)を返す -- 非同期で UI 更新を行う場合は `Controller.run(...)` により `/run/...` 形式の通知が stdout に出力される - - 代表例: - - `/run/send_message_box` - request.data: {"id": , "message": ""} - response.result: { - "id": , - "original": {"message": "", "transliteration": [] }, - "translations": [ {"message":"", "transliteration":[...]}, ... ] - } - - - `/run/download_ctranslate2_weight` - request.data: "" (string) - response.result: true - progress: `/run/download_progress_ctranslate2_weight` -> {"weight_type":"...","progress":0.0..1.0} - complete: `/run/downloaded_ctranslate2_weight` -> "" - -5) WebSocket / OSC / Watchdog 関連 -- `/set/data/websocket_host` : request.data:string(host) → response: {status:200, result: host} または 400 (not available) -- `/set/data/osc_ip_address` : request.data:string(ip) → validation via `isValidIpAddress` → 400 on invalid -- `/run/feed_watchdog`: request.data:null → response: {status:200,result:true} - -6) エラー応答の標準形 -- Validation / domain error : status 400, result: {"message": "<説明>", "data": } -- Locked endpoint: status 423, result: "Locked endpoint"(mainloop が再試行のためキューに戻す) -- Internal error: status 500, result: "" - -7) run events(UI 更新通知)- 参考(主要イベントのみ再掲) -- `/run/connected_network` : bool -- `/run/enable_ai_models` : bool -- `/run/initialization_progress` : int (1..4) - - `/run/transcription_send_mic_message` / `/run/transcription_receive_speaker_message` : オブジェクト(original/translations, see above) - -追加の run イベント(ランタイム検証で未記載と判定されたため追記): - -- `/run/enable_transcription_receive` : bool - - 説明: スピーカー側転写(transcription receive)の有効/無効を UI に通知します。 - -- `/run/transcription_send_mic_message` : object - - payload: 同 `/run/transcription_send_mic_message` の構造(original + translations) - - 説明: マイク側で転写結果が生成され、UI に送信するための通知です。 - -- `/run/transcription_receive_speaker_message` : object - - payload: 同 `/run/transcription_receive_speaker_message` の構造 - - 説明: スピーカー側で転写結果が生成されたときに発行されます。 - -- `/run/error_transcription_mic_vram_overflow` : object (400) - - payload: {"message": "VRAM out of memory during mic transcription", "data": ""} - - 説明: マイク転写中に VRAM OOM が発生した際に通知します。 - -- `/run/error_transcription_speaker_vram_overflow` : object (400) - - payload: {"message": "VRAM out of memory during speaker transcription", "data": ""} - - 説明: スピーカー転写中に VRAM OOM が発生した際に通知します。 - -補遺: 全エンドポイント一覧と期待型の速見表 -- `/get/data/*` : data=null -> result: primitive|array|object -- `/set/data/*` : data: 型指定 (int|string|dict|list) -> result: new value or validation error -- `/set/enable/*` `/set/disable/*` : data=null -> result: bool -- `/run/*` : data: action-specific -> result: action result object / bool - -ファイルの更新履歴 -- このドキュメントは `mainloop.py` の `mapping` と `controller.py` の `run_mapping` を参照して作成しました。将来的にエンドポイントを追加した場合は同じ箇所を参照して本ドキュメントを更新してください。 - ----- - -完了: エンドポイント別スキーマの補完を行いました。次は軽い品質ゲート(lint/typecheck)の実行を提案します。 - diff --git a/src-python/docs/architecture.md b/src-python/docs/architecture.md deleted file mode 100644 index 8fcf20e9..00000000 --- a/src-python/docs/architecture.md +++ /dev/null @@ -1,21 +0,0 @@ -# アーキテクチャ概観 - -VRCT(src-python)は、ローカル音声キャプチャ・音声認識・翻訳・VR 表示・OSC/ WebSocket 連携を統合するアプリケーションです。主な責務は次の通り。 - -- device_manager: オーディオ入出力デバイスの発見、監視、コールバック通知。 -- transcription (models/transcription/*): マイク/スピーカーからの音声取得、認識(Google/Whisper)、議事録管理。 -- translation (models/translation/*): 翻訳エンジン(DeepL/API、CTranslate2、Google など)管理と実行。 -- overlay (models/overlay/*): VR オーバーレイの画像生成と OpenVR を使った描画管理。 -- osc (models/osc/osc.py): VRChat 等との OSC(および OSCQuery)でのやり取り。 -- websocket (models/websocket/*): 外部クライアント向け WebSocket ブロードキャスト。 -- model.py: 高レベルなファサード。各機能のインスタンス化とランタイム操作。 -- controller.py: UI/外部メッセージを受け、config を更新・機能を起動するコマンド実行層。 -- mainloop.py: stdin 経由のコマンド受付ループとマッピング定義。GUI からの操作を受ける想定。 -- utils.py: ロギング、ネットワークチェック、デバイス/計算デバイスタイプ判定などのユーティリティ。 -- config.py: シングルトン設定ストア。アプリ起動中に共有して使うすべての設定値。 - -設計上のポイント: -- シングルトン/ファサード: `model` と `config` はシングルトンでグローバルに参照される。これにより UI 層(Controller)と低レイヤ(models/*)の橋渡しを行う。 -- 非同期処理: デバイス監視、音声録音・認識、WebSocket サーバー、Overlay のループはそれぞれ別スレッド/非同期ループで実行される。 -- フォールバック: 翻訳はまず選択されたエンジンを使い、失敗時に CTranslate2 にフォールバックする仕組みがある。 -- VRAM エラー検出: Whisper / CTranslate2 等で VRAM 不足が起きた場合、特殊なエラー検出を行い翻訳/音声機能を無効化して回復を試みる。 diff --git a/src-python/docs/config.md b/src-python/docs/config.md new file mode 100644 index 00000000..7d7d3f6e --- /dev/null +++ b/src-python/docs/config.md @@ -0,0 +1,433 @@ +# config.py ドキュメント + +## 概要 +`config.py` は、アプリケーションの全設定を一元管理するシングルトンクラス `Config` を提供するモジュール。設定値の読み込み・保存・検証を行い、JSON ファイルへの永続化をデバウンス機能付きで実現する。 + +## 主要機能 +- シングルトンパターンによる設定の一元管理 +- JSONファイル (`config.json`) からの設定読み込みと自動保存 +- デバウンス機能による書き込み最適化(デフォルト2秒) +- 読み取り専用プロパティと読み書き可能プロパティの明確な分離 +- オプショナルモジュールのセーフガードインポート(環境依存の依存関係を安全に処理) +- プロパティセッター内での型チェックとバリデーション +- `@json_serializable` デコレータによる永続化対象プロパティの管理 + +## アーキテクチャ + +### デザインパターン +- **シングルトンパターン**: `__new__` メソッドで単一インスタンスを保証 +- **プロパティパターン**: getter/setter による型安全なアクセス制御 + +### 設定の分類 +1. **読み取り専用設定** (Read Only) + - アプリケーションバージョン、パス、URL、定数など + - プロパティのみ(setter なし) + +2. **ランタイム設定** (Read Write) + - 機能の有効/無効フラグ + - 実行時の状態管理 + - JSON保存されない一時的な設定 + +3. **永続化設定** (Save Json Data) + - ユーザー設定、デバイス選択、UI設定など + - `@json_serializable` デコレータでマーク + - `saveConfig()` 経由で自動保存 + +## 使用方法 + +### 基本的な使い方 + +```python +from config import config + +# 設定値の取得(読み取り専用) +version = config.VERSION +app_path = config.PATH_LOCAL + +# 設定値の取得(読み書き可能) +current_tab = config.SELECTED_TAB_NO +mic_threshold = config.MIC_THRESHOLD + +# 設定値の変更(自動保存される) +config.SELECTED_TAB_NO = "2" +config.MIC_THRESHOLD = 500 +config.TRANSPARENCY = 80 + +# 即座に保存する場合 +config.MAIN_WINDOW_GEOMETRY = {"x_pos": 100, "y_pos": 200, "width": 900, "height": 700} +# MESSAGE_BOX_RATIO と MAIN_WINDOW_GEOMETRY は immediate_save=True で即座に保存 +``` + +### デバウンス保存の仕組み + +```python +# 通常の設定変更: 2秒後に保存 +config.UI_LANGUAGE = "ja" +config.FONT_FAMILY = "Arial" # 前のタイマーがキャンセルされ、新たに2秒のタイマー開始 + +# 即座保存が必要な設定: デバウンスなし +config.MESSAGE_BOX_RATIO = 15 # 即座にファイル書き込み +``` + +## 動作環境・依存関係 + +### 必須依存 +- Python 3.10以上(match-case 構文使用) +- `torch`: CUDA利用可否の判定に使用 +- `threading`: デバウンスタイマー用 + +### オプション依存(セーフガード付き) +以下のモジュールはインポートに失敗しても動作する: +- `device_manager`: デバイス管理(マイク/スピーカー) +- `models.translation.translation_languages`: 翻訳言語リスト +- `models.translation.translation_utils`: CTranslate2 重みリスト +- `models.transcription.transcription_languages`: 音声認識言語リスト +- `models.transcription.transcription_whisper`: Whisper モデルリスト + +### プロジェクト内依存 +- `utils`: エラーロギング、辞書構造検証、計算デバイスリスト取得 + +## ファイル構成 + +### 主要クラス: `Config` + +#### クラス属性 +```python +_instance: Config | None # シングルトンインスタンス +_config_data: Dict[str, Any] # JSON保存用データ +_timer: Optional[threading.Timer] # デバウンスタイマー +_debounce_time: int = 2 # デバウンス時間(秒) +``` + +#### 主要メソッド + +**初期化・保存** +- `__new__(cls)`: シングルトンインスタンス生成・初期化 +- `init_config()`: デフォルト値の設定 +- `load_config()`: JSONファイルから設定読み込み +- `saveConfig(key, value, immediate_save=False)`: 設定の保存(デバウンス付き) +- `saveConfigToFile()`: JSONファイルへの即座書き込み + +**デコレータ** +- `@json_serializable(var_name)`: 永続化対象プロパティのマーク + +### 設定プロパティ一覧 + +#### 読み取り専用設定(23項目) + +| プロパティ名 | 型 | 説明 | デフォルト値 | +|------------|----|----|------------| +| `VERSION` | str | アプリケーションバージョン | "3.3.0" | +| `PATH_LOCAL` | str | アプリケーションローカルパス | 実行時決定 | +| `PATH_CONFIG` | str | 設定ファイルパス | `{PATH_LOCAL}/config.json` | +| `PATH_LOGS` | str | ログディレクトリパス | `{PATH_LOCAL}/logs` | +| `GITHUB_URL` | str | GitHub API URL | リポジトリURL | +| `UPDATER_URL` | str | アップデーターAPIの URL | アップデーターURL | +| `BOOTH_URL` | str | Booth 販売ページURL | Booth URL | +| `DOCUMENTS_URL` | str | ドキュメントURL | Notion URL | +| `DEEPL_AUTH_KEY_PAGE_URL` | str | DeepL認証キー取得ページ | DeepL URL | +| `MAX_MIC_THRESHOLD` | int | マイクしきい値の最大値 | 2000 | +| `MAX_SPEAKER_THRESHOLD` | int | スピーカーしきい値の最大値 | 4000 | +| `WATCHDOG_TIMEOUT` | int | Watchdog タイムアウト(秒) | 60 | +| `WATCHDOG_INTERVAL` | int | Watchdog チェック間隔(秒) | 20 | +| `SELECTABLE_TAB_NO_LIST` | List[str] | 選択可能タブ番号 | ["1", "2", "3"] | +| `SELECTED_TAB_TARGET_LANGUAGES_NO_LIST` | List[str] | ターゲット言語タブ番号 | ["1", "2", "3"] | +| `SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST` | List[str] | CTranslate2重みタイプリスト | 動的取得 | +| `SELECTABLE_WHISPER_WEIGHT_TYPE_LIST` | List[str] | Whisper重みタイプリスト | 動的取得 | +| `SELECTABLE_TRANSLATION_ENGINE_LIST` | List[str] | 翻訳エンジンリスト | 動的取得 | +| `SELECTABLE_TRANSCRIPTION_ENGINE_LIST` | List[str] | 音声認識エンジンリスト | 動的取得 | +| `SELECTABLE_UI_LANGUAGE_LIST` | List[str] | UI言語リスト | ["en", "ja", "ko", "zh-Hant", "zh-Hans"] | +| `COMPUTE_MODE` | str | 計算モード | "cuda" or "cpu" | +| `SELECTABLE_COMPUTE_DEVICE_LIST` | List[Dict] | 選択可能な計算デバイスリスト | 動的取得 | +| `SEND_MESSAGE_BUTTON_TYPE_LIST` | List[str] | 送信ボタンタイプリスト | ["show", "hide", "show_and_disable_enter_key"] | + +#### ランタイム設定(10項目) + +| プロパティ名 | 型 | 説明 | デフォルト値 | JSON保存 | +|------------|----|----|-----------|---------| +| `ENABLE_TRANSLATION` | bool | 翻訳機能有効フラグ | False | なし | +| `ENABLE_TRANSCRIPTION_SEND` | bool | 送信音声認識有効フラグ | False | なし | +| `ENABLE_TRANSCRIPTION_RECEIVE` | bool | 受信音声認識有効フラグ | False | なし | +| `ENABLE_FOREGROUND` | bool | フォアグラウンド有効フラグ | False | なし | +| `ENABLE_CHECK_ENERGY_SEND` | bool | 送信エネルギーチェック有効 | False | なし | +| `ENABLE_CHECK_ENERGY_RECEIVE` | bool | 受信エネルギーチェック有効 | False | なし | +| `SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT` | Dict[str, bool] | CTranslate2重み状態辞書 | {} | なし | +| `SELECTABLE_WHISPER_WEIGHT_TYPE_DICT` | Dict[str, bool] | Whisper重み状態辞書 | {} | なし | +| `SELECTABLE_TRANSLATION_ENGINE_STATUS` | Dict[str, bool] | 翻訳エンジン状態辞書 | {} | なし | +| `SELECTABLE_TRANSCRIPTION_ENGINE_STATUS` | Dict[str, bool] | 音声認識エンジン状態辞書 | {} | なし | + +#### 永続化設定(60項目以上) + +**メインウィンドウ設定** +- `SELECTED_TAB_NO`: 選択中のタブ番号 +- `SELECTED_TRANSLATION_ENGINES`: タブごとの翻訳エンジン選択 +- `SELECTED_YOUR_LANGUAGES`: タブごとの入力言語設定 +- `SELECTED_TARGET_LANGUAGES`: タブごとのターゲット言語設定 +- `SELECTED_TRANSCRIPTION_ENGINE`: 音声認識エンジン +- `CONVERT_MESSAGE_TO_ROMAJI`: ローマ字変換有効フラグ +- `CONVERT_MESSAGE_TO_HIRAGANA`: ひらがな変換有効フラグ +- `MAIN_WINDOW_SIDEBAR_COMPACT_MODE`: サイドバーコンパクトモード +- `SEND_MESSAGE_FORMAT_PARTS`: 送信メッセージフォーマット +- `RECEIVED_MESSAGE_FORMAT_PARTS`: 受信メッセージフォーマット + +**UIウィンドウ設定** +- `TRANSPARENCY`: ウィンドウ透明度(0-100) +- `UI_SCALING`: UIスケーリング(%) +- `TEXTBOX_UI_SCALING`: テキストボックススケーリング(%) +- `MESSAGE_BOX_RATIO`: メッセージボックス比率(即座保存) +- `SEND_MESSAGE_BUTTON_TYPE`: 送信ボタンタイプ +- `SHOW_RESEND_BUTTON`: 再送信ボタン表示フラグ +- `FONT_FAMILY`: フォントファミリー +- `UI_LANGUAGE`: UI言語 +- `MAIN_WINDOW_GEOMETRY`: ウィンドウ位置・サイズ(即座保存) + +**マイク設定** +- `AUTO_MIC_SELECT`: 自動マイク選択 +- `SELECTED_MIC_HOST`: 選択されたマイクホスト +- `SELECTED_MIC_DEVICE`: 選択されたマイクデバイス +- `MIC_THRESHOLD`: マイクしきい値 +- `MIC_AUTOMATIC_THRESHOLD`: 自動しきい値調整 +- `MIC_RECORD_TIMEOUT`: 録音タイムアウト(秒) +- `MIC_PHRASE_TIMEOUT`: フレーズタイムアウト(秒) +- `MIC_MAX_PHRASES`: 最大フレーズ数 +- `MIC_WORD_FILTER`: ワードフィルターリスト +- `MIC_AVG_LOGPROB`: 平均対数確率しきい値 +- `MIC_NO_SPEECH_PROB`: 無音確率しきい値 + +**スピーカー設定** +- `AUTO_SPEAKER_SELECT`: 自動スピーカー選択 +- `SELECTED_SPEAKER_DEVICE`: 選択されたスピーカーデバイス +- `SPEAKER_THRESHOLD`: スピーカーしきい値 +- `SPEAKER_AUTOMATIC_THRESHOLD`: 自動しきい値調整 +- `SPEAKER_RECORD_TIMEOUT`: 録音タイムアウト(秒) +- `SPEAKER_PHRASE_TIMEOUT`: フレーズタイムアウト(秒) +- `SPEAKER_MAX_PHRASES`: 最大フレーズ数 +- `SPEAKER_AVG_LOGPROB`: 平均対数確率しきい値 +- `SPEAKER_NO_SPEECH_PROB`: 無音確率しきい値 + +**モデル設定** +- `SELECTED_TRANSLATION_COMPUTE_DEVICE`: 翻訳計算デバイス +- `SELECTED_TRANSCRIPTION_COMPUTE_DEVICE`: 音声認識計算デバイス +- `CTRANSLATE2_WEIGHT_TYPE`: CTranslate2重みタイプ +- `SELECTED_TRANSLATION_COMPUTE_TYPE`: 翻訳計算タイプ +- `WHISPER_WEIGHT_TYPE`: Whisper重みタイプ +- `SELECTED_TRANSCRIPTION_COMPUTE_TYPE`: 音声認識計算タイプ + +**通信設定** +- `OSC_IP_ADDRESS`: OSC IPアドレス(デフォルト: "127.0.0.1") +- `OSC_PORT`: OSCポート(デフォルト: 9000) +- `AUTH_KEYS`: 認証キー辞書(DeepL API等) +- `WEBSOCKET_HOST`: WebSocketホスト +- `WEBSOCKET_PORT`: WebSocketポート +- `WEBSOCKET_SERVER`: WebSocketサーバー有効フラグ(非永続化) + +**オーバーレイ設定** +- `OVERLAY_SMALL_LOG`: 小ログオーバーレイ有効 +- `OVERLAY_SMALL_LOG_SETTINGS`: 小ログオーバーレイ設定(位置、回転、表示時間等) +- `OVERLAY_LARGE_LOG`: 大ログオーバーレイ有効 +- `OVERLAY_LARGE_LOG_SETTINGS`: 大ログオーバーレイ設定 +- `OVERLAY_SHOW_ONLY_TRANSLATED_MESSAGES`: 翻訳メッセージのみ表示 + +**その他設定** +- `HOTKEYS`: ホットキー設定辞書(即座保存) +- `PLUGINS_STATUS`: プラグイン状態リスト(即座保存) +- `USE_EXCLUDE_WORDS`: 除外ワード機能使用フラグ +- `AUTO_CLEAR_MESSAGE_BOX`: メッセージボックス自動クリア +- `SEND_ONLY_TRANSLATED_MESSAGES`: 翻訳メッセージのみ送信 +- `SEND_MESSAGE_TO_VRC`: VRChatへメッセージ送信 +- `SEND_RECEIVED_MESSAGE_TO_VRC`: 受信メッセージをVRChatへ送信 +- `LOGGER_FEATURE`: ロガー機能有効 +- `VRC_MIC_MUTE_SYNC`: VRChatマイクミュート同期 +- `NOTIFICATION_VRC_SFX`: VRChat通知効果音 + +## 内部実装の詳細 + +### デバウンス保存の実装 + +```python +def saveConfig(self, key: str, value: Any, immediate_save: bool = False) -> None: + self._config_data[key] = value + + # 既存のタイマーをキャンセル + if isinstance(self._timer, threading.Timer) and self._timer.is_alive(): + self._timer.cancel() + + if immediate_save: + self.saveConfigToFile() + else: + # 2秒後に保存するタイマーをセット + self._timer = threading.Timer(self._debounce_time, self.saveConfigToFile) + self._timer.daemon = True + self._timer.start() +``` + +### プロパティのバリデーション例 + +```python +@SELECTED_TAB_NO.setter +def SELECTED_TAB_NO(self, value): + if isinstance(value, str): + if value in self.SELECTABLE_TAB_NO_LIST: + self._SELECTED_TAB_NO = value + self.saveConfig(inspect.currentframe().f_code.co_name, value) +``` + +各setterは以下のパターンを実装: +1. 型チェック (`isinstance`) +2. 値の範囲・有効性チェック +3. 内部変数への代入 +4. `saveConfig` 呼び出し(永続化対象の場合) + +### メッセージフォーマット構造 + +```python +{ + "message": { + "prefix": "", # メッセージ前置文字列 + "suffix": "" # メッセージ後置文字列 + }, + "separator": "\n", # メッセージと翻訳の区切り + "translation": { + "prefix": "", # 翻訳前置文字列 + "separator": "\n", # 複数翻訳の区切り + "suffix": "" # 翻訳後置文字列 + }, + "translation_first": False # 翻訳を先に表示するか +} +``` + +### オーバーレイ設定構造 + +```python +{ + "x_pos": 0.0, # X座標 + "y_pos": 0.0, # Y座標 + "z_pos": 0.0, # Z座標 + "x_rotation": 0.0, # X軸回転 + "y_rotation": 0.0, # Y軸回転 + "z_rotation": 0.0, # Z軸回転 + "display_duration": 5, # 表示時間(秒) + "fadeout_duration": 2, # フェードアウト時間(秒) + "opacity": 1.0, # 不透明度(0.0-1.0) + "ui_scaling": 1.0, # UIスケーリング + "tracker": "HMD" # トラッカー ("HMD", "LeftHand", "RightHand") +} +``` + +## エラーハンドリング + +### セーフガードインポート +```python +try: + from device_manager import device_manager +except Exception: + device_manager = None # フォールバック値 +``` + +全ての外部モジュールインポートはtry-exceptでラップされており、インポート失敗時でも `Config` クラスは正常に動作する。 + +### 初期化エラー +```python +def __new__(cls): + if cls._instance is None: + cls._instance = super(Config, cls).__new__(cls) + try: + cls._instance.init_config() + except Exception: + errorLogging() # エラーをログに記録 + try: + cls._instance.load_config() + except Exception: + errorLogging() + return cls._instance +``` + +初期化とロード処理はそれぞれ独立してエラーハンドリングされる。 + +### 設定ロード時のエラー +```python +for key, value in self._config_data.items(): + try: + setattr(self, key, value) + except Exception: + errorLogging() # 個別設定の読み込み失敗は継続 +``` + +JSONから読み込んだ設定のうち、不正な値があっても他の設定の読み込みは継続される。 + +## パフォーマンス考慮事項 + +1. **デバウンス保存**: 頻繁な設定変更時にI/Oを削減 +2. **遅延初期化**: オプションモジュールは必要時のみロード +3. **シングルトン**: 設定オブジェクトの複製を防止 +4. **デーモンスレッド**: タイマースレッドはメインスレッド終了時に自動終了 + +## セキュリティ考慮事項 + +1. **認証キー**: `AUTH_KEYS` に格納される外部APIキーは平文でJSON保存される +2. **パス検証**: IP アドレスは `isValidIpAddress` でバリデーション +3. **型安全性**: 全てのセッターで型チェック実施 + +## テスト推奨事項 + +### 単体テスト +```python +def test_config_singleton(): + config1 = Config() + config2 = Config() + assert config1 is config2 + +def test_debounce_save(): + config.UI_LANGUAGE = "ja" + time.sleep(1) + config.UI_LANGUAGE = "en" + # 2秒以内の変更は1回のみ保存される + time.sleep(2.5) + # ここで保存完了 +``` + +### バリデーションテスト +```python +def test_invalid_tab_no(): + config.SELECTED_TAB_NO = "invalid" # 無視される + assert config.SELECTED_TAB_NO != "invalid" +``` + +### オプション依存のテスト +```python +def test_missing_device_manager(): + # device_manager が None でも動作すること + assert config.SELECTABLE_COMPUTE_DEVICE_LIST is not None +``` + +## マイグレーション + +### 設定ファイルのバージョンアップ +`load_config()` は存在しないキーを無視し、`init_config()` のデフォルト値を使用する。新しいバージョンでキーが追加された場合: + +1. 既存キーはJSONから読み込まれる +2. 新規キーは `init_config()` のデフォルト値が使用される +3. 次回保存時に全てのキーがJSON に書き込まれる + +## 制限事項 + +1. **マルチプロセス**: シングルトンはプロセス単位。マルチプロセス環境では各プロセスが独立したインスタンスを持つ +2. **スレッドセーフティ**: プロパティアクセス自体はスレッドセーフではない(保存タイマーのみスレッド対応) +3. **循環参照**: `device_manager` と `config` 間の循環参照に注意 +4. **JSON制限**: JSON にシリアライズ可能な型のみ保存可能 + +## ライセンス +プロジェクトのルートディレクトリの `LICENSE` ファイルを参照 + +## 関連ドキュメント +- `controller.md`: Controller クラスの設定使用方法 +- `mainloop.md`: メインループでの設定参照 +- `仕様書.md`: 全体仕様 +- `設計書.md`: システム設計 + +## 変更履歴 + +### v3.3.0 +- 現行バージョン +- WebSocket サーバー設定追加 +- オーバーレイ設定の拡張 diff --git a/src-python/docs/controller.md b/src-python/docs/controller.md new file mode 100644 index 00000000..d27ebb08 --- /dev/null +++ b/src-python/docs/controller.md @@ -0,0 +1,1225 @@ +# controller.py 設計書 + +## 概要 + +`controller.py` は VRCT アプリケーションのビジネスロジック層であり、フロントエンド(UI)とバックエンド(Model)の間の制御フローを担当する。音声認識、翻訳、OSC通信、オーバーレイ表示など、VRCT の全機能の調整役として動作し、各種設定の取得・更新、デバイス管理、エラーハンドリングを提供する。 + +## アーキテクチャ上の位置づけ + +``` +┌─────────────┐ +│ Frontend │ (Tauri/React) +│ (UI Layer) │ +└──────┬──────┘ + │ JSON-RPC (stdin/stdout) +┌──────▼──────┐ +│ mainloop.py │ (Communication Layer) +└──────┬──────┘ + │ Function Calls +┌──────▼──────┐ +│controller.py│ ◄── このファイル +└──────┬──────┘ + │ Facade Pattern +┌──────▼──────┐ +│ model.py │ (Business Logic Facade) +└──────┬──────┘ + │ +┌──────▼──────┐ +│ Subsystems │ (transcription, translation, osc, overlay, etc.) +└─────────────┘ +``` + +## 主要コンポーネント + +### 1. Controllerクラス + +#### コンストラクタ `__init__()` + +**責務:** Controller インスタンスの初期化と依存関係のセットアップ + +**初期化処理:** +1. **マッピング辞書の初期化:** + - `init_mapping`: 初期化時に実行するエンドポイント群 + - `run_mapping`: フロントエンドへの通知用エンドポイント +2. **コールバック関数の設定:** + - `run`: フロントエンドへの通知を送信する関数(デフォルトは no-op) +3. **Model の初期化:** + - `model.init()` を呼び出し、サブシステムを準備 + - 失敗時は `errorLogging()` でログ記録して継続 +4. **デバイスアクセス状態:** + - `device_access_status`: デバイスへの排他アクセス制御用フラグ + +**型ヒント:** +```python +self.init_mapping: dict +self.run_mapping: dict +self.run: Callable[[int, str, Any], None] +self.device_access_status: bool +``` + +#### セットアップメソッド + +##### `setInitMapping(init_mapping: dict) -> None` +初期化時に実行するエンドポイントマッピングを設定。`mainloop.py` から呼び出される。 + +##### `setRunMapping(run_mapping: dict) -> None` +フロントエンド通知用のエンドポイントマッピングを設定。 + +##### `setRun(run: Callable[[int, str, Any], None]) -> None` +フロントエンドへの通知関数を設定。`mainloop.py` の `printResponse()` ラッパーが渡される。 + +#### ヘルパーメソッド + +##### `_is_overlay_available() -> bool` +オーバーレイ機能が利用可能かを安全にチェック。Model が未初期化の場合の `AttributeError` を回避。 + +**実装:** +```python +try: + overlay = getattr(model, "overlay", None) + return overlay is not None and getattr(overlay, "initialized", False) +except Exception: + errorLogging() + return False +``` + +--- + +### 2. 通知メソッド(Response Functions) + +フロントエンドに状態変化を通知するメソッド群。すべて `self.run()` を介して JSON を stdout に送信。 + +#### ネットワーク関連 + +##### `connectedNetwork() -> None` +ネットワーク接続を検出したことを通知。 + +##### `disconnectedNetwork() -> None` +ネットワーク切断を検出したことを通知。 + +#### AI モデル関連 + +##### `enableAiModels() -> None` +AI モデル(CTranslate2/Whisper)が利用可能であることを通知。 + +##### `disableAiModels() -> None` +AI モデルが利用不可(ダウンロード失敗等)であることを通知。 + +#### デバイス管理関連 + +##### `updateMicHostList() -> None` +マイクホスト一覧(MME/WASAPI等)を更新。 + +##### `updateMicDeviceList() -> None` +マイクデバイス一覧を更新。 + +##### `updateSpeakerDeviceList() -> None` +スピーカーデバイス一覧を更新。 + +##### `updateSelectedMicDevice(host: str, device: str) -> None` +選択されたマイクデバイスを通知。自動デバイス選択時に使用。 + +##### `updateSelectedSpeakerDevice(device: str) -> None` +選択されたスピーカーデバイスを通知。 + +#### エネルギーレベル通知 + +##### `progressBarMicEnergy(energy: Union[bool, int]) -> None` +マイクの音量レベルを通知。`False` の場合はデバイスエラーを送信。 + +##### `progressBarSpeakerEnergy(energy: Union[bool, int]) -> None` +スピーカーの音量レベルを通知。 + +#### 設定同期 + +##### `updateConfigSettings() -> None` +初期化完了時に全設定値をフロントエンドに送信。`init_mapping` の全エンドポイントを実行。 + +--- + +### 3. デバイス制御メソッド + +#### 再起動系 + +##### `restartAccessMicDevices() -> None` +マイクアクセスを再起動。以下の条件で各機能を開始: +- `config.ENABLE_TRANSCRIPTION_SEND` が True: 音声認識開始 +- `config.ENABLE_CHECK_ENERGY_SEND` が True: 音量監視開始 + +##### `restartAccessSpeakerDevices() -> None` +スピーカーアクセスを再起動。 + +#### 停止系 + +##### `stopAccessMicDevices() -> None` +マイク関連機能を停止。 + +##### `stopAccessSpeakerDevices() -> None` +スピーカー関連機能を停止。 + +**使用場面:** +- デバイス変更時 +- 自動デバイス選択によるデバイス切り替え時 +- アプリケーション終了時 + +--- + +### 4. メッセージ処理メソッド + +#### `micMessage(result: dict) -> None` + +**責務:** マイク音声認識結果の処理と配信 + +**処理フロー:** +1. **結果の検証:** + - `result["text"]` と `result["language"]` を取得 + - `False` の場合はデバイスエラーを通知して終了 +2. **フィルタリング:** + - `model.checkKeywords()`: 禁止ワードチェック + - `model.detectRepeatSendMessage()`: 重複メッセージチェック +3. **翻訳処理:** + - `config.ENABLE_TRANSLATION` が True の場合: + - `model.getInputTranslate()` で翻訳実行 + - 翻訳エンジンエラー時は CTranslate2 に切り替え + - VRAM不足エラー時は翻訳機能を無効化 +4. **音訳処理:** + - `config.CONVERT_MESSAGE_TO_HIRAGANA/ROMAJI` が True の場合: + - `model.convertMessageToTransliteration()` で変換 +5. **配信処理:** + - **VRChat OSC:** `config.SEND_MESSAGE_TO_VRC` が True の場合 + - `messageFormatter()` でフォーマット + - `model.oscSendMessage()` で送信 + - **UI通知:** `self.run()` で transcription_mic エンドポイントに通知 + - **オーバーレイ:** `config.OVERLAY_LARGE_LOG` が True の場合 + - `model.createOverlayImageLargeLog()` で画像生成 + - `model.updateOverlayLargeLog()` で表示更新 + - **WebSocket:** サーバーが起動中の場合 + - `model.websocketSendMessage()` でブロードキャスト + - **ログファイル:** `config.LOGGER_FEATURE` が True の場合 + +**VRAM エラーハンドリング:** +```python +try: + translation, success = model.getInputTranslate(message, source_language=language) +except Exception as e: + is_vram_error, error_message = model.detectVRAMError(e) + if is_vram_error: + # 翻訳機能を無効化 + self.setDisableTranslation() + self.run(400, self.run_mapping["error_translation_mic_vram_overflow"], {...}) + return +``` + +#### `speakerMessage(result: dict) -> None` + +**責務:** スピーカー音声認識結果の処理と配信 + +**処理フロー:** `micMessage()` と同様だが、以下の違いがある: +- **オーバーレイ:** + - Small Log: 受信メッセージ用の小さなログウィンドウ + - Large Log: 送受信両方を表示するログウィンドウ +- **OSC送信:** `config.SEND_RECEIVED_MESSAGE_TO_VRC` の設定に依存 +- **翻訳:** `model.getOutputTranslate()` を使用(受信メッセージ用) + +#### `chatMessage(data: dict) -> dict` + +**責務:** UI のチャットボックスからのメッセージ処理 + +**パラメータ:** +- `data["id"]`: メッセージ ID(UI でのレスポンスマッピング用) +- `data["message"]`: 送信メッセージ + +**特殊処理:** +- **除外ワード処理:** + - `config.USE_EXCLUDE_WORDS` が True の場合 + - `replaceExclamationsWithRandom()`: `![word]` を一時的なトークンに置換 + - 翻訳後に `restoreText()` で復元 + - 最終メッセージから `![...]` を削除 +- **同期レスポンス:** + - 他のメッセージ処理と異なり、結果を `dict` で返却 + - UI が翻訳結果を待機する必要があるため + +**レスポンス形式:** +```python +{ + "status": 200, + "result": { + "id": "msg-123", + "original": { + "message": "Hello", + "transliteration": ["he", "ro"] + }, + "translations": [ + { + "message": "こんにちは", + "transliteration": ["ko", "n", "ni", "chi", "wa"] + } + ] + } +} +``` + +--- + +### 5. メッセージフォーマット + +#### `messageFormatter(format_type: str, translation: list, message: str) -> str` + +**責務:** OSC 送信用メッセージの整形 + +**パラメータ:** +- `format_type`: "SEND" または "RECEIVED" +- `translation`: 翻訳結果のリスト +- `message`: 元のメッセージ + +**処理ロジック:** +1. フォーマット設定を取得: + - `config.SEND_MESSAGE_FORMAT_PARTS` または `config.RECEIVED_MESSAGE_FORMAT_PARTS` +2. 各部分を構築: + - `message_part`: prefix + message + suffix + - `translation_part`: prefix + separator.join(translation) + suffix +3. 組み合わせ: + - 両方存在: `translation_first` の設定に応じて順序決定 + - 翻訳のみ: translation_part のみ + - メッセージのみ: message_part のみ + +**設定例:** +```python +config.SEND_MESSAGE_FORMAT_PARTS = { + "message": {"prefix": "[", "suffix": "] "}, + "translation": {"prefix": "", "suffix": "", "separator": " / "}, + "translation_first": False, + "separator": "" +} +# 出力例: [Hello] こんにちは / 你好 +``` + +--- + +### 6. 除外ワード処理 + +#### `replaceExclamationsWithRandom(text: str) -> Tuple[str, dict]` + +**責務:** 翻訳対象外の単語を保護 + +**処理:** +1. `![word]` パターンを検出 +2. 各マッチを `$` に置換(4096から連番) +3. 置換マップを辞書で返却 + +**用途:** 固有名詞や翻訳不要な単語を保護 + +#### `restoreText(escaped_text: str, escape_dict: dict) -> str` + +**責務:** 翻訳後のテキストに元の単語を復元 + +**処理:** 正規表現で `$` を検出し、元の単語に置換(大文字小文字を無視) + +#### `removeExclamations(text: str) -> str` + +**責務:** 最終メッセージから `![...]` マーカーを削除 + +**処理:** `![word]` を `word` に置換 + +--- + +### 7. 設定取得・更新メソッド(GET/SET) + +Controller には約200個の設定項目に対する getter/setter が定義されている。以下、代表的なパターンを示す。 + +#### パターン1: 単純な設定値 + +```python +@staticmethod +def getTransparency(*args, **kwargs) -> dict: + return {"status": 200, "result": config.TRANSPARENCY} + +@staticmethod +def setTransparency(data, *args, **kwargs) -> dict: + config.TRANSPARENCY = int(data) + return {"status": 200, "result": config.TRANSPARENCY} +``` + +#### パターン2: 有効/無効の切り替え + +```python +@staticmethod +def getOverlaySmallLog(*args, **kwargs) -> dict: + return {"status": 200, "result": config.OVERLAY_SMALL_LOG} + +@staticmethod +def setEnableOverlaySmallLog(*args, **kwargs) -> dict: + if config.OVERLAY_SMALL_LOG is False: + if config.OVERLAY_LARGE_LOG is False: + model.startOverlay() # 副作用: オーバーレイシステムを起動 + config.OVERLAY_SMALL_LOG = True + return {"status": 200, "result": config.OVERLAY_SMALL_LOG} + +@staticmethod +def setDisableOverlaySmallLog(*args, **kwargs) -> dict: + if config.OVERLAY_SMALL_LOG is True: + model.clearOverlayImageSmallLog() + if config.OVERLAY_LARGE_LOG is False: + model.shutdownOverlay() # 副作用: オーバーレイシステムを停止 + config.OVERLAY_SMALL_LOG = False + return {"status": 200, "result": config.OVERLAY_SMALL_LOG} +``` + +#### パターン3: バリデーション付き設定 + +```python +@staticmethod +def setMicThreshold(data, *args, **kwargs) -> dict: + try: + data = int(data) + if 0 <= data <= config.MAX_MIC_THRESHOLD: + config.MIC_THRESHOLD = data + status = 200 + else: + raise ValueError() + except Exception: + response = { + "status": 400, + "result": { + "message": "Mic energy threshold value is out of range", + "data": config.MIC_THRESHOLD + } + } + else: + response = {"status": status, "result": config.MIC_THRESHOLD} + return response +``` + +#### パターン4: 依存関係のある設定 + +```python +def setSelectedTranslationComputeDevice(self, device: str, *args, **kwargs) -> dict: + config.SELECTED_TRANSLATION_COMPUTE_DEVICE = device + config.SELECTED_TRANSLATION_COMPUTE_TYPE = "auto" + # 依存する設定を自動更新 + self.run(200, self.run_mapping["selected_translation_compute_type"], + config.SELECTED_TRANSLATION_COMPUTE_TYPE) + # モデルの再読み込みフラグを設定 + model.setChangedTranslatorParameters(True) + return {"status": 200, "result": config.SELECTED_TRANSLATION_COMPUTE_DEVICE} +``` + +--- + +### 8. 翻訳機能制御 + +#### `setEnableTranslation(*args, **kwargs) -> dict` + +**責務:** 翻訳機能の有効化とモデルのロード + +**処理フロー:** +1. 既に有効な場合は何もしない +2. モデル未ロードまたはパラメータ変更時: + - `model.changeTranslatorCTranslate2Model()` でモデルをロード + - VRAM不足エラーの場合: + - デフォルト設定に戻す + - エラー通知を送信 + - 翻訳を無効化 +3. `config.ENABLE_TRANSLATION = True` に設定 + +**エラーハンドリング:** +```python +try: + model.changeTranslatorCTranslate2Model() +except Exception as e: + is_vram_error, error_message = model.detectVRAMError(e) + if is_vram_error: + self.run(400, self.run_mapping["error_translation_enable_vram_overflow"], {...}) + self.setDisableTranslation() +``` + +#### `setDisableTranslation(*args, **kwargs) -> dict` + +**責務:** 翻訳機能の無効化(メモリ解放) + +#### `changeToCTranslate2Process() -> None` + +**責務:** 外部翻訳APIエラー時に CTranslate2 へ切り替え + +**処理:** +1. 現在の翻訳エンジンを無効化 +2. CTranslate2 に切り替え +3. フロントエンドに通知 + +--- + +### 9. 音声認識制御 + +#### スレッド管理メソッド + +##### `startTranscriptionSendMessage() -> None` +マイク音声認識を開始。デバイスアクセスの排他制御を行う。 + +**排他制御:** +```python +while self.device_access_status is False: + sleep(1) # 他の処理がデバイスを使用中なら待機 +self.device_access_status = False # ロック取得 +try: + model.startMicTranscript(self.micMessage) +finally: + self.device_access_status = True # ロック解放 +``` + +**VRAMエラーハンドリング:** +- `model.detectVRAMError()` でエラーを検出 +- 音声認識を停止 +- フロントエンドに通知 + +##### `stopTranscriptionSendMessage() -> None` +マイク音声認識を停止。 + +##### `startThreadingTranscriptionSendMessage() -> None` +別スレッドで音声認識を開始。 + +##### `stopThreadingTranscriptionSendMessage() -> None` +別スレッドで音声認識を停止し、完了を待機(`join()`)。 + +**対応するスピーカー用メソッド:** +- `startTranscriptionReceiveMessage()` +- `stopTranscriptionReceiveMessage()` +- `startThreadingTranscriptionReceiveMessage()` +- `stopThreadingTranscriptionReceiveMessage()` + +--- + +### 10. エネルギー監視 + +#### `startCheckMicEnergy() -> None` +マイクの音量レベル監視を開始。`progressBarMicEnergy()` をコールバックとして渡す。 + +#### `stopCheckMicEnergy() -> None` +マイクの音量レベル監視を停止。 + +#### `startThreadingCheckMicEnergy() -> None` +別スレッドでエネルギー監視を開始。 + +#### `stopThreadingCheckMicEnergy() -> None` +別スレッドでエネルギー監視を停止し、完了を待機。 + +**対応するスピーカー用メソッド:** +- `startCheckSpeakerEnergy()` +- `stopCheckSpeakerEnergy()` +- `startThreadingCheckSpeakerEnergy()` +- `stopThreadingCheckSpeakerEnergy()` + +--- + +### 11. モデルウェイト管理 + +#### DownloadCTranslate2 クラス + +**責務:** CTranslate2 モデルのダウンロード進捗管理 + +**メソッド:** +- `progressBar(progress: float)`: 進捗率をフロントエンドに通知 +- `downloaded()`: ダウンロード完了時の処理 + - モデルの存在確認 + - 選択可能モデルリストに追加 + - フロントエンドに通知 + +#### DownloadWhisper クラス + +**責務:** Whisper モデルのダウンロード進捗管理(CTranslate2 と同様の構造) + +#### `downloadCtranslate2Weight(data: str, asynchronous: bool = True, *args, **kwargs) -> dict` + +**責務:** CTranslate2 モデルのダウンロード開始 + +**パラメータ:** +- `data`: モデルタイプ("tiny", "small", "medium" 等) +- `asynchronous`: 非同期ダウンロードの有効化 + +**処理:** +1. `DownloadCTranslate2` インスタンスを作成 +2. `asynchronous` が True の場合: + - `startThreadingDownloadCtranslate2Weight()` で別スレッド実行 +3. `asynchronous` が False の場合: + - `model.downloadCTranslate2ModelWeight()` で同期実行(初期化時に使用) +4. トークナイザーのダウンロード + +#### `downloadWhisperWeight(data: str, asynchronous: bool = True, *args, **kwargs) -> dict` + +**責務:** Whisper モデルのダウンロード開始(CTranslate2 と同様の構造) + +--- + +### 12. 自動デバイス選択 + +#### `applyAutoMicSelect() -> None` + +**責務:** マイクの自動選択機能を適用 + +**処理:** +1. コールバック設定: + - `device_manager.setCallbackProcessBeforeUpdateMicDevices(self.stopAccessMicDevices)` + - `device_manager.setCallbackDefaultMicDevice(self.updateSelectedMicDevice)` + - `device_manager.setCallbackProcessAfterUpdateMicDevices(self.restartAccessMicDevices)` +2. デバイス更新を強制実行: `device_manager.forceUpdateAndSetMicDevices()` +3. 監視開始: `device_manager.startMonitoring()` + +**動作フロー:** +``` +デバイス変更検出 + ↓ +stopAccessMicDevices() ← デバイス使用中の処理を停止 + ↓ +updateSelectedMicDevice() ← 新しいデフォルトデバイスを選択 + ↓ +restartAccessMicDevices() ← 新しいデバイスで処理を再開 +``` + +#### `setEnableAutoMicSelect(*args, **kwargs) -> dict` +自動マイク選択を有効化。 + +#### `setDisableAutoMicSelect(*args, **kwargs) -> dict` +自動マイク選択を無効化。両方の自動選択が無効になった場合のみ監視を停止。 + +**対応するスピーカー用メソッド:** +- `applyAutoSpeakerSelect()` +- `setEnableAutoSpeakerSelect()` +- `setDisableAutoSpeakerSelect()` + +--- + +### 13. 言語・翻訳エンジン管理 + +#### `updateTranslationEngineAndEngineList() -> None` + +**責務:** 選択された言語に応じて利用可能な翻訳エンジンを更新 + +**処理:** +1. 現在のタブの選択エンジンを取得 +2. `getTranslationEngines()` で利用可能なエンジンリストを取得 +3. 選択中のエンジンが利用不可の場合、CTranslate2 にフォールバック +4. **特殊ケース:** 入力言語と出力言語が同一の場合: + - CTranslate2 のみ利用可能(音訳のみ) +5. フロントエンドに通知 + +#### `getTranslationEngines(*args, **kwargs) -> dict` + +**責務:** 現在の言語設定で利用可能な翻訳エンジンを返却 + +**ロジック:** +1. `model.findTranslationEngines()` で言語ペアをサポートするエンジンを検索 +2. 入力言語と出力言語が同一の場合: + - CTranslate2 が有効なら ["CTranslate2"] + - それ以外は [] + +#### `setSelectedYourLanguages(select: dict, *args, **kwargs) -> dict` +入力言語を設定し、`updateTranslationEngineAndEngineList()` を呼び出す。 + +#### `setSelectedTargetLanguages(select: dict, *args, **kwargs) -> dict` +出力言語を設定し、`updateTranslationEngineAndEngineList()` を呼び出す。 + +#### `swapYourLanguageAndTargetLanguage(*args, **kwargs) -> dict` + +**責務:** 入力言語と出力言語を入れ替え + +**処理:** +1. 現在のタブの入力言語と出力言語(最初の1つ)を取得 +2. 相互に入れ替え +3. `setSelectedYourLanguages()` と `setSelectedTargetLanguages()` を呼び出し +4. 両方の結果を返却 + +--- + +### 14. 音声認識エンジン管理 + +#### `updateTranscriptionEngine() -> None` + +**責務:** Whisper モデルの利用可能状況に応じて音声認識エンジンを更新 + +**処理:** +1. 現在選択されている Whisper モデルの存在確認 +2. 利用可能なエンジンリストを取得 +3. 現在のエンジンが利用不可の場合: + - Whisper ⇔ Google で切り替え + - どちらも利用不可なら Whisper にフォールバック + +#### `updateDownloadedWhisperModelWeight() -> None` + +**責務:** ダウンロード済み Whisper モデルの一覧を更新 + +**処理:** +全てのモデルタイプについて `model.checkTranscriptionWhisperModelWeight()` で存在確認。 + +--- + +### 15. OSC 通信制御 + +#### `setOscIpAddress(data, *args, **kwargs) -> dict` + +**責務:** VRChat への送信先 IP アドレスを設定 + +**処理:** +1. `isValidIpAddress()` でバリデーション +2. `model.setOscIpAddress()` で設定を適用 +3. OSC Query の状態に応じて再初期化: + - 有効な場合: `enableOscQuery()` を呼び出し + - 無効な場合: `disableOscQuery()` を呼び出し + - マイクミュート同期が有効だった場合は無効化して通知 + +**エラーハンドリング:** +- IP アドレスが無効: status 400 +- 設定適用失敗: 元の IP に戻して status 400 + +#### `setOscPort(data, *args, **kwargs) -> dict` +OSC ポート番号を設定。 + +#### `enableOscQuery() -> None` +OSC Query 機能が有効になったことをフロントエンドに通知。 + +#### `disableOscQuery(mute_sync_info: bool = False) -> None` +OSC Query 機能が無効になったことを通知。無効化された機能リストも送信。 + +--- + +### 16. DeepL API 認証 + +#### `setDeeplAuthKey(data, *args, **kwargs) -> dict` + +**責務:** DeepL API キーを設定し、認証を実行 + +**処理:** +1. キー長のバリデーション(36 または 39 文字) +2. `model.authenticationTranslatorDeepLAuthKey()` で認証 +3. 認証成功時: + - `config.AUTH_KEYS["DeepL_API"]` に保存 + - `config.SELECTABLE_TRANSLATION_ENGINE_STATUS["DeepL_API"]` を True に + - `updateTranslationEngineAndEngineList()` を呼び出し +4. 認証失敗時: status 400 を返却 + +#### `delDeeplAuthKey(*args, **kwargs) -> dict` + +**責務:** DeepL API キーを削除 + +**処理:** +1. `config.AUTH_KEYS["DeepL_API"]` を None に +2. `config.SELECTABLE_TRANSLATION_ENGINE_STATUS["DeepL_API"]` を False に +3. `updateTranslationEngineAndEngineList()` を呼び出し + +--- + +### 17. WebSocket サーバー制御 + +#### `setWebSocketHost(data, *args, **kwargs) -> dict` + +**責務:** WebSocket サーバーのホストアドレスを変更 + +**処理:** +1. `isValidIpAddress()` でバリデーション +2. サーバーが停止中の場合: + - 設定のみ変更 +3. サーバーが起動中の場合: + - 新しいホストが利用可能か確認(`isAvailableWebSocketServer()`) + - サーバーを停止 → 再起動 + - 利用不可の場合は status 400 + +#### `setWebSocketPort(data, *args, **kwargs) -> dict` +WebSocket サーバーのポート番号を変更(ロジックは `setWebSocketHost()` と同様)。 + +#### `setEnableWebSocketServer(*args, **kwargs) -> dict` + +**責務:** WebSocket サーバーを起動 + +**処理:** +1. 既に起動中なら何もしない +2. ホストとポートが利用可能か確認 +3. `model.startWebSocketServer()` で起動 +4. 利用不可の場合は status 400 + +#### `setDisableWebSocketServer(*args, **kwargs) -> dict` +WebSocket サーバーを停止。 + +--- + +### 18. VRChat マイクミュート同期 + +#### `setEnableVrcMicMuteSync(*args, **kwargs) -> dict` + +**責務:** VRChat のマイクミュート状態と音声認識の連動を有効化 + +**前提条件:** OSC Query が有効であること + +**処理:** +1. OSC Query が無効の場合は status 400 を返却 +2. `model.setMuteSelfStatus()`: 現在のミュート状態を取得 +3. `model.changeMicTranscriptStatus()`: ミュート状態に応じて音声認識を制御 +4. `config.VRC_MIC_MUTE_SYNC = True` + +#### `setDisableVrcMicMuteSync(*args, **kwargs) -> dict` +マイクミュート同期を無効化し、`model.changeMicTranscriptStatus()` を呼び出す。 + +--- + +### 19. Watchdog 管理 + +Watchdog は UI とバックエンド間の通信監視機能。UI からの定期的な "feed" 信号がない場合、バックエンドを強制終了する。 + +#### `startWatchdog(*args, **kwargs) -> dict` +Watchdog を起動。 + +#### `feedWatchdog(*args, **kwargs) -> dict` +Watchdog にハートビート信号を送信(UI が定期的に呼び出す)。 + +#### `setWatchdogCallback(callback) -> dict` +Watchdog タイムアウト時に呼び出すコールバック関数を設定。`mainloop.stop()` が渡される。 + +#### `stopWatchdog(*args, **kwargs) -> dict` +Watchdog を停止。 + +--- + +### 20. ソフトウェアアップデート + +#### `checkSoftwareUpdated() -> dict` + +**責務:** 最新バージョンの確認 + +**処理:** +1. `model.checkSoftwareUpdated()` でバージョン情報を取得 +2. フロントエンドに通知(`software_update_info` エンドポイント) +3. 結果を返却 + +**バージョン情報形式:** +```python +{ + "current_version": "1.2.3", + "latest_version": "1.2.4", + "update_available": True, + "download_url": "https://..." +} +``` + +#### `updateSoftware(*args, **kwargs) -> dict` + +**責務:** 通常版のアップデートを実行 + +**処理:** +1. 別スレッドで `model.updateSoftware()` を起動(ブロッキングを避けるため) +2. 即座に status 200 を返却 + +#### `updateCudaSoftware(*args, **kwargs) -> dict` + +**責務:** CUDA版のアップデートを実行 + +**処理:** `updateSoftware()` と同様だが、`model.updateCudaSoftware()` を呼び出す。 + +--- + +### 21. 初期化処理 + +#### `init(*args, **kwargs) -> None` + +**責務:** アプリケーションの完全な初期化 + +**処理フロー:** + +**1. ログのクリア** +```python +removeLog() +printLog("Start Initialization") +``` + +**2. ネットワーク接続確認** +```python +connected_network = isConnectedNetwork() +if connected_network: + self.connectedNetwork() +else: + self.disconnectedNetwork() +``` + +**3. モデルウェイトのダウンロード(進捗1/4)** +```python +self.initializationProgress(1) +if connected_network: + # CTranslate2 と Whisper を並列ダウンロード + th_download_ctranslate2 = Thread(target=self.downloadCtranslate2Weight, args=(weight_type, False)) + th_download_whisper = Thread(target=self.downloadWhisperWeight, args=(weight_type, False)) + th_download_ctranslate2.start() + th_download_whisper.start() + th_download_ctranslate2.join() + th_download_whisper.join() +``` + +**4. AI モデル状態の確認** +```python +if (model.checkTranslatorCTranslate2ModelWeight(...) is False or + model.checkTranscriptionWhisperModelWeight(...) is False): + self.disableAiModels() +else: + self.enableAiModels() +``` + +**5. 翻訳・音声認識エンジンの初期化(進捗2/4)** +```python +self.initializationProgress(2) +# 翻訳エンジン +for engine in config.SELECTABLE_TRANSLATION_ENGINE_LIST: + match engine: + case "CTranslate2": + # モデルウェイトの存在確認 + case "DeepL_API": + # API キーの認証 + case _: + # ネットワーク接続が必要なエンジン + +# 音声認識エンジン +for engine in config.SELECTABLE_TRANSCRIPTION_ENGINE_LIST: + # 同様のロジック +``` + +**6. エンジンと音訳の設定(進捗3/4)** +```python +self.updateDownloadedCTranslate2ModelWeight() +self.updateTranslationEngineAndEngineList() +self.updateDownloadedWhisperModelWeight() +self.updateTranscriptionEngine() + +if config.CONVERT_MESSAGE_TO_ROMAJI or config.CONVERT_MESSAGE_TO_HIRAGANA: + model.startTransliteration() +``` + +**7. 周辺機能の初期化(進捗4/4)** +```python +self.initializationProgress(4) +model.addKeywords() # ワードフィルター +self.checkSoftwareUpdated() # バージョンチェック +if config.LOGGER_FEATURE: + model.startLogger() # ログ記録 +model.startReceiveOSC() # OSC 受信 + +# OSC Query +osc_query_enabled = model.getIsOscQueryEnabled() +if osc_query_enabled: + self.enableOscQuery() + if config.VRC_MIC_MUTE_SYNC: + self.setEnableVrcMicMuteSync() +else: + # マイクミュート同期を無効化 + self.disableOscQuery(...) +``` + +**8. デバイス管理の初期化** +```python +device_manager.setCallbackHostList(self.updateMicHostList) +device_manager.setCallbackMicDeviceList(self.updateMicDeviceList) +device_manager.setCallbackSpeakerDeviceList(self.updateSpeakerDeviceList) + +if config.AUTO_MIC_SELECT: + self.applyAutoMicSelect() +if config.AUTO_SPEAKER_SELECT: + self.applyAutoSpeakerSelect() +``` + +**9. オーバーレイと WebSocket の起動** +```python +if config.OVERLAY_SMALL_LOG or config.OVERLAY_LARGE_LOG: + model.startOverlay() + +if config.WEBSOCKET_SERVER: + if isAvailableWebSocketServer(...): + model.startWebSocketServer(...) +``` + +**10. 設定の同期と完了** +```python +self.updateConfigSettings() # 全設定をフロントエンドに送信 +printLog("End Initialization") +self.startWatchdog() # 監視開始 +``` + +--- + +## エラーハンドリング戦略 + +### 1. VRAM不足エラー + +**検出箇所:** +- 翻訳実行時(`micMessage()`, `speakerMessage()`, `chatMessage()`) +- 翻訳機能有効化時(`setEnableTranslation()`) +- 音声認識開始時(`startTranscriptionSendMessage()`, `startTranscriptionReceiveMessage()`) + +**処理:** +1. `model.detectVRAMError(e)` で VRAM エラーを検出 +2. 該当機能を無効化 +3. フロントエンドにエラー通知 +4. ログファイルに記録 + +**自動リカバリ:** +- 翻訳機能: 無効化して継続 +- 音声認識: 停止して継続 + +### 2. デバイスアクセスエラー + +**検出箇所:** +- マイク・スピーカーのアクセス時 + +**処理:** +1. `energy` が `False` の場合 +2. `error_device` エンドポイントにエラー通知 +3. 処理を継続(他の機能は影響を受けない) + +### 3. ネットワークエラー + +**検出箇所:** +- 翻訳APIの呼び出し時 +- モデルウェイトのダウンロード時 + +**処理:** +1. 外部API エラー: `changeToCTranslate2Process()` で CTranslate2 に切り替え +2. ダウンロードエラー: エラー通知を送信、AI機能を無効化 + +### 4. 設定バリデーションエラー + +**処理:** +- status 400 とエラーメッセージを返却 +- 現在の有効な設定値を `data` フィールドに含める + +**例:** +```python +{ + "status": 400, + "result": { + "message": "Mic energy threshold value is out of range", + "data": 1000 # 現在の有効な値 + } +} +``` + +--- + +## スレッド安全性 + +### 排他制御 + +#### デバイスアクセス制御 + +**問題:** 複数の機能が同時にデバイスにアクセスすると衝突 + +**解決策:** `device_access_status` フラグによる排他制御 +```python +while self.device_access_status is False: + sleep(1) # 待機 +self.device_access_status = False # ロック取得 +try: + # デバイスアクセス処理 +finally: + self.device_access_status = True # ロック解放 +``` + +**使用箇所:** +- `startTranscriptionSendMessage()` +- `startTranscriptionReceiveMessage()` +- `startCheckMicEnergy()` +- `startCheckSpeakerEnergy()` + +### デーモンスレッド + +**すべてのワーカースレッドは `daemon = True`:** +- メインスレッド終了時に自動的に終了 +- 明示的な join は必要に応じて実行(停止処理等) + +**例:** +```python +th_startTranscriptionSendMessage = Thread(target=self.startTranscriptionSendMessage) +th_startTranscriptionSendMessage.daemon = True +th_startTranscriptionSendMessage.start() +``` + +--- + +## パフォーマンス考慮事項 + +### 1. 非同期ダウンロード + +**初期化時:** 同期ダウンロード(`asynchronous=False`) +- UI をブロックして確実にダウンロード完了を待つ + +**ユーザー操作時:** 非同期ダウンロード(`asynchronous=True`) +- 別スレッドで実行し、進捗バーで通知 + +### 2. 並列初期化 + +CTranslate2 と Whisper のダウンロードを並列実行: +```python +th_download_ctranslate2.start() +th_download_whisper.start() +th_download_ctranslate2.join() +th_download_whisper.join() +``` + +### 3. モデルの遅延ロード + +翻訳モデルは `setEnableTranslation()` が呼ばれるまでロードされない。 + +--- + +## 依存関係 + +### 外部モジュール + +```python +from typing import Callable, Any, List, Optional +from time import sleep +from subprocess import Popen +from threading import Thread +import re +``` + +### 内部モジュール + +```python +from device_manager import device_manager +from config import config +from model import model +from utils import removeLog, printLog, errorLogging, isConnectedNetwork, isValidIpAddress, isAvailableWebSocketServer +``` + +--- + +## 設定項目の分類 + +### UI関連(約20項目) +- 透明度、スケーリング、フォント、言語、ウィンドウ位置等 + +### 音声認識関連(約30項目) +- デバイス選択、閾値、タイムアウト、フィルター等 + +### 翻訳関連(約25項目) +- エンジン選択、言語ペア、モデルタイプ、計算デバイス等 + +### OSC通信関連(約15項目) +- IP アドレス、ポート、メッセージフォーマット、送信設定等 + +### オーバーレイ関連(約10項目) +- 表示設定、位置、サイズ、透明度等 + +### その他(約20項目) +- WebSocket、ログ、ホットキー、プラグイン等 + +**合計:** 約120の設定項目(getter/setter で約240メソッド) + +--- + +## 制限事項 + +### 1. グローバル状態依存 + +すべての設定が `config` モジュールのグローバル変数として管理されている。 +- **利点:** シンプルなアクセス +- **欠点:** テスタビリティの低下、並列実行時の競合リスク + +### 2. 同期レスポンスの制限 + +ほとんどのメソッドが同期的にレスポンスを返すため、重い処理(モデルロード等)は UI をブロックする可能性がある。 + +**対策:** 重い処理は別スレッドで実行し、完了通知は `self.run()` で送信 + +### 3. エラー回復の限界 + +一部のエラー(VRAM不足等)は自動回復するが、設定ファイル破損やモデルファイル破損等は手動対処が必要。 + +--- + +## テストシナリオ + +### 1. 初期化テスト + +**ケース:** +- ネットワーク接続あり・なし +- モデルウェイトあり・なし +- 不正な設定値 + +**確認項目:** +- 全エンジンの状態が正しく設定されているか +- エラーがログに記録されているか +- フロントエンドに正しい初期設定が送信されているか + +### 2. 音声認識テスト + +**ケース:** +- デバイス切り替え中に音声認識 +- VRAM不足エラーの発生 +- 重複メッセージのフィルタリング + +**確認項目:** +- 排他制御が正しく動作しているか +- エラー発生時に適切にリカバリしているか + +### 3. 翻訳テスト + +**ケース:** +- 複数の翻訳エンジンの切り替え +- API制限エラー +- 除外ワードの処理 + +**確認項目:** +- エンジン切り替えが正しく動作するか +- 除外ワードが正しく復元されるか + +### 4. 設定変更テスト + +**ケース:** +- 無効な値の設定 +- 依存関係のある設定の変更 +- 有効/無効の切り替え + +**確認項目:** +- バリデーションが正しく動作するか +- 依存する設定が自動更新されるか + +--- + +## 今後の拡張性 + +### 1. 非同期化の推進 + +`asyncio` への移行で UI ブロッキングを完全に排除。 + +### 2. 依存性注入 + +`config` と `model` を DI コンテナで管理し、テスタビリティを向上。 + +### 3. イベント駆動アーキテクチャ + +設定変更時のイベントを発火し、各サブシステムが独立して反応。 + +### 4. エラーリカバリの強化 + +- 自動再試行メカニズム +- フォールバック設定の自動適用 +- エラー発生時の部分的な機能継続 + +--- + +## 関連ファイル + +- **mainloop.py** - 通信レイヤー、リクエストルーティング +- **model.py** - ビジネスロジックのファサード +- **config.py** - 設定管理 +- **device_manager.py** - デバイス監視・自動選択 +- **utils.py** - ログとユーティリティ関数 + +--- + +## コーディング規約 + +- **PEP 8 スタイルガイド** +- **型ヒント:** `typing` モジュールを使用 +- **Docstring:** Google スタイル(一部未実装) +- **静的メソッド:** 状態を持たないメソッドは `@staticmethod` +- **エラーハンドリング:** 防御的プログラミングを徹底 + +--- + +## まとめ + +`controller.py` は VRCT の中核となるビジネスロジック制御レイヤーであり、約120の設定項目と約200のエンドポイントを管理する。フロントエンドとバックエンドの橋渡しとして、設定の取得・更新、機能の有効化・無効化、エラーハンドリング、デバイス管理など、アプリケーション全体の動作を制御する。排他制御とスレッド管理により、複数の機能が同時に動作する環境でも安定性を保っている。VRAM不足エラーや外部APIエラーに対する自動リカバリ機能により、ユーザーエクスペリエンスの向上を実現している。 diff --git a/src-python/docs/device_manager.md b/src-python/docs/device_manager.md new file mode 100644 index 00000000..858acdc9 --- /dev/null +++ b/src-python/docs/device_manager.md @@ -0,0 +1,1427 @@ +# device_manager.py 設計書 + +## 概要 + +`device_manager.py` は VRCT アプリケーションの音声デバイス管理を担当するモジュールであり、マイクとスピーカーデバイスの検出、監視、自動選択機能を提供する。Windows の WASAPI や pycaw ライブラリを使用してリアルタイムなデバイス変更を検知し、登録されたコールバック関数を通じてアプリケーションに通知する。シングルトンパターンで実装され、遅延初期化により import 時のパフォーマンス低下を回避している。 + +## アーキテクチャ上の位置づけ + +``` +┌─────────────┐ +│controller.py│ (Business Logic Control Layer) +└──────┬──────┘ + │ Callback Registration & Query +┌──────▼──────────┐ +│device_manager.py│ ◄── このファイル +└──────┬──────────┘ + │ Device Monitoring & Enumeration +┌──────▼─────────────────────────────┐ +│ OS Audio Subsystems │ +│ - PyAudio (PortAudio wrapper) │ +│ - pyaudiowpatch (WASAPI loopback) │ +│ - pycaw (COM notifications) │ +│ - comtypes (COM initialization) │ +└────────────────────────────────────┘ +``` + +## 主要コンポーネント + +### 1. Client クラス + +**責務:** Windows の COM イベントコールバックを受け取り、デバイス変更を検知 + +**継承:** `pycaw.callbacks.MMNotificationClient` + +**設計パターン:** Observer パターンのコールバック実装 + +#### コンストラクタ `__init__()` + +**処理:** +```python +try: + super().__init__() +except Exception: + pass # 非 Windows 環境ではプレースホルダーオブジェクトのため例外を無視 +self.loop: bool = True +``` + +**`self.loop` フラグ:** +- True: デバイス変更なし、監視継続 +- False: デバイス変更検知、監視ループを中断 + +#### イベントハンドラー + +##### `on_default_device_changed(*args, **kwargs) -> None` +デフォルトデバイスが変更された時に Windows から呼び出される。 + +##### `on_device_added(*args, **kwargs) -> None` +新しいデバイスが接続された時に呼び出される。 + +##### `on_device_removed(*args, **kwargs) -> None` +デバイスが取り外された時に呼び出される。 + +##### `on_device_state_changed(*args, **kwargs) -> None` +デバイスの状態(有効/無効/存在しない等)が変更された時に呼び出される。 + +**すべてのハンドラーの動作:** +```python +self.loop = False # 監視ループに変更を通知 +``` + +**コメントアウトされたメソッド:** +```python +# def on_property_value_changed(self, device_id, key): +# self.loop = False +``` +デバイスプロパティの変更イベント。使用しない理由は不明だが、頻繁なイベント発火によるパフォーマンス低下を避けるためと推測される。 + +--- + +### 2. DeviceManager クラス + +**責務:** アプリケーション全体のデバイス管理機能を提供 + +**パターン:** シングルトン(`__new__` で制御) + +**プラットフォーム対応:** +- Windows: 完全な機能サポート(COM イベント監視、WASAPI loopback) +- 非 Windows: グレースフルデグレード(デフォルト値を返却、監視機能は制限的) + +--- + +### 3. 初期化メソッド + +#### `__new__(cls) -> DeviceManager` + +**責務:** シングルトンインスタンスの生成と軽量な初期化 + +**処理フロー:** +1. **インスタンスチェック:** + ```python + if cls._instance is None: + cls._instance = super(DeviceManager, cls).__new__(cls) + ``` +2. **軽量な初期化:** + ```python + cls._instance._initialized = False + try: + cls._instance.init() + except Exception: + try: + errorLogging() + except Exception: + pass # import 時のクラッシュを絶対に避ける + ``` +3. **既存インスタンスの返却:** + ```python + return cls._instance + ``` + +**設計思想:** +- `__new__` では重い初期化を避ける(スレッド起動、OS API アクセスなし) +- `init()` を呼び出すが、監視スレッドは起動しない +- エラー時も必ずインスタンスを返却(防御的プログラミング) + +#### `init() -> None` + +**責務:** 内部状態の初期化とデバイス情報の初回取得 + +**処理フロー:** + +**1. 初期化済みチェック:** +```python +if getattr(self, "_initialized", False): + return # 既に初期化済みなら何もしない +``` + +**2. デバイス情報の初期化(デフォルト値):** +```python +self.mic_devices: Dict[str, List[Dict[str, Any]]] = { + "NoHost": [{"index": -1, "name": "NoDevice"}] +} +self.default_mic_device: Dict[str, Any] = { + "host": {"index": -1, "name": "NoHost"}, + "device": {"index": -1, "name": "NoDevice"} +} +self.speaker_devices: List[Dict[str, Any]] = [ + {"index": -1, "name": "NoDevice"} +] +self.default_speaker_device: Dict[str, Any] = { + "device": {"index": -1, "name": "NoDevice"} +} +``` + +**3. 前回状態のトラッカー:** +```python +self.prev_mic_host: List[str] = [host for host in self.mic_devices] +self.prev_mic_devices: Dict[str, List[Dict[str, Any]]] = self.mic_devices +self.prev_default_mic_device: Dict[str, Any] = self.default_mic_device +self.prev_speaker_devices: List[Dict[str, Any]] = self.speaker_devices +self.prev_default_speaker_device: Dict[str, Any] = self.default_speaker_device +``` + +**4. 更新フラグ:** +```python +self.update_flag_default_mic_device: bool = False +self.update_flag_default_speaker_device: bool = False +self.update_flag_host_list: bool = False +self.update_flag_mic_device_list: bool = False +self.update_flag_speaker_device_list: bool = False +``` + +**5. コールバック関数:** +```python +self.callback_default_mic_device: Optional[Callable[..., None]] = None +self.callback_default_speaker_device: Optional[Callable[..., None]] = None +self.callback_host_list: Optional[Callable[..., None]] = None +self.callback_mic_device_list: Optional[Callable[..., None]] = None +self.callback_speaker_device_list: Optional[Callable[..., None]] = None +self.callback_process_before_update_mic_devices: Optional[Callable[..., None]] = None +self.callback_process_after_update_mic_devices: Optional[Callable[..., None]] = None +self.callback_process_before_update_speaker_devices: Optional[Callable[..., None]] = None +self.callback_process_after_update_speaker_devices: Optional[Callable[..., None]] = None +``` + +**6. 監視制御:** +```python +self.monitoring_flag: bool = False +self.th_monitoring: Optional[Thread] = None +``` + +**7. 初期化完了フラグ:** +```python +self._initialized = True +``` + +**8. ベストエフォートのデバイス情報取得:** +```python +try: + if PyAudio is not None: + try: + self.update() # 実デバイス情報を取得 + except Exception: + errorLogging() +except Exception: + pass # 初期化失敗でもクラッシュしない +``` + +**設計思想:** +- すべての属性をデフォルト値で初期化(未初期化エラーを回避) +- `update()` の失敗は許容(デバイスがない環境でも動作) +- エラーは記録するが、例外を外部に投げない + +--- + +### 4. デバイス情報更新メソッド + +#### `update() -> None` + +**責務:** 現在の音声デバイス一覧とデフォルトデバイスを取得 + +**処理フロー:** + +**1. バッファの初期化:** +```python +buffer_mic_devices: Dict[str, List[Dict[str, Any]]] = {} +buffer_default_mic_device: Dict[str, Any] = { + "host": {"index": -1, "name": "NoHost"}, + "device": {"index": -1, "name": "NoDevice"} +} +buffer_speaker_devices: List[Dict[str, Any]] = [] +buffer_default_speaker_device: Dict[str, Any] = { + "device": {"index": -1, "name": "NoDevice"} +} +``` + +**2. PyAudio 可用性チェック:** +```python +if PyAudio is None: + # デフォルト値のまま終了 + self.mic_devices = buffer_mic_devices or {"NoHost": [{"index": -1, "name": "NoDevice"}]} + # ... 他のデバイス情報も設定 + return +``` + +**3. マイクデバイスの収集:** +```python +with PyAudio() as p: + for host_index in range(p.get_host_api_count()): + host = p.get_host_api_info_by_index(host_index) + device_count = host.get('deviceCount', 0) + for device_index in range(device_count): + device = p.get_device_info_by_host_api_device_index(host_index, device_index) + # 入力チャンネルがあり、ループバックではないデバイス + if device.get("maxInputChannels", 0) > 0 and not device.get("isLoopbackDevice", True): + buffer_mic_devices.setdefault(host["name"], []).append(device) +``` + +**ホスト API の例:** +- Windows: "MME", "Windows DirectSound", "Windows WASAPI" +- Linux: "ALSA", "PulseAudio" +- macOS: "Core Audio" + +**4. デフォルトマイクデバイスの取得:** +```python +api_info = p.get_default_host_api_info() +default_mic_device = api_info.get("defaultInputDevice", -1) + +for host_index in range(p.get_host_api_count()): + host = p.get_host_api_info_by_index(host_index) + device_count = host.get('deviceCount', 0) + for device_index in range(device_count): + device = p.get_device_info_by_host_api_device_index(host_index, device_index) + if device.get("index") == default_mic_device: + buffer_default_mic_device = {"host": host, "device": device} + break + else: + continue + break +``` + +**5. スピーカーループバックデバイスの収集:** +```python +speaker_devices: List[Dict[str, Any]] = [] +if paWASAPI is not None: + try: + wasapi_info = p.get_host_api_info_by_type(paWASAPI) + wasapi_name = wasapi_info.get("name") + for host_index in range(p.get_host_api_count()): + host = p.get_host_api_info_by_index(host_index) + if host.get("name") == wasapi_name: + device_count = host.get('deviceCount', 0) + for device_index in range(device_count): + device = p.get_device_info_by_host_api_device_index(host_index, device_index) + if not device.get("isLoopbackDevice", True): + # ループバックデバイスを検索 + for loopback in p.get_loopback_device_info_generator(): + if device.get("name") in loopback.get("name", ""): + speaker_devices.append(loopback) + except Exception: + pass # WASAPI が利用できない場合は無視 +``` + +**ループバックデバイスとは:** +- スピーカーから出力される音声を「録音」できる仮想デバイス +- "Stereo Mix" や "What U Hear" のような名前 +- VRChat の相手の音声を認識するために使用 + +**6. 重複排除とソート:** +```python +speaker_devices = [dict(t) for t in {tuple(d.items()) for d in speaker_devices}] or [{"index": -1, "name": "NoDevice"}] +buffer_speaker_devices = sorted(speaker_devices, key=lambda d: d.get('index', -1)) +``` + +**7. デフォルトスピーカーデバイスの取得:** +```python +if paWASAPI is not None: + try: + wasapi_info = p.get_host_api_info_by_type(paWASAPI) + default_speaker_device_index = wasapi_info.get("defaultOutputDevice", -1) + for host_index in range(p.get_host_api_count()): + host_info = p.get_host_api_info_by_index(host_index) + device_count = host_info.get('deviceCount', 0) + for device_index in range(0, device_count): + device = p.get_device_info_by_host_api_device_index(host_index, device_index) + if device.get("index") == default_speaker_device_index: + default_speakers = device + if not default_speakers.get("isLoopbackDevice", True): + for loopback in p.get_loopback_device_info_generator(): + if default_speakers.get("name") in loopback.get("name", ""): + buffer_default_speaker_device = {"device": loopback} + break + break + if buffer_default_speaker_device["device"].get("name") != "NoDevice": + break + except Exception: + pass +``` + +**8. エラーハンドリングと最終設定:** +```python +except Exception: + errorLogging() + +self.mic_devices = buffer_mic_devices +self.default_mic_device = buffer_default_mic_device +self.speaker_devices = buffer_speaker_devices +self.default_speaker_device = buffer_default_speaker_device +``` + +**デバイス情報の構造例:** +```python +# マイクデバイス +self.mic_devices = { + "Windows WASAPI": [ + {"index": 0, "name": "Microphone (Realtek)", "maxInputChannels": 2, ...}, + {"index": 3, "name": "Line In (USB Audio)", "maxInputChannels": 2, ...} + ], + "MME": [ + {"index": 10, "name": "マイク (Realtek)", "maxInputChannels": 2, ...} + ] +} + +# デフォルトマイクデバイス +self.default_mic_device = { + "host": {"index": 0, "name": "Windows WASAPI", ...}, + "device": {"index": 0, "name": "Microphone (Realtek)", ...} +} +``` + +--- + +### 5. 変更検出メソッド + +#### `checkUpdate() -> bool` + +**責務:** 前回取得したデバイス情報との差分を検出し、更新フラグを設定 + +**処理:** + +**1. デフォルトマイクデバイスの変更チェック:** +```python +if self.prev_default_mic_device["device"]["name"] != self.default_mic_device["device"]["name"]: + self.update_flag_default_mic_device = True + self.prev_default_mic_device = self.default_mic_device +``` + +**2. デフォルトスピーカーデバイスの変更チェック:** +```python +if self.prev_default_speaker_device["device"]["name"] != self.default_speaker_device["device"]["name"]: + self.update_flag_default_speaker_device = True + self.prev_default_speaker_device = self.default_speaker_device +``` + +**3. マイクホストリストの変更チェック:** +```python +if self.prev_mic_host != [host for host in self.mic_devices]: + self.update_flag_host_list = True + self.prev_mic_host = [host for host in self.mic_devices] +``` + +**4. マイクデバイスリストの変更チェック:** +```python +if ({key: [device['name'] for device in devices] for key, devices in self.prev_mic_devices.items()} != + {key: [device['name'] for device in devices] for key, devices in self.mic_devices.items()}): + self.update_flag_mic_device_list = True + self.prev_mic_devices = self.mic_devices +``` + +**比較方法:** +- デバイス名のリストのみを比較(`index` の変化は無視) +- ホストごとにグループ化して比較 + +**5. スピーカーデバイスリストの変更チェック:** +```python +if [device['name'] for device in self.prev_speaker_devices] != [device['name'] for device in self.speaker_devices]: + self.update_flag_speaker_device_list = True + self.prev_speaker_devices = self.speaker_devices +``` + +**6. 総合的な更新フラグの判定:** +```python +update_flag = ( + self.update_flag_default_mic_device or + self.update_flag_default_speaker_device or + self.update_flag_host_list or + self.update_flag_mic_device_list or + self.update_flag_speaker_device_list +) +return update_flag +``` + +**戻り値:** +- `True`: いずれかのデバイス情報が変更された +- `False`: すべてのデバイス情報が前回と同一 + +--- + +### 6. 監視メソッド + +#### `monitoring() -> None` + +**責務:** バックグラウンドでデバイス変更を監視し、変更時にコールバックを実行 + +**実行環境:** 別スレッド(`startMonitoring()` で起動) + +**処理フロー:** + +**1. 監視ループ:** +```python +try: + while self.monitoring_flag is True: + try: + # 監視処理 + except Exception: + errorLogging() +except Exception: + errorLogging() +``` + +**2. COM イベント監視(Windows のみ):** +```python +if comtypes is not None and AudioUtilities is not None: + try: + comtypes.CoInitialize() # COM の初期化 + cb = Client() + enumerator = AudioUtilities.GetDeviceEnumerator() + enumerator.RegisterEndpointNotificationCallback(cb) + + while cb.loop is True and self.monitoring_flag is True: + sleep(1) # イベント待機 + + try: + enumerator.UnregisterEndpointNotificationCallback(cb) + except Exception: + pass # ベストエフォート + comtypes.CoUninitialize() + except Exception: + errorLogging() +``` + +**COM 監視の動作:** +- `Client` クラスのイベントハンドラーがデバイス変更を検知 +- `cb.loop` が `False` になるとループを抜ける +- COM が利用できない場合はポーリングにフォールバック + +**3. ポーリングと更新サイクル:** +```python +# 更新前の処理 +self.runProcessBeforeUpdateMicDevices() +self.runProcessBeforeUpdateSpeakerDevices() + +sleep(2) # デバイス状態の安定を待つ + +# 最大10回(20秒間)ポーリング +for _ in range(10): + self.update() + if self.checkUpdate(): + break # 変更を検知したら終了 + sleep(2) + +# コールバック通知 +self.noticeUpdateDevices() + +# 更新後の処理 +self.runProcessAfterUpdateMicDevices() +self.runProcessAfterUpdateSpeakerDevices() +``` + +**ポーリング戦略:** +- 初回 2 秒待機: デバイスの接続/切断後の不安定期間を回避 +- 最大 10 回ポーリング: デバイス変更を見逃さない +- 変更検知後は即座に次の処理へ + +**4. 監視サイクルの繰り返し:** +```python +# while self.monitoring_flag is True の先頭に戻る +``` + +#### `startMonitoring() -> None` + +**責務:** 監視スレッドの起動 + +**処理:** +```python +if self.monitoring_flag: + return # 既に起動中 +self.monitoring_flag = True +self.th_monitoring = Thread(target=self.monitoring) +self.th_monitoring.daemon = True +self.th_monitoring.start() +``` + +**デーモンスレッド:** +- メインスレッド終了時に自動的に終了 +- アプリケーション終了を妨げない + +#### `stopMonitoring() -> None` + +**責務:** 監視スレッドの停止 + +**処理:** +```python +self.monitoring_flag = False +if getattr(self, "th_monitoring", None) is not None: + try: + self.th_monitoring.join(timeout=5) # 最大5秒待機 + except Exception: + pass # ベストエフォート +``` + +**タイムアウト設定:** +- 5 秒以内に終了しない場合は待機を諦める +- スレッドの join に失敗してもエラーを無視(防御的) + +--- + +### 7. コールバック管理メソッド + +#### デフォルトデバイス変更コールバック + +##### `setCallbackDefaultMicDevice(callback: Callable[..., None]) -> None` +デフォルトマイクデバイス変更時のコールバックを登録。 + +**コールバックシグネチャ:** +```python +def callback(host_name: str, device_name: str) -> None: + pass +``` + +##### `clearCallbackDefaultMicDevice() -> None` +コールバックをクリア。 + +##### `setCallbackDefaultSpeakerDevice(callback: Callable[..., None]) -> None` +デフォルトスピーカーデバイス変更時のコールバックを登録。 + +**コールバックシグネチャ:** +```python +def callback(device_name: str) -> None: + pass +``` + +##### `clearCallbackDefaultSpeakerDevice() -> None` +コールバックをクリア。 + +#### デバイスリスト変更コールバック + +##### `setCallbackHostList(callback: Callable[..., None]) -> None` +マイクホストリスト変更時のコールバックを登録。 + +##### `clearCallbackHostList() -> None` +コールバックをクリア。 + +##### `setCallbackMicDeviceList(callback: Callable[..., None]) -> None` +マイクデバイスリスト変更時のコールバックを登録。 + +##### `clearCallbackMicDeviceList() -> None` +コールバックをクリア。 + +##### `setCallbackSpeakerDeviceList(callback: Callable[..., None]) -> None` +スピーカーデバイスリスト変更時のコールバックを登録。 + +##### `clearCallbackSpeakerDeviceList() -> None` +コールバックをクリア。 + +#### 処理フックコールバック + +##### `setCallbackProcessBeforeUpdateMicDevices(callback: Callable[..., None]) -> None` +マイクデバイス更新前の処理を登録。 + +**使用例:** 音声認識を停止してデバイスを解放 + +##### `clearCallbackProcessBeforeUpdateMicDevices() -> None` +コールバックをクリア。 + +##### `setCallbackProcessAfterUpdateMicDevices(callback: Callable[..., None]) -> None` +マイクデバイス更新後の処理を登録。 + +**使用例:** 新しいデバイスで音声認識を再開 + +##### `clearCallbackProcessAfterUpdateMicDevices() -> None` +コールバックをクリア。 + +##### `setCallbackProcessBeforeUpdateSpeakerDevices(callback: Callable[..., None]) -> None` +スピーカーデバイス更新前の処理を登録。 + +##### `clearCallbackProcessBeforeUpdateSpeakerDevices() -> None` +コールバックをクリア。 + +##### `setCallbackProcessAfterUpdateSpeakerDevices(callback: Callable[..., None]) -> None` +スピーカーデバイス更新後の処理を登録。 + +##### `clearCallbackProcessAfterUpdateSpeakerDevices() -> None` +コールバックをクリア。 + +--- + +### 8. コールバック実行メソッド + +#### `runProcessBeforeUpdateMicDevices() -> None` + +**責務:** マイクデバイス更新前の処理コールバックを実行 + +**処理:** +```python +if isinstance(self.callback_process_before_update_mic_devices, Callable): + try: + self.callback_process_before_update_mic_devices() + except Exception: + errorLogging() +``` + +**型チェック:** +- `isinstance(callback, Callable)` で呼び出し可能性を確認 +- `None` の場合は何もしない + +#### `runProcessAfterUpdateMicDevices() -> None` +マイクデバイス更新後の処理コールバックを実行(同様の実装)。 + +#### `runProcessBeforeUpdateSpeakerDevices() -> None` +スピーカーデバイス更新前の処理コールバックを実行(同様の実装)。 + +#### `runProcessAfterUpdateSpeakerDevices() -> None` +スピーカーデバイス更新後の処理コールバックを実行(同様の実装)。 + +--- + +### 9. 通知メソッド + +#### `noticeUpdateDevices() -> None` + +**責務:** 更新フラグに応じて対応するコールバックを呼び出し、フラグをリセット + +**処理:** +```python +if self.update_flag_default_mic_device is True: + self.setMicDefaultDevice() +if self.update_flag_default_speaker_device is True: + self.setSpeakerDefaultDevice() +if self.update_flag_host_list is True: + self.setMicHostList() +if self.update_flag_mic_device_list is True: + self.setMicDeviceList() +if self.update_flag_speaker_device_list is True: + self.setSpeakerDeviceList() + +# すべてのフラグをリセット +self.update_flag_default_mic_device = False +self.update_flag_default_speaker_device = False +self.update_flag_host_list = False +self.update_flag_mic_device_list = False +self.update_flag_speaker_device_list = False +``` + +#### `setMicDefaultDevice() -> None` + +**責務:** デフォルトマイクデバイス変更コールバックの実行 + +**処理:** +```python +if isinstance(self.callback_default_mic_device, Callable): + try: + self.callback_default_mic_device( + self.default_mic_device["host"]["name"], + self.default_mic_device["device"]["name"] + ) + except Exception: + errorLogging() +``` + +#### `setSpeakerDefaultDevice() -> None` + +**責務:** デフォルトスピーカーデバイス変更コールバックの実行 + +**処理:** +```python +if isinstance(self.callback_default_speaker_device, Callable): + try: + self.callback_default_speaker_device( + self.default_speaker_device["device"]["name"] + ) + except Exception: + errorLogging() +``` + +#### `setMicHostList() -> None` +マイクホストリスト変更コールバックの実行(引数なし)。 + +#### `setMicDeviceList() -> None` +マイクデバイスリスト変更コールバックの実行(引数なし)。 + +#### `setSpeakerDeviceList() -> None` +スピーカーデバイスリスト変更コールバックの実行(引数なし)。 + +--- + +### 10. デバイス情報取得メソッド + +#### `getMicDevices() -> Dict[str, List[Dict[str, Any]]]` + +**責務:** マイクデバイス一覧を取得 + +**処理:** +```python +if not getattr(self, '_initialized', False): + try: + self.init() + except Exception: + try: + errorLogging() + except Exception: + pass +return getattr(self, 'mic_devices', {"NoHost": [{"index": -1, "name": "NoDevice"}]}) +``` + +**安全性:** +- 未初期化の場合は `init()` を呼び出す +- 失敗時はデフォルト値を返却 + +**戻り値の例:** +```python +{ + "Windows WASAPI": [ + {"index": 0, "name": "Microphone (Realtek)", ...}, + {"index": 3, "name": "Line In (USB Audio)", ...} + ], + "MME": [ + {"index": 10, "name": "マイク (Realtek)", ...} + ] +} +``` + +#### `getDefaultMicDevice() -> Dict[str, Any]` + +**責務:** デフォルトマイクデバイスを取得 + +**戻り値の例:** +```python +{ + "host": {"index": 0, "name": "Windows WASAPI", ...}, + "device": {"index": 0, "name": "Microphone (Realtek)", ...} +} +``` + +#### `getSpeakerDevices() -> List[Dict[str, Any]]` + +**責務:** スピーカーデバイス一覧を取得 + +**戻り値の例:** +```python +[ + {"index": 5, "name": "Stereo Mix (Realtek)", "isLoopbackDevice": True, ...}, + {"index": 7, "name": "Speakers (USB Audio) [Loopback]", ...} +] +``` + +#### `getDefaultSpeakerDevice() -> Dict[str, Any]` + +**責務:** デフォルトスピーカーデバイスを取得 + +**戻り値の例:** +```python +{ + "device": {"index": 5, "name": "Stereo Mix (Realtek)", ...} +} +``` + +--- + +### 11. 強制更新メソッド + +#### `forceUpdateAndSetMicDevices() -> None` + +**責務:** マイクデバイス情報を強制的に更新し、すべてのコールバックを実行 + +**処理:** +```python +self.update() +self.setMicHostList() +self.setMicDeviceList() +self.setMicDefaultDevice() +``` + +**使用場面:** +- 自動デバイス選択機能の初回適用時 +- ユーザーが手動で更新を要求した時 + +#### `forceUpdateAndSetSpeakerDevices() -> None` + +**責務:** スピーカーデバイス情報を強制的に更新 + +**処理:** +```python +self.update() +self.setSpeakerDeviceList() +self.setSpeakerDefaultDevice() +``` + +--- + +### 12. モジュールレベルの使用方法 + +#### シングルトンインスタンス + +```python +device_manager = DeviceManager() +``` + +**モジュールをインポートするだけで使用可能:** +```python +from device_manager import device_manager + +# デバイス情報取得 +mic_devices = device_manager.getMicDevices() +``` + +#### デモスクリプト + +```python +if __name__ == "__main__": + print("DeviceManager demo. Call device_manager.init() and device_manager.startMonitoring() to run live monitoring.") + try: + while True: + sleep(1) + except KeyboardInterrupt: + print("exiting") +``` + +**実行方法:** +```powershell +python device_manager.py +``` + +--- + +## 依存関係 + +### 外部ライブラリ + +```python +from typing import Callable, Dict, List, Optional, Any +from time import sleep +from threading import Thread +``` + +### オプショナル依存(Windows 専用) + +```python +import comtypes # COM 初期化・終了 +from pyaudiowpatch import PyAudio, paWASAPI # WASAPI loopback サポート +from pycaw.callbacks import MMNotificationClient # デバイス変更イベント +from pycaw.utils import AudioUtilities # デバイス列挙 +``` + +**非 Windows 環境での動作:** +- すべてのオプショナル依存は `try-except` でガード +- インポート失敗時は `None` または placeholder を設定 +- デフォルト値(`NoDevice`)を返す機能は維持 + +### 内部モジュール + +```python +from utils import errorLogging +``` + +--- + +## 自動デバイス選択の動作フロー + +### Controller 側の設定(例) + +```python +# controller.py の applyAutoMicSelect() メソッド + +def applyAutoMicSelect(self) -> None: + # 1. 更新前の処理: デバイス使用中の機能を停止 + device_manager.setCallbackProcessBeforeUpdateMicDevices( + self.stopAccessMicDevices + ) + + # 2. デフォルトデバイス変更時: 新しいデバイスを選択 + device_manager.setCallbackDefaultMicDevice( + self.updateSelectedMicDevice + ) + + # 3. 更新後の処理: 新しいデバイスで機能を再開 + device_manager.setCallbackProcessAfterUpdateMicDevices( + self.restartAccessMicDevices + ) + + # 4. 初回実行 + device_manager.forceUpdateAndSetMicDevices() + + # 5. 監視開始 + device_manager.startMonitoring() +``` + +### デバイス変更時のシーケンス図 + +``` +[ユーザーがヘッドセットを接続] + ↓ +[Windows がデフォルトデバイスを変更] + ↓ +[pycaw の Client.on_device_added() が呼ばれる] + ↓ +[client.loop = False に設定] + ↓ +[monitoring() の COM 監視ループが終了] + ↓ +[runProcessBeforeUpdateMicDevices() 実行] + ↓ +[controller.stopAccessMicDevices()] + - 音声認識を停止 + - デバイスを解放 + ↓ +[update() でデバイス情報を更新] + ↓ +[checkUpdate() で変更を検出] + ↓ +[noticeUpdateDevices() でコールバック呼び出し] + ↓ +[setMicDefaultDevice() 実行] + ↓ +[controller.updateSelectedMicDevice(host, device)] + - 設定を更新 + - フロントエンドに通知 + ↓ +[runProcessAfterUpdateMicDevices() 実行] + ↓ +[controller.restartAccessMicDevices()] + - 新しいデバイスで音声認識を開始 + ↓ +[COM 監視ループが再開] +``` + +--- + +## エラーハンドリング戦略 + +### 1. import 時のエラー + +**問題:** Windows 専用ライブラリが非 Windows 環境でインポートされる + +**対策:** +```python +try: + import comtypes +except Exception: + comtypes = None # type: ignore +``` + +**結果:** +- インポートエラーは発生しない +- `comtypes is None` で可用性を判定 +- 機能は制限されるがアプリケーションは動作 + +### 2. 初期化時のエラー + +**問題:** デバイス情報の取得に失敗 + +**対策:** +```python +try: + if PyAudio is not None: + try: + self.update() + except Exception: + errorLogging() +except Exception: + pass # デフォルト値のまま継続 +``` + +**結果:** +- 初期化は完了(`_initialized = True`) +- デバイス情報はデフォルト値(`NoDevice`) +- ログにエラーを記録 + +### 3. 監視スレッド内のエラー + +**問題:** デバイス更新中の予期しない例外 + +**対策:** +```python +try: + while self.monitoring_flag is True: + try: + # 監視処理 + except Exception: + errorLogging() # ログに記録して継続 +except Exception: + errorLogging() # 外側のループでもキャッチ +``` + +**結果:** +- エラーが発生しても監視は継続 +- ログにエラーを記録 +- スレッドはクラッシュしない + +### 4. コールバック実行時のエラー + +**問題:** 登録されたコールバック関数内で例外が発生 + +**対策:** +```python +if isinstance(self.callback_default_mic_device, Callable): + try: + self.callback_default_mic_device(host_name, device_name) + except Exception: + errorLogging() # ログに記録して継続 +``` + +**結果:** +- コールバックのエラーは分離される +- 他のコールバックには影響しない +- デバイス監視は継続 + +--- + +## スレッド構成 + +### メインスレッド +- アプリケーションのメインループ + +### 監視スレッド(`th_monitoring`) +- `monitoring()` メソッドを実行 +- デーモンスレッド(メインスレッド終了時に自動終了) +- `startMonitoring()` で起動 +- `stopMonitoring()` で停止 + +### スレッド同期 + +**監視フラグ:** +```python +self.monitoring_flag: bool = False +``` + +**動作:** +- `True`: 監視継続 +- `False`: 監視停止(次回ループで終了) + +**停止時の安全性:** +```python +self.monitoring_flag = False # フラグを False に +if self.th_monitoring is not None: + self.th_monitoring.join(timeout=5) # 最大5秒待機 +``` + +--- + +## パフォーマンス考慮事項 + +### 1. 遅延初期化 + +**戦略:** +- `__new__`: 軽量(インスタンス生成のみ) +- `init()`: 中程度(デバイス情報の初回取得) +- `startMonitoring()`: 重い(スレッド起動、COM 初期化) + +**利点:** +- `import device_manager` は高速 +- アプリケーション起動時のレスポンス向上 +- 使用しない機能のリソースを消費しない + +### 2. COM イベント vs ポーリング + +**COM イベント:** +- リアルタイム検知(即座に反応) +- CPU 使用率が低い(イベント待機) +- Windows 専用 + +**ポーリング:** +- 最大 20 秒の遅延(10 回 × 2 秒) +- CPU 使用率がやや高い(定期的な `update()` 呼び出し) +- クロスプラットフォーム + +**ハイブリッド方式:** +- COM が利用可能ならイベント駆動 +- COM が失敗またはポーリングにフォールバック + +### 3. デバイス情報のキャッシング + +**戦略:** +```python +self.mic_devices # キャッシュ +self.prev_mic_devices # 前回の状態 +``` + +**利点:** +- `getMicDevices()` は `update()` を呼ばない(高速) +- 変更検出が効率的(差分のみ処理) + +### 4. ポーリングの最適化 + +**初回待機(2 秒):** +```python +sleep(2) +``` +- デバイス接続後の不安定期間を回避 +- デバイスドライバーの初期化を待つ + +**最大 10 回ポーリング:** +```python +for _ in range(10): + self.update() + if self.checkUpdate(): + break # 変更検出後は即座に終了 + sleep(2) +``` +- 不要なポーリングを削減 +- 変更検出後は即座に次の処理へ + +--- + +## テストシナリオ + +### 1. 初期化テスト + +**ケース:** +- PyAudio が利用可能 +- PyAudio が利用不可(非 Windows 環境) +- デバイスが1つもない環境 + +**確認項目:** +- `_initialized` フラグが `True` になるか +- デバイス情報がデフォルト値または実デバイスで設定されているか +- エラーが適切にログされているか + +### 2. デバイス検出テスト + +**ケース:** +- 複数のホスト API(MME、WASAPI 等) +- 複数のマイクデバイス +- WASAPI ループバックデバイス + +**確認項目:** +- すべてのデバイスが検出されるか +- デフォルトデバイスが正しく識別されるか +- ループバックデバイスが正しく識別されるか + +### 3. 変更検出テスト + +**ケース:** +- デフォルトデバイスの変更 +- デバイスの接続・切断 +- ホスト API の変更 + +**確認項目:** +- 変更が正しく検出されるか +- 適切なフラグが設定されるか +- コールバックが呼び出されるか + +### 4. 監視スレッドテスト + +**ケース:** +- 監視の起動・停止 +- デバイス変更時の動作 +- エラー発生時の継続性 + +**確認項目:** +- スレッドが正しく起動・停止するか +- デバイス変更が検知されるか +- エラー発生時もスレッドが継続するか + +### 5. 自動デバイス選択テスト + +**ケース:** +- デフォルトデバイスの変更 +- デバイスの接続中に音声認識が動作中 +- コールバック内でエラーが発生 + +**確認項目:** +- デバイス変更前に処理が停止されるか +- デバイス変更後に処理が再開されるか +- エラーが分離されるか + +--- + +## 制限事項 + +### 1. Windows 依存機能 + +**問題:** COM イベント監視と WASAPI ループバックは Windows 専用 + +**影響:** +- 非 Windows 環境ではポーリングのみ +- リアルタイム性が低下 +- ループバックデバイスが利用不可 + +**緩和策:** +- グレースフルデグレード(デフォルト値を返却) +- プラットフォーム固有のコードを分離 + +### 2. デバイス名の曖昧性 + +**問題:** デバイス名に特殊文字やロケール依存の名前が含まれる + +**影響:** +- 名前による比較が不正確になる可能性 +- ループバックデバイスのマッチングが失敗する可能性 + +**緩和策:** +- `index` による識別も併用 +- 部分一致でループバックデバイスを検索 + +### 3. ポーリング遅延 + +**問題:** 最大 20 秒の遅延が発生する可能性 + +**影響:** +- デバイス変更の検知が遅れる +- ユーザー体験の低下 + +**緩和策:** +- COM イベント監視を優先使用 +- ポーリング間隔を短縮(2 秒) + +### 4. エラーの握りつぶし + +**問題:** 多くのエラーがログに記録されるのみで例外が投げられない + +**影響:** +- デバッグが困難 +- エラーの発生に気づきにくい + +**緩和策:** +- 詳細なエラーログ(`errorLogging()`) +- 重要なエラーは status を返却(future work) + +--- + +## 今後の改善案 + +### 1. クロスプラットフォーム対応の強化 + +**Linux (PulseAudio / ALSA):** +```python +# PulseAudio の D-Bus API でデバイス監視 +# ALSA の udev イベントでデバイス変更を検知 +``` + +**macOS (Core Audio):** +```python +# Core Audio の kAudioDevicePropertyDataSource 監視 +# IOKit でデバイスイベントを検知 +``` + +### 2. デバイス識別の改善 + +**問題:** 名前のみによる識別は不安定 + +**解決策:** +```python +device_id = { + "index": device["index"], + "name": device["name"], + "host": host["name"], + "unique_id": device.get("uniqueDeviceID", "") # WASAPI 固有 ID +} +``` + +### 3. 非同期化(asyncio) + +**問題:** スレッド管理の複雑性 + +**解決策:** +```python +async def monitoring_async(self): + while self.monitoring_flag: + await asyncio.sleep(2) + await self.update_async() + if self.checkUpdate(): + await self.noticeUpdateDevices_async() +``` + +**利点:** +- スレッド管理が不要 +- エラーハンドリングが統一 +- パフォーマンスの向上 + +### 4. イベントログの記録 + +**問題:** デバイス変更の履歴が残らない + +**解決策:** +```python +device_change_history = [] + +def log_device_change(event_type, device_info): + device_change_history.append({ + "timestamp": datetime.now(), + "event": event_type, + "device": device_info + }) +``` + +**利点:** +- デバッグが容易 +- ユーザーサポートの向上 + +### 5. 設定の永続化 + +**問題:** 選択されたデバイスが再起動後に失われる + +**解決策:** +```python +# config.py に保存 +config.SELECTED_MIC_DEVICE_ID = { + "host": "Windows WASAPI", + "name": "Microphone (Realtek)", + "unique_id": "{0.0.0.00000000}.{...}" +} + +# 起動時に復元 +def restore_selected_device(): + saved_id = config.SELECTED_MIC_DEVICE_ID + current_devices = device_manager.getMicDevices() + # unique_id でマッチング +``` + +--- + +## 関連ファイル + +- **controller.py** - デバイス管理のコールバックを登録 +- **model.py** - デバイス情報を使用して音声認識を開始 +- **config.py** - デバイス選択の設定を保存 +- **utils.py** - エラーロギング関数 + +--- + +## コーディング規約への準拠 + +### 命名規則 + +- クラス名: `DeviceManager`, `Client` (PascalCase) +- メソッド名: `startMonitoring`, `getMicDevices` (snake_case) +- 変数名: `mic_devices`, `default_mic_device` (snake_case) +- 定数: 使用していない(`config.py` で管理) + +### 型注釈 + +**現状:** +```python +def init(self) -> None: + self.mic_devices: Dict[str, List[Dict[str, Any]]] = {...} +``` + +**改善案:** +```python +DeviceInfo = Dict[str, Any] +DeviceList = List[DeviceInfo] +HostDeviceMap = Dict[str, DeviceList] + +def init(self) -> None: + self.mic_devices: HostDeviceMap = {...} +``` + +### Docstring + +**現状:** 一部のメソッドのみ docstring あり + +**改善案:** +```python +def getMicDevices(self) -> Dict[str, List[Dict[str, Any]]]: + """Get the list of microphone devices grouped by host API. + + Returns: + A dict mapping host names (e.g., "Windows WASAPI") to lists of device info dicts. + Each device dict contains keys like "index", "name", "maxInputChannels", etc. + If no devices are available, returns {"NoHost": [{"index": -1, "name": "NoDevice"}]}. + """ +``` + +--- + +## まとめ + +`device_manager.py` は VRCT のデバイス管理機能を提供する重要なモジュールであり、以下の特徴を持つ: + +1. **シングルトンパターン:** アプリケーション全体で1つのインスタンスのみ +2. **遅延初期化:** import 時のパフォーマンス低下を回避 +3. **プラットフォーム対応:** Windows で完全な機能、非 Windows でもグレースフルデグレード +4. **リアルタイム監視:** COM イベントとポーリングのハイブリッド方式 +5. **コールバックパターン:** 柔軟なイベント通知機構 +6. **防御的プログラミング:** エラーが発生してもクラッシュしない + +このモジュールは自動デバイス選択機能の中核として動作し、ユーザーがデバイスを切り替えた際に音声認識を自動的に再開することで、シームレスな体験を提供する。 diff --git a/src-python/docs/diagrams.md b/src-python/docs/diagrams.md deleted file mode 100644 index 71b42975..00000000 --- a/src-python/docs/diagrams.md +++ /dev/null @@ -1,51 +0,0 @@ -# システム図 - -以下はシステム構成の概要(Mermaid シーケンス図とテキスト版の両方)です。Mermaid がサポートされているビューアでは下のシーケンス図が描画されます。 - -```mermaid -sequenceDiagram - participant GUI as GUI (stdin/stdout) - participant Main as mainloop - participant Controller as Controller - participant Model as Model - participant Recorder as Recorder - participant Transcriber as Transcriber - participant Translator as Translator - participant Overlay as Overlay - participant OSC as OSC - participant WS as WebSocket - - GUI->>Main: send JSON endpoint - Main->>Controller: dispatch - Controller->>Model: startMicTranscript(callback) - Recorder->>Transcriber: audio data - Transcriber->>Controller: result (text, language) - Controller->>Translator: getInputTranslate(text) - Translator-->>Controller: translations - Controller->>Overlay: updateOverlay(translation) - Controller->>OSC: sendMessage(osc_message) - Controller->>WS: websocketSendMessage(event) - Controller-->>GUI: run(status, endpoint, result) -``` - -## テキスト版(簡易) - -Main process (`mainloop.py`) - - stdin -> JSON コマンド -> Main.receiver -> queue - - Main.handler -> Controller (コマンド実行) - - run(status, endpoint, result) -> stdout (GUI に通知) - -Controller - - config (読み書き) - - model (起動/停止/アクション) - -Model サブシステム - - device_manager (デバイス列挙/監視) - - transcription (recorder -> transcriber) - - translation (Translator) - - overlay (OverlayImage -> Overlay) - - osc (OSCHandler) - - websocket (WebSocketServer) - -データフロー(代表): 録音 -> audio_queue -> AudioTranscriber -> Controller.micMessage -> Translator -> (OSC / Overlay / WebSocket / ログ) - diff --git a/src-python/docs/mainloop.md b/src-python/docs/mainloop.md new file mode 100644 index 00000000..4536d49e --- /dev/null +++ b/src-python/docs/mainloop.md @@ -0,0 +1,346 @@ +# mainloop.py 設計書 + +## 概要 + +`mainloop.py` は VRCT アプリケーションのバックエンドエントリーポイントであり、stdin/stdout を介したフロントエンド(Tauri/React UI)との通信を担当する。JSON ベースのリクエスト/レスポンスプロトコルを実装し、複数のワーカースレッドによる並列処理と排他制御を提供する。 + +## 主要コンポーネント + +### 1. グローバル変数 + +#### `run_mapping` (dict) +フロントエンドへの通知用エンドポイントマッピング。Controllerが `run()` コールバックを通じてフロントエンドに状態変化を通知する際に使用。 + +**主要なエンドポイント:** +- `/run/enable_translation` - 翻訳機能の有効/無効状態 +- `/run/transcription_mic_message` - マイク音声認識結果 +- `/run/transcription_speaker_message` - スピーカー音声認識結果 +- `/run/error_*` - 各種エラー通知 +- `/run/initialization_complete` - 初期化完了通知 + +#### `mapping` (dict) +フロントエンドからのリクエストを処理する関数マッピング。各エンドポイントに対して: +- `status`: ロック状態(True: 処理可能, False: ロック中) +- `variable`: 実行する Controller メソッド + +**エンドポイント分類:** +- `/get/data/*` - 設定値の取得(初期化時に使用) +- `/set/data/*` - 設定値の更新 +- `/set/enable/*` - 機能の有効化 +- `/set/disable/*` - 機能の無効化 +- `/run/*` - アクション実行(メッセージ送信、ダウンロード等) + +#### `init_mapping` (dict) +初期化時に実行される `/get/data/*` エンドポイントのサブセット。アプリケーション起動時に全設定値をフロントエンドに送信するために使用。 + +### 2. Mainクラス + +#### コンストラクタ `__init__(controller_instance, mapping_data, worker_count)` + +**パラメータ:** +- `controller_instance`: Controller インスタンス +- `mapping_data`: エンドポイントマッピング辞書 +- `worker_count`: ハンドラワーカースレッド数(デフォルト: 3) + +**初期化処理:** +1. リクエストキュー (`Queue[Tuple[str, Any]]`) の作成 +2. 停止イベント (`Event`) の作成 +3. エンドポイント別 Lock の生成: + - `/set/enable/xxx` と `/set/disable/xxx` を `/lock/set/xxx` に正規化 + - 同一機能の有効化/無効化リクエストが競合しないよう排他制御 + +**正規化ロジックの例:** +```python +"/set/enable/translation" → "/lock/set/translation" +"/set/disable/translation" → "/lock/set/translation" +# 両方が同じロックを共有 → 排他的に実行される +``` + +#### `receiver()` メソッド + +**責務:** stdin から JSON リクエストを読み取り、キューに投入 + +**処理フロー:** +1. `sys.stdin.readline()` でブロッキング読み取り +2. JSON パース (`json.loads()`) +3. エンドポイントとデータを抽出 +4. データが存在する場合は Base64 デコード (`encodeBase64()`) +5. ログ出力 (`printLog()`) +6. キューに投入 `self.queue.put((endpoint, data))` + +**エラー処理:** +- JSON パースエラー: ログ出力して継続 +- EOF 到達: 0.1秒待機して再試行 +- その他の例外: `errorLogging()` でトレースバック記録 + +**スレッド:** デーモンスレッド `main_receiver` として起動 + +#### `handler()` メソッド + +**責務:** キューからリクエストを取り出し、適切なロックを取得して処理 + +**処理フロー:** +1. キューから `(endpoint, data)` を取得(0.5秒タイムアウト) +2. エンドポイントを正規化キーに変換 +3. 対応する Lock を取得試行(非ブロッキング) + - 取得成功 → 処理実行 → ロック解放 + - 取得失敗 → 0.05秒待機して再キュー +4. `_call_handler(endpoint, data)` を呼び出し +5. レスポンスを stdout に出力 (`printResponse()`) + +**排他制御の意義:** +- 例: 翻訳機能の有効化中に無効化リクエストが来た場合、無効化は待機 +- 異なる機能のリクエストは並列実行可能 + +**再キューロジック:** +- status == 423 (Locked): 0.1秒待機して再キュー +- これにより、初期化中の設定変更リクエストが適切にリトライされる + +**ワーカー数:** `worker_count` 個のスレッド `main_handler_0`, `main_handler_1`, ... として起動 + +#### `_call_handler(endpoint, data)` メソッド + +**責務:** 実際のビジネスロジック実行 + +**処理フロー:** +1. `mapping` から対応するハンドラを取得 +2. エンドポイントが存在しない → status 404 +3. ハンドラの `status` が False → status 423 (Locked) +4. ハンドラの `variable` 関数を実行 → `response = handler["variable"](data)` +5. 0.2秒待機(処理安定化のため) +6. status と result を抽出して返却 + +**エラー処理:** +- 例外発生時: `errorLogging()` でトレースバック記録、status 500 を返却 + +#### `start()` / `stop(wait)` メソッド + +**start():** +- `startReceiver()` - stdin 読み取りスレッド起動 +- `startHandler()` - ハンドラワーカースレッド起動 + +**stop(wait):** +- `_stop_event.set()` - 全スレッドに停止シグナル送信 +- 各スレッドを `join(timeout=remaining)` で待機(最大 `wait` 秒) + +### 3. 初期化シーケンス + +**`if __name__ == "__main__":` ブロック:** + +1. `main_instance` 作成 +2. `startReceiver()` - stdin リスニング開始 +3. `startHandler()` - リクエスト処理開始 +4. **Watchdog 設定:** + - `controller.setWatchdogCallback(main_instance.stop)` + - Watchdog がタイムアウトした場合にプロセス全体を停止 +5. **Controller 初期化:** + - `controller.init()` + - Model の遅延初期化、デバイス列挙、ネットワーク接続チェック + - `init_mapping` のすべてのエンドポイントを実行して初期設定をフロントエンドに送信 +6. **マッピングのアンロック:** + - すべての `mapping[key]["status"]` を True に設定 + - これにより初期化中だった機能が利用可能になる +7. `main_instance.start()` - 実質的には何もしない(既に起動済み) + +## 並列処理とスレッドセーフティ + +### スレッド構成 + +| スレッド名 | 役割 | 生存期間 | +|-----------|------|---------| +| `main_receiver` | stdin からの JSON 読み取り | プロセス終了まで | +| `main_handler_0` ~ `main_handler_N` | リクエスト処理ワーカー | プロセス終了まで | + +### 同期メカニズム + +1. **キュー (`Queue`):** + - スレッドセーフな FIFO キュー + - receiver → handler への通信チャネル + +2. **エンドポイント別 Lock (`dict[str, Lock]`):** + - 同一リソースへの競合アクセスを防止 + - 正規化キーによる enable/disable ペアの統合 + +3. **停止イベント (`Event`):** + - グレースフルシャットダウン用のシグナル + +### デッドロック回避 + +- **非ブロッキング Lock 取得:** `lock.acquire(blocking=False)` +- **失敗時の再キュー:** ロック取得失敗時は即座に諦めて再キュー +- **タイムアウト付きキュー取得:** `queue.get(timeout=0.5)` で無限待機を回避 + +## プロトコル仕様 + +### リクエストフォーマット (stdin) + +```json +{ + "endpoint": "/set/data/transparency", + "data": "ODU=" // Base64 encoded: "85" +} +``` + +**フィールド:** +- `endpoint`: 実行するエンドポイント(必須) +- `data`: パラメータ(オプション、Base64 エンコード) + +### レスポンスフォーマット (stdout) + +```json +{ + "status": 200, + "endpoint": "/set/data/transparency", + "result": 85 +} +``` + +**フィールド:** +- `status`: HTTP ステータスコード相当 + - 200: 成功 + - 400: バリデーションエラー + - 404: 無効なエンドポイント + - 423: ロック中(リトライされる) + - 500: 内部エラー +- `endpoint`: リクエストされたエンドポイント +- `result`: 処理結果(型はエンドポイントに依存) + +### ログフォーマット (stdout) + +```json +{ + "status": 348, // 専用ステータスコード + "log": "setSelectedTabNo", + "data": "1" +} +``` + +## エラーハンドリング + +### 1. JSON パースエラー +- **発生箇所:** `receiver()` の `json.loads()` +- **処理:** `errorLogging()` でトレースバック記録、リクエストをスキップ + +### 2. ハンドラ実行エラー +- **発生箇所:** `_call_handler()` の `handler["variable"](data)` +- **処理:** + - `errorLogging()` でトレースバック記録 + - status 500 と "Internal error" を返却 + - プロセスは継続 + +### 3. JSON シリアライズエラー +- **発生箇所:** `printResponse()` の `json.dumps()` +- **処理:** + - エラーログに詳細を記録 + - フォールバック JSON を出力(status 500) + - プロセスは継続 + +### 4. EOF (stdin 終了) +- **発生箇所:** `receiver()` の `readline()` +- **処理:** 0.1秒待機して再試行(フロントエンドの再起動待ち) + +## パフォーマンス最適化 + +### 1. 複数ワーカースレッド +- デフォルト3スレッドで並列処理 +- CPU バウンドな処理(翻訳、文字起こし)を効率化 + +### 2. 非ブロッキングロック +- ロック競合時に即座に再キュー +- スレッドのブロッキング時間を最小化 + +### 3. 処理安定化待機 +- 各ハンドラ実行後に 0.2秒待機 +- 連続リクエストによる競合状態を回避 + +## 制限事項 + +### 1. 初期化中の制限 +- `mapping[key]["status"] = False` の間はリクエストが 423 でリトライされる +- 初期化完了まで最大数秒のレイテンシが発生 + +### 2. stdin の単方向性 +- stdin → キュー → ハンドラの一方向フロー +- 複数のフロントエンドからの同時接続は非対応 + +### 3. シリアル実行の保証 +- 同一エンドポイントのリクエストは排他的に実行されるが、 +- 異なるエンドポイントは並列実行される可能性がある +- 依存関係のある操作は呼び出し側で順序制御が必要 + +## デバッグとトラブルシューティング + +### ログファイル + +| ファイル名 | 内容 | +|-----------|------| +| `process.log` | 全リクエスト/レスポンスの記録 | +| `error.log` | 例外トレースバック | + +### デバッグ手法 + +1. **リクエストトレース:** + - `process.log` で endpoint と data を確認 + - Base64 デコードは `base64.b64decode(data).decode('utf-8')` で手動実行 + +2. **ロック競合の検出:** + - 同一エンドポイントで status 423 が頻発する場合 + - `_canonical_lock_key()` の正規化ロジックを確認 + +3. **パフォーマンス分析:** + - 各リクエストの処理時間は status 前後のタイムスタンプから算出 + - worker_count を増やして並列度を調整 + +## 今後の拡張性 + +### 1. 双方向通信 +- WebSocket への移行でリアルタイム通知を改善 +- stdin/stdout は互換性のため維持 + +### 2. 動的ワーカー数調整 +- キューの深さに応じてスレッド数を自動調整 +- CPU 負荷に応じた適応的なスケーリング + +### 3. 優先度キュー +- 重要なリクエスト(エラー通知等)を優先処理 +- `queue.PriorityQueue` への移行 + +## 関連ファイル + +- `controller.py` - ビジネスロジック実装 +- `model.py` - 機能ファサード +- `utils.py` - ログとユーティリティ +- `config.py` - 設定管理 + +## コーディング規約 + +本ファイルは以下の規約に従う: +- PEP 8 スタイルガイド +- 型ヒント (`typing` モジュール) +- Docstring は Google スタイル +- エラーハンドリングは防御的に実装 + +## テストシナリオ + +### 1. 基本動作テスト +```python +# stdin に JSON を送信 +echo '{"endpoint": "/get/data/version", "data": null}' | python mainloop.py +# 期待される出力: {"status": 200, "endpoint": "/get/data/version", "result": "1.0.0"} +``` + +### 2. 並列リクエストテスト +- 複数の設定変更リクエストを同時送信 +- すべてが正常に処理されることを確認 + +### 3. ロック競合テスト +- 翻訳の有効化と無効化を連続送信 +- 両方が排他的に実行されることを確認 + +### 4. エラー回復テスト +- 不正なJSON、無効なエンドポイント、不正なデータを送信 +- プロセスがクラッシュせずエラーレスポンスを返すことを確認 + +## まとめ + +`mainloop.py` は VRCT の中核となる通信レイヤーであり、stdin/stdout を介したフロントエンドとの JSON ベースプロトコルを実装する。複数のワーカースレッドと細粒度のロックにより、高い並列性と排他制御を両立させている。初期化シーケンスとエラーハンドリングは堅牢に設計されており、プロセスの安定稼働を保証する。 diff --git a/src-python/docs/model.md b/src-python/docs/model.md new file mode 100644 index 00000000..41e3ee3f --- /dev/null +++ b/src-python/docs/model.md @@ -0,0 +1,1277 @@ +# model.py 設計書 + +## 概要 + +`model.py` は VRCT アプリケーションのビジネスロジックファサードとして機能し、音声認識、翻訳、オーバーレイ表示、OSC通信、WebSocket通信など、すべてのサブシステムへの統一されたインターフェースを提供する。シングルトンパターンで実装され、重い初期化処理を遅延実行することで、アプリケーションの起動時間を短縮している。 + +## アーキテクチャ上の位置づけ + +``` +┌─────────────┐ +│controller.py│ (Business Logic Control Layer) +└──────┬──────┘ + │ Facade Pattern +┌──────▼──────┐ +│ model.py │ ◄── このファイル +└──────┬──────┘ + │ Aggregation & Delegation +┌──────▼────────────────────────────────┐ +│ Subsystems │ +│ - Translator │ +│ - AudioTranscriber │ +│ - Overlay / OverlayImage │ +│ - OSCHandler │ +│ - WebSocketServer │ +│ - Transliterator │ +│ - Watchdog │ +│ - DeviceManager (via device_manager) │ +└───────────────────────────────────────┘ +``` + +## 主要コンポーネント + +### 1. threadFnc クラス + +**責務:** 関数を繰り返し実行するスレッドラッパー + +**特徴:** +- デーモンスレッドとして動作 +- ループ制御(停止・一時停止・再開)機能を提供 +- 終了時のクリーンアップ関数をサポート + +**メソッド:** + +#### `__init__(fnc, end_fnc=None, daemon=True, *args, **kwargs)` + +**パラメータ:** +- `fnc`: 繰り返し実行する関数 +- `end_fnc`: スレッド終了時に実行する関数(オプション) +- `daemon`: デーモンフラグ(デフォルト: True) +- `*args, **kwargs`: `fnc` に渡す引数 + +#### `stop() -> None` +ループを停止し、スレッドを終了させる。 + +#### `pause() -> None` +ループを一時停止する(関数の実行を停止)。 + +#### `resume() -> None` +一時停止したループを再開する。 + +#### `run() -> None` +スレッドのメインループ。`self.loop` が True の間、`self.fnc()` を繰り返し呼び出す。 + +**使用例:** +```python +def print_message(): + print("Hello") + sleep(1) + +def cleanup(): + print("Thread ended") + +th = threadFnc(print_message, end_fnc=cleanup) +th.start() +# ... しばらく実行 ... +th.stop() +th.join() +``` + +--- + +### 2. Model クラス + +**責務:** アプリケーションのすべてのサブシステムへのファサードインターフェース + +**パターン:** シングルトン(`__new__` で制御) + +**初期化戦略:** 遅延初期化(Lazy Initialization) +- `__new__`: インスタンスの生成のみ(軽量) +- `init()`: 重い初期化処理(明示的な呼び出しが必要) +- `ensure_initialized()`: 初期化が必要なメソッドで自動的に呼び出される + +--- + +### 3. 初期化メソッド + +#### `__new__(cls) -> Model` + +**責務:** シングルトンインスタンスの生成 + +**処理:** +1. `cls._instance` が None の場合のみ新規インスタンスを生成 +2. `_inited` フラグを False に設定(実際の初期化は未実施) +3. 既存のインスタンスがあればそれを返却 + +**重要:** このメソッドでは重い初期化を行わない(import 時のパフォーマンス向上) + +#### `init() -> None` + +**責務:** すべてのサブシステムの初期化 + +**処理:** +1. **初期化済みチェック:** `_inited` フラグが True なら何もしない +2. **属性の初期化:** + ```python + self.logger = None + self.mic_audio_queue = None + self.mic_mute_status = None + self.previous_send_message = "" + self.previous_receive_message = "" + ``` +3. **サブシステムの初期化:** + - `Translator()`: 翻訳エンジン + - `KeywordProcessor()`: 禁止ワードフィルター + - `Overlay()`: オーバーレイシステム + - `OverlayImage()`: オーバーレイ画像生成 + - `Transliterator()`: 音訳(ひらがな・ローマ字変換) + - `Watchdog()`: プロセス監視 + - `OSCHandler()`: OSC通信 + - `WebSocketServer()`: WebSocket通信 +4. **コールバック関数の初期化:** + ```python + self.check_mic_energy_fnc: Callable[[float], None] = lambda v: None + self.check_speaker_energy_fnc: Callable[[float], None] = lambda v: None + ``` +5. **初期化完了フラグ:** `_inited = True` + +#### `ensure_initialized() -> None` + +**責務:** 初期化が未実施の場合に `init()` を呼び出す + +**使用箇所:** 初期化が必要なすべての public メソッド + +**エラーハンドリング:** +```python +try: + self.init() +except Exception: + errorLogging() +``` + +--- + +### 4. 翻訳機能 + +#### モデルウェイト管理 + +##### `checkTranslatorCTranslate2ModelWeight(weight_type: str) -> bool` +指定されたモデルウェイトが存在するかチェック。 + +**パラメータ:** +- `weight_type`: "tiny", "small", "medium", "large" 等 + +**戻り値:** モデルが存在する場合 True + +##### `downloadCTranslate2ModelWeight(weight_type, callback=None, end_callback=None) -> bool` + +**責務:** CTranslate2 モデルウェイトのダウンロード + +**パラメータ:** +- `weight_type`: モデルタイプ +- `callback`: 進捗通知用コールバック(`progress: float` を受け取る) +- `end_callback`: 完了時のコールバック + +**実装:** `downloadCTranslate2Weight()` ユーティリティ関数に委譲 + +##### `downloadCTranslate2ModelTokenizer(weight_type) -> bool` +トークナイザーファイルのダウンロード。 + +#### 翻訳モデル制御 + +##### `changeTranslatorCTranslate2Model() -> None` + +**責務:** 翻訳モデルの変更・再ロード + +**処理:** +```python +self.translator.changeCTranslate2Model( + path=config.PATH_LOCAL, + model_type=config.CTRANSLATE2_WEIGHT_TYPE, + device=config.SELECTED_TRANSLATION_COMPUTE_DEVICE["device"], + device_index=config.SELECTED_TRANSLATION_COMPUTE_DEVICE["device_index"], + compute_type=config.SELECTED_TRANSLATION_COMPUTE_TYPE +) +``` + +**VRAMエラー:** `ValueError("VRAM_OUT_OF_MEMORY")` を送出する可能性がある + +##### `isLoadedCTranslate2Model() -> bool` +CTranslate2 モデルがロード済みかチェック。 + +##### `isChangedTranslatorParameters() -> bool` +翻訳パラメータが変更されたかチェック。 + +##### `setChangedTranslatorParameters(is_changed: bool) -> None` +翻訳パラメータ変更フラグを設定。 + +#### DeepL 認証 + +##### `authenticationTranslatorDeepLAuthKey(auth_key: str) -> bool` + +**責務:** DeepL API キーの検証 + +**処理:** `translator.authenticationDeepLAuthKey()` に委譲 + +**戻り値:** 認証成功時 True + +#### 翻訳実行 + +##### `getTranslate(translator_name, source_language, target_language, target_country, message) -> Tuple[str, bool]` + +**責務:** メッセージの翻訳 + +**パラメータ:** +- `translator_name`: "CTranslate2", "DeepL", "DeepL_API" 等 +- `source_language`: 元言語("ja", "en" 等) +- `target_language`: 翻訳先言語 +- `target_country`: 翻訳先国(方言対応用) +- `message`: 翻訳するテキスト + +**戻り値:** +- `translation`: 翻訳結果(文字列) +- `success_flag`: 成功時 True + +**エラーハンドリング:** +```python +translation = self.translator.translate(...) +if isinstance(translation, str): + success_flag = True +else: + # 翻訳失敗時のリトライロジック + while True: + # フェールセーフ処理 +``` + +##### `getInputTranslate(message, source_language=None) -> Tuple[list, list]` + +**責務:** 送信メッセージの翻訳(複数言語対応) + +**処理:** +1. `config.SELECTED_TRANSLATION_ENGINES[config.SELECTED_TAB_NO]` で翻訳エンジンを取得 +2. `config.SELECTED_TARGET_LANGUAGES` で翻訳先言語リストを取得 +3. 有効な各言語について `getTranslate()` を呼び出し + +**戻り値:** +- `translations`: 翻訳結果のリスト +- `success_flags`: 各翻訳の成功フラグのリスト + +##### `getOutputTranslate(message, source_language=None) -> Tuple[list, list]` + +**責務:** 受信メッセージの翻訳(単一言語) + +**処理:** `getInputTranslate()` と同様だが、翻訳先が自分の言語(1つ)のみ + +--- + +### 5. 音声認識機能 + +#### Whisper モデル管理 + +##### `checkTranscriptionWhisperModelWeight(weight_type: str) -> bool` +Whisper モデルウェイトの存在確認。 + +##### `downloadWhisperModelWeight(weight_type, callback=None, end_callback=None) -> bool` +Whisper モデルウェイトのダウンロード。 + +#### マイク音声認識 + +##### `startMicTranscript(fnc: Callable[[dict], None]) -> None` + +**責務:** マイク音声認識の開始 + +**パラメータ:** +- `fnc`: 認識結果を受け取るコールバック関数 + +**処理フロー:** +1. **デバイス取得:** + ```python + mic_host_name = config.SELECTED_MIC_HOST + mic_device_name = config.SELECTED_MIC_DEVICE + mic_device_list = device_manager.getMicDevices().get(mic_host_name, [...]) + selected_mic_device = [device for device in mic_device_list if device["name"] == mic_device_name] + ``` +2. **デバイス検証:** + - デバイスがない場合、`fnc({"text": False, "language": None})` を呼び出して終了 +3. **音声キューの作成:** + ```python + self.mic_audio_queue = Queue() + ``` +4. **レコーダーの初期化:** + ```python + self.mic_audio_recorder = SelectedMicEnergyAndAudioRecorder( + device=mic_device, + energy_threshold=config.MIC_THRESHOLD, + dynamic_energy_threshold=config.MIC_AUTOMATIC_THRESHOLD, + phrase_time_limit=config.MIC_RECORD_TIMEOUT, + ) + self.mic_audio_recorder.recordIntoQueue(self.mic_audio_queue, None) + ``` +5. **文字起こし器の初期化:** + ```python + self.mic_transcriber = AudioTranscriber( + speaker=False, + source=self.mic_audio_recorder.source, + phrase_timeout=config.MIC_PHRASE_TIMEOUT, + max_phrases=config.MIC_MAX_PHRASES, + transcription_engine=config.SELECTED_TRANSCRIPTION_ENGINE, + root=config.PATH_LOCAL, + whisper_weight_type=config.WHISPER_WEIGHT_TYPE, + device=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device"], + device_index=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device_index"], + compute_type=config.SELECTED_TRANSCRIPTION_COMPUTE_TYPE, + ) + ``` +6. **文字起こしスレッドの起動:** + ```python + def sendMicTranscript(): + # キューから音声データを取得 + # AudioTranscriber で文字起こし + # fnc() で結果を送信 + + def endMicTranscript(): + # クリーンアップ処理 + + self.mic_print_transcript = threadFnc(sendMicTranscript, end_fnc=endMicTranscript) + self.mic_print_transcript.start() + ``` +7. **ミュート状態の同期:** + ```python + self.changeMicTranscriptStatus() + ``` + +##### `resumeMicTranscript() -> None` + +**責務:** 一時停止したマイク音声認識の再開 + +**処理:** +1. 音声キューをクリア +2. レコーダーを再開: `self.mic_audio_recorder.resume()` + +##### `pauseMicTranscript() -> None` + +**責務:** マイク音声認識の一時停止 + +**処理:** `self.mic_audio_recorder.pause()` + +##### `changeMicTranscriptStatus() -> None` + +**責務:** VRChat のマイクミュート状態に応じて音声認識を制御 + +**処理:** +```python +if config.VRC_MIC_MUTE_SYNC is True: + match self.mic_mute_status: + case True: + self.pauseMicTranscript() + case False: + self.resumeMicTranscript() + case None: + self.resumeMicTranscript() # 不明な場合は一時停止しない +else: + self.resumeMicTranscript() +``` + +##### `stopMicTranscript() -> None` + +**責務:** マイク音声認識の停止とリソース解放 + +**処理:** +1. 文字起こしスレッドの停止 +2. レコーダーの再開(一時停止中の場合)と停止 +3. インスタンスの破棄 + +**VRAMエラー検出:** + +##### `detectVRAMError(error: Exception) -> Tuple[bool, Optional[str]]` + +**責務:** VRAM不足エラーの検出 + +**処理:** +```python +error_str = str(error) +if isinstance(error, ValueError) and len(error.args) > 0 and error.args[0] == "VRAM_OUT_OF_MEMORY": + return True, error_str +if "CUDA out of memory" in error_str or "CUBLAS_STATUS_ALLOC_FAILED" in error_str: + return True, error_str +return False, None +``` + +**使用箇所:** +- 翻訳実行時 +- 音声認識開始時 + +#### スピーカー音声認識 + +以下のメソッドはマイク音声認識と同様の構造: +- `startSpeakerTranscript(fnc)` +- `stopSpeakerTranscript()` + +**相違点:** +- `speaker=True` で AudioTranscriber を初期化 +- `SelectedSpeakerEnergyAndAudioRecorder` を使用 + +#### エネルギーレベル監視 + +##### `startCheckMicEnergy(fnc: Optional[Callable[[float], None]] = None) -> None` + +**責務:** マイクの音量レベル監視の開始 + +**処理:** +1. コールバック関数を設定: `self.check_mic_energy_fnc = fnc` +2. マイクデバイスを取得 +3. エネルギーレコーダーを初期化: + ```python + mic_energy_queue = Queue() + self.mic_energy_recorder = SelectedMicEnergyRecorder(mic_device) + self.mic_energy_recorder.recordIntoQueue(mic_energy_queue) + ``` +4. エネルギー送信スレッドを起動: + ```python + def sendMicEnergy(): + if not mic_energy_queue.empty(): + energy = mic_energy_queue.get() + self.check_mic_energy_fnc(energy) + sleep(0.01) + + self.mic_energy_plot_progressbar = threadFnc(sendMicEnergy) + self.mic_energy_plot_progressbar.start() + ``` + +##### `stopCheckMicEnergy() -> None` +エネルギー監視の停止とリソース解放。 + +**対応するスピーカー用メソッド:** +- `startCheckSpeakerEnergy(fnc)` +- `stopCheckSpeakerEnergy()` + +--- + +### 6. オーバーレイ機能 + +#### 画像生成 + +##### `createOverlayImageSmallLog(message, your_language, translation, target_language) -> object` + +**責務:** 小さなログウィンドウ用の画像生成 + +**パラメータ:** +- `message`: 元のメッセージ(オプション) +- `your_language`: 元の言語(オプション) +- `translation`: 翻訳結果のリスト +- `target_language`: 翻訳先言語の辞書(オプション) + +**処理:** +```python +target_language_list = [] +if isinstance(target_language, dict): + target_language_list = list(target_language.values()) +return self.overlay_image.createOverlayImageSmallLog( + message, your_language, translation, target_language_list +) +``` + +##### `createOverlayImageSmallMessage(message: str) -> object` + +**責務:** 小さなメッセージウィンドウ用の画像生成(単一言語) + +**処理:** +```python +ui_language = config.UI_LANGUAGE +convert_languages = { + "en": "Default", + "jp": "Japanese", + "ko": "Korean", + "zh-Hans": "Chinese Simplified", + "zh-Hant": "Chinese Traditional", +} +language = convert_languages.get(ui_language, "Default") +return self.overlay_image.createOverlayImageSmallLog(message, language) +``` + +##### `createOverlayImageLargeLog(message_type, message, your_language, translation, target_language=None) -> object` + +**責務:** 大きなログウィンドウ用の画像生成 + +**パラメータ:** +- `message_type`: "send" または "received" + +**処理:** `createOverlayImageSmallLog()` と同様 + +##### `createOverlayImageLargeMessage(message: str) -> object` + +**責務:** 大きなメッセージウィンドウ用の画像生成 + +**特殊処理:** +```python +overlay_image = OverlayImage(config.PATH_LOCAL) +for _ in range(2): + # 2回繰り返して画像を生成(理由は不明、バグ修正のため?) + overlay_image.createOverlayImageLargeLog("send", message, language) +return overlay_image.createOverlayImageLargeLog("send", message, language) +``` + +#### 表示制御 + +##### `clearOverlayImageSmallLog() -> None` +小さなログウィンドウをクリア。 + +##### `updateOverlaySmallLog(img: object) -> None` +小さなログウィンドウの画像を更新。 + +##### `updateOverlaySmallLogSettings() -> None` + +**責務:** 小さなログウィンドウの設定更新 + +**処理:** 設定の変更を検出し、オーバーレイに反映: +```python +size = "small" +if (self.overlay.settings[size]["x_pos"] != config.OVERLAY_SMALL_LOG_SETTINGS["x_pos"] or + # ... 他の設定項目 ...): + self.overlay.updateSettings(config.OVERLAY_SMALL_LOG_SETTINGS, size) +``` + +**設定項目:** +- 位置(x_pos, y_pos, z_pos) +- 回転(x_rotation, y_rotation, z_rotation) +- トラッカー(tracker) +- 表示時間(display_duration) +- フェードアウト時間(fadeout_duration) +- 透明度(opacity) +- UIスケーリング(ui_scaling) + +##### `clearOverlayImageLargeLog() -> None` +大きなログウィンドウをクリア。 + +##### `updateOverlayLargeLog(img: object) -> None` +大きなログウィンドウの画像を更新。 + +##### `updateOverlayLargeLogSettings() -> None` +大きなログウィンドウの設定更新(`updateOverlaySmallLogSettings()` と同様)。 + +#### オーバーレイシステム制御 + +##### `startOverlay() -> None` +オーバーレイシステムを起動(OpenVR の初期化)。 + +##### `shutdownOverlay() -> None` +オーバーレイシステムを終了(リソース解放)。 + +--- + +### 7. OSC 通信機能 + +#### 設定 + +##### `setOscIpAddress(ip_address: str) -> None` +VRChat への送信先 IP アドレスを設定。 + +##### `setOscPort(port: int) -> None` +OSC ポート番号を設定。 + +#### メッセージ送信 + +##### `oscStartSendTyping() -> None` +タイピング中の通知を送信(VRChat のチャットボックスにインジケーターが表示される)。 + +##### `oscStopSendTyping() -> None` +タイピング終了の通知を送信。 + +##### `oscSendMessage(message: str) -> None` + +**責務:** VRChat へメッセージを送信 + +**パラメータ:** +- `message`: 送信するテキスト + +**処理:** +```python +self.osc_handler.sendMessage( + message=message, + notification=config.NOTIFICATION_VRC_SFX +) +``` + +#### OSC 受信 + +##### `setMuteSelfStatus() -> None` +VRChat の現在のマイクミュート状態を取得。 + +##### `startReceiveOSC() -> None` + +**責務:** OSC パラメータの受信開始 + +**処理:** +```python +def changeHandlerMute(address, osc_arguments): + if config.ENABLE_TRANSCRIPTION_SEND is True: + self.mic_mute_status = osc_arguments[0] + self.changeMicTranscriptStatus() + +dict_filter_and_target = { + self.osc_handler.osc_parameter_muteself: changeHandlerMute, +} +self.osc_handler.setDictFilterAndTarget(dict_filter_and_target) +self.osc_handler.receiveOscParameters() +``` + +**監視パラメータ:** +- `/avatar/parameters/MuteSelf`: マイクミュート状態 + +##### `stopReceiveOSC() -> None` +OSC 受信を停止。 + +##### `getIsOscQueryEnabled() -> bool` +OSC Query 機能が有効かチェック。 + +--- + +### 8. 音訳機能 + +#### 音訳システム制御 + +##### `startTransliteration() -> None` +音訳システムを起動(`Transliterator` インスタンスを生成)。 + +##### `stopTransliteration() -> None` +音訳システムを停止(インスタンスを破棄)。 + +#### 音訳実行 + +##### `convertMessageToTransliteration(message, hiragana=True, romaji=True) -> list` + +**責務:** メッセージをひらがな・ローマ字に変換 + +**パラメータ:** +- `message`: 変換するテキスト +- `hiragana`: ひらがなを含める +- `romaji`: ローマ字を含める + +**処理:** +```python +if hiragana is False and romaji is False: + return [] + +keys_to_keep = {"orig"} +if hiragana: + keys_to_keep.add("hira") +if romaji: + keys_to_keep.add("hepburn") + +if self.transliterator is None: + self.startTransliteration() + +data_list = self.transliterator.analyze(message, use_macron=False) +filtered_list = [ + {key: value for key, value in item.items() if key in keys_to_keep} + for item in data_list +] +return filtered_list +``` + +**戻り値の例:** +```python +[ + {"orig": "こんにちは", "hira": "こんにちは", "hepburn": "konnichiwa"}, + {"orig": "世界", "hira": "せかい", "hepburn": "sekai"} +] +``` + +--- + +### 9. キーワードフィルター + +#### フィルター管理 + +##### `resetKeywordProcessor() -> None` +キーワードプロセッサをリセット(すべてのキーワードを削除)。 + +##### `addKeywords() -> None` +禁止ワードをキーワードプロセッサに追加。 + +**処理:** +```python +for f in config.MIC_WORD_FILTER: + self.keyword_processor.add_keyword(f) +``` + +#### フィルタリング + +##### `checkKeywords(message: str) -> bool` +メッセージに禁止ワードが含まれているかチェック。 + +**戻り値:** 禁止ワードが含まれている場合 True + +**実装:** +```python +return len(self.keyword_processor.extract_keywords(message)) != 0 +``` + +--- + +### 10. 重複検出 + +##### `detectRepeatSendMessage(message: str) -> bool` + +**責務:** 送信メッセージの重複検出 + +**処理:** +```python +repeat_flag = False +if self.previous_send_message == message: + repeat_flag = True +self.previous_send_message = message +return repeat_flag +``` + +##### `detectRepeatReceiveMessage(message: str) -> bool` +受信メッセージの重複検出(`detectRepeatSendMessage()` と同様)。 + +--- + +### 11. デバイス管理 + +#### マイクデバイス + +##### `getListMicHost() -> list` + +**責務:** マイクホストのリスト取得 + +**戻り値:** ["MME", "WASAPI", ...] 等 + +**処理:** +```python +try: + dm = device_manager.getMicDevices() + result = [host for host in dm.keys()] +except Exception: + errorLogging() + result = [] +return result +``` + +##### `getMicDefaultDevice() -> str` +選択されたホストのデフォルトマイクデバイス名を取得。 + +##### `getListMicDevice() -> list` +選択されたホストのマイクデバイス一覧を取得。 + +#### スピーカーデバイス + +##### `getListSpeakerDevice() -> list` +スピーカーデバイス一覧を取得。 + +**処理:** +```python +try: + sd = device_manager.getSpeakerDevices() + result = [device["name"] for device in sd] +except Exception: + errorLogging() + result = ["NoDevice"] +return result +``` + +--- + +### 12. 言語管理 + +##### `getListLanguageAndCountry() -> list` + +**責務:** 音声認識と翻訳の両方をサポートする言語・国のリスト取得 + +**処理:** +1. `transcription_lang` から音声認識サポート言語を取得 +2. `translation_lang` から翻訳サポート言語を取得 +3. 両方でサポートされている言語を抽出 +4. 各言語の国バリエーションを列挙 + +**戻り値の例:** +```python +[ + {"language": "en", "country": "US"}, + {"language": "en", "country": "UK"}, + {"language": "ja", "country": "JP"}, + # ... +] +``` + +##### `findTranslationEngines(source_lang, target_lang, engines_status) -> list` + +**責務:** 指定された言語ペアをサポートする翻訳エンジンの検索 + +**パラメータ:** +- `source_lang`: 元言語の辞書(複数の言語が有効化されている可能性) +- `target_lang`: 翻訳先言語の辞書 +- `engines_status`: 各エンジンの有効/無効状態 + +**処理:** +```python +selectable_engines = [key for key, value in engines_status.items() if value is True] +compatible_engines = [] +for engine in list(translation_lang.keys()): + languages = translation_lang.get(engine, {}).get("source", {}) + source_langs = [e["language"] for e in list(source_lang.values()) if e["enable"] is True] + target_langs = [e["language"] for e in list(target_lang.values()) if e["enable"] is True] + language_list = list(languages.keys()) + + if all(e in language_list for e in source_langs) and all(e in language_list for e in target_langs): + if engine in selectable_engines: + compatible_engines.append(engine) + +return compatible_engines +``` + +--- + +### 13. ロギング + +##### `startLogger() -> None` + +**責務:** ファイルロギングの開始 + +**処理:** +```python +os_makedirs(config.PATH_LOGS, exist_ok=True) +file_name = os_path.join(config.PATH_LOGS, f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log") +self.logger = setupLogger("log", file_name) +self.logger.disabled = False +``` + +**ログファイル名の例:** `2023-10-13_15-30-45.log` + +##### `stopLogger() -> None` +ファイルロギングの停止。 + +--- + +### 14. ソフトウェアアップデート + +##### `checkSoftwareUpdated() -> dict` + +**責務:** 最新バージョンの確認 + +**処理:** +```python +update_flag = False +version = "" +try: + # GitHub API 等から最新バージョン情報を取得 + # packaging.version.parse でバージョン比較 +except Exception: + errorLogging() +return { + "is_update_available": update_flag, + "new_version": version, +} +``` + +##### `updateSoftware() -> None` + +**責務:** 通常版のアップデート実行 + +**処理:** +1. アップデーターをダウンロード(最大5回リトライ) +2. `Popen()` でアップデーターを起動 +3. 現在のプロセスを終了 + +##### `updateCudaSoftware() -> None` +CUDA版のアップデート実行(`--cuda` オプション付きでアップデーターを起動)。 + +--- + +### 15. Watchdog 機能 + +##### `startWatchdog() -> None` + +**責務:** Watchdog 監視スレッドの起動 + +**処理:** +```python +self.th_watchdog = threadFnc(self.watchdog.start) +self.th_watchdog.daemon = True +self.th_watchdog.start() +``` + +##### `feedWatchdog() -> None` +Watchdog にハートビート信号を送信(タイムアウトをリセット)。 + +##### `setWatchdogCallback(callback: Callable) -> None` +Watchdog タイムアウト時のコールバック関数を設定。 + +##### `stopWatchdog() -> None` +Watchdog を停止し、スレッドの終了を待機。 + +--- + +### 16. WebSocket サーバー + +#### サーバー制御 + +##### `startWebSocketServer(host: str, port: int) -> None` + +**責務:** WebSocket サーバーの起動 + +**処理:** +1. 既に起動中なら何もしない +2. `websocket_server_loop = True` に設定 +3. 別スレッドで asyncio イベントループを実行: + ```python + async def WebSocketServerMain(): + self.websocket_server = WebSocketServer(host, port) + self.websocket_server_alive = True + await self.websocket_server.start() + # ループ終了まで待機 + self.websocket_server_alive = False + + self.th_websocket_server = Thread(target=lambda: asyncio.run(WebSocketServerMain())) + self.th_websocket_server.daemon = True + self.th_websocket_server.start() + ``` + +##### `stopWebSocketServer() -> None` + +**責務:** WebSocket サーバーの停止 + +**処理:** +1. `websocket_server_loop = False` に設定 +2. サーバーの停止を要求 +3. スレッドの終了を待機(タイムアウト付き) + +**エラーハンドリング:** +```python +try: + # サーバー停止処理 +except Exception: + errorLogging() +finally: + self.th_websocket_server = None + self.websocket_server = None + self.websocket_server_alive = False +``` + +##### `checkWebSocketServerAlive() -> bool` +WebSocket サーバーの稼働状態を確認。 + +#### メッセージ送信 + +##### `websocketSendMessage(message_dict: dict) -> bool` + +**責務:** すべての接続クライアントにメッセージをブロードキャスト + +**パラメータ:** +- `message_dict`: 送信する辞書(JSON にシリアライズされる) + +**処理:** +```python +if not self.websocket_server_alive or not self.websocket_server: + return False +try: + self.websocket_server.broadcast(message_dict) + return True +except Exception: + errorLogging() + return False +``` + +--- + +## 依存関係 + +### 外部ライブラリ + +```python +from subprocess import Popen +from os import makedirs as os_makedirs +from os import path as os_path +from datetime import datetime +from time import sleep +from queue import Queue +from threading import Thread +from requests import get as requests_get +from typing import Callable, Optional, cast +from packaging.version import parse +from flashtext import KeywordProcessor +``` + +### 内部モジュール + +```python +from device_manager import device_manager +from config import config +from models.translation.translation_translator import Translator +from models.osc.osc import OSCHandler +from models.transcription.transcription_recorder import SelectedMicEnergyAndAudioRecorder, SelectedSpeakerEnergyAndAudioRecorder +from models.transcription.transcription_recorder import SelectedMicEnergyRecorder, SelectedSpeakerEnergyRecorder +from models.transcription.transcription_transcriber import AudioTranscriber +from models.translation.translation_languages import translation_lang +from models.transcription.transcription_languages import transcription_lang +from models.translation.translation_utils import checkCTranslate2Weight, downloadCTranslate2Weight, downloadCTranslate2Tokenizer +from models.transcription.transcription_whisper import checkWhisperWeight, downloadWhisperWeight +from models.transliteration.transliteration_transliterator import Transliterator +from models.overlay.overlay import Overlay +from models.overlay.overlay_image import OverlayImage +from models.watchdog.watchdog import Watchdog +from models.websocket.websocket_server import WebSocketServer +from utils import errorLogging, setupLogger +``` + +--- + +## スレッド構成 + +### メインスレッド +- アプリケーションのメインループ(`mainloop.py` が管理) + +### Model 管理のスレッド + +#### 音声認識スレッド +- `mic_print_transcript`: マイク音声認識結果の処理 +- `speaker_print_transcript`: スピーカー音声認識結果の処理 + +#### エネルギー監視スレッド +- `mic_energy_plot_progressbar`: マイクの音量レベル監視 +- `speaker_energy_plot_progressbar`: スピーカーの音量レベル監視 + +#### その他のスレッド +- `th_watchdog`: Watchdog 監視 +- `th_websocket_server`: WebSocket サーバー(asyncio イベントループ) + +### サブシステム管理のスレッド +- `device_manager.th_monitoring`: デバイス変更監視 +- `mic_audio_recorder.th_record`: マイク音声録音 +- `speaker_audio_recorder.th_record`: スピーカー音声録音 +- `osc_handler.th_receive`: OSC パラメータ受信 + +--- + +## エラーハンドリング + +### VRAM不足エラー + +**検出:** +```python +is_vram_error, error_message = self.detectVRAMError(e) +``` + +**対応:** +1. エラーを `ValueError("VRAM_OUT_OF_MEMORY")` として送出 +2. Controller 側でキャッチして機能を無効化 +3. ユーザーに通知 + +### デバイスアクセスエラー + +**検出:** +- デバイスが見つからない場合: `NoDevice` +- アクセス失敗時: コールバックに `False` を渡す + +**対応:** +1. エラーをログに記録 +2. Controller に通知 +3. 処理を継続(他の機能に影響なし) + +### ネットワークエラー + +**検出:** +- 翻訳API呼び出し失敗 +- モデルウェイトダウンロード失敗 + +**対応:** +1. リトライロジック(翻訳の場合) +2. フォールバック(CTranslate2 への切り替え) +3. エラー通知 + +--- + +## パフォーマンス最適化 + +### 1. 遅延初期化 + +重い初期化処理を `init()` に分離し、必要になるまで実行しない。 + +**利点:** +- アプリケーションの起動時間を短縮 +- 未使用の機能のリソースを消費しない + +### 2. シングルトンパターン + +Model クラスはアプリケーション全体で1つのインスタンスのみ存在。 + +**利点:** +- メモリ使用量の削減 +- 状態の一貫性 + +### 3. スレッドによる並列処理 + +音声認識、エネルギー監視、WebSocket サーバーなど、ブロッキング処理を別スレッドで実行。 + +**利点:** +- UI のレスポンス性向上 +- 複数機能の同時実行 + +--- + +## テストシナリオ + +### 1. 初期化テスト + +**ケース:** +- 初回初期化 +- 既に初期化済みの場合 +- 初期化失敗時 + +**確認項目:** +- `_inited` フラグが正しく設定されているか +- すべてのサブシステムが初期化されているか +- エラーが適切にログされているか + +### 2. 音声認識テスト + +**ケース:** +- デバイスがない場合 +- 音声認識開始・停止・一時停止・再開 +- VRAMエラーの発生 + +**確認項目:** +- コールバックが正しく呼び出されているか +- スレッドが適切に管理されているか +- エラーが検出されているか + +### 3. 翻訳テスト + +**ケース:** +- 単一言語翻訳 +- 複数言語翻訳 +- 翻訳エンジンの切り替え +- API エラー + +**確認項目:** +- 翻訳結果が正しいか +- エラー時のフォールバックが動作するか + +### 4. オーバーレイテスト + +**ケース:** +- 画像生成 +- 設定更新 +- オーバーレイの起動・停止 + +**確認項目:** +- 画像が正しく生成されるか +- 設定変更が反映されるか + +--- + +## 制限事項 + +### 1. シングルトンの制約 + +**問題:** テストやマルチインスタンスが困難 + +**影響:** +- ユニットテストでモックが難しい +- 複数の VRChat インスタンスへの対応が不可能 + +### 2. グローバル状態依存 + +**問題:** `config` モジュールへの強い依存 + +**影響:** +- テスタビリティの低下 +- 設定変更の追跡が困難 + +### 3. エラーハンドリングの不完全性 + +**問題:** 一部のエラーは握りつぶされる + +**影響:** +- デバッグが困難 +- ユーザーへの適切なエラー通知が不足 + +### 4. スレッドの管理複雑性 + +**問題:** 多数のスレッドとその状態管理 + +**影響:** +- デッドロックのリスク +- リソースリークの可能性 + +--- + +## 今後の改善案 + +### 1. 依存性注入(DI)の導入 + +```python +class Model: + def __init__(self, config, device_manager, translator, ...): + self.config = config + self.device_manager = device_manager + self.translator = translator + # ... +``` + +**利点:** +- テスタビリティの向上 +- モジュール間の疎結合 + +### 2. 非同期化(asyncio) + +```python +async def startMicTranscript(self, callback): + async for result in self.mic_transcriber.transcribe(): + await callback(result) +``` + +**利点:** +- スレッド管理の簡素化 +- パフォーマンスの向上 + +### 3. イベント駆動アーキテクチャ + +```python +class Model: + def __init__(self): + self.event_bus = EventBus() + + def on_transcription_result(self, result): + self.event_bus.emit("transcription_result", result) +``` + +**利点:** +- モジュール間の疎結合 +- 拡張性の向上 + +### 4. エラーハンドリングの統一 + +```python +class ModelError(Exception): + pass + +class VRAMError(ModelError): + pass + +class DeviceError(ModelError): + pass +``` + +**利点:** +- エラーの分類と処理の統一 +- エラー情報の追跡 + +--- + +## 関連ファイル + +- **controller.py** - ビジネスロジック制御レイヤー +- **config.py** - 設定管理 +- **device_manager.py** - デバイス監視・自動選択 +- **mainloop.py** - 通信レイヤー +- **utils.py** - ログとユーティリティ関数 +- **models/** - サブシステムの実装 + +--- + +## まとめ + +`model.py` は VRCT のすべてのサブシステムへの統一されたファサードインターフェースを提供し、音声認識、翻訳、オーバーレイ、OSC通信、WebSocket通信など、複雑な機能を簡潔なAPIで公開する。シングルトンパターンと遅延初期化により、リソースの効率的な利用を実現している。スレッドを活用した並列処理により、複数の機能を同時に実行しながらUIのレスポンス性を維持している。VRAMエラーやデバイスエラーに対する適切なハンドリングにより、ユーザーエクスペリエンスを向上させている。 diff --git a/src-python/docs/modules/config.md b/src-python/docs/modules/config.md deleted file mode 100644 index cdd83b8c..00000000 --- a/src-python/docs/modules/config.md +++ /dev/null @@ -1,212 +0,0 @@ -# config.py クラス仕様書 - -目的: アプリケーションの全設定を集中管理するシングルトン `config`(クラス名: `Config`、インスタンス: `config`)。 - -特徴: -- JSON シリアライズ対象のプロパティには `@json_serializable('KEY_NAME')` デコレータが付いており、`load_config()` / `saveConfig()` によって `config.json` に永続化されます。 -- プロパティは「読み取り専用 (Read Only)」と「読み書き (Read/Write)」に分類されます。読み書き可能なプロパティはバリデーション処理とともに setter が用意されています。 -- 設定は内部的に `_config_data` に保持され、`saveConfig()` はデバウンス(2秒)でファイルへ書き込みます。即時書き込みオプションも可能です(saveConfig(..., immediate_save=True))。 - -## 生成とライフサイクル -- `Config()` はシングルトン(__new__ で単一インスタンスを生成)。 -- `init_config()` でデフォルト値を初期化し、その後 `load_config()` が `config.json` を読み込んで既存値を適用します。 - -## 主要プロパティ一覧(型・デフォルト・説明) - -注: 下は `config.py` の初期化ロジックに基づく抜粋です。`json_serializable` が付与されたキーは `config.json` に書き出されます。 - -- Read only - - `VERSION` (str) = "3.2.2" - - `PATH_LOCAL` (str) = フォロー実行ファイルのディレクトリか、ソースの __file__ のディレクトリ - - `PATH_CONFIG` (str) = PATH_LOCAL/config.json - - `PATH_LOGS` (str) = PATH_LOCAL/logs - - `GITHUB_URL`, `UPDATER_URL`, `BOOTH_URL`, `DOCUMENTS_URL`, `DEEPL_AUTH_KEY_PAGE_URL` (str) - - `MAX_MIC_THRESHOLD` (int) = 2000 - - `MAX_SPEAKER_THRESHOLD` (int) = 4000 - - `WATCHDOG_TIMEOUT` (int) = 60 - - `WATCHDOG_INTERVAL` (int) = 20 - - `SELECTABLE_*` 系: 各種選択肢のリスト/イテレータ(モデルの重みや言語、UI 言語等)。 - -- Read/Write(主な項目) - - `SEND_MESSAGE_FORMAT_PARTS` (dict) = デフォルトで message/translation/translation_first 等を含むフォーマット定義。json_serializable キー: 'SEND_MESSAGE_FORMAT_PARTS' - - `RECEIVED_MESSAGE_FORMAT_PARTS` (dict) - - `ENABLE_TRANSLATION` (bool) = False - - `ENABLE_TRANSCRIPTION_SEND` (bool) = False - - `ENABLE_TRANSCRIPTION_RECEIVE` (bool) = False - - `ENABLE_FOREGROUND` (bool) = False - - `ENABLE_CHECK_ENERGY_SEND` (bool) = False - - `ENABLE_CHECK_ENERGY_RECEIVE` (bool) = False - - `SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT` (dict) = {: False, ...} - - `SELECTABLE_WHISPER_WEIGHT_TYPE_DICT` (dict) - - `SELECTABLE_TRANSLATION_ENGINE_STATUS` (dict) - - `SELECTABLE_TRANSCRIPTION_ENGINE_STATUS` (dict) - - `SELECTED_TAB_NO` (str) = "1" (json_serializable: 'SELECTED_TAB_NO') - - `SELECTED_TRANSLATION_ENGINES` (dict) = tab毎に選択 ('CTranslate2' 等) - - `SELECTED_YOUR_LANGUAGES`, `SELECTED_TARGET_LANGUAGES` (dict) = 翻訳元/先の選択と有効フラグ - - `SELECTED_TRANSCRIPTION_ENGINE` (str) = 'Google' - - `CONVERT_MESSAGE_TO_ROMAJI` / `CONVERT_MESSAGE_TO_HIRAGANA` (bool) - - UI 設定: `TRANSPARENCY` (int), `UI_SCALING` (int), `TEXTBOX_UI_SCALING` (int), `MESSAGE_BOX_RATIO` (int) - - `SEND_MESSAGE_BUTTON_TYPE` (str) = 'show'(候補は SEND_MESSAGE_BUTTON_TYPE_LIST) - - `SHOW_RESEND_BUTTON` (bool) - - `FONT_FAMILY` (str) = 'Yu Gothic UI' - - `UI_LANGUAGE` (str) = 'en'(候補は SELECTABLE_UI_LANGUAGE_LIST) - - `MAIN_WINDOW_GEOMETRY` (dict) = {x_pos, y_pos, width, height} - - マイク/スピーカー関係: `AUTO_MIC_SELECT`, `SELECTED_MIC_HOST`, `SELECTED_MIC_DEVICE`, `MIC_THRESHOLD`, `MIC_AUTOMATIC_THRESHOLD`, `MIC_RECORD_TIMEOUT`, `MIC_PHRASE_TIMEOUT`, `MIC_MAX_PHRASES`, `MIC_WORD_FILTER`, `HOTKEYS` 等 - - `PLUGINS_STATUS` (list) - - マイク転写確度閾値: `MIC_AVG_LOGPROB`, `MIC_NO_SPEECH_PROB` - - スピーカー関連(同様の項目): `AUTO_SPEAKER_SELECT`, `SELECTED_SPEAKER_DEVICE`, `SPEAKER_THRESHOLD`, ... - - `OSC_IP_ADDRESS` (str) = '127.0.0.1' - - `OSC_PORT` (int) = 9000 - - `AUTH_KEYS` (dict) = {'DeepL_API': None} - - `USE_EXCLUDE_WORDS` (bool) = True - - 計算デバイス選択: `SELECTED_TRANSLATION_COMPUTE_DEVICE` / `SELECTED_TRANSCRIPTION_COMPUTE_DEVICE`(`getComputeDeviceList()` に基づくデバイス辞書) - - 重み/計算タイプ: `CTRANSLATE2_WEIGHT_TYPE`, `WHISPER_WEIGHT_TYPE`, `SELECTED_TRANSLATION_COMPUTE_TYPE`, `SELECTED_TRANSCRIPTION_COMPUTE_TYPE` - - オーバーレイ設定: `OVERLAY_SMALL_LOG`, `OVERLAY_SMALL_LOG_SETTINGS`, `OVERLAY_LARGE_LOG`, `OVERLAY_LARGE_LOG_SETTINGS`, `OVERLAY_SHOW_ONLY_TRANSLATED_MESSAGES` 等 - - VRC/ログ/WebSocket: `SEND_MESSAGE_TO_VRC`, `SEND_RECEIVED_MESSAGE_TO_VRC`, `LOGGER_FEATURE`, `VRC_MIC_MUTE_SYNC`, `NOTIFICATION_VRC_SFX`, `WEBSOCKET_SERVER`, `WEBSOCKET_HOST`, `WEBSOCKET_PORT` - -# config.py — 完全上書きドキュメント - -目的: アプリケーションの全設定を集中管理するシングルトン `config`(クラス名: `Config`、インスタンス: `config`)。 - -特徴: -- JSON シリアライズ対象のプロパティには `@json_serializable('KEY_NAME')` デコレータが付いており、`load_config()` / `saveConfig()` によって `config.json` に永続化されます。 -- プロパティは「読み取り専用 (Read Only)」と「読み書き (Read/Write)」に分類されます。読み書き可能なプロパティはバリデーション処理とともに setter が用意されています。 -- 設定は内部的に `_config_data` に保持され、`saveConfig()` はデバウンス(2秒)でファイルへ書き込みます。即時書き込みオプションも可能です(saveConfig(..., immediate_save=True))。 - -## 生成とライフサイクル -- `Config()` はシングルトン(__new__ で単一インスタンスを生成)。 -- `init_config()` でデフォルト値を初期化し、その後 `load_config()` が `config.json` を読み込んで既存値を適用します。 - -## 主要プロパティ一覧(型・デフォルト・説明) - -注: 下は `config.py` の初期化ロジックに基づく抜粋です。`json_serializable` が付与されたキーは `config.json` に書き出されます。 - -- Read only - - `VERSION` (str) = "3.2.2" - - `PATH_LOCAL` (str) = フォロー実行ファイルのディレクトリか、ソースの __file__ のディレクトリ - - `PATH_CONFIG` (str) = PATH_LOCAL/config.json - - `PATH_LOGS` (str) = PATH_LOCAL/logs - - `GITHUB_URL`, `UPDATER_URL`, `BOOTH_URL`, `DOCUMENTS_URL`, `DEEPL_AUTH_KEY_PAGE_URL` (str) - - `MAX_MIC_THRESHOLD` (int) = 2000 - - `MAX_SPEAKER_THRESHOLD` (int) = 4000 - - `WATCHDOG_TIMEOUT` (int) = 60 - - `WATCHDOG_INTERVAL` (int) = 20 - - `SELECTABLE_*` 系: 各種選択肢のリスト/イテレータ(モデルの重みや言語、UI 言語等)。 - -- Read/Write(主な項目) - - `SEND_MESSAGE_FORMAT_PARTS` (dict) = デフォルトで message/translation/translation_first 等を含むフォーマット定義。json_serializable キー: 'SEND_MESSAGE_FORMAT_PARTS' - - `RECEIVED_MESSAGE_FORMAT_PARTS` (dict) - - `ENABLE_TRANSLATION` (bool) = False - - `ENABLE_TRANSCRIPTION_SEND` (bool) = False - - `ENABLE_TRANSCRIPTION_RECEIVE` (bool) = False - - `ENABLE_FOREGROUND` (bool) = False - - `ENABLE_CHECK_ENERGY_SEND` (bool) = False - - `ENABLE_CHECK_ENERGY_RECEIVE` (bool) = False - - `SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT` (dict) = {: False, ...} - - `SELECTABLE_WHISPER_WEIGHT_TYPE_DICT` (dict) - - `SELECTABLE_TRANSLATION_ENGINE_STATUS` (dict) - - `SELECTABLE_TRANSCRIPTION_ENGINE_STATUS` (dict) - - `SELECTED_TAB_NO` (str) = "1" (json_serializable: 'SELECTED_TAB_NO') - - `SELECTED_TRANSLATION_ENGINES` (dict) = tab毎に選択 ('CTranslate2' 等) - - `SELECTED_YOUR_LANGUAGES`, `SELECTED_TARGET_LANGUAGES` (dict) = 翻訳元/先の選択と有効フラグ - - `SELECTED_TRANSCRIPTION_ENGINE` (str) = 'Google' - - `CONVERT_MESSAGE_TO_ROMAJI` / `CONVERT_MESSAGE_TO_HIRAGANA` (bool) - - UI 設定: `TRANSPARENCY` (int), `UI_SCALING` (int), `TEXTBOX_UI_SCALING` (int), `MESSAGE_BOX_RATIO` (int) - - `SEND_MESSAGE_BUTTON_TYPE` (str) = 'show'(候補は SEND_MESSAGE_BUTTON_TYPE_LIST) - - `SHOW_RESEND_BUTTON` (bool) - - `FONT_FAMILY` (str) = 'Yu Gothic UI' - - `UI_LANGUAGE` (str) = 'en'(候補は SELECTABLE_UI_LANGUAGE_LIST) - - `MAIN_WINDOW_GEOMETRY` (dict) = {x_pos, y_pos, width, height} - - マイク/スピーカー関係: `AUTO_MIC_SELECT`, `SELECTED_MIC_HOST`, `SELECTED_MIC_DEVICE`, `MIC_THRESHOLD`, `MIC_AUTOMATIC_THRESHOLD`, `MIC_RECORD_TIMEOUT`, `MIC_PHRASE_TIMEOUT`, `MIC_MAX_PHRASES`, `MIC_WORD_FILTER`, `HOTKEYS` 等 - - `PLUGINS_STATUS` (list) - - マイク転写確度閾値: `MIC_AVG_LOGPROB`, `MIC_NO_SPEECH_PROB` - - スピーカー関連(同様の項目): `AUTO_SPEAKER_SELECT`, `SELECTED_SPEAKER_DEVICE`, `SPEAKER_THRESHOLD`, ... - - `OSC_IP_ADDRESS` (str) = '127.0.0.1' - - `OSC_PORT` (int) = 9000 - - `AUTH_KEYS` (dict) = {'DeepL_API': None} - - `USE_EXCLUDE_WORDS` (bool) = True - - 計算デバイス選択: `SELECTED_TRANSLATION_COMPUTE_DEVICE` / `SELECTED_TRANSCRIPTION_COMPUTE_DEVICE`(`getComputeDeviceList()` に基づくデバイス辞書) - - 重み/計算タイプ: `CTRANSLATE2_WEIGHT_TYPE`, `WHISPER_WEIGHT_TYPE`, `SELECTED_TRANSLATION_COMPUTE_TYPE`, `SELECTED_TRANSCRIPTION_COMPUTE_TYPE` - - オーバーレイ設定: `OVERLAY_SMALL_LOG`, `OVERLAY_SMALL_LOG_SETTINGS`, `OVERLAY_LARGE_LOG`, `OVERLAY_LARGE_LOG_SETTINGS`, `OVERLAY_SHOW_ONLY_TRANSLATED_MESSAGES` 等 - - VRC/ログ/WebSocket: `SEND_MESSAGE_TO_VRC`, `SEND_RECEIVED_MESSAGE_TO_VRC`, `LOGGER_FEATURE`, `VRC_MIC_MUTE_SYNC`, `NOTIFICATION_VRC_SFX`, `WEBSOCKET_SERVER`, `WEBSOCKET_HOST`, `WEBSOCKET_PORT` - -## セッタのバリデーション -- 多くの setter は型チェックと候補値チェック(リストや辞書のキー整合性)を行います。例: - - `SELECTED_MIC_DEVICE` は `device_manager.getMicDevices()` の一覧に存在する名前であること。 - - `SELECTED_TRANSLATION_COMPUTE_TYPE` は `SELECTED_TRANSLATION_COMPUTE_DEVICE['compute_types']` に含まれる文字列であること。 - - UI 関連の集合は `SELECTABLE_UI_LANGUAGE_LIST` などの一覧に従う。 - -## 永続化の詳細 -- `load_config()` は `config.json` が存在し、かつ中身がある場合に読み込みを試み、ファイル中のキーを `setattr(self, key, value)` して既存の setter を利用して適用します。 -- 読み込み後、`json_serializable` 指定された全キーを `_config_data` に書き戻し、ファイルを上書き(常に書く)。 - -## 使い方の例 - -以下は `config` を使った典型的なコード例です。 - -```python -from config import config - -# 値の参照 -print('App version:', config.VERSION) -print('Current UI language:', config.UI_LANGUAGE) - -# 値の更新(setter を通す) -config.UI_LANGUAGE = 'ja' -config.SEND_MESSAGE_TO_VRC = False - -# 複雑な dict を設定する例(メッセージフォーマットを上書き) -config.SEND_MESSAGE_FORMAT_PARTS = { - 'message': {'prefix': '[YOU] ', 'suffix': ''}, - 'separator': '\n', - 'translation': {'prefix': '[TR] ', 'separator': '\n', 'suffix': ''}, - 'translation_first': True, -} - -# 即時保存したい場合(即座に config.json を上書き) -config.saveConfig('CUSTOM_SAVE', {'foo': 'bar'}, immediate_save=True) -``` - -## エッジケース / 注意点 -- `load_config()` はファイル値を setter 経由で当てはめるため、ファイルに古いキーや予期しない型があると setter によって無視されることがあります(例: 言語キーが不正の場合)。 -- `saveConfig()` はデバウンスされるため、高頻度の設定変更では複数の変更がまとめて書き込まれます。即時書き込みが必要な操作(重要な鍵の更新など)は `immediate_save=True` を使ってください。 -- `SELECTABLE_*` 系や `*_DICT` 系は初期化時に外部モジュール(翻訳リソース、whisper_models、device_manager 等)から生成されます。これらが利用できない環境ではデフォルトが空になる可能性があります。 - -### 2025-10-13 の変更(device_manager / config に関する挙動改善) - -- `DeviceManager` のシングルトン生成時に軽量 `init()` を実行するようになりました。これにより、モジュールのインポート順序に依存して `config` の `SELECTED_*` が `NoDevice` のままになる問題が軽減されます(監視スレッドは自動起動しません)。 -- `config.init_config()` はこれまで `device_manager._initialized` をチェックしていた箇所を見直し、`device_manager.getDefaultMicDevice()` / `getDefaultSpeakerDevice()` といったアクセサを利用して値を取得するように変更しました。アクセサは必要なら遅延初期化を行うため、`controller` と `config` のトップレベルインポート順に依存しません。 -- 影響: 起動時に PyAudio 等の依存が利用可能であれば、起動中に実機デバイス名が `config` に反映される確率が高くなります。依存がない場合は従来どおり `NoDevice` にフォールバックします。 - -推奨運用: -- `controller.init()` でコールバック登録後、`mainloop` の起動シーケンスで `device_manager.startMonitoring()` を明示的に呼ぶと、起動後もデバイス変更がコールバック経由で確実に届きます(この呼び出しは任意です)。 - -## 推奨改善点(将来的なドキュメント/実装) -- 設定スキーマを JSON Schema で定義し、load 時の検証を明確化すると安全性が向上します。 -- 設定変更イベントを発火する仕組み(observer パターン)を導入すると、Controller/Model 側の再初期化処理をより明確に実装できます。 - ---- - -このファイルは `config.py` の実装に基づいて自動生成的に作成されたドキュメント(overwrite)です。実装の微細な差分は `config.py` を参照してください。 - -## 詳細設計 - -目的: アプリケーションの全設定を保持するシングルトン `config`。 - -ポイント: -- JSON シリアライズ可能な設定値には `@json_serializable` デコレータが付与され、save 操作でファイルへ書き出される。 -- 多数のプロパティが定義され、読み取り専用 (Read Only) と 読み書き (Read/Write) が混在する。 -- 設定項目の例: - - ENABLE_TRANSLATION, ENABLE_TRANSCRIPTION_SEND, ENABLE_TRANSCRIPTION_RECEIVE - - SELECTED_MIC_HOST, SELECTED_MIC_DEVICE, SELECTED_SPEAKER_DEVICE - - SELECTED_TRANSLATION_ENGINES, SELECTED_YOUR_LANGUAGES, SELECTED_TARGET_LANGUAGES - - PATH_LOCAL, PATH_LOGS, VERSION, GITHUB_URL, UPDATER_URL - - SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT / SELECTABLE_WHISPER_WEIGHT_TYPE_DICT - - COMPUTE 関連: SELECTABLE_COMPUTE_DEVICE_LIST, SELECTED_TRANSLATION_COMPUTE_DEVICE, SELECTED_TRANSCRIPTION_COMPUTE_DEVICE - -設計上の契約: -- 全ての get/set は辞書形で status/result を返す Controller の呼び出しに合わせて変換される。 -- 外部から設定を変更した際は必要に応じて Model/Controller による再初期化処理を呼ぶ。 - -検討事項: -- 現状は設定変更が即時反映されるが、一部操作は再初期化(モデルロード、デバイス再取得)を要求するため Controller 側で連携している。 diff --git a/src-python/docs/modules/config_ref.md b/src-python/docs/modules/config_ref.md deleted file mode 100644 index 776a2f1a..00000000 --- a/src-python/docs/modules/config_ref.md +++ /dev/null @@ -1,39 +0,0 @@ -# config.py 変更参照ドキュメント - -このファイルは `config.py` に対して行った最近のリファクタリング / 安全化についての参照資料です。 - -目的: import 時の副作用を抑止し、`device_manager` などの外部モジュールがない環境でも安全に `config` をインポートできるようにすること。 - -主な変更点 - -- import-time の初期化保護 - - `Config.__new__` の中で `init_config()` / `load_config()` を呼び出しますが、これらを try/except で保護し、初期化に失敗しても例外を上位に伝播させずログ記録のみで処理を継続します。 - - このため、アプリ起動環境に必須ではない外部依存が欠けている場合でも、`import config` によるクラッシュを防止します。 - -- 外部モジュールの呼び出しをガード - - `device_manager`、翻訳/文字起こし関連のモデル一覧 (`whisper_models`, `ctranslate2_weights`) などは import 時に直接呼び出さず、存在チェック(try/except)を行って安全なデフォルト(空リストや "NoDevice" など)にフォールバックします。 - - これによりヘビーな依存(Windows 固有パッケージや大きな ML ライブラリ)がない CI 環境や軽量実行環境での import が安定します。 - -- エラーロギング - - 初期化やデフォルト取得に失敗した場合は、例外を握りつぶすのではなく `utils.errorLogging()` を経由してエラーメッセージを残します。これにより問題の診断が容易になります。 - -- 設定デフォルト値の扱い - - `getDefaultMicDevice()` / `getDefaultSpeakerDevice()` などを呼ぶ箇所は try/except で保護され、失敗時には `"NoHost"` / `"NoDevice"` 等の安全な文字列で代替されます。 - -利用上の注意 - -- 既存のコードは `config` をインポートしただけで `device_manager` を起動することを想定している箇所があるかもしれません。今回のリファクタリングでは "import 時に副作用を起こさない" ことを優先しているため、もし明示的な初期化を必要とする場合は、呼び出し側で `device_manager.init()` を明示的に行ってください。 - -- もし `config` のロードで致命的な設定エラーが発生した場合でも、アプリは継続動作しますが、ログを確認して手動で修復することが必要になる場合があります。 - -ドキュメントの提案差分 - -- 既存 `docs/modules/config.md` の "生成とライフサイクル" セクションに次の一文を追加することを推奨します: - - > 注意: `Config()` のインポートは副作用を起こさないよう保護されています。プラットフォーム依存のコンポーネント(例: `device_manager`)は明示的に初期化してください。 - -- `SELECTABLE_*` 系の説明に、起動環境に依存して空になる可能性があることを明示するパラグラフを追加してください(CI 環境や headless 環境では空になる)。 - ---- - -作業済み: このファイルはワークスペースに `docs/modules/config_ref.md` として作成済みです。既存 `docs/modules/config.md` は上書きしていません。上書き/マージの希望があれば続けます。 diff --git a/src-python/docs/modules/controller.md b/src-python/docs/modules/controller.md deleted file mode 100644 index ccbb6cfd..00000000 --- a/src-python/docs/modules/controller.md +++ /dev/null @@ -1,162 +0,0 @@ -## Controller クラス仕様書 - -概要 -- `Controller` はアプリケーションのコントロール層(Facade)で、`model` と `device_manager`、および外部 UI / mainloop とを仲介します。 -- UI からのコマンドを受け取り、`model` の開始/停止、設定の変更、ダウンロードの開始、各種フラグの切り替え、進捗通知(`run` コールバック経由)を行います。 -- 多くのメソッドは JSON 系の応答オブジェクトを返します: {"status": int, "result": Any}。副作用で `self.run(status, run_mapping[key], payload)` を呼び出して UI に通知します。 - -### mainloop のマルチワーカー化とカノニカルロックについて (2025-10-13) - -- `mainloop.Main` はデフォルトで複数(デフォルト 3)のハンドラワーカースレッドを動かすようになりました。これにより、モデルロードなどの重い操作で他のリクエストが待たされることが少なくなります。 -- `/set/enable/` と `/set/disable/` のように同一機能の ON/OFF を切り替えるエンドポイントは、内部的にカノニカルロックキー(例: `/lock/set/`)に正規化してロック取得されます。これにより、遅い disable の処理が後から来て最終状態を書き換えてしまうレースが防がれます。 -初期化とランタイムフック -- __init__() -> None - - フィールド: `init_mapping: dict`, `run_mapping: dict`, `run: Callable`, `device_access_status: bool` - - `setInitMapping(init_mapping: dict)` / `setRunMapping(run_mapping: dict)` / `setRun(run: Callable)` で mainloop からマッピング・コールバックを注入されることを想定。 - -コールバック通知用メソッド(UI への通知) -- connectedNetwork() / disconnectedNetwork() -> None -- enableAiModels() / disableAiModels() -> None -- updateMicHostList() / updateMicDeviceList() / updateSpeakerDeviceList() -> None -- updateConfigSettings() -> None - - これらは `self.run(status, run_mapping[key], payload)` を使って UI にイベントを送ります。 - -ダウンロード用ヘルパークラス -- class DownloadCTranslate2(run_mapping: dict, weight_type: str, run: Callable) - - progressBar(progress: float) -> None - - downloaded() -> None -- class DownloadWhisper(run_mapping: dict, weight_type: str, run: Callable) - - progressBar(progress: float) -> None - - downloaded() -> None - -音声・翻訳イベントハンドラ -- micMessage(result: dict) -> None - - 引数: result: {"text": str|False, "language": str} - - 挙動: ワードフィルタ、繰り返し検出、翻訳(`model.getInputTranslate`)、音声送信(OSC)・オーバーレイ更新・WebSocket ブロードキャスト等を行う。 - - エラー: 翻訳中に VRAM OOM が起きた場合は model.detectVRAMError を使って検出し、翻訳機能を無効化して UI に 400 を通知。 - -- speakerMessage(result: dict) -> None - - 引数: result: {"text": str|False, "language": str} - - micMessage と同様だが、受信(speaker)側のロジックやオーバーレイの扱いが異なる。 - -- chatMessage(data: dict) -> dict - - 引数: {"id": Any, "message": str} - - 戻り値: {"status": int, "result": {"id":..., "original":..., "translations":[...]}} - - 挙動詳細: - - 翻訳処理は `model.getInputTranslate` を呼び出します。翻訳処理中に VRAM 関連の例外が発生した場合、`model.detectVRAMError` によって検出し、翻訳機能を自動で無効化します。 - - VRAM エラー検出時は Controller は UI に対して 400 系の run イベントを発行する(例: `error_translation_chat_vram_overflow`, `enable_translation` で無効化通知)。 - - エラー発生時の戻り値: 翻訳を行わずに基本情報を含む 200 応答を返すコードパスがあり、クライアント側でのハンドリングを想定しています。 - -設定取得/変更系メソッド(代表例) -- getVersion() -> {"status":200, "result": config.VERSION} -- getComputeMode() / getComputeDeviceList() / getSelectedTranslationComputeDevice() -> dict -- setSelectedTranslationComputeDevice(device: str) -> {"status":200, "result": device} -- getSelectableCtranslate2WeightTypeDict() -> dict -- setEnableTranslation() / setDisableTranslation() -> dict - - setEnableTranslation はモデルロード時に VRAM エラーを検知するロジックを内包している。 - - 多くの setXXX / getXXX メソッドは config を直接操作して即時反映する。 - -自動デバイス選択 -- applyAutoMicSelect() / applyAutoSpeakerSelect() - - `device_manager` にコールバックを登録して自動選択を有効化する。 - -トランスクリプション制御(スレッドで実行) -- startTranscriptionSendMessage() / stopTranscriptionSendMessage() / startThreadingTranscriptionSendMessage() / stopThreadingTranscriptionSendMessage() -- startTranscriptionReceiveMessage() / stopTranscriptionReceiveMessage() / startThreadingTranscriptionReceiveMessage() / stopThreadingTranscriptionReceiveMessage() - - 実際の処理は `model.startMicTranscript` / `model.startSpeakerTranscript` に委譲される。VRAM エラーは検出して UI に通知し、自動的に停止する処理あり。 - -閾値・チェック系 -- startCheckMicEnergy() / stopCheckMicEnergy() / startThreadingCheckMicEnergy() / stopThreadingCheckMicEnergy() -- startCheckSpeakerEnergy() / stopCheckSpeakerEnergy() / startThreadingCheckSpeakerEnergy() / stopThreadingCheckSpeakerEnergy() - -ダウンロード開始(非同期/同期) -- downloadCtranslate2Weight(data: str, asynchronous: bool=True) -> dict -- downloadWhisperWeight(data: str, asynchronous: bool=True) -> dict - - 非同期なら別スレッドでダウンロードを行い progressBar コールバックを経由して UI に進捗を返す。 - -Watchdog / WebSocket / OSC 周り -- startWatchdog() / feedWatchdog() / stopWatchdog() -- getWebSocketHost() / setWebSocketHost(data) -> dict -- setEnableWebSocketServer() / setDisableWebSocketServer() -- setOscIpAddress(data) / setOscPort(data) - - ネットワーク周りの設定は検証ロジック(IP アドレス検証、サーバー利用可否のチェック)を含む。 - -ユーティリティ関数 -- messageFormatter(format_type: str, translation: list, message: str) -> str - - OSC に送る文面のフォーマットを生成(設定に基づく)。 -- replaceExclamationsWithRandom(text) -> (str, dict) -- restoreText(escaped_text, escape_dict) -> str -- removeExclamations(text) -> str - -重要な戻り値規約 -- 成功: {"status": 200, "result": ...} -- 失敗: {"status": 400, "result": {"message": str, "data": Any}} -- 多くのメソッドは UI への通知として `self.run(status, run_mapping[key], payload)` を行う。 - -エッジケース / エラー処理 -- VRAM OOM 検出: モデル例外が上がると model.detectVRAMError(e) を呼び出し、VRAM エラーが検出された場合は関連機能を自動で無効化して UI に 400 を通知する。 -- デバイスアクセスの競合: `device_access_status` による簡易ロックで、デバイス操作中は待機する。 -- ネットワーク依存: DeepL 等の外部翻訳 API 利用可否は `model.authenticationTranslatorDeepLAuthKey` で検査し、無効時は選択肢を更新する。 - -呼び出し例(Python から直接) -```python -from controller import Controller -ctrl = Controller() -# run コールバックの例: (status:int, event_name:str, payload:any) -def ui_run(status, event, payload): - print(status, event, payload) - -ctrl.setRun(ui_run) -resp = ctrl.setEnableTranslation() -print(resp) # {'status':200, 'result': True} - -data = {"id": 123, "message": "Hello"} -resp = ctrl.chatMessage(data) -print(resp) -``` - -シーケンス図(簡易: マイク入力 -> 翻訳 -> UI 通知) -```mermaid -sequenceDiagram - participant UI - participant Mainloop - participant Controller - participant Model - - UI->>Mainloop: ユーザ操作 (send message) - Mainloop->>Controller: chatMessage(data) - Controller->>Model: getInputTranslate(message) - Model-->>Controller: translation - Controller->>Model: oscSendMessage(...) - Controller->>UI: run(200, run_mapping['transcription_send_mic_message'], payload) -``` - -次の作業 -- `docs/api.md` を `mainloop.py` のマッピングに基づいて拡張し、各エンドポイントの request/response 例を追加してください。 - -参考: 実装詳細は `src-python/controller.py` を参照してください(メソッドごとに細かな条件分岐や run_mapping キーが存在します)。 -# controller.py — 詳細設計 - -目的: UI(または外部プロセス)からの操作を受け、`config` と `model` を操作して副作用を生じさせるコマンド層。 - -主要クラス/関数: -- class Controller - - 属性: - - init_mapping: アプリ起動時の読み出し用マッピング(/get/data/*) - - run_mapping: イベント通知先のエンドポイントマップ(run 関数で使用) - - run: run(status, endpoint, result) を格納 - - - 主要メソッド: - - setEnableTranslation / setDisableTranslation: 翻訳機能の切替(モデル切替や VRAM エラー回復処理を含む) - - start/stop transcription/energy checks: Model の startMicTranscript 等を呼ぶ - - downloadCtranslate2Weight / downloadWhisperWeight: ダウンロードを非同期で開始し進捗を run 経由で通知 - - micMessage / speakerMessage / chatMessage: 認識結果を受け、翻訳/OSC/Overlay/WebSocket/ログ記録を行う主要ハンドラ - - messageFormatter: OSC 用メッセージ整形 - - 多数の get/set 系関数: config の各種設定を読み書きし status/result を返す - -エラー/例外: -- VRAM 関連は特に注意し、検出時は該当機能を無効化してユーザーへ通知する。 - -API マッピング: -- `mainloop.py` の `mapping` と連携しており、多くの `/get/data/*` `/set/data/*` `/run/*` が Controller のメソッドにマッピングされる(詳細は docs/api.md を参照)。 - diff --git a/src-python/docs/modules/controller_ref.md b/src-python/docs/modules/controller_ref.md deleted file mode 100644 index 2f7cd570..00000000 --- a/src-python/docs/modules/controller_ref.md +++ /dev/null @@ -1,25 +0,0 @@ -## Controller リファクタリングノート (2025-10-09) - -概要: -このドキュメントは `controller.py` に適用した互換性修正と実装上の注意点をまとめた参照用メモです。既存の `controller.md` を直接上書きするのではなく、参照版として保存しています。 - -実施内容(要約): -- Model の lazy-init 対応に合わせ、`Controller.__init__()` 内で明示的に `model.init()` を呼び出す互換レイヤを追加しました。これにより、既存コードが import 時に model の属性へアクセスしていても安全に動作します。 -- オーバーレイの存在チェックを安全に行うため、`_is_overlay_available()` ヘルパを導入しました。以前に直接参照していた `model.overlay.initialized` をこのヘルパで置換しています(合計 5 箇所を置換)。 -- `micMessage` 内の翻訳周りで発生していたインデントの回帰を修正しました(try/except ブロックの整合性を回復)。 -- 未使用の `import copy` を削除しました。 -- ドキュメント編集は非破壊を原則とし、既存ファイルの安全な上書きが困難な場合は参照版(このファイル)を作成する方針を採りました。 - -互換性と注意点: -- Controller は起動時に model を初期化するため、多くの通常の利用ケースで変更の影響はありません。 -- ただし、外部のモジュールやテストコードが import 時に model の内部属性(例: `model.overlay` や `model.translator`)へ直接アクセスしている場合は、明示的に `model.init()` を呼ぶか、Controller を経由して初期化することを推奨します。 - -検証: -- 軽量なローカル検証を行い、`from controller import Controller; Controller()` の実行で初期化が成功することを確認しました。 - -今後の作業候補: -- 既存の `docs/modules/controller.md` とこの参照ドキュメントのマージ(必要であれば差分を反映して上書きを行う)。 -- linter/mypy を通して型安全性の追加と残存する静的解析の問題を解消する。 -- テスト: Controller の初期化・主要ハンドラ(micMessage/chatMessage)を対象にしたユニットテストを追加して、model.lazy-init による破壊的変更が再発しないことを保証する。 - -このファイルは自動生成ではなく、安全に変更履歴を残すための参照メモです。上書きを希望する場合はご指示ください。 diff --git a/src-python/docs/modules/device_manager.md b/src-python/docs/modules/device_manager.md deleted file mode 100644 index 316384c2..00000000 --- a/src-python/docs/modules/device_manager.md +++ /dev/null @@ -1,84 +0,0 @@ -# device_manager.py — デバイス検出と監視(overwrite) - -目的: システムのマイク/スピーカー(主に Windows の WASAPI)を列挙し、変更を監視してコールバックで通知する `DeviceManager` シングルトンを提供します。 - -主要コンポーネント: -- class Client(MMNotificationClient) - - オーディオデバイスのシステムイベント(追加/削除/デフォルト変更)を受け取り、監視ループの再起動をトリガーします。 - -- class DeviceManager - - シングルトンインスタンス: `device_manager` - - 主要プロパティ: - - `mic_devices` (dict): {host_name: [device_info, ...]} - - `default_mic_device` (dict): {'host': {...}, 'device': {...}} - - `speaker_devices` (list): [device_info, ...] - - `default_speaker_device` (dict) - - 各種 prev_/update_flag_: 差分検出用 - - callback 関連プロパティ: `callback_default_mic_device`, `callback_mic_device_list`, など多数 - - - 主要メソッド (抜粋): - - `update()` -> None: PyAudio を利用してホスト毎の入力デバイスとループバック(スピーカー)を列挙し内部状態を更新します。 - - `checkUpdate()` -> bool: 前回値との差分を計算して変更フラグを返します。 - - `monitoring()` -> None: pycaw/MMNotificationClient を使った長時間監視ループ。変化を検出すると各コールバックを呼び出す。 - - `startMonitoring()` / `stopMonitoring()` - - `getMicDevices()` / `getDefaultMicDevice()` / `getSpeakerDevices()` / `getDefaultSpeakerDevice()` - - `forceUpdateAndSetMicDevices()` / `forceUpdateAndSetSpeakerDevices()` - -コールバックAPI(例): -- `setCallbackMicDeviceList(callback)` — マイクデバイスリスト変更時に呼ばれる -- `setCallbackDefaultMicDevice(callback)` — デフォルトマイク変更時に呼ばれる -- `setCallbackProcessBeforeUpdateMicDevices(callback)` / `setCallbackProcessAfterUpdateMicDevices(callback)` — 更新前後のフック - -例: - -```python -from device_manager import device_manager - -def on_default_mic(host_name, device_name): - print('Default mic changed:', host_name, device_name) - -device_manager.setCallbackDefaultMicDevice(on_default_mic) -device_manager.forceUpdateAndSetMicDevices() -``` - -注意点: -- Windows 固有のモジュール(PyAudio paWASAPI, pycaw)に依存します。クロスプラットフォーム対応が必要な場合は別実装が必要です。 -- 監視スレッドは永続的に動作するため、アプリケーション終了時は `stopMonitoring()` を呼んで安全に停止してください。 - -変更点(2025-10-13): -- `DeviceManager` のシングルトン生成時(`__new__`)に軽量な `init()` を実行するようになりました。これによりモジュールのインポート順に依存せず、最小限の内部構造が常に確立されます(※監視スレッドは自動で起動しません)。 -- `init()` は監視スレッドを開始しませんが、PyAudio が利用可能な場合に限りベストエフォートで一度だけ `update()` を呼び、起動時に可能な限り実機デバイス情報を埋めるようになりました(例外は握り潰して安全性を維持)。 -- アクセサ (`getDefaultMicDevice()` / `getDefaultSpeakerDevice()` など) は遅延初期化を行い、呼び出し時に `init()` が動いていない場合は安全に初期化されるようになりました。これにより `controller` と `config` がトップレベルインポートで互いに依存している状況でも、`config` に正しいデバイス情報が入るようになります。 - -推奨起動シーケンス: -- `controller.init()` でコールバック登録が完了した直後に、`mainloop` の起動シーケンス中で明示的に `device_manager.startMonitoring()` を呼ぶことを推奨します。これにより以降のデバイス変更がコールバックを通じて確実に届きます。なお、`startMonitoring()` は任意で、軽量にしたい場合は呼ばなくても構いません(ただし動的変化は検出されません)。 - -ドキュメントにおける重要な注意: -- この変更は "import-time に重大な副作用を持たせない" という方針を維持しつつ、インポート順の違いによる初期化漏れを解消するために行われています。`init()` は監視スレッドを開始しないため、インポートだけでスレッドが走ることはありません。 - -## 詳細設計 - -目的: ローカルの入力(マイク)と出力(ループバックから抽出されたスピーカー)デバイスを列挙し、変更を監視してコールバックで通知する。Windows の WASAPI 等に依存。 - -主要クラス/関数: -- class Client(MMNotificationClient) - - Audio デバイスの変更イベントを受けると `loop = False` にして監視ループを再起動させる設計。 - -- class DeviceManager - - シングルトン: `device_manager = DeviceManager()` - - 主要属性: - - mic_devices: {host: [device_info...]} - - default_mic_device: {host, device} - - speaker_devices: [device_info...] - - default_speaker_device: {device} - - 各種 prev_*, update_flag_*: 差分検出のために保持 - - コールバック属性: callback_default_mic_device, callback_host_list など - - 主要メソッド: - - update(): PyAudio を使ってホストごとにデバイス列挙。Loopback デバイスを speaker_devices に集める。 - - monitoring(): MMNotificationClient と組み合わせてデバイスの変化を検出し、コールバックを発行 - - set/clear Callback 系: UI や Controller が登録して自動選択や再起動をトリガーできる - - forceUpdateAndSetMicDevices / forceUpdateAndSetSpeakerDevices: 即時更新とコールバック通知 - -注意点: -- Windows 固有の処理(paWASAPI, pycaw)に依存する。 -- デバイス取得はリソースに依存するので try/except で例外を吸収し errorLogging() を呼ぶ。 diff --git a/src-python/docs/modules/device_manager_ref.md b/src-python/docs/modules/device_manager_ref.md deleted file mode 100644 index 893bfb8b..00000000 --- a/src-python/docs/modules/device_manager_ref.md +++ /dev/null @@ -1,93 +0,0 @@ -# device_manager.py — デバイス検出と監視 (改訂版) - -### 概要 -`device_manager.py` はローカルのマイク(入力)とスピーカー(ループバックから抽出)を列挙し、デフォルトデバイスの変更やデバイスリストの変化を監視してコールバックで通知するユーティリティです。 - -設計上のポイント: -- Windows 固有の依存 (`comtypes`, `pyaudiowpatch` (PyAudio + WASAPI), `pycaw`) はオプショナルです。モジュールを import してもこれらが無ければ例外にならず、プレースホルダ値を返すようになっています。 -- モジュールの import 時点では監視は開始されません。リソースやスレッドの副作用を避けるため、`init()` と `startMonitoring()` は呼び出し側で明示的に実行してください。 - ---- - -### 使い方(簡単な流れ) - -1. モジュールをインポート - -```py -from device_manager import device_manager -``` - -2. 初期化(内部状態のセットアップ) - -```py -device_manager.init() -``` - -3. 監視の開始(バックグラウンドスレッド) - -```py -device_manager.startMonitoring() -``` - -4. 停止(アプリ終了時など) - -```py -device_manager.stopMonitoring() -``` - ---- - -### 主な API - -- `device_manager.init()` - - internal state の初期化。import 後に必ず呼ぶ必要はないが、実機デバイスを取得する前に呼ぶことを推奨します。 -- `device_manager.startMonitoring()` / `device_manager.stopMonitoring()` - - 監視の開始 / 停止。`startMonitoring()` はデーモンスレッドを作成します。`stopMonitoring()` は best-effort で join を試みます。 -- `device_manager.getMicDevices()` - - ホストごとにグループ化された入力デバイスの辞書を返します。例: `{ 'Realtek': [ {index: 2, name: 'Microphone (Realtek)'} ] }`。 -- `device_manager.getDefaultMicDevice()` / `device_manager.getSpeakerDevices()` / `device_manager.getDefaultSpeakerDevice()` - - デフォルトデバイスやスピーカーループバックの情報を返します。 -- `device_manager.forceUpdateAndSetMicDevices()` / `device_manager.forceUpdateAndSetSpeakerDevices()` - - 即時に update() を実行して対応するコールバックを呼びます。 - ---- - -### コールバック登録(例) - -コールバックは例外を内部で捕捉してログを出すため、コールバック実装側でもエラーハンドリングしてください。 - -- `setCallbackDefaultMicDevice(callback)` — デフォルト入力が変わったときに `callback(host_name, device_name)` が呼ばれます。 -- `setCallbackDefaultSpeakerDevice(callback)` — デフォルト出力が変わったときに `callback(device_name)` が呼ばれます。 -- `setCallbackHostList(callback)` / `setCallbackMicDeviceList(callback)` / `setCallbackSpeakerDeviceList(callback)` — それぞれ list 変更時に `callback()` が呼ばれます。 -- `setCallbackProcessBeforeUpdateMicDevices(callback)` / `setCallbackProcessAfterUpdateMicDevices(callback)` — 更新の前後に呼ばれるフックです。 - -簡単な例: - -```py -from device_manager import device_manager - -def on_default_mic(host, device): - print('default mic changed', host, device) - -device_manager.init() -device_manager.setCallbackDefaultMicDevice(on_default_mic) -device_manager.startMonitoring() - -# 後で停止 -# device_manager.stopMonitoring() -``` - ---- - -### 注意点 / トラブルシュート - -- Windows 固有の依存が無い場合、`getMicDevices()` などはデフォルトのプレースホルダ(`NoHost` / `NoDevice`)を返します。実機のデバイス検出や WASAPI によるループバック検出は Windows 環境でのみ保証されます。 -- `startMonitoring()` は監視用のデーモンスレッドを作るため、アプリケーションの終了時には `stopMonitoring()` を呼ぶかプロセスを終了してください。`stopMonitoring()` は join を行いますが、失敗した場合でも致命的にならないよう best-effort 実装です。 -- コールバック内部で例外が発生してもモジュール側で捕捉してログ出力します(`utils.errorLogging()`)。コールバック側で詳細なハンドリングやリトライが必要な場合は呼び出し側で行ってください。 - ---- - -### 実装メモ - -- `monitoring()` は可能なら Windows の COM (pycaw / MMNotificationClient) を使ってイベント駆動で待ち受け、失敗時や非Windows 環境では PyAudio を使ったポーリング(定期的な update()) にフォールバックします。 -- 外部ライブラリが原因の例外は内部で捕捉し、`errorLogging()` を呼んで記録する設計です。 diff --git a/src-python/docs/modules/mainloop.md b/src-python/docs/modules/mainloop.md deleted file mode 100644 index ba38c6a8..00000000 --- a/src-python/docs/modules/mainloop.md +++ /dev/null @@ -1,50 +0,0 @@ -## mainloop モジュール(src-python/mainloop.py) - -このドキュメントは `mainloop.py` の実装と、最近行ったリファクタの概要をまとめます。`mainloop` は標準入力から JSON を受け取り、`controller` のメソッドにルーティングして標準出力へ JSON で応答を返す小さなメインループです。 - -重要な変更点: -- 2025-10-09: `Main` クラスに `start()` / `stop()` を追加し、受信スレッドとハンドラスレッドのライフサイクル管理を明示化しました。`queue.get(timeout=...)` による安全なシャットダウンを可能にしています。 -- 2025-10-13: ハンドラの振る舞いを改善しました(マルチワーカー化とロック正規化): - - マルチワーカー化: ハンドラ処理はデフォルトで複数ワーカー(例: 3 本)で並列実行されます。これにより、1 つの重い処理が他のすべてのリクエストをブロックしてしまう問題を緩和します。 - - ロック正規化: `/set/enable/` と `/set/disable/` のような on/off ペアは同一のロックキーに正規化され、同一機能の on と off が同時に別スレッドで実行されることを防ぎます。これにより、遅い方の処理結果が後から上書きして最終状態が意図しないものになる不具合を防止します。 - -クラス: Main -- __init__(controller_instance: Controller, mapping_data: dict, worker_count: int = 3) -> None - - `controller_instance`: `Controller` のインスタンス。 - - `mapping_data`: `mainloop` 内で使用する `mapping`(エンドポイント -> ハンドラ情報)辞書。 - - `worker_count`: ハンドラワーカー数(デフォルト 3)。実行環境に応じて調整可能です。 -- start() -> None - - 内部で `startReceiver()` と `startHandler()` を呼び、受信とハンドラのスレッド群を起動します。 -- stop(wait: float = 2.0) -> None - - シャットダウンシグナルをセットし、スレッド終了を待ちます(デフォルト 2 秒)。 - -動作の重要ポイント -- キュー運用: 受信した JSON は内部キューに入れられ、ハンドラワーカーが順次取り出して処理します。`queue.get(timeout=...)` を使っているため CPU 負荷を抑えつつ安全に停止できます。 -- 同期応答設計: 各エンドポイントは基本的に呼び出し元に同期的に結果を返します(`handler` が戻り値としてステータスと結果を返す)。今回の変更でもこの設計は維持されています。 -- 同一機能直列化: `/set/enable/X` と `/set/disable/X` のような on/off ペアは内部で同一の "ロックキー" に正規化され、同時に両方が実行されることを防ぎます。これにより、enable と disable が競合して遅い方が勝つ問題が解消されます。 - -使い方(例): - -```python -from mainloop import Main, mapping, controller - -main_instance = Main(controller_instance=controller, mapping_data=mapping) -main_instance.start() - -# 実行中に別スレッドや外部シグナルで停止させる -main_instance.stop() -``` - -確認手順(変更の検証): -1. バックエンドを起動しておく。 -2. UI/テストスクリプトから `/set/enable/translation` と `/set/disable/translation` を高速に交互送信する(数十〜数百ミリ秒間隔で連打)。 -3. ログ(`printLog` 出力)を確認し、同一機能の複数実行が同時に走っていないこと、最終状態が遅い方に常に上書きされないことを確認する。 -4. 必要に応じて `worker_count` を増減して挙動を確認する(PC リソースに応じて 1〜6 程度を推奨)。 - -注意点と推奨事項: -- `worker_count` を増やすと他のエンドポイントの並列処理性は上がりますが、controller/model 側で共有リソース(GPU メモリやデバイスハンドルなど)への同時アクセスが許可されていない場合は、controller 側で機能単位のロック(例: translation_lock)を追加してください。 -- このドキュメントの変更は `mainloop` の外側から見える挙動(同期応答、ログ、ロックの方針)を説明するものです。controller 内の処理自体は引き続き同期的に実行されます。必要があれば、enable 系の重い処理を非同期化して完了通知をイベントで返す設計(UI 変更が必要)も検討してください。 - -変更履歴: -- 2025-10-09: start/stop ライフサイクル、タイムアウト付きキュー取得、エラー処理強化を追加。 -- 2025-10-13: マルチワーカー化(デフォルト 3)と enable/disable のロック正規化を実装。これにより同一機能の on/off の同時実行を防止し、UI からの高速トグルで最終状態が遅い方に上書きされる問題を修正しました。 diff --git a/src-python/docs/modules/model.md b/src-python/docs/modules/model.md deleted file mode 100644 index 391976e9..00000000 --- a/src-python/docs/modules/model.md +++ /dev/null @@ -1,118 +0,0 @@ -# model.py — クラスと主要メソッド -目的: アプリケーションの中核オーケストレータ。翻訳器 (Translator)、オーバーレイ、トランスクリプタ、OSC、WebSocket、Watchdog などのインスタンスを保持し、これらの起動/停止/操作を担います。`model` は `Model` のシングルトンインスタンスです。 - -主要クラスとシグネチャ: -- class threadFnc(Thread) - - __init__(self, fnc, end_fnc=None, daemon=True, *args, **kwargs) - - stop(self) -> None - - pause(self) -> None - - resume(self) -> None - -- class Model - - __new__(cls) -> Model - - init(self) -> None - - checkTranslatorCTranslate2ModelWeight(self, weight_type: str) -> bool - - changeTranslatorCTranslate2Model(self) -> None - - downloadCTranslate2ModelWeight(self, weight_type, callback=None, end_callback=None) -> Any - - isLoadedCTranslate2Model(self) -> bool - - getListLanguageAndCountry(self) -> list - - getTranslate(self, translator_name, source_language, target_language, target_country, message) -> tuple - - getInputTranslate(self, message, source_language=None) -> (list, list) - - getOutputTranslate(self, message, source_language=None) -> (list, list) - - startMicTranscript(self, fnc) -> None - - stopMicTranscript(self) -> None - - startSpeakerTranscript(self, fnc: Optional[Callable[[dict], None]] = None) -> None - - stopSpeakerTranscript(self) -> None - - startWebSocketServer(self, host, port) -> None - - stopWebSocketServer(self) -> None - - websocketSendMessage(self, message_dict: dict) -> bool - - 変更点(2025-10-09): - - - startCheckMicEnergy(self, fnc: Optional[Callable[[float], None]] = None) -> None - - 説明: 進捗/エネルギー表示用のコールバックを受け取ります。fnc が None の場合は内部で no-op を使い、呼び出し前に callable チェックを行います。これにより呼び出し側が None を渡しても安全になりました。 - - - startCheckSpeakerEnergy(self, fnc: Optional[Callable[[float], None]] = None) -> None - - 説明: 同上(fnc を Optional として受け取り、呼び出し時に callable を確認します)。内部では Queue を作成して録音データを受け取り、定期的にコールバックを呼びます。 - - - convertMessageToTransliteration(self, message: str, hiragana: bool = True, romaji: bool = True) -> list - - 説明: 以前は単一の文字列や別形を返す箇所がありましたが、現在は常にリスト(トークン単位の dict を要素とする list)を返します。hiragana/romaji の両方が False の場合は空リストを返します。 - - - createOverlayImageLargeLog(self, message_type: str, message: Optional[str], your_language: Optional[str], translation: list, target_language: Optional[dict] = None) -> object - - 説明: `target_language` は辞書形式で渡される場合があり、内部で言語リストに正規化されます(enabled な言語のみ抽出)。`message` / `your_language` は Optional となり、`None` を渡して翻訳のみのログを作ることが可能です。 - -使用例(簡易): - -```python -from model import model - -# 翻訳を呼び出す -translation, success = model.getTranslate('CTranslate2', 'Japanese', 'English', 'United States', 'こんにちは') -print(translation, success) - -# マイク文字起こしの開始(コールバックで結果を受け取る) -def on_mic_transcript(result): - print('mic transcript:', result) - -model.startMicTranscript(on_mic_transcript) - -# WebSocket サーバー起動 -model.startWebSocketServer('127.0.0.1', 2231) - -``` - -注意点: -- `Model` は多くの外部リソース(GPU、ファイル、ネットワーク)に依存するため、各操作は例外処理で保護されています。 -- 大きなモデルのロードで VRAM OOM を検出する `detectVRAMError` を備え、Controller 側でのフォールバック処理に使われます。 - -## 詳細設計 - -### 2025-10-09 のリファクタリング要約 - -- 遅延初期化 (lazy-init): `Model` のコンストラクタで重い初期化を行わず、`model.init()` を明示的に呼ぶか、各メソッド先頭で呼ばれる `ensure_initialized()` によって必要時に初期化する設計に変更しました。これによりインポート時の副作用(外部環境依存の初期化)が抑止されます。 - -- `threadFnc` の堅牢化: スレッドユーティリティは args/kwargs をインスタンスで保持し、内部で発生する例外を捕捉して `utils.errorLogging()` に委ねるようになりました。これによりバックグラウンドスレッドが例外で終了するリスクを減らしています。 - -- `device_manager` 呼び出しのガード: `getListMicHost()` / `getListMicDevice()` / `getMicDefaultDevice()` / `getListSpeakerDevice()` など、`device_manager` を参照する箇所は try/except で保護され、失敗時は安全なデフォルト(空リストや `"NoDevice"`)を返すようになりました。 - -- WebSocket/Overlay/Watchdog 等の起動系メソッドは `ensure_initialized()` を先頭に呼ぶようになり、遅延初期化の恩恵を受けるようになっています。 - -これらの変更は非破壊で既存の API を維持することを目的としていますが、起動フローで確実にリソースを確保したい場合はアプリ起動時に `model.init()` を呼ぶことを推奨します。 - - -目的: 各モデル(翻訳/転写/Overlay/Watchdog/OSC/WebSocket 等)のインスタンスを保持し、高レベルの操作を提供するファサード。 - -主要クラス/変数: -- class threadFnc(Thread) - - 説明: ループする関数をバックグラウンドで呼ぶヘルパ。pause/stop/end callback をサポート。 - -- class Model - - シングルトン: ファイル末で `model = Model()` として公開。 - - 主な属性: - - translator (Translator) - - overlay (Overlay) - - overlay_image (OverlayImage) - - mic_audio_queue, mic_audio_recorder, mic_transcriber - - speaker_audio_queue, speaker_audio_recorder, speaker_transcriber - - watchdog (Watchdog) - - osc_handler (OSCHandler) - - websocket_server (WebSocketServer) - - 主なメソッド: - - start/stop logger, overlay, watchdog - - startMicTranscript / stopMicTranscript: 録音、transcriber の起動とキュー処理 - - startSpeakerTranscript / stopSpeakerTranscript - - startCheckMicEnergy / stopCheckMicEnergy - - startCheckSpeakerEnergy / stopCheckSpeakerEnergy - - getTranslate / getInputTranslate / getOutputTranslate: Translator を利用する高レベル関数 - - createOverlayImage* / updateOverlay* : OverlayImage と Overlay を結合して VR 表示を作成 - - startWebSocketServer / stopWebSocketServer / websocketSendMessage - -エラー処理: -- 音声認識や翻訳で VRAM エラーが発生した場合、detectVRAMError() で特殊な例外内容を検査し、Controller 経由で翻訳機能を OFF にする処理がある。 - -非同期/リソース: -- Recorder/Transcriber/Overlay/Watchdog/WebSocket はそれぞれ別スレッドで動作する。Model はそれらの開始/停止を管理する。 - -依存: -- models/translation, models/transcription, models/overlay, models/osc, models/websocket - diff --git a/src-python/docs/modules/model_extra.md b/src-python/docs/modules/model_extra.md deleted file mode 100644 index 5d3af186..00000000 --- a/src-python/docs/modules/model_extra.md +++ /dev/null @@ -1,60 +0,0 @@ -# model.py — クラス一覧と使用例 - -以下は `model.py` で提供される主要クラスのシグネチャ概要と、簡単な呼び出し例です。 - -## クラス / 主要シグネチャ - -- class threadFnc(Thread) - - __init__(self, fnc: Callable, interval: float = 0.1, end_callback: Callable | None = None) - - start(self) -> None - - pause(self) -> None - - resume(self) -> None - - stop(self) -> None - -- class Model - - startLogger(self) -> None - - stopLogger(self) -> None - - startOverlay(self) -> None - - shutdownOverlay(self) -> None - - startMicTranscript(self, callback: Callable[[dict], None]) -> None - - stopMicTranscript(self) -> None - - startSpeakerTranscript(self, callback: Callable[[dict], None]) -> None - - stopSpeakerTranscript(self) -> None - - startCheckMicEnergy(self, progress_callback: Callable[[int], None]) -> None - - stopCheckMicEnergy(self) -> None - - startCheckSpeakerEnergy(self, progress_callback: Callable[[int], None]) -> None - - stopCheckSpeakerEnergy(self) -> None - - startWebSocketServer(self, host: str, port: int) -> None - - stopWebSocketServer(self) -> None - - websocketSendMessage(self, message: dict) -> None - - getListMicHost(self) -> dict - - getListMicDevice(self) -> list - - getListSpeakerDevice(self) -> list - - getInputTranslate(self, text: str, source_language: str = None) -> tuple[list[str], list[bool]] - - getOutputTranslate(self, text: str, source_language: str = None) -> tuple[list[str], list[bool]] - - detectVRAMError(self, exception: Exception) -> tuple[bool, str] - -## サンプル(呼び出し例) - -以下は Model の簡単な呼び出し例です。 - -```python -from model import model - -# マイク転写のコールバック例 -def on_mic_result(result: dict): - # result の想定形: {"text": str|False, "language": str} - text = result.get("text") - language = result.get("language") - print('mic:', text, language) - -# マイク転写を開始(別スレッドで動く) -model.startMicTranscript(on_mic_result) - -# 一度だけ翻訳を呼ぶ -translation, success = model.getInputTranslate('Hello', source_language='English') -print('translation:', translation, 'success:', success) - -# WebSocket 経由で外部クライアントへイベント送信 -model.websocketSendMessage({'type': 'INFO', 'message': 'VRCT ready'}) -``` diff --git a/src-python/docs/modules/osc.md b/src-python/docs/modules/osc.md deleted file mode 100644 index 1dc15e48..00000000 --- a/src-python/docs/modules/osc.md +++ /dev/null @@ -1,47 +0,0 @@ -## OSC モジュール (models.osc) - -このドキュメントは `models/osc/osc.py` の使い方と注意点を簡潔にまとめたものです。 - -### 概要 -- `OSCHandler` クラスは OSC メッセージの送信 (/chatbox/input, /chatbox/typing 等) と、 - ローカル環境では OSCQuery でエンドポイントを公開するための薄いラッパーを提供します。 - -### 依存関係 -- `python-osc` — UDP クライアント/サーバ -- `tinyoscquery` — OSCQuery を利用する場合に必要(オプショナル) - -### 使い方(例) - -```python -from models.osc.osc import OSCHandler - -handler = OSCHandler(ip_address="127.0.0.1", port=9000) -handler.setDictFilterAndTarget({ - "/chatbox/input": lambda addr, *args: print(args), -}) -handler.receiveOscParameters() -handler.sendTyping(True) -handler.sendMessage("Hello") -handler.oscServerStop() -``` - -### 注意点 -- `tinyoscquery` がインストールされていない場合、OSCQuery 関連機能は無効になりますが、送信(UDP クライアント)は動作します。 -- サービスのアドバタイズ中に例外が発生した場合、内部でリトライします。 -# models/osc — 詳細設計 - -目的: VRChat 等と OSC / OSCQuery 経由で値の取得やチャット送信を行う。 - -主要クラス/関数: -- class OSCHandler - - sendMessage(message: str, notification: bool=True): OSC で chatbox/input を送信 - - sendTyping(flag: bool): chatbox/typing を送信 - - receiveOscParameters(): OSCQuery を立て、指定したフィルタに対してローカルでサーバを実装してイベントを受ける - - getOSCParameterValue(address: str): OSCQuery を通じて現在値を問い合わせる(use tinyoscquery) - -注意点: -- `is_osc_query_enabled` が True のときに OSCQuery を使う(127.0.0.1 や localhost の場合に True) -- 受信ハンドラは dispatcher にマップしてコールバックを呼ぶ。 -- ネットワーク環境や OSCQuery の可否により動作が変わるため例外処理が多く入っている。 - -依存: python-osc, tinyoscquery diff --git a/src-python/docs/modules/overlay.md b/src-python/docs/modules/overlay.md deleted file mode 100644 index 648cbdcf..00000000 --- a/src-python/docs/modules/overlay.md +++ /dev/null @@ -1,59 +0,0 @@ -# overlay.py — OpenVR オーバーレイ管理 - -目的: OpenVR を使ったオーバーレイ表示(複数サイズ: small/large)を管理する `Overlay` クラスを提供します。 - -主要メソッド: -- __init__(self, settings_dict: dict) -- init(self) -> None -- startOverlay(self) -> None -- shutdownOverlay(self) -> None -- reStartOverlay(self) -> None -- updateImage(self, img: PIL.Image.Image, size: str) -> None -- updateOpacity(self, opacity: float, size: str, with_fade: bool = False) -> None -- updateUiScaling(self, ui_scaling: float, size: str) -> None -- updatePosition(self, x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation, tracker, size) -> None -- mainloop(self) -> None # アニメーション / フェード評価ループ - -使用上の注意: -- OpenVR (SteamVR) が稼働していることが前提です。`checkSteamvrRunning()` で `vrmonitor.exe` の存在チェックを行います。 -- 例外が発生した場合は `errorLogging()` を呼んでスタックトレースを残します。 - -短い使用例: - -```py -from models.overlay.overlay_image import OverlayImage -from models.overlay.overlay import Overlay -from PIL import Image - -settings = { - "small": { - "x_pos": 0.0, "y_pos": 0.0, "z_pos": 0.0, - "x_rotation": 0.0, "y_rotation": 0.0, "z_rotation": 0.0, - "display_duration": 5, "fadeout_duration": 2, - "opacity": 1.0, "ui_scaling": 1.0, "tracker": "HMD" - } -} - -overlay_img = OverlayImage() -overlay = Overlay(settings) -overlay.startOverlay() - -# wait until initialized -while not overlay.initialized: - time.sleep(0.5) - -# push a simple blank image -overlay.updateImage(Image.new("RGBA", (256, 64), (255,255,255,255)), "small") -``` - -## モジュール構成(補足) - -- overlay.py — OpenVR を使ったオーバーレイ管理。Overlay クラスは複数サイズ(small/large)を扱い、位置/回転/透明度/フェードを制御する。 -- overlay_image.py — PIL を使ってオーバーレイに表示する画像を生成(テキストボックス、ログレイアウト、フォント管理)。 -- overlay_utils.py — 行列演算や座標変換ユーティリティ。 - -注意点: -- OpenVR(SteamVR)に依存。SteamVR が動作していることが前提。 -- フォントファイルは repo の fonts フォルダか、ランタイム内パスを探索して読み込む。 -- 生成画像は RGBA バイト列に変換され `overlay.setOverlayRaw` で渡される。 - diff --git a/src-python/docs/modules/overlay_image.md b/src-python/docs/modules/overlay_image.md deleted file mode 100644 index 04735623..00000000 --- a/src-python/docs/modules/overlay_image.md +++ /dev/null @@ -1,115 +0,0 @@ -# overlay_image.py — 画像生成ユーティリティ -目的: `models.overlay.overlay_image.OverlayImage` の実装に基づき、オーバーレイ用のテキストボックス/ログ画像を PIL (Pillow) で生成するための仕様書です。 - -このドキュメントは実装に合わせて書かれており、主要な公開メソッドの振る舞い、引数、返り値、例外、使用例、注意点を含みます。 - -概要 ------- -- 提供クラス: `OverlayImage` -- 役割: 文字列(元文/翻訳)やメッセージタイプ(send/receive) を受け取り、Small/Large 向けの RGBA PIL.Image を生成する。 -- 依存: Pillow (PIL)、フォントファイル群(`fonts/` ディレクトリまたは環境配下) - -主要機能 --------- -- テキストをラップして画像化する(行折り返しを含む) -- 複数テキストブロック(原文+複数の翻訳)を縦に連結して一つの画像にする -- 背景(角丸矩形)を合成して最終的な RGBA 画像を返す -- Small と Large で UI 設定(幅、高さ、フォントサイズ等)を切り替え -- フォント探索: 実行環境の `fonts/` 配下または相対パスからフォントを探し、見つからない場合は FileNotFoundError を投げる - -公開 API(要約) ------------------ -- class OverlayImage(root_path: str | None = None) - - コンストラクタ引数 - - root_path: フォント等のリソースのベースディレクトリ。None の場合は実装に合わせて repo の `fonts/` を候補パスとして探索する。 - -- OverlayImage.createOverlayImageSmallLog(message: str, your_language: str, translation: list | None = None, target_language: list | None = None) -> PIL.Image.Image - - 説明: Small ログ向け(横長・1行〜複数行)にテキストブロックを作成して結合し、角丸背景と合成して RGBA 画像を返す。 - - 引数 - - message: 表示する原文テキスト(None を許容しない想定) - - your_language: 原文の言語キー(フォントマッピングに使用) - - translation: 翻訳テキストのリスト(省略可) - - target_language: 翻訳それぞれに対応する言語キーのリスト(省略可) - - 戻り値: PIL.Image.Image (RGBA) - - 例外: フォントが見つからない場合は FileNotFoundError を投げる可能性あり - -- OverlayImage.createOverlayImageLargeLog(message_type: str, message: str | None = None, your_language: str | None = None, translation: list | None = None, target_language: list | None = None) -> PIL.Image.Image - - 説明: Large ログ(複数行 + ヘッダ(Send/Receive)や時刻)向けに、複数ブロックを作成して縦結合し、背景を合成して返す。 - - 引数 - - message_type: 'send' または 'receive'(UI 向けアンカー/色指定に使用) - - message: 表示する原文テキスト(None 可。この場合翻訳のみを表示することもある) - - your_language: 原文の言語キー(フォント選定に使用) - - translation: 翻訳テキストのリスト(省略可) - - target_language: 翻訳それぞれに対応する言語キーのリスト(省略可) - - 戻り値: PIL.Image.Image (RGBA) - - 例外: フォント未発見などで FileNotFoundError を投げる可能性あり - -内部で使われる補助メソッド(要旨) ---------------------------------- -- concatenateImagesVertically(img1, img2, margin=0) -> Image -- addImageMargin(image, top, right, bottom, left, color) -> Image -- createTextboxSmallLog(...) -> Image -- createTextImageLargeLog(...) -> Image -- createTextboxLargeLog(...) -> Image -- getUiSizeSmallLog(), getUiColorSmallLog(), getUiSizeLargeLog(), getUiColorLargeLog() - -フォントとローカライズ ------------------------ -- 実装は `LANGUAGES` マッピングを持ち、言語キーからフォントファイル名を決定します(例: "Japanese" -> "NotoSansJP-Regular.ttf")。 -- フォントは `root_path` を基準に探索します。実行環境によりフォントファイルの場所が異なるため、実装は複数パスを順に試します。フォントが見つからない場合は FileNotFoundError を発生させる設計です。 - -描画と折り返しロジック(実装に基づく注意点) --------------------------------------------- -- テキスト幅を計算し、基準幅に収まるように文字数ベースで分割して折り返す単純なロジックを採用しています。厳密な単語単位折り返しではなく、文字数ベースの分割になります。 -- Small/Large でフォントサイズや余白、角丸半径などを分けており、複数行のテキストブロックを縦結合することで最終画像を作ります。 - -使用例 ------- -Small ログ画像を作る例: - -```python -from models.overlay.overlay_image import OverlayImage - -overlay = OverlayImage() -img = overlay.createOverlayImageSmallLog( - message='こんにちは、世界!', - your_language='Japanese', - translation=['Hello, world!'], - target_language=['English'] -) -img.save('overlay_small.png') -``` - -Large ログ(複数メッセージ履歴)を作る例: - -```python -from models.overlay.overlay_image import OverlayImage -from datetime import datetime - -overlay = OverlayImage() -img = overlay.createOverlayImageLargeLog( - message_type='send', - message='Hello from VRCT', - your_language='English', - translation=['こんにちは'], - target_language=['Japanese'] -) -img.save('overlay_large.png') -``` - -実装上の注意と推奨事項 ------------------------ -- 実行環境にフォントが存在することを確認してください(`fonts/` に主要フォントを置くのが簡単です)。 -- Pillow (PIL) のバージョンに依存する描画 API を使っています。Pillow は v8〜最新程度で問題ありません。 -- 長いテキストの折り返しは単純な文字幅分割ロジックです。より自然な折り返し(単語単位・ルビ考慮等)が必要なら実装拡張を推奨します。 -- 生成画像は RGBA(透過)です。Overlay 側の API(`overlay.setOverlayRaw` 相当)へ渡して使う前提です。 - -復元メモ --------- -このファイルは実装ファイル `models/overlay/overlay_image.py` を参照して復元しました。実装を変更した場合は本ドキュメントも同期して更新してください。 - -関連ファイル -------------- -- 実装: `models/overlay/overlay_image.py` -- ヘルパ: `models/overlay/overlay_utils.py` -- フォント: `fonts/` ディレクトリ diff --git a/src-python/docs/modules/transcription.md b/src-python/docs/modules/transcription.md deleted file mode 100644 index a8f5abc9..00000000 --- a/src-python/docs/modules/transcription.md +++ /dev/null @@ -1,126 +0,0 @@ -## 文字起こしモジュール (models.transcription) - -このドキュメントでは `models/transcription` に関する設計・セットアップ・使用例・テスト方針・トラブルシュートをまとめます。 - -### 概要 -- `models/transcription` は音声入力をテキストに変換する機能を提供します。主に: - - `transcription_recorder.py` — マイクやスピーカからの音声取得ラッパー - - `transcription_transcriber.py` — 音声バッファを認識エンジンに渡して文字起こしを行うロジック - - `transcription_whisper.py` — faster-whisper(WhisperModel)周りのダウンロード/ロード補助 - - `transcription_languages.py` — 各言語・国別のエンジン別コードマップ - -### 最近の変更点 -- 各モジュールに型注釈と docstring を追加しました。これによりメンテナンス性が向上します。 -- `transcription_whisper.py` にダウンロード進捗コールバックを明記した実装を追加しました。 - -### 依存関係 -主要な依存: -- `speech_recognition` — オーディオ録音と Google 音声認識のラッパー -- `pyaudiowpatch` — クロスプラットフォームのオーディオ設定 -- `pydub` — 音声のチャンネル変換や処理 -- `faster_whisper`(オプショナル)— ローカルで Whisper を使う場合 -- `huggingface_hub`(オプショナル)— モデルアーティファクトのダウンロード - -注意: `pydub` は `ffmpeg` が必要です。環境に ffmpeg が無いとワーニングが出ます。 - -推奨インストール(任意): - -```powershell -pip install speechrecognition pyaudiowpatch pydub faster-whisper huggingface-hub -``` - -テストでは多くの外部依存をモックするため、全てをインストールする必要はありません。 - -### 初回セットアップ -1. 必要に応じて `ffmpeg` をインストールしてください(pydub の動作に必要)。 -2. Whisper ローカルモデルを使う場合、`transcription_whisper.downloadWhisperWeight(root, weight_type, callback, end_callback)` を呼んでモデルを取得します。 - - `callback(progress: float)` は 0.0〜1.0 の進捗通知です。 - - 例: - -```python -from models.transcription import transcription_whisper as tw -tw.downloadWhisperWeight("./", "tiny", callback=lambda p: print(f"{p*100:.1f}%"), end_callback=lambda: print("done")) -``` - -### API 使用例 -簡単な `AudioTranscriber` の使い方: - -```python -from models.transcription.transcription_transcriber import AudioTranscriber - -# source はライブラリが提供するオーディオソースオブジェクト -tr = AudioTranscriber(speaker=False, source=source, phrase_timeout=3, max_phrases=10, transcription_engine="Google") -# audio_queue は録音スレッドがプッシュするキュー -tr.transcribeAudioQueue(audio_queue, languages=["English"], countries=["United States"]) -``` - -戻り値やエラー処理のルールについては各関数の docstring を参照してください。 - -### テスト方針 -- `AudioTranscriber` と `Whisper` ラッパーはユニットテストでモック化して検証します。 -- 推奨: `pytest` と `unittest.mock` を使い、以下のケースをカバーします: - - 正常系: Google/Whisper の成功パス(モックで期待テキストを返す) - - エッジ: 無音、低確信、複数言語 - - フォールバック: Whisper が利用不可の場合のフォールバック動作 - -### トラブルシュート -- ffmpeg が見つからない: `pydub` がワーニングを出します。OS に合わせて ffmpeg をインストールしてください。 -- Whisper のロード時に VRAM エラー: `getWhisperModel` は VRAM 不足を検出して `ValueError("VRAM_OUT_OF_MEMORY", message)` を投げます。デバイス設定や compute_type を調整してください。 -- ハッシュ不一致やダウンロード失敗: キャッシュや weights ディレクトリを削除して再ダウンロードしてください。 - -### 変更履歴 -- 2025-10-09: 型注釈と docstring を追加、ダウンロード/コールバック仕様を明記。 - ---- -このドキュメントは簡潔な参照用です。さらに詳細な実行手順(ログ収集方法、ffmpeg のインストール手順例など)が必要であれば追記します。 -# transcription — 文字起こしモジュール -概要: マイク/スピーカー音声の録音と Whisper/Google などのエンジンを使った文字起こしを提供するモジュール群です。主なクラスは録音用の Recorder と `AudioTranscriber` です。 - -主要クラス/シグネチャ: -- SelectedMicEnergyAndAudioRecorder(device, energy_threshold, dynamic_energy_threshold, phrase_time_limit) -- SelectedSpeakerEnergyAndAudioRecorder(...) -- SelectedMicEnergyRecorder(device) -- SelectedSpeakerEnergyRecorder(device) -- AudioTranscriber(speaker: bool, source, phrase_timeout: int, max_phrases: int, transcription_engine: str, root: str, whisper_weight_type: str, device: str, device_index: int, compute_type: str) - - transcribeAudioQueue(queue, languages:list, countries:list, avg_logprob: float, no_speech_prob: float) -> bool - - getTranscript() -> dict - -使用例: - -```python -from models.transcription.transcription_recorder import SelectedMicEnergyAndAudioRecorder -from models.transcription.transcription_transcriber import AudioTranscriber - -# 録音 -rec = SelectedMicEnergyAndAudioRecorder(device, energy_threshold=300, dynamic_energy_threshold=False, phrase_time_limit=3) -queue = Queue() -rec.recordIntoQueue(queue, None) - -# 文字起こし -transcriber = AudioTranscriber(speaker=False, source=rec.source, phrase_timeout=3, max_phrases=10, transcription_engine='Google', root='.', whisper_weight_type='base', device='cpu', device_index=0, compute_type='auto') -transcriber.transcribeAudioQueue(queue, ['Japanese'], ['Japan'], -0.8, 0.6) -print(transcriber.getTranscript()) -``` - -注意点: -- Whisper のモデルロードは VRAM を消費します。`Model.detectVRAMError` のような検知と回復策が必要です。 -- 録音は OS のデバイス依存のため `device_manager` でのデバイス取得と組み合わせて利用してください。 - -# models/transcription — 詳細設計 - -構成ファイル: -- transcription_recorder.py — 各デバイス向け Recorder クラス群(Base, SelectedMic*, SelectedSpeaker*)。speech_recognition をラップし、Audio / Energy をキューへ出す。 -- transcription_transcriber.py — AudioTranscriber: Google Speech API または faster-whisper を使った音声→テキスト変換の実行ロジック。複数言語に対する最良候補選択と confidence に基づく選出。 -- transcription_whisper.py — Whisper(faster-whisper)重みのダウンロードとモデル生成のユーティリティ。 - -主要契約: -- Recorder は recordIntoQueue(audio_queue, energy_queue) を提供し、バックグラウンドで音声データをキューに流す。 -- AudioTranscriber.transcribeAudioQueue(audio_queue, languages, countries, avg_logprob, no_speech_prob) -> bool - - audio_queue から音声を取り出し認識を試みる。結果は getTranscript() で取得する。常に True/False を返して呼び出し側がループ継続を制御。 - -VRAM エラー対策: -- Whisper のモデルロードで GPU メモリ不足が発生すると、ValueError("VRAM_OUT_OF_MEMORY", message) を投げる実装。Controller で捕捉して機能停止/通知する。 - -外部依存: -- speech_recognition, faster_whisper, pydub, numpy, torch - diff --git a/src-python/docs/modules/translation.md b/src-python/docs/modules/translation.md deleted file mode 100644 index a391da23..00000000 --- a/src-python/docs/modules/translation.md +++ /dev/null @@ -1,113 +0,0 @@ -## 翻訳モジュール (models.translation) - -このドキュメントは `models/translation` 配下に対して行った最近の変更点、セットアップ手順、API の使い方、テスト方針、トラブルシュートをまとめたものです。 - -### 概要 -- モジュールの責務: テキストの翻訳を行う高レベルの `Translator` クラス、言語コードのマッピング、CTranslate2 用の重み・トークナイザのダウンロード/検証ユーティリティを提供します。 -- 変更点の狙い: 型注釈と docstring を追加し、`translation_utils.py` のダウンロード/検証ロジックをシンプルで堅牢な実装へ置換しました。これにより初回セットアップの手順が明確になります。 - -### 主な変更点(サマリ) -- `translation_translator.py`: 型注釈、docstring を追記。外部依存は存在するが、例外が発生してもモジュールが壊れないように保護されています。 -- `translation_languages.py`: 言語コードマッピングの説明を追加。 -- `translation_utils.py`: 重みファイルの検証(SHA-256 ハッシュ照合)、zip 展開、`transformers.AutoTokenizer` を使ったトークナイザ取得、ダウンロード進捗用のコールバックを備えた実装へ置換。 - -### インストール(依存関係) -必須ではないものが含まれます。開発・最小稼働に必要なパッケージはプロジェクト全体の要件に従ってください。 - -主に使うパッケージ: -- `requests` — ダウンロード処理 -- `transformers` — トークナイザ取得(AutoTokenizer) -- `ctranslate2` — CTranslate2 を使う場合(ランタイムのみ、テストではモック推奨) - -推奨インストール例(任意): - -```powershell -pip install requests transformers ctranslate2 -``` - -DeepL や `translators` といった外部 API ラッパーはオプショナルです。CI やローカルテストではモックして動作確認してください。 - -### 初回セットアップ / 重みの準備 -`translation_utils.py` に含まれるユーティリティ関数: - -- `checkCTranslate2Weight(root: str, weight_type: str = "small") -> bool` - - 指定した `root/weights/ctranslate2/` 以下に必要なファイルが存在し、既知のハッシュと一致するかをチェックします。 - -- `downloadCTranslate2Weight(root: str, weight_type: str = "small", callback: Optional[Callable[[float], None]] = None, end_callback: Optional[Callable[[], None]] = None) -> None` - - 重みを ZIP 形式でダウンロードして展開します。 - - `callback(progress: float)` は 0.0〜1.0 の進捗通知に使えます。 - - `end_callback()` は処理完了時に呼び出されます。 - -- `downloadCTranslate2Tokenizer(path: str, weight_type: str = "small") -> None` - - `transformers.AutoTokenizer.from_pretrained` を利用してトークナイザをダウンロード/キャッシュします(`cache_dir` に保存)。 - -呼び出し例(簡単): - -```python -from models.translation import translation_utils as tu - -# ルートディレクトリ(プロジェクトルートなど) -root = "." -if not tu.checkCTranslate2Weight(root, "small"): - tu.downloadCTranslate2Weight(root, "small", callback=lambda p: print(f"{p*100:.1f}%")) - tu.downloadCTranslate2Tokenizer(root, "small") -``` - -注意: 大きなモデル(`large`)はダウンロードに時間とディスク容量を要します。 - -### API 使用例 (`Translator` の簡易例) - -以下は `Translator` の想定されるシンプルな使い方です(実装は `translation_translator.py` を参照してください)。 - -```python -from models.translation.translation_translator import Translator - -tr = Translator() -result = tr.translate("Hello", src_lang="en", target_lang="ja") -if result: - print(result) -else: - print("翻訳に失敗しました") -``` - -戻り値とエラー: 既存のコードベースとの互換性を重視し、失敗時は False を返すケースがあります。API 呼び出し前に戻り値の型を確認してください。 - -### テスト方針 -- 外部サービス(DeepL、web 翻訳ラッパー、ctranslate2、transformers)はユニットテストでモックします。 -- 推奨: `pytest` と `unittest.mock` を使い、`Translator.translate` の成功パス・失敗パスを検証するテストを追加してください。 - -簡単なテスト設計: -- 正常系: ctranslate2 経由の翻訳が正しく呼ばれる(モックで期待レスポンスを返す) -- フォールバック系: ctranslate2 が利用できない場合に別の翻訳経路を辿る(モック) - -### トラブルシュート -- `ModuleNotFoundError` (例: `sudachidict_full`) — transliteration/別モジュールで必要な辞書が無い場合。該当パッケージのインストールか、当該機能を無効にしてください。 -- ハッシュ不一致 — ダウンロード済みファイルの破損が疑われます。該当ファイルを削除して再ダウンロードしてください。 -- `transformers` のトークナイザが取得できない場合、ネットワークやキャッシュ先の権限を確認してください。 - -### 変更履歴 -- 2025-10-09: 型注釈と docstring の追加、`translation_utils.py` を再実装してダウンロード/検証ロジックを整理。 - ---- -このドキュメントは簡潔な参照用です。必要なら実行例やさらに詳細なトラブルシュート手順(コマンド出力例、ログの取り方など)を追加します。 -# models/translation — 詳細設計 - -構成ファイル: -- translation_translator.py — Translator クラス。DeepL/API、Google、Bing、Papago、CTranslate2 を統一インターフェースで扱う。 -- translation_utils.py — 重みファイルのダウンロード・検証ロジック(CTranslate2 用)。 -- translation_languages.py — 各エンジンの対応言語マップ。 - -Translator の契約: -- translate(translator_name, source_language, target_language, target_country, message) -> str|False - - 成功時は文字列、失敗または一時的エラーは False を返す。 -- changeCTranslate2Model(path, model_type, device, device_index, compute_type) - - CTranslate2 の Translator オブジェクトと Tokenizer を初期化する。 - -フォールバック: -- Controller/Model 層で翻訳が失敗した場合に CTranslate2 にフォールバックする実装がある。 - -外部依存: -- ctranslate2, transformers, deepl(オプション)、translators(任意) - -安全性: -- 翻訳 API キー(DeepL)は Translator.authenticationDeepLAuthKey で検証して保持。 diff --git a/src-python/docs/modules/transliteration.md b/src-python/docs/modules/transliteration.md deleted file mode 100644 index 577d055d..00000000 --- a/src-python/docs/modules/transliteration.md +++ /dev/null @@ -1,17 +0,0 @@ -# models/transliteration — 詳細設計 - -目的: 日本語テキストの仮名読みを解析し、ひらがな/ローマ字(Hepburn)に変換する。 - -主要クラス/関数: -- class Transliterator - - analyze(text: str, use_macron: bool=False) -> List[dict] - - 入力: テキスト - - 出力: トークンのリスト。各要素は { orig, kana, hira, hepburn } - - split_kanji_okurigana(surface, reading_kana): 漢字+送り仮名を分割して kana を割り当てるロジックを持つ(詳細設計あり) - -実装上のポイント: -- SudachiPy を使い形態素解析して読みを得る。 -- Katakana を Hiragana に変換し、katakana_to_hepburn モジュールでローマ字化を行う。 -- 文脈ルールを `transliteration_context_rules.apply_context_rules` で適用できる設計(ルールエンジン)。 - -依存: sudachipy diff --git a/src-python/docs/modules/utils.md b/src-python/docs/modules/utils.md deleted file mode 100644 index 9b7d4e71..00000000 --- a/src-python/docs/modules/utils.md +++ /dev/null @@ -1,132 +0,0 @@ -## utils モジュール(src-python/utils.py) - -このドキュメントは `src-python/utils.py` に対する最近のリファクタ内容、公開 API、利用上の注意点、テスト方法をまとめたものです。 - -### 概要 -- `utils.py` はプロジェクト全体で使われる汎用ユーティリティ群を提供します。主な内容: - - ネットワーク接続チェック (`isConnectedNetwork`) - - ソケットの空きポート確認 (`isAvailableWebSocketServer`) - - IP アドレス検証 (`isValidIpAddress`) - - 計算デバイス一覧取得 (`getComputeDeviceList` / `getBestComputeType`) - - Base64 デコード (JSON) (`encodeBase64`) - - ロガー設定/ログ出力ヘルパー (`setupLogger`, `printLog`, `printResponse`, `errorLogging`) - -### 今回のリファクタ(要点) -- Optional 依存へのフォールバック: `torch` と `ctranslate2` が存在しない環境でも動作するよう、import をガードし、安全なデフォルトを返す実装にしました。 -- 型注釈と docstring を追加して可読性を向上させました。 -- ログ設定の重複ハンドラ追加を防ぐチェックを導入しました。 -- `encodeBase64` はデコード失敗時に例外を投げず空辞書を返すように(安全側)変更しました。 -- `getComputeDeviceList` は GPU 情報取得で失敗しても CPU 情報を返すように例外保護を行いました。 - -### 重要な利用上の注意(breaking/behavior changes) -- Optional 依存 - - `torch` が無い環境では GPU 情報は取得できません(`getComputeDeviceList` は CPU エントリのみ返します)。 - - `ctranslate2` の `get_supported_compute_types` が無い場合は空リストを返します。 - → 環境に依存する挙動を想定して、呼び出し側は存在チェックやフォールバックを実装してください。 - -- `encodeBase64` の挙動 - - 不正な base64/JSON を入力した場合、例外を投げず `{}` を返します。既存コードが例外を期待している場合は注意してください。 - -- `isAvailableWebSocketServer` の仕様 - - 指定した host:port に対して bind が成功すれば True を返します(「使用中かどうか」を判定する用途と逆の意味合いになることがあるため注意)。 - -- ロギング - - `setupLogger` は同じログファイルに対するハンドラを重複して追加しません。`errorLogging()` はログ書き込みに失敗した場合でも最後に trace を stdout に出力するフォールバックがあります。 - -### API 使い方(短い例) - -```python -from utils import getComputeDeviceList, encodeBase64, printResponse - -devices = getComputeDeviceList() -print(devices) - -obj = encodeBase64('eyAia2V5IjogInZhbHVlIiB9') # -> {'key': 'value'} - -printResponse(200, '/health', {'status': 'ok'}) -``` - -### テスト方針 -- optional 依存の違いを扱うため、ユニットテストは `torch` と `ctranslate2` をモックして行うことを推奨します。 -- 例: `getComputeDeviceList()` は GPU がない環境でも CPU のエントリを返すことを確認するテスト。 - -### トラブルシュート -- ログファイルの書き込みエラー: 権限やディスク容量を確認してください。`error.log` と `process.log` の存在と権限をチェックします。 -- `getComputeDeviceList()` が空しか返さない場合、`torch` または `ctranslate2` のインストールを確認してください。 - -### 変更履歴 -- 2025-10-09: 型注釈・docstring 追加、optional import ガード、ロギング堅牢化。 -# utils.py — 関数一覧と使用例 -目的: 共通ユーティリティ(ログ、JSON 出力、ネットワーク/ポート検査、デバイス/計算タイプ列挙、バリデーション等)を提供します。 - -主要関数とシグネチャ: -- validateDictStructure(data: dict, structure: dict) -> bool -- isConnectedNetwork(url: str = "http://www.google.com", timeout: int = 3) -> bool -- isAvailableWebSocketServer(host: str, port: int) -> bool -- isValidIpAddress(ip_address: str) -> bool -- getComputeDeviceList() -> dict -- getBestComputeType(device: str, device_index: int) -> str -- encodeBase64(data: str) -> dict -- removeLog() -> None -- setupLogger(name, log_file, level=logging.INFO) -> logging.Logger -- printLog(log: str, data: Any = None) -> None -- printResponse(status: int, endpoint: str, result: Any = None) -> None -- errorLogging() -> None - -使用例: - -```python -from utils import printResponse, getComputeDeviceList, validateDictStructure - -# JSON 形式で mainloop に応答を返す -printResponse(200, '/get/data/version', {'version': '3.2.2'}) - -# 利用可能な計算デバイス一覧を取得 -devices = getComputeDeviceList() -print(devices) - -# 辞書構造のバリデーション -data = {'a': 1, 'b': {'c': 'x'}} -structure = {'a': int, 'b': {'c': str}} -ok = validateDictStructure(data, structure) -print('valid:', ok) -``` - -注意点: -- `printResponse` は stdout に JSON を出力しつつログファイルにも書き込みます。大きなオブジェクトは json.dumps で失敗する可能性があるため、例外処理が含まれています。 - -# utils.py — 詳細設計 - -目的: 小さなユーティリティ関数群。ロギング、ネットワーク検査、型検証、計算デバイス列挙など。 - -主要関数/変数: -- validateDictStructure(data: dict, structure: dict) -> bool - - 説明: 辞書が期待される構造(キーセットと値の型/入れ子)に完全一致するか検証する。 - - 入力: data(検証対象), structure(期待構造: 値が型または入れ子 dict) - - 出力: bool - - 例外: 型不一致や欠落時は False を返す(例外は投げない)。 - -- isConnectedNetwork(url="http://www.google.com", timeout=3) -> bool - - 説明: 指定 URL に HTTP GET して接続可否を判定。requests を使用。 - -- isAvailableWebSocketServer(host: str, port: int) -> bool - - 説明: 指定ポートへ bind できるかを試し、使用中かを判別する(True=利用可能)。 - -- isValidIpAddress(ip_address: str) -> bool - - 説明: ipaddress.ip_address で検証。 - -- getComputeDeviceList() -> dict - - 説明: CPU と CUDA(利用可能なら)を列挙し、各デバイスでサポートされる compute types を取得する。 - - 依存: torch, ctranslate2.get_supported_compute_types - -- getBestComputeType(device: str, device_index: int) -> str - - 説明: デバイス名に基づき優先 compute_type を選び、利用可能なものを返す。デフォルトは "float32"。 - -- setupLogger(name, log_file, level=logging.INFO) -> Logger - - 説明: RotatingFileHandler を使って UTF-8 ログを作る。10MB ローテーション。 - -- printLog / printResponse / errorLogging - - 説明: mainloop と通信するために標準出力へ JSON を flush するユーティリティ。内部で file ログへも書く。 - -注意点: -- ネットワーク検査やファイル生成で例外が発生した場合、errorLogging() を呼んでトレースを error.log に保存する。 diff --git a/src-python/docs/modules/watchdog.md b/src-python/docs/modules/watchdog.md deleted file mode 100644 index 92a05669..00000000 --- a/src-python/docs/modules/watchdog.md +++ /dev/null @@ -1,80 +0,0 @@ -# models/watchdog — 詳細設計 - -目的: 外部(Process 管理側)へ定期的に "生存" を知らせるために使う軽量ウォッチドッグ。 - -設計: -- class Watchdog(timeout: int = 60, interval: int = 20) - - feed(): 最終フィード時刻を更新 - - setCallback(callback): タイムアウト時に呼ぶコールバックを登録(zero-arg を想定) - - start(): 単一チェックを行い、`interval` 秒の sleep を行う(継続監視は呼び出し側でループまたはスレッド化) - -注意: -- 現行実装は非常にシンプルで、長時間のブロッキングやスレッド運用の見直しが必要になり得る。 - -変更点(実装に入れた改善): -- コールバック属性を初期化しておくことで AttributeError を防止 -- コールバック呼び出し内の例外はウォッチドッグ本体に影響を与えないよう try/except で保護 -- メソッドに型注釈と docstring を追加 - -短い使用例(ポーリング方式): - -```py -import time -from models.watchdog.watchdog import Watchdog - -def on_timeout(): - print('watchdog timed out') - -wd = Watchdog(timeout=5, interval=1) -wd.setCallback(on_timeout) - -# 別スレッドにせず、単純なループでポーリングする例 -while True: - wd.start() # ここで timeout をチェックし、必要なら callback を呼ぶ - # アプリケーションの他処理... - time.sleep(0.5) - - # 正常時に feed を呼ぶ例 - # wd.feed() -``` - -使用例(スレッド化ヘルパを用意するアプローチ): - -```py -import time -from threading import Thread, Event -from models.watchdog.watchdog import Watchdog - -stop_event = Event() - -def run_watchdog(wd: Watchdog, stop_event: Event): - # シンプルなバックグラウンド実行ループ(安全な停止用フラグ付き) - while not stop_event.is_set(): - wd.start() - -wd = Watchdog(timeout=10, interval=1) -wd.setCallback(lambda: print('timed out')) -thread = Thread(target=run_watchdog, args=(wd, stop_event), daemon=True) -thread.start() - -# 正常動作時 -wd.feed() -time.sleep(2) - -# 停止する場合は stop_event.set() を呼ぶ -stop_event.set() -thread.join() -``` - -拡張案(将来の改善): -- `start_in_thread()` / `stop()` を Watchdog に組み込む(内部で Thread と Event を管理して安全に停止できるようにする) -- コールバックに引数を渡せるようにする(context 情報、呼び出し回数など) -- asyncio と相互運用できるバージョン(async/await ベース)を用意する -- ロギング統合(標準 logging を使って状態変化を記録) -- 単発(one-shot)/繰り返しの動作モード指定 - -簡易テスト済み: -- 基本的なコールバックの有効/無効挙動をローカルで確認済み(feed 後は呼ばれず、タイムアウト状態で呼ばれる)。 - -注意事項: -- フル自動化(CI での運用)を行う場合は、スレッド起動・停止のテストを追加することを推奨します。 diff --git a/src-python/docs/modules/websocket.md b/src-python/docs/modules/websocket.md deleted file mode 100644 index 936d61de..00000000 --- a/src-python/docs/modules/websocket.md +++ /dev/null @@ -1,18 +0,0 @@ -# models/websocket — 詳細設計 - -目的: 外部クライアント(例えば第三者のアプリ)へ翻訳済みテキストやイベントをブロードキャストする軽量 WebSocket サーバー。 - -API: -- class WebSocketServer(host='127.0.0.1', port=8765) - - start(): 別スレッドで asyncio ループを生成しサーバを起動。 - - stop(): サーバ停止、全クライアント切断。 - - set_message_handler(handler): クライアントからのメッセージ受信時のコールバックを登録。handler(server, websocket, message) - - send(message): 非同期キューに積んで全クライアントへ送信(スレッドセーフ)。 - - broadcast(message): asyncio を経由して即時ブロードキャスト。 - -実装上の工夫: -- サーバ本体は別スレッドで asyncio イベントループを run_forever している。 -- 送信用に内部キュー `_send_queue` を持ち、_send_loop で順次送信する。これにより GUI 等から安全に send() を呼べる。 - -依存: websockets(asyncio) - diff --git a/src-python/docs/run_events_payloads.md b/src-python/docs/run_events_payloads.md deleted file mode 100644 index a30f7b8d..00000000 --- a/src-python/docs/run_events_payloads.md +++ /dev/null @@ -1,125 +0,0 @@ -# Run events payloads - -このファイルは `controller.py` 内で `self.run(status, run_mapping["key"], payload)` として発行される全ての run イベントの鍵と、実際に渡されるペイロードの具体例を列挙します。 - ---- - -## 抽出済み run イベント一覧(正規化済み) - -以下は controller.py の self.run 呼び出しを解析して抽出した run イベントです。名称は `mainloop.py` の `run_mapping` に合わせて正規化しています。 - -- connected_network (200) - - payload: true | false - -- enable_ai_models (200) - - payload: true | false - -- mic_host_list (200) - - payload: list[str] - -- mic_device_list (200) - - payload: list[str] - -- speaker_device_list (200) - - payload: list[str] - -- initialization_complete (200) - - payload: dict mapping endpoint -> current value (constructed from init_mapping) - -- selected_mic_device (200) - - payload: {"host": , "device": } - -- selected_speaker_device (200) - - payload: string (device name) - -- error_device (400) - - payload: {"message": , "data": null} - -- check_mic_volume (200) - - payload: numeric energy value (float) - -- check_speaker_volume (200) - - payload: numeric energy value (float) - -- download_progress_ctranslate2_weight (200) - - payload: {"weight_type": , "progress": } - -- downloaded_ctranslate2_weight (200) - - payload: - -- error_ctranslate2_weight (400) - - payload: {"message":"CTranslate2 weight download error","data": null} - -- download_progress_whisper_weight (200) - - payload: {"weight_type": , "progress": } - -- downloaded_whisper_weight (200) - - payload: - -- error_whisper_weight (400) - - payload: {"message":"Whisper weight download error","data": null} - -- word_filter (200) - - payload: {"message": "Detected by word filter: "} - -- error_translation_engine (400) - - payload: {"message":"Translation engine limit error","data": null} - -- error_translation_mic_vram_overflow (400) - - payload: {"message":"VRAM out of memory during translation of mic","data": } - -- error_translation_speaker_vram_overflow (400) - - payload: {"message":"VRAM out of memory during translation of speaker","data": } - -- error_translation_chat_vram_overflow (400) - - payload: {"message":"VRAM out of memory during translation of chat","data": } - -- enable_translation (400 or 200) - - payload example on OOM: {"message":"Translation disabled due to VRAM overflow","data": false} - -- transcription_send_mic_message (200) - - payload: { - "original": {"message": , "transliteration": }, - "translations": [ {"message": , "transliteration": }, ... ] - } - -- transcription_receive_speaker_message (200) - - payload: same shape as transcription_send_mic_message - -- software_update_info (200) - - payload: dict (e.g. {"has_update": true, "latest_version": "3.3.0"}) - -- selected_translation_compute_type (200) - - payload: string e.g. "auto" | "cpu" | "cuda:0" - -- selected_transcription_compute_type (200) - - payload: string - -- selected_translation_compute_device (200) - - payload: device descriptor (object) — `config.SELECTED_TRANSLATION_COMPUTE_DEVICE` の現在値。 - -- selected_translation_engines (200) - - payload: config.SELECTED_TRANSLATION_ENGINES (list/dict per tab) - -- translation_engines (200) - - payload: list of selectable engines (e.g. ["CTranslate2"]) - -- initialization_progress (200) - - payload: integer stage (used values in code: 1..4) - -- enable_osc_query (200) - - payload: {"data": true|false, "disabled_functions": [...]} - -- enable_transcription_receive (200) - - payload: boolean (true when transcription receive enabled) - -- error_transcription_mic_vram_overflow (400) - - payload: {"message":"VRAM out of memory during mic transcription","data": } - -- error_transcription_speaker_vram_overflow (400) - - payload: {"message":"VRAM out of memory during speaker transcription","data": } - ---- - -注: 上記は controller.py の self.run 呼び出しを解析して作成した "実際に送られる" ペイロード例です。UI 側はこれらの形を期待してコーディングしてください。状況によっては model 層からの戻り値の具象型が変化するため、実装では型チェック/存在チェックを行ってください。 - diff --git a/src-python/docs/runtime.md b/src-python/docs/runtime.md deleted file mode 100644 index f633e9f4..00000000 --- a/src-python/docs/runtime.md +++ /dev/null @@ -1,43 +0,0 @@ -# 実行手順と依存関係 - -対象 OS: Windows を想定(device_manager は WASAPI / pycaw を使う)。 - -必須依存(概略): -- Python 3.10+ 推奨 -- pip パッケージ: - - torch - - ctranslate2 - - transformers - - requests - - pyaudiowpatch - - pycaw - - speech_recognition - - pydub - - websockets - - python-osc - - tinyoscquery - - sudachipy - - pillow - - flashtext - - faster_whisper (オプション: Whisper をローカルで使う場合) - - deepl / translators(外部翻訳を使う場合) - -実行手順 (開発環境): -1. 仮想環境を作成し有効化 -2. 必要パッケージをインストール - - requirements.txt を用意する場合はそこからインストール -3. `src-python` をワークディレクトリにして `python mainloop.py` を実行 - -注意点: -- Whisper / CTranslate2 の重みは初回にダウンロードする必要がある。Controller の downloadCtranslate2Weight / downloadWhisperWeight エンドポイントからトリガできる。 -- OpenVR (SteamVR) を使う Overlay は SteamVR が動作している環境でのみ動作。 -- Windows 固有: device_manager が pyaudiowpatch と pycaw に依存。Linux/Mac での互換性は保証されない。 - -ログ: -- process.log (標準動作ログ) -- error.log (トレースバック) -- models 用のロガーは `model.startLogger()` により PATH_LOGS 配下に日付付きファイルを作成する。 - -デバッグ: -- `utils.printLog` と `utils.printResponse` が stdout に JSON を出すため、GUI 側はそれをパースして UI 更新を行う。 -- WebSocket を有効にすると外部クライアントに JSON をブロードキャストできる。 diff --git a/src-python/docs/utils.md b/src-python/docs/utils.md new file mode 100644 index 00000000..1abb554b --- /dev/null +++ b/src-python/docs/utils.md @@ -0,0 +1,940 @@ +# utils.py ドキュメント + +## 概要 +`utils.py` は VRCT アプリケーション全体で使用される汎用ユーティリティ関数とロギング機能を提供するモジュール。辞書構造の検証、ネットワーク接続確認、計算デバイス管理、Base64エンコーディング、構造化ログ出力など、複数のサブシステムで共有される基盤機能を集約している。 + +## 主要機能 +- 辞書構造の厳密な検証 +- ネットワーク接続状態の診断 +- WebSocketサーバーのアドレス可用性チェック +- IPアドレスのバリデーション +- CUDA/CPU計算デバイスの検出と最適化 +- 構造化ログ出力(process.log, error.log) +- Base64エンコード/デコード +- ログファイルのローテーション管理 + +## アーキテクチャ上の位置づけ + +``` +┌─────────────────┐ +│ All Modules │ (controller, model, device_manager, etc.) +└────────┬────────┘ + │ Import +┌────────▼────────┐ +│ utils.py │ ◄── このファイル +└─────────────────┘ + │ +┌────────▼────────┐ +│ External Deps │ (torch, ctranslate2, requests, ipaddress) +└─────────────────┘ +``` + +全てのモジュールから参照される共通基盤として機能し、循環参照を避けるため他の内部モジュールへの依存を持たない。 + +## 依存関係 + +### 標準ライブラリ +```python +import base64 +import json +import traceback +import logging +from logging.handlers import RotatingFileHandler +from typing import Any, List, Dict, Optional +``` + +### サードパーティライブラリ(オプション依存) +```python +import torch # GPU検出用(インポート失敗時はNoneにフォールバック) +from ctranslate2 import get_supported_compute_types # 計算タイプ取得用 +import requests # ネットワーク接続確認用 +import ipaddress # IPアドレス検証用 +import socket # WebSocketサーバー可用性チェック用 +``` + +**セーフガードインポート:** +```python +try: + import torch +except Exception: + torch = None # type: ignore + +try: + from ctranslate2 import get_supported_compute_types +except Exception: + def get_supported_compute_types(device: str, device_index: int) -> List[str]: + return [] +``` + +オプション依存が満たされない環境でもモジュールは正常にロード可能。 + +## 関数リファレンス + +### 1. 辞書構造検証 + +#### `validateDictStructure(data: dict, structure: dict) -> bool` + +**責務:** 辞書の構造と型が期待される仕様と完全に一致するかを検証 + +**アルゴリズム:** +1. 両方が辞書型であることを確認 +2. キーの数と名前が完全一致するかチェック +3. 各キーの値について: + - 期待値が辞書の場合: 再帰的に検証(多重入れ子対応) + - 期待値が型オブジェクトの場合: `isinstance()` で型チェック + +**引数:** +- `data` (dict): 検証対象の辞書 +- `structure` (dict): 期待される構造定義 + - 値には型(str, int, bool等)または入れ子の辞書を指定 + +**返り値:** +- `True`: 構造が完全に一致 +- `False`: 不一致(キー不足、余分なキー、型不一致等) + +**使用例:** +```python +# 単純な構造検証 +data = {"name": "Alice", "age": 30} +structure = {"name": str, "age": int} +assert validateDictStructure(data, structure) is True + +# 入れ子構造の検証 +data = { + "user": { + "id": 123, + "profile": {"name": "Bob", "active": True} + } +} +structure = { + "user": { + "id": int, + "profile": {"name": str, "active": bool} + } +} +assert validateDictStructure(data, structure) is True + +# 不一致の検出 +data = {"name": "Alice", "extra_key": "value"} +structure = {"name": str, "age": int} +assert validateDictStructure(data, structure) is False # キーが不一致 +``` + +**使用場面:** +- フロントエンドからのリクエストペイロード検証 +- 設定ファイルのスキーマ検証 +- API レスポンスの構造確認 + +--- + +### 2. ネットワーク診断 + +#### `isConnectedNetwork(url: str = "http://www.google.com", timeout: int = 3) -> bool` + +**責務:** インターネット接続の可用性を高速チェック + +**処理:** +1. 指定URLに HTTP GET リクエストを送信 +2. `timeout` 秒以内に 200 OK レスポンスを受信したら接続あり +3. タイムアウトまたは例外発生時は接続なし + +**引数:** +- `url` (str): 接続確認先URL(デフォルト: Google) +- `timeout` (int): タイムアウト時間(秒) + +**返り値:** +- `True`: ネットワーク接続あり +- `False`: ネットワーク接続なし + +**使用例:** +```python +if isConnectedNetwork(): + # モデルウェイトをダウンロード + downloadModelWeights() +else: + # オフラインモードで動作 + useLocalModels() +``` + +**注意事項:** +- ファイアウォールやプロキシ環境では正しく動作しない場合がある +- 初期化時の1回のみチェックを推奨(頻繁な呼び出しは避ける) + +--- + +#### `isAvailableWebSocketServer(host: str, port: int) -> bool` + +**責務:** 指定したホスト/ポートでWebSocketサーバーが起動可能かを確認 + +**処理:** +1. TCP ソケットを作成 +2. `SO_REUSEADDR` オプションを設定 +3. `bind()` を試行 +4. 成功 → アドレス利用可能、失敗 → アドレス使用中 + +**引数:** +- `host` (str): バインドするIPアドレス +- `port` (int): バインドするポート番号 + +**返り値:** +- `True`: アドレスが利用可能 +- `False`: アドレスが使用中 + +**使用例:** +```python +if isAvailableWebSocketServer("127.0.0.1", 8080): + startWebSocketServer("127.0.0.1", 8080) +else: + print("Port 8080 is already in use") +``` + +**注意事項:** +- `SO_REUSEADDR` により、TIME_WAIT 状態のアドレスも利用可能と判定される +- 管理者権限が必要なポート(1024未満)では失敗する場合がある + +--- + +#### `isValidIpAddress(ip_address: str) -> bool` + +**責務:** IPv4/IPv6アドレスの妥当性を検証 + +**処理:** +- `ipaddress.ip_address()` でパース +- 成功 → 有効なIPアドレス、失敗 → 無効 + +**引数:** +- `ip_address` (str): 検証対象のIPアドレス文字列 + +**返り値:** +- `True`: 有効なIPアドレス +- `False`: 無効なIPアドレス + +**使用例:** +```python +assert isValidIpAddress("127.0.0.1") is True +assert isValidIpAddress("2001:db8::1") is True +assert isValidIpAddress("invalid") is False +``` + +**サポート形式:** +- IPv4: "192.168.1.1", "127.0.0.1" +- IPv6: "2001:db8::1", "fe80::1" + +--- + +### 3. 計算デバイス管理 + +#### `getComputeDeviceList() -> List[Dict[str, Any]]` + +**責務:** 利用可能な計算デバイス(CPU/GPU)とサポートされる計算タイプを列挙 + +**返り値構造:** +```python +[ + { + "device": "cpu", + "device_index": 0, + "device_name": "cpu", + "compute_types": ["auto", "float32", "int8", ...] + }, + { + "device": "cuda", + "device_index": 0, + "device_name": "NVIDIA GeForce RTX 3090", + "compute_types": ["auto", "int8_bfloat16", "int8_float16", ...] + }, + ... +] +``` + +**処理フロー:** +1. CPU デバイスを常に追加(最低限の計算環境を保証) +2. PyTorch と CUDA が利用可能な場合: + - 全GPUデバイスを列挙 + - 各GPUの計算タイプを `get_supported_compute_types()` で取得 + - GPU アーキテクチャに応じて計算タイプを制限: + - **GTX シリーズ**: `int8_bfloat16`, `bfloat16`, `float16`, `int8` を除外 + - **RTX, Tesla, A100, Quadro**: 全計算タイプをサポート + - **その他**: `float32` のみ + +**GPU別の計算タイプ制限:** +```python +if "GTX" in gpu_device_name: + unsupported_types = {"int8_bfloat16", "bfloat16", "float16", "int8"} + gpu_compute_types = [t for t in gpu_compute_types if t not in unsupported_types] +elif not any(keyword in gpu_device_name for keyword in ["RTX", "Tesla", "A100", "Quadro"]): + gpu_compute_types = ["float32"] +``` + +**使用例:** +```python +devices = getComputeDeviceList() +for device in devices: + print(f"{device['device_name']}: {', '.join(device['compute_types'])}") + +# 出力例: +# cpu: auto, float32, int8 +# NVIDIA GeForce RTX 3090: auto, int8_bfloat16, int8_float16, int8, bfloat16, float16, int8_float32, float32 +``` + +**エラーハンドリング:** +- GPU検出中の例外は `errorLogging()` でログ記録し、CPU デバイスのみ返却 + +--- + +#### `getBestComputeType(device: str, device_index: int) -> str` + +**責務:** デバイスアーキテクチャに最適な計算タイプを自動選択 + +**優先順位:** +```python +preferred_types = { + "default": [ + "int8_bfloat16", # 最も効率的(対応GPUのみ) + "int8_float16", # 2番目に効率的 + "int8", # 整数演算高速化 + "bfloat16", # 混合精度 + "float16", # 半精度浮動小数点 + "int8_float32", # 互換性重視 + "float32" # フォールバック + ], + "GTX": ["float32"], # GTXシリーズは制限あり + "RTX": ["int8_bfloat16", "int8_float16", ...], + "Tesla": [...], + "A100": [...], + "Quadro": [...] +} +``` + +**処理フロー:** +1. `get_supported_compute_types()` で利用可能な計算タイプを取得 +2. デバイス名に基づいて優先リストを選択 +3. 優先順に計算タイプをチェックし、最初に利用可能なものを返却 +4. 全て利用不可の場合は `"float32"` を返却(安全なフォールバック) + +**引数:** +- `device` (str): "cpu" または "cuda" +- `device_index` (int): GPUデバイスのインデックス(CPUの場合は0) + +**返り値:** +- 最適な計算タイプ文字列(例: "int8_bfloat16", "float32") + +**使用例:** +```python +best_type = getBestComputeType("cuda", 0) +model.load_model(compute_type=best_type) +``` + +**計算タイプの特性:** + +| 計算タイプ | メモリ使用量 | 速度 | 精度 | 対応GPU | +|----------|------------|------|------|--------| +| int8_bfloat16 | 最小 | 最速 | 高 | RTX 30xx以降 | +| int8_float16 | 最小 | 最速 | 高 | RTX 20xx以降 | +| int8 | 小 | 高速 | 中 | 多くのGPU | +| bfloat16 | 中 | 高速 | 高 | RTX 30xx以降 | +| float16 | 中 | 高速 | 高 | RTX 20xx以降 | +| float32 | 大 | 標準 | 最高 | 全GPU/CPU | + +--- + +### 4. エンコーディング + +#### `encodeBase64(data: str) -> Dict[str, Any]` + +**責務:** Base64エンコードされたJSON文字列をデコードしてパース + +**処理:** +1. Base64デコード +2. UTF-8文字列に変換 +3. JSON パース +4. 失敗時は空の辞書を返却 + +**引数:** +- `data` (str): Base64エンコードされたJSON文字列 + +**返り値:** +- パース成功: JSON オブジェクト +- パース失敗: `{}`(空の辞書) + +**使用例:** +```python +# エンコード例(参考) +import base64 +import json +payload = {"message": "Hello", "id": 123} +encoded = base64.b64encode(json.dumps(payload).encode('utf-8')).decode('utf-8') + +# デコード +decoded = encodeBase64(encoded) +assert decoded == {"message": "Hello", "id": 123} +``` + +**エラーハンドリング:** +- 不正なBase64文字列 +- 不正なJSON形式 +- 文字エンコーディングエラー + +全て `errorLogging()` でログ記録し、空の辞書を返却。 + +**注意事項:** +- 関数名が `encodeBase64` だが、実際には**デコード**を行う(命名の歴史的経緯) +- セキュリティ: Base64は暗号化ではないため、機密情報の保護には使用しない + +--- + +### 5. ロギング + +#### `removeLog() -> None` + +**責務:** プロセスログファイル(process.log)を初期化 + +**処理:** +- `process.log` を空の内容で上書き +- ファイルが存在しない場合は新規作成 + +**使用例:** +```python +# アプリケーション起動時にログをクリア +removeLog() +printLog("Application started") +``` + +**エラーハンドリング:** +- ファイル書き込み失敗時は `errorLogging()` でエラーログに記録 + +--- + +#### `setupLogger(name: str, log_file: str, level: int = logging.INFO) -> logging.Logger` + +**責務:** ローテーション機能付きロガーインスタンスを生成 + +**設定:** +- **最大ログサイズ**: 10MB +- **バックアップ数**: 1(最大2ファイル) +- **ローテーション動作**: 10MB到達時に `.1` バックアップを作成し、新規ログを開始 +- **エンコーディング**: UTF-8 +- **遅延書き込み**: `delay=True`(最初の書き込み時にファイルを開く) + +**引数:** +- `name` (str): ロガー名(例: "process", "error") +- `log_file` (str): ログファイルパス +- `level` (int): ログレベル(デフォルト: `logging.INFO`) + +**返り値:** +- 設定済み `logging.Logger` インスタンス + +**ログフォーマット:** +``` +%(asctime)s - %(name)s - %(levelname)s - %(message)s +``` + +**出力例:** +``` +2025-10-13 14:30:45,123 - process - INFO - Application started +2025-10-13 14:30:46,456 - error - ERROR - Connection failed +``` + +**重複ハンドラー防止:** +```python +if not any(isinstance(h, RotatingFileHandler) and getattr(h, 'baseFilename', None) == getattr(file_handler, 'baseFilename', None) for h in logger.handlers): + logger.addHandler(file_handler) +``` + +同じファイルへの重複ハンドラー追加を防止し、複数回呼び出されても安全。 + +--- + +#### `printLog(log: str, data: Any = None) -> None` + +**責務:** 構造化プロセスログの出力 + +**出力先:** +1. `process.log` ファイル +2. 標準出力(JSON形式) + +**出力形式:** +```python +{ + "status": 348, # プロセスログ専用ステータス + "log": "User action performed", + "data": "additional context" +} +``` + +**引数:** +- `log` (str): ログメッセージ +- `data` (Any): 追加のコンテキスト情報(オプション) + +**使用例:** +```python +printLog("Model loading started", {"model_type": "whisper", "weight": "medium"}) +# 出力(stdout): +# {"status": 348, "log": "Model loading started", "data": "{'model_type': 'whisper', 'weight': 'medium'}"} +``` + +**実装の詳細:** +```python +global process_logger +if process_logger is None: + process_logger = setupLogger("process", "process.log", logging.INFO) + +response = { + "status": 348, + "log": log, + "data": str(data), +} +process_logger.info(response) +serialized = json.dumps(response) +print(serialized, flush=True) +``` + +**注意事項:** +- `data` は `str()` で文字列化されるため、複雑なオブジェクトは読みにくくなる可能性がある +- `flush=True` により即座に出力(バッファリングを無効化) + +--- + +#### `printResponse(status: int, endpoint: str, result: Any = None) -> None` + +**責務:** 構造化APIレスポンスの出力 + +**出力先:** +1. `process.log` ファイル +2. 標準出力(JSON形式) + +**出力形式:** +```python +{ + "status": 200, + "endpoint": "/get/config/version", + "result": {"version": "3.3.0"} +} +``` + +**引数:** +- `status` (int): HTTPステータスコード風のステータス番号 +- `endpoint` (str): エンドポイント識別子 +- `result` (Any): レスポンスペイロード(オプション) + +**使用例:** +```python +printResponse(200, "/set/config/language", {"language": "ja"}) +printResponse(400, "/set/config/threshold", {"error": "Value out of range"}) +``` + +**JSONシリアライズエラーハンドリング:** +```python +try: + serialized_response = json.dumps(response) +except Exception as e: + errorLogging() # 完全なトレースバックをログ + process_logger.error(f"Problematic response object: {response}") + process_logger.error(f"Exception during json.dumps: {e}") + # フォールバックエラーペイロード + error_json = json.dumps({ + "status": 500, + "endpoint": endpoint, + "result": {"error": "Failed to serialize response", "details": str(e)}, + }) + print(error_json, flush=True) +else: + print(serialized_response, flush=True) +``` + +**シリアライズ不可能なオブジェクトの例:** +- `datetime` オブジェクト +- カスタムクラスインスタンス +- 循環参照を持つ辞書 + +**対策:** +- `result` を構築する際に JSON シリアライズ可能な型のみ使用 +- 必要に応じて `str()` や専用のシリアライザーで変換 + +--- + +#### `errorLogging() -> None` + +**責務:** 現在の例外トレースバックをエラーログに記録 + +**処理:** +1. `error.log` ファイルにトレースバックを出力 +2. ロガー初期化失敗時は標準出力にフォールバック + +**使用例:** +```python +try: + risky_operation() +except Exception: + errorLogging() # トレースバックをerror.logに記録 + # 必要に応じて追加処理 +``` + +**出力例(error.log):** +``` +2025-10-13 14:35:12,789 - error - ERROR - Traceback (most recent call last): + File "model.py", line 123, in loadModel + model.load() + File "ctranslate2/model.py", line 456, in load + raise RuntimeError("CUDA out of memory") +RuntimeError: CUDA out of memory +``` + +**注意事項:** +- **例外コンテキスト内でのみ呼び出し可能**(`traceback.format_exc()` を使用) +- 例外をキャッチせずに呼び出すと空のトレースバックが記録される + +**ベストプラクティス:** +```python +try: + dangerous_function() +except SpecificException as e: + errorLogging() # 詳細をログ + # ユーザーフレンドリーなエラー処理 + printResponse(400, endpoint, {"error": "Operation failed"}) +except Exception: + errorLogging() # 予期しないエラーもログ + raise # 上位へ伝播 +``` + +--- + +## グローバル変数 + +### `process_logger: Optional[logging.Logger] = None` +プロセスログ用のグローバルロガーインスタンス。初回 `printLog()` または `printResponse()` 呼び出し時に初期化される。 + +### `error_logger: Optional[logging.Logger] = None` +エラーログ用のグローバルロガーインスタンス。初回 `errorLogging()` 呼び出し時に初期化される。 + +**遅延初期化の理由:** +- モジュールインポート時のオーバーヘッド削減 +- ファイルシステムへの不要なアクセスを回避 + +--- + +## エラーハンドリング戦略 + +### 1. 防御的プログラミング +全てのユーティリティ関数は例外を内部で処理し、呼び出し元に例外を伝播しない: + +```python +def isConnectedNetwork(url="http://www.google.com", timeout=3) -> bool: + try: + response = requests.get(url, timeout=timeout) + return response.status_code == 200 + except requests.RequestException: + return False # 例外をキャッチして安全な値を返却 +``` + +### 2. フォールバック値 +- `encodeBase64()`: パース失敗時は `{}` +- `getComputeDeviceList()`: GPU検出失敗時はCPUのみ +- `getBestComputeType()`: 全て失敗時は `"float32"` + +### 3. ログ記録 +全てのエラーは `errorLogging()` でトレースバックを記録し、デバッグを容易にする。 + +--- + +## パフォーマンス考慮事項 + +### 1. ネットワーク接続チェック +`isConnectedNetwork()` はブロッキング操作(最大3秒)のため、起動時の1回のみ実行を推奨: + +```python +# 良い例 +if isConnectedNetwork(): + downloadModels() + +# 悪い例(UI フリーズの原因) +while True: + if isConnectedNetwork(): # 毎回3秒待機 + processData() +``` + +### 2. ログローテーション +10MB のログファイルローテーションにより、ディスク容量を制御(最大20MB)。 + +### 3. グローバルロガーの遅延初期化 +ロガーは初回使用時に初期化されるため、インポート時のオーバーヘッドを最小化。 + +--- + +## 使用パターン + +### パターン1: ネットワーク依存機能の初期化 +```python +def initialize_online_features(): + if not isConnectedNetwork(): + printLog("Offline mode: skipping model download") + return + + printLog("Online mode: downloading models") + downloadModels() +``` + +### パターン2: デバイス自動選択 +```python +devices = getComputeDeviceList() +if len(devices) > 1: + # GPU利用可能 + best_device = devices[1] # 最初のGPU + best_type = getBestComputeType(best_device["device"], best_device["device_index"]) + printLog(f"Using GPU: {best_device['device_name']}", {"compute_type": best_type}) +else: + # CPUのみ + printLog("No GPU detected, using CPU") + best_type = "float32" +``` + +### パターン3: 構造化リクエスト検証 +```python +def handle_request(payload): + expected_structure = { + "action": str, + "data": { + "id": int, + "value": str + } + } + + if not validateDictStructure(payload, expected_structure): + printResponse(400, "/handle_request", {"error": "Invalid request structure"}) + return + + # 処理続行 + printLog("Valid request received", payload) +``` + +### パターン4: WebSocketサーバー起動 +```python +def start_websocket(host, port): + if not isValidIpAddress(host): + printResponse(400, "/websocket/start", {"error": "Invalid IP address"}) + return + + if not isAvailableWebSocketServer(host, port): + printResponse(400, "/websocket/start", {"error": f"Port {port} is in use"}) + return + + # サーバー起動 + printLog(f"Starting WebSocket server", {"host": host, "port": port}) + startServer(host, port) +``` + +--- + +## テスト推奨事項 + +### 単体テスト例 + +**辞書構造検証:** +```python +def test_validate_dict_structure_simple(): + data = {"name": "Alice", "age": 30} + structure = {"name": str, "age": int} + assert validateDictStructure(data, structure) is True + +def test_validate_dict_structure_nested(): + data = {"user": {"id": 1, "active": True}} + structure = {"user": {"id": int, "active": bool}} + assert validateDictStructure(data, structure) is True + +def test_validate_dict_structure_invalid(): + data = {"name": "Alice"} + structure = {"name": str, "age": int} # 'age'キーが不足 + assert validateDictStructure(data, structure) is False +``` + +**ネットワーク診断:** +```python +def test_network_connection(): + # 実際のネットワーク接続をテスト + result = isConnectedNetwork() + assert isinstance(result, bool) + +def test_network_timeout(): + # タイムアウト動作を確認 + result = isConnectedNetwork(url="http://192.0.2.1", timeout=1) + assert result is False +``` + +**計算デバイス:** +```python +def test_get_compute_device_list(): + devices = getComputeDeviceList() + assert len(devices) >= 1 # 最低限CPUが含まれる + assert devices[0]["device"] == "cpu" + +def test_get_best_compute_type(): + compute_type = getBestComputeType("cpu", 0) + assert compute_type in ["float32", "int8"] +``` + +**ロギング:** +```python +def test_print_log(capsys): + printLog("Test message", {"key": "value"}) + captured = capsys.readouterr() + output = json.loads(captured.out) + assert output["status"] == 348 + assert output["log"] == "Test message" + +def test_print_response(capsys): + printResponse(200, "/test", {"result": "success"}) + captured = capsys.readouterr() + output = json.loads(captured.out) + assert output["status"] == 200 + assert output["endpoint"] == "/test" +``` + +--- + +## セキュリティ考慮事項 + +### 1. IPアドレス検証 +`isValidIpAddress()` はフォーマット検証のみで、プライベートアドレス範囲のチェックは行わない: + +```python +# セキュリティを強化する場合 +import ipaddress + +def is_public_ip(ip_str): + if not isValidIpAddress(ip_str): + return False + ip = ipaddress.ip_address(ip_str) + return not (ip.is_private or ip.is_loopback or ip.is_reserved) +``` + +### 2. Base64デコード +`encodeBase64()` は入力検証を行わないため、信頼できないソースからのデータには注意: + +```python +# 安全な使用例 +if source_is_trusted: + data = encodeBase64(base64_string) +else: + # 追加の検証を実施 + pass +``` + +### 3. ログファイルへの機密情報記録 +ログに機密情報(API キー、パスワード等)が含まれないよう注意: + +```python +# 悪い例 +printLog("API key loaded", api_key) + +# 良い例 +printLog("API key loaded", "***REDACTED***") +``` + +--- + +## 制限事項 + +1. **プラットフォーム依存性:** + - GPU検出は CUDA 環境でのみ動作(ROCm/Metal非対応) + +2. **ネットワークチェックの制限:** + - ファイアウォール、プロキシ環境で誤判定の可能性 + - IPv6専用環境での動作は未検証 + +3. **ログファイルのスレッドセーフティ:** + - `RotatingFileHandler` は基本的にスレッドセーフだが、高負荷時のローテーション中にログ損失の可能性 + +4. **計算タイプの最適化:** + - `getBestComputeType()` の優先順位は一般的な推奨値であり、特定のモデルやタスクでは最適でない場合がある + +--- + +## 依存モジュールとの関係 + +### controller.py +- デバイス管理の設定変更時にデバイスリスト取得 +- エラー時のログ記録 +- ネットワーク接続確認 + +### model.py +- 計算デバイスとタイプの決定 +- エラー時のトレースバック記録 + +### config.py +- 起動時のネットワーク接続確認 +- 計算デバイスリストの提供 + +### mainloop.py +- リクエスト/レスポンスの構造化ログ出力 +- エラー時のトレースバック記録 + +--- + +## 今後の拡張性 + +### 1. 非同期ネットワークチェック +```python +import asyncio +import aiohttp + +async def isConnectedNetworkAsync(url="http://www.google.com", timeout=3) -> bool: + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=aiohttp.ClientTimeout(total=timeout)) as response: + return response.status == 200 + except Exception: + return False +``` + +### 2. 構造化ログの拡張 +```python +def printStructuredLog(level: str, message: str, context: dict = None): + """ + より詳細な構造化ログ出力 + - timestamp + - level + - message + - context (key-value pairs) + - stack trace (error時) + """ + pass +``` + +### 3. メトリクス収集 +```python +def recordMetric(metric_name: str, value: float, tags: dict = None): + """ + パフォーマンスメトリクスの記録 + - function execution time + - memory usage + - GPU utilization + """ + pass +``` + +--- + +## 関連ドキュメント +- `controller.md`: Controller での utils 関数使用例 +- `config.md`: Config での計算デバイス管理 +- `model.md`: Model でのエラーハンドリング +- `コーディングルール.md`: ロギングとエラーハンドリングの規約 + +--- + +## ライセンス +プロジェクトのルートディレクトリの `LICENSE` ファイルを参照 + +--- + +## まとめ + +`utils.py` は VRCT プロジェクトの基盤インフラストラクチャとして、以下の重要な責務を担う: + +1. **安全性**: 全ての関数が例外を内部処理し、安全なフォールバック値を提供 +2. **可観測性**: 構造化ログとローテーション機能により、問題の診断を容易化 +3. **互換性**: オプション依存のセーフガードにより、様々な環境で動作 +4. **最適化**: GPU アーキテクチャに応じた計算タイプの自動選択 +5. **検証**: 辞書構造、IPアドレス、ネットワーク接続の厳密なバリデーション + +全てのサブシステムから依存される中核モジュールとして、高い信頼性と保守性を維持している。 diff --git a/src-python/docs/CODING_RULES.md b/src-python/docs/コーディングルール.md similarity index 100% rename from src-python/docs/CODING_RULES.md rename to src-python/docs/コーディングルール.md diff --git a/src-python/docs/仕様書.md b/src-python/docs/仕様書.md new file mode 100644 index 00000000..1ccd8ea2 --- /dev/null +++ b/src-python/docs/仕様書.md @@ -0,0 +1,58 @@ +# 仕様書 + +概要 +- プロジェクト名: VRCT (VR Chat Translator) +- 目的: マイク入力とスピーカー出力をリアルタイムに文字起こし・翻訳し、VR オーバーレイや OSC/WebSocket 経由で外部に送出するバックエンドロジック。 +- 言語: Python + +対象ユーザー +- VR 環境でリアルタイム翻訳・文字起こしを利用したいエンドユーザー +- フロントエンド(GUI)や VR クライアント(OSC)と連携するアプリケーション開発者 + +主要機能(機能要件) +1. 音声の取り込み・文字起こし + - マイク(送信)およびスピーカー(受信)から音声を取得し、ローカル Whisper(faster-whisper)または外部サービスによりテキスト化する。 + - 音声エネルギー(音量)監視を行い、閾値ベースで検出する。 + +2. 翻訳 + - DeepL / DeepL API / 各クラウド翻訳 / ローカル CTranslate2 モデルの複数バックエンドをサポート。 + - 複数出力言語への一括翻訳、翻訳エンジンのフォールバック(CTranslate2 など)。 + - 翻訳モデルのダウンロードと管理機能。 + +3. 表示・通知 + - OpenVR オーバーレイ(small/large)用の画像生成と更新。 + - OSC による VR へのメッセージ送信(typing/通知等)。 + - WebSocket サーバーを介した外部クライアントへの JSON ブロードキャスト。 + +4. 入出力インターフェース + - stdin ラインベースの JSON コマンド受信(mainloop が実装)。 + - stdout に対して構造化された JSON レスポンスを出力(printResponse/printLog)。 + +5. 設定・永続化 + - JSON ベースの設定ファイルを使用(`config.py` による読み書きとデバウンス保存)。 + +6. ロギングと監視 + - プロセスログ(process.log)とエラーログ(error.log)をローテーションで管理。 + - ウォッチドッグ機構で定期的に死活チェック・コールバック。 + +非機能要件 +- プラットフォーム: 主に Windows(Audio 周りは WASAPI を利用)を想定。クロスプラットフォームでの import 安全性を考慮。 +- 可用性: 外部依存(PyAudio, CUDA, ctranslate2 等)が無い環境でも安全にインポートでき、機能劣化しつつ動作する。 +- パフォーマンス: ローカルモデル利用時は GPU を利用して計算性能を確保。compute type 選択ロジックを実装。 +- セキュリティ: 外部への API キー(DeepL など)は設定で扱い、コード上では平文保持を避ける(設定ファイルに保存)。 + +運用フロー +- 起動: stdin でコマンドを受け付ける mainloop を実行。必要な初期化は遅延実行(lazy init)を採用。 +- モデル重ダウンロード: CTranslate2/Whisper 重みは `weights/` 配下にダウンロードし、チェックサム等で整合性確認。 +- 障害時: 例外は utils.errorLogging() でトレースを error.log に出力。重要機能はフォールバック実装。 + +インターフェース(抜粋) +- stdin(JSON): {"endpoint": "/set/..." | "/get/..." | "/run/...", "data": } +- stdout(JSON): 標準化されたレスポンスを printResponse/printLog が出力(status, endpoint, result など)。 + +依存関係(オプション含む) +- 必須(実装時想定): requests, packaging, flashtext, pillow, pyaudiowpatch, speech_recognition +- ローカル推奨: faster-whisper, ctranslate2, torch(GPU 利用時) +- Windows 固有(音声ループバック): pycaw, comtypes + +参考: 実装上の安全設計として optional な import は try/except でガードしており、存在しない依存があっても import 時にクラッシュしない。 diff --git a/src-python/docs/設計書.md b/src-python/docs/設計書.md new file mode 100644 index 00000000..2b37c101 --- /dev/null +++ b/src-python/docs/設計書.md @@ -0,0 +1,57 @@ +# 設計書 + +概要 +- 本設計書はアプリケーションのアーキテクチャ、主要コンポーネント、並列化モデル、エラー処理ポリシー、設定の保存方針を記述する。 + +アーキテクチャ概要 +- 層構造 + - mainloop: stdin ベースのコマンド受け取り、ワーカー(複数スレッド)で実行。 + - controller: GUI/フロントエンドからの操作とモデルの仲介。`Controller` がビジネスロジックを実行。 + - model: 実際の機能(翻訳、文字起こし、オーバーレイ、OSC、WebSocket、デバイス管理)を提供するファサード的シングルトン。 + - models/*: 翻訳、文字起こしなどのドメイン別実装(Translator, AudioTranscriber, Overlay, WebSocketServer ...)。 + - device_manager: 音声デバイス検出・監視(Windows の場合は WASAPI/pycaw を利用)。 + - utils: 共通ユーティリティ(ロギング、ネットワークチェック、compute device 列挙など)。 + +初期化ポリシー +- 重い初期化(GPU モデルロード、OpenVR 初期化など)は import 時に行わず、`model.init()` か要求時の `ensure_initialized()` にて遅延実行。 +- `DeviceManager` は import 時に軽量な init を行い、監視スレッドは `startMonitoring()` で開始する。 + +並列化・同期モデル +- mainloop.Main は 1 つの受信スレッド(stdin 읽取り)と N 個のハンドラワーカースレッドを持つ。 +- 各リクエストはキューに入れられ、handler() により処理される。 +- 有効/無効の切替(/set/enable/**, /set/disable/**)は同一リソースを競合しないよう正規化キーで Lock を割り当てる。 +- モデル内部では threadFnc(Thread ラッパ)で周期的な送信処理や監視処理を実装。 +- Audio 録音や文字起こしは専用の Queue を用い、Producer(Recorder)と Consumer(AudioTranscriber)を分離。 + +エラー処理 +- すべての外周呼び出しは try/except で保護し、`utils.errorLogging()` によってトレースバックを error.log に出力する。 +- JSON シリアライズに失敗した場合はフォールバック JSON を stdout に出力してプロセスを止めない。 +- VRAM 関連のエラーは model.detectVRAMError() で判定し、該当する機能(翻訳等)を無効化してユーザーに通知する。 + +設定管理 +- `config.py` が単一の Config シングルトンを持ち、変更はデバウンスして JSON ファイルへ保存。 +- GUI からの操作は Controller が受け取り、Config を更新する。 + +ログ +- `utils.setupLogger` によりローテートファイルハンドラを使ったログを実装(process.log / error.log)。 +- stdout には構造化ログを出力してフロントエンドと通信する。 + +インターフェース一覧(抜粋) +- STDIN/STDOUT プロトコル: mainloop の JSON 入出力(詳細は `mainloop.py` の mapping を参照) +- OSC: `models.osc.OSCHandler` が OSC 送受信と OSCQuery を管理 +- WebSocket: `models.websocket.WebSocketServer` がクライアント管理とメッセージブロードキャストを担う + +スレッド図(要点) +- main_thread: メイン(stdin 読み取り、キュー投入) +- handler_threads: キューから取り出し処理 +- device_manager.th_monitoring: デバイス監視 +- model.mic_print_transcript / speaker_print_transcript: 音声 -> 翻訳結果送出のループ +- websocket_server_thread: WebSocket サーバの asyncio ループを別スレッドで実行 + +拡張性・互換性設計 +- 依存性は try/except でガードして optional 機能として扱う(例: faster-whisper が無くても import は成功する)。 +- 翻訳エンジンは backend 名で抽象化され、Translator クラスにより統一インターフェースを提供。 + +運用上の考慮 +- 大きなモデルファイル(Whisper, CTranslate2)をダウンロードする仕組みを持ち、進捗を GUI に報告する。 +- GPU 計算タイプは utils.getBestComputeType で選択し、不適切な設定を検出した場合はフォールバック。 diff --git a/src-python/docs/詳細設計書.md b/src-python/docs/詳細設計書.md new file mode 100644 index 00000000..11b539ee --- /dev/null +++ b/src-python/docs/詳細設計書.md @@ -0,0 +1,66 @@ +# 詳細設計書 + +この文書は主要クラス・関数の詳細、データ構造、例外ケース、スレッドの振る舞いを記載する。 + +目次 +- Model +- Controller +- Main (mainloop) +- DeviceManager +- Utils +- モデルの重みダウンロードと整合性 + +## Model +- シングルトン: `model = Model()` +- 遅延初期化: `init()` と `ensure_initialized()` を備え、init は重いリソース(Overlay, Translator, Watchdog, OSC ハンドラ等)を構築する。 +- 主な責務 + - 翻訳/文字起こし関連の起動停止ラッパ + - Overlay/OSChandler/WebSocket の操作 + - キーワード検出(flashtext)と重複検出 + - VRAM エラー検出とフォールバック +- 重要属性(抜粋) + - `translator` : Translator インスタンス + - `overlay` / `overlay_image` : Overlay 系 + - `mic_*`, `speaker_*` : 録音、トランスクリプタ、energy recorder + - `watchdog` : Watchdog + - `osc_handler`, `websocket_server` +- スレッド制御 + - threadFnc を用いて周期処理を回す。stop/pause/resume が可能。 + +## Controller +- GUI からの要求を受け、Model を操作して結果を run() コールバックへ返す。 +- 各種設定変更 (/set/ や /get/ エンドポイント) を実装。 +- 翻訳/文字起こし/オーバーレイ連携ロジックを持ち、メッセージ整形(messageFormatter)や OSC の送信を行う。 +- ダウンロード作業は別スレッドで行い、進捗を run_mapping を通して通知。 + +## Main (mainloop.Main) +- stdin を readline() で受け取り JSON を parse、endpoint と data をキューへ投入。 +- worker_count 個の handler スレッドが queue を取り出し `_call_handler` を実行。 +- endpoint ロック正規化: `/set/enable/...` と `/set/disable/...` は同じ正規化キー `/lock/set/...` を共有して排他制御。 +- エラーレスポンスの標準化と再試行ロジック(status==423 は再キュー化)。 + +## DeviceManager +- シングルトン。初期化は軽量で、`init()` により内部構造をセット、実デバイスは `update()` で取得。 +- Windows 環境では COM イベント (pycaw/MMNotificationClient) を用いた検出か、PyAudio によるポーリングでデバイス一覧を構成。 +- コールバック設計: 変更検出時に Controller のコールバックを呼び出して UI 更新を促す。 + +## Utils +- `validateDictStructure(data, structure)` : JSON 構造検証。 +- `getComputeDeviceList()` / `getBestComputeType()` : CPU/CUDA を列挙し、推奨 compute_type を返す。 +- `setupLogger()` / `printLog()` / `printResponse()` / `errorLogging()` : ログ、標準出力の整形、エラー記録。 +- ネットワーク/ソケット/IP アドレス検査ユーティリティ。 + +## モデル重みダウンロード +- `models.translation.translation_utils` と `models.transcription.transcription_whisper` にダウンロード/チェック関数があり、チェックサムやファイル存在を検証する。 +- GUI からの要求は Controller により非同期スレッドで実行され、進捗コールバックが run_mapping を介してフロントエンドに渡る。 + +## エッジケース / 例外処理 +- 外部 API のレート制限や認証エラーは呼び出し元に 400 系のレスポンスを返し、必要であればフォールバック実装(CTranslate2 への切替)を行う。 +- 大きなモデル実行時の VRAM エラーは検出し、当該機能を無効化してユーザへ通知する。 +- 音声デバイスが存在しない場合は NoDevice を返し、UI 側で扱う。 + +## テスト観点 +- メッセージ受信/送信のエンドツーエンド: stdin -> handler -> Controller -> Model -> printResponse の流れ。 +- デバイス挙動: DeviceManager.update() がデバイス一覧を取得できるか(PyAudio 経由)。 +- モデルダウンロード: ダウンロード成功・失敗、チェックサム検証。 +- ログ/エラー: errorLogging() による例外トレースが error.log に記録されるか。 diff --git a/src-python/mypy.ini b/src-python/mypy.ini deleted file mode 100644 index d9f53b5e..00000000 --- a/src-python/mypy.ini +++ /dev/null @@ -1,32 +0,0 @@ -[mypy] -# Temporarily ignore missing type stubs for third-party libraries to focus on -# type errors inside the project. We'll tighten this later. -ignore_missing_imports = True -python_version = 3.11 -show_error_codes = True - -# Per-module ignores can be added later for specific noisy modules. - -[mypy-tests.*] -ignore_errors = True - -# Temporarily ignore entire implementation areas that produce many non-actionable -# mypy errors (third-party untyped libs or large unannotated modules). We'll -# progressively remove these ignores as we annotate the codebase. -[mypy-models.transliteration.*] -ignore_errors = True - -[mypy-models.overlay.*] -ignore_errors = True - -[mypy-models.osc.*] -ignore_errors = True - -[mypy-models.transcription.*] -ignore_errors = True - -[mypy-models.translation.*] -ignore_errors = True - -[mypy-device_manager] -ignore_errors = True diff --git a/src-python/scripts/cleanup_docs_placeholders.py b/src-python/scripts/cleanup_docs_placeholders.py deleted file mode 100644 index 9da108fd..00000000 --- a/src-python/scripts/cleanup_docs_placeholders.py +++ /dev/null @@ -1,16 +0,0 @@ -from pathlib import Path -p=Path(__file__).resolve().parents[1]/'docs'/'api.md' -text=p.read_text(encoding='utf-8') -lines=[] -for line in text.splitlines(): - stripped=line.strip() - # Remove exact umbrella placeholder tokens or standalone list entries - if stripped in ('- /set/enable', '- /set/disable', '- /get/data/', '/set/enable', '/set/disable', '/get/data/'): - continue - # Remove lines that are just '/get/data' or '/set/data' or '/run/' etc - if stripped in ('/get/data', '/set/data', '/run/', '/get', '/set', '/run'): - continue - lines.append(line) -new='\n'.join(lines) -p.write_text(new,encoding='utf-8') -print('cleaned') diff --git a/src-python/scripts/find_doc_tokens.py b/src-python/scripts/find_doc_tokens.py deleted file mode 100644 index 7b9fae1a..00000000 --- a/src-python/scripts/find_doc_tokens.py +++ /dev/null @@ -1,21 +0,0 @@ -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -DOC_DIR = ROOT / 'docs' - -tokens = [ - 'transcription_mic', - 'transcription_speaker', - 'selected_translation_compute_device', - '/run/transcription_mic', - '/run/transcription_speaker', -] - -for p in DOC_DIR.rglob('*.md'): - text = p.read_text(encoding='utf-8') - for i, line in enumerate(text.splitlines(), start=1): - for t in tokens: - if t in line: - print(f"{p}:{i}:{line.strip()}") - -print('done') diff --git a/src-python/scripts/print_mapping.py b/src-python/scripts/print_mapping.py deleted file mode 100644 index 8b66e177..00000000 --- a/src-python/scripts/print_mapping.py +++ /dev/null @@ -1,28 +0,0 @@ -from pathlib import Path -import re -ROOT = Path(__file__).resolve().parents[1] -MAINLOOP = ROOT / 'mainloop.py' -text = MAINLOOP.read_text(encoding='utf-8') -run_mapping = {} -mapping = {} -for mm in re.finditer(r"[\'\"]([^\'\"]+)[\'\"]\s*:\s*[\'\"](/run/[a-zA-Z0-9_\-\\/]+)[\'\"]", text): - run_mapping[mm.group(1)] = mm.group(2) -for mm in re.finditer(r"[\'\"](/(?:get|set)/[a-zA-Z0-9_\-\\/]+)[\'\"]", text): - mapping[mm.group(1)] = True -print('run_mapping entries:', len(run_mapping)) -print('sample run_mapping keys:', sorted(run_mapping.items())[:10]) -print('\nmapping endpoints count:', len(mapping)) -# show any endpoints that are exactly '/get/data/' -print('\ncontains /get/data/?', '/get/data/' in mapping) -if '/get/data/' in mapping: - print('Found /get/data/ literal in mainloop.py text') -# show ones containing '/get/data' -has_get_data = [k for k in mapping.keys() if '/get/data' in k] -print('\nendpoints containing /get/data:', len(has_get_data)) -if has_get_data: - for k in sorted(has_get_data)[:30]: - print(' -', k) -# print first 20 mapping endpoints -print('\nFirst 40 endpoints:') -for k in sorted(mapping.keys())[:40]: - print(' -', k) diff --git a/src-python/scripts/verify_docs_vs_code.py b/src-python/scripts/verify_docs_vs_code.py deleted file mode 100644 index 7aa47c40..00000000 --- a/src-python/scripts/verify_docs_vs_code.py +++ /dev/null @@ -1,161 +0,0 @@ -import importlib.util -import re -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -MAINLOOP = ROOT / 'mainloop.py' -CONTROLLER = ROOT / 'controller.py' -DOC_API = ROOT / 'docs' / 'api.md' -DOC_RUN = ROOT / 'docs' / 'run_events_payloads.md' - - -def extract_mapping_from_mainloop(): - """ - Import mainloop.py and read 'mapping' and 'run_mapping' objects directly. - This executes the module in an isolated module object; mainloop has some - initialization but exposing these dicts is acceptable for verification. - """ - run_mapping = {} - mapping = {} - try: - spec = importlib.util.spec_from_file_location('project_mainloop', str(MAINLOOP)) - module = importlib.util.module_from_spec(spec) - loader = spec.loader - if loader is None: - raise RuntimeError('Could not load mainloop module') - loader.exec_module(module) - mapping = getattr(module, 'mapping', {}) or {} - run_mapping = getattr(module, 'run_mapping', {}) or {} - return mapping, run_mapping - except Exception as e: - print('Error importing mainloop.py', e) - - # Fallback: simple regex-based extraction from mainloop.py text - try: - text = MAINLOOP.read_text(encoding='utf-8') - # run_mapping entries like: "transcription_mic": "/run/transcription_send_mic_message", - for mm in re.finditer(r"[\'\"]([^\'\"]+)[\'\"]\s*:\s*[\'\"](/run/[a-zA-Z0-9_\-\/]+)[\'\"]", text): - run_mapping[mm.group(1)] = mm.group(2) - # mapping endpoints: any '/get/...' or '/set/...' literal in file - for mm in re.finditer(r"[\'\"](/(?:get|set)/[a-zA-Z0-9_\-\/]+)[\'\"]", text): - mapping[mm.group(1)] = True - except Exception as e: - print('Error parsing mainloop.py via fallback', e) - - return mapping, run_mapping - - -def extract_run_events_from_controller(): - code = CONTROLLER.read_text(encoding='utf-8') - # find self.run( ... , self.run_mapping["key"], ... ) and direct self.run(..., - run_keys = set() - # pattern for self.run(..., self.run_mapping["xxx"], ...) - pattern = re.compile(r"self\.run\([^\)]*self\.run_mapping\[\s*[\'\"]([^\'\"]+)[\'\"]\s*\]", re.M) - for m in pattern.finditer(code): - run_keys.add(m.group(1)) - # also find self.run(..., "/run/xxx", ...) - pattern2 = re.compile(r"self\.run\([^\)]*\"(/run/[^\'\"]+)\"", re.M) - for m in pattern2.finditer(code): - run_keys.add(m.group(1)) - return run_keys - - -def extract_endpoints_from_docs(): - api = DOC_API.read_text(encoding='utf-8') - run = DOC_RUN.read_text(encoding='utf-8') if DOC_RUN.exists() else '' - endpoints = set() - run_events = set() - # conservative extraction: match endpoints that start with /get/ /set/ /run/ - pattern = re.compile(r"(/(?:get|set|run)(?:/[a-zA-Z0-9_\-]+)+)") - for m in pattern.finditer(api): - token = m.group(1) - # drop umbrella placeholders and tokens that end with '/' - if token in ('/get', '/set', '/run', '/get/data', '/set/data'): - continue - if token.endswith('/'): - continue - if token.startswith('/run/'): - run_events.add(token) - else: - endpoints.add(token) - for m in pattern.finditer(run): - token = m.group(1) - if token in ('/get', '/set', '/run', '/get/data', '/set/data'): - continue - if token.endswith('/'): - continue - if token.startswith('/run/'): - run_events.add(token) - else: - endpoints.add(token) - return endpoints, run_events - - -def main(): - mapping, run_mapping = extract_mapping_from_mainloop() - code_endpoints = set(mapping.keys()) - code_run_events = set(run_mapping.values()) - # normalize run events: run_mapping values likely like '/run/…' - controller_run_keys = extract_run_events_from_controller() - - doc_endpoints, doc_run_events = extract_endpoints_from_docs() - - report = [] - report.append('=== Summary ===') - report.append(f'Code endpoints (/get,/set,/run): {len(code_endpoints)}') - report.append(f'Code run_mapping entries: {len(code_run_events)}') - report.append(f'Controller-run keys found by scan: {len(controller_run_keys)}') - report.append(f'Documented endpoints found in docs/api.md: {len(doc_endpoints)}') - report.append(f'Documented run events found in docs: {len(doc_run_events)}') - - # endpoints present in code but not in docs - missing_in_docs = code_endpoints - doc_endpoints - extra_in_docs = doc_endpoints - code_endpoints - - report.append('\n=== Endpoints present in code but NOT documented ===') - if missing_in_docs: - for e in sorted(missing_in_docs): - report.append(' - ' + e) - else: - report.append(' - None') - - report.append('\n=== Endpoints documented but NOT in code ===') - if extra_in_docs: - for e in sorted(extra_in_docs): - report.append(' - ' + e) - else: - report.append(' - None') - - report.append('\n=== Run events present in code (run_mapping) but NOT documented ===') - missing_run_in_docs = code_run_events - doc_run_events - if missing_run_in_docs: - for e in sorted(missing_run_in_docs): - report.append(' - ' + e) - else: - report.append(' - None') - - report.append('\n=== Run keys emitted in controller (self.run mapping keys) but NOT in run_mapping values ===') - # controller_run_keys are keys like 'connected_network' or '/run/connected_network' - # normalize controller keys to values: if key starts with '/run/' keep, else map via run_mapping if possible - normalized = set() - for k in controller_run_keys: - if k.startswith('/run/'): - normalized.add(k) - else: - if k in run_mapping: - normalized.add(run_mapping[k]) - else: - normalized.add(k) - # compare normalized with code_run_events - extra_controller_keys = normalized - code_run_events - if extra_controller_keys: - for e in sorted(extra_controller_keys): - report.append(' - ' + e) - else: - report.append(' - None') - - out = '\n'.join(report) - print(out) - -if __name__ == '__main__': - main() diff --git a/src-python/scripts/verify_docs_vs_code_runtime.py b/src-python/scripts/verify_docs_vs_code_runtime.py deleted file mode 100644 index 187575f3..00000000 --- a/src-python/scripts/verify_docs_vs_code_runtime.py +++ /dev/null @@ -1,126 +0,0 @@ -import re -import json -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -DOC_API = ROOT / 'docs' / 'api.md' -DOC_RUN = ROOT / 'docs' / 'run_events_payloads.md' - -# Ensure project root is importable so `import mainloop` works when this script is -# executed from the scripts/ folder. -sys.path.insert(0, str(ROOT)) - - -def main(): - # Delayed imports to avoid module-level import ordering issues (E402 in linters) - import mainloop - import controller as controller_module - - mapping_keys = set(mainloop.mapping.keys()) - run_mapping_values = set(mainloop.run_mapping.values()) - - # extract controller emitted run keys by source scan - controller_src = Path(controller_module.__file__).read_text(encoding='utf-8') - controller_run_keys = set() - for m in re.finditer(r"self\.run\([^\)]*self\.run_mapping\[\s*[\'\"]([^\'\"]+)[\'\"]\s*\]", controller_src): - controller_run_keys.add(m.group(1)) - for m in re.finditer(r"self\.run\([^\)]*\"(/run/[a-zA-Z0-9_\-/]+)\"", controller_src): - controller_run_keys.add(m.group(1)) - # read docs and extract endpoints conservatively (only full endpoints starting with /get/ /set/ /run/) - api_text = DOC_API.read_text(encoding='utf-8') - run_text = DOC_RUN.read_text(encoding='utf-8') if DOC_RUN.exists() else '' - - # include delete endpoints as well (e.g. /delete/data/deepl_auth_key) - endpoint_pattern = re.compile(r"(/(?:get|set|run|delete)[A-Za-z0-9_\-/]*)") - - doc_endpoints = set(m.group(1) for m in endpoint_pattern.finditer(api_text + '\n' + run_text)) - - # Remove umbrella placeholder artifacts that sometimes appear due to - # comma-separated lists or pattern fragments in the markdown. These are - # not concrete endpoints and should not be treated as documented endpoints - # for parity checking. - umbrella_tokens = { - '/get', '/set', '/run', '/get/data', '/set/data', '/set/enable', '/set/disable' - } - # Remove exact umbrella tokens and any accidental entries that end with a - # trailing slash (these are artifacts of pattern matching in markdown). - doc_endpoints = {e for e in doc_endpoints if e not in umbrella_tokens and not e.endswith('/')} - - # Compare - missing_in_docs = mapping_keys - doc_endpoints - # A documented endpoint is valid if it corresponds to either an incoming mapping (mapping_keys) - # or an outgoing run event (run_mapping_values). Treat extra_in_docs as anything documented - # that is neither in mapping_keys nor in run_mapping_values. - extra_in_docs = doc_endpoints - (mapping_keys | run_mapping_values) - - missing_run_in_docs = run_mapping_values - doc_endpoints - - # Normalize controller keys to run_mapping values - normalized = set() - for k in controller_run_keys: - if k.startswith('/run/'): - normalized.add(k) - else: - if k in mainloop.run_mapping: - normalized.add(mainloop.run_mapping[k]) - else: - normalized.add(k) - - extra_controller_keys = normalized - run_mapping_values - - report = [] - report.append('=== Runtime verification report ===') - report.append(f'Code mapping endpoints: {len(mapping_keys)}') - report.append(f'Code run_mapping entries: {len(run_mapping_values)}') - report.append(f'Controller emitted run keys: {len(controller_run_keys)}') - report.append(f'Documented endpoints (docs): {len(doc_endpoints)}') - - report.append('\n--- Endpoints present in code but NOT documented ---') - if missing_in_docs: - for e in sorted(missing_in_docs): - report.append(' - ' + e) - else: - report.append(' - None') - - report.append('\n--- Endpoints documented but NOT in code ---') - if extra_in_docs: - for e in sorted(extra_in_docs): - report.append(' - ' + e) - else: - report.append(' - None') - - report.append('\n--- Run events present in code (run_mapping) but NOT documented ---') - if missing_run_in_docs: - for e in sorted(missing_run_in_docs): - report.append(' - ' + e) - else: - report.append(' - None') - - report.append('\n--- Run keys emitted in controller (normalized) but NOT in run_mapping values ---') - if extra_controller_keys: - for e in sorted(extra_controller_keys): - report.append(' - ' + e) - else: - report.append(' - None') - - print('\n'.join(report)) - - # Also output JSON for downstream processing - out = { - 'mapping_keys': sorted(mapping_keys), - 'run_mapping_values': sorted(run_mapping_values), - 'controller_run_keys': sorted(controller_run_keys), - 'doc_endpoints': sorted(doc_endpoints), - 'missing_in_docs': sorted(missing_in_docs), - 'extra_in_docs': sorted(extra_in_docs), - 'missing_run_in_docs': sorted(missing_run_in_docs), - 'extra_controller_keys': sorted(extra_controller_keys), - } - print('\nJSON_OUTPUT_START') - print(json.dumps(out)) - print('JSON_OUTPUT_END') - - -if __name__ == '__main__': - main() diff --git a/src-python/tests/test_osc_imports.py b/src-python/tests/test_osc_imports.py deleted file mode 100644 index 1df699c3..00000000 --- a/src-python/tests/test_osc_imports.py +++ /dev/null @@ -1,6 +0,0 @@ -def test_import_osc_module(): - try: - import importlib - importlib.import_module('models.osc.osc') - except Exception as e: - raise AssertionError(f"Failed importing models.osc.osc: {e}") diff --git a/src-python/tests/test_overlay_imports.py b/src-python/tests/test_overlay_imports.py deleted file mode 100644 index b90389e7..00000000 --- a/src-python/tests/test_overlay_imports.py +++ /dev/null @@ -1,30 +0,0 @@ -import sys -import time -from PIL import Image - -sys.path.append(r"d:\WORKSPACE\WORK\VRChatProject\VRCT\src-python") - -from models.overlay import overlay_image, overlay_utils - - -def test_overlay_image_create(): - oi = overlay_image.OverlayImage() - img = oi.createOverlayImageSmallLog("hello", "English", [], []) - assert isinstance(img, Image.Image) - - -def test_utils_transform(): - import numpy as np - base = np.array([ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0] - ]) - res = overlay_utils.transform_matrix(base, (0, 0, 0), (0, 0, 0)) - assert res.shape == (3, 4) - - -if __name__ == '__main__': - test_overlay_image_create() - test_utils_transform() - print('tests passed')