# 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 --- #### Groq API 統合 ##### `authenticationTranslatorGroqAuthKey(auth_key: str) -> bool` **責務:** Groq API キーの検証 **処理:** `translator.authenticationGroqAuthKey()` に委譲し、`root_path=config.PATH_LOCAL` を渡す **戻り値:** 認証成功時 True ##### `getTranslatorGroqModelList() -> list[str]` **責務:** 利用可能な Groq モデルリストの取得 **処理:** `translator.getGroqModelList()` に委譲 **戻り値:** モデル名のリスト(例: `["llama3-8b-8192", "mixtral-8x7b-32768"]`) ##### `setTranslatorGroqModel(model: str) -> bool` **責務:** 使用する Groq モデルの設定 **処理:** `translator.setGroqModel(model)` に委譲 **戻り値:** 設定成功時 True(モデルが利用可能リストに含まれない場合 False) ##### `updateTranslatorGroqClient() -> None` **責務:** Groq クライアントの更新(モデル変更後に呼び出し) **処理:** `translator.updateGroqClient()` に委譲し、新しいモデルで LangChain `ChatOpenAI` インスタンスを再生成 --- #### OpenRouter API 統合 ##### `authenticationTranslatorOpenRouterAuthKey(auth_key: str) -> bool` **責務:** OpenRouter API キーの検証 **処理:** `translator.authenticationOpenRouterAuthKey()` に委譲し、`root_path=config.PATH_LOCAL` を渡す **戻り値:** 認証成功時 True ##### `getTranslatorOpenRouterModelList() -> list[str]` **責務:** 利用可能な OpenRouter モデルリストの取得 **処理:** `translator.getOpenRouterModelList()` に委譲 **戻り値:** モデル名のリスト(例: `["anthropic/claude-3-sonnet", "google/gemini-pro"]`) ##### `setTranslatorOpenRouterModel(model: str) -> bool` **責務:** 使用する OpenRouter モデルの設定 **処理:** `translator.setOpenRouterModel(model)` に委譲 **戻り値:** 設定成功時 True(モデルが利用可能リストに含まれない場合 False) ##### `updateTranslatorOpenRouterClient() -> None` **責務:** OpenRouter クライアントの更新(モデル変更後に呼び出し) **処理:** `translator.updateOpenRouterClient()` に委譲し、新しいモデルで LangChain `ChatOpenAI` インスタンスを再生成 --- #### 翻訳実行 ##### `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エラーやデバイスエラーに対する適切なハンドリングにより、ユーザーエクスペリエンスを向上させている。