# 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エラーに対する自動リカバリ機能により、ユーザーエクスペリエンスの向上を実現している。