Files
VRCT/src-python/docs/controller.md

43 KiB
Raw Permalink Blame History

controller.py 設計書

概要

controller.py は VRCT アプリケーションのビジネスロジック層であり、フロントエンドUIとバックエンドModelの間の制御フローを担当する。音声認識、翻訳、OSC通信、オーバーレイ表示など、VRCT の全機能の調整役として動作し、各種設定の取得・更新、デバイス管理、エラーハンドリングを提供する。

最近の更新 (2026-01-03)

  • 起動高速化: 初期化時間を約12.6s→8.9sに短縮
  • AI Models Check 並列化: CTranslate2/Whisperの重みチェックを2並列で実行
  • 翻訳エンジン判定の非同期化: LMStudio/Ollamaをバックグラウンド判定、他APIは4並列
  • 重みチェック結果のキャッシュ: _ctranslate2_available_cache / _whisper_available_cache を導入し後続処理で再利用
  • 音声認識エンジン判定の高速化: Whisperはキャッシュ結果を利用し0.56s→0.00s
  • ソフトウェア更新チェックの非同期化: GitHub APIチェックをバックグラウンド化

アーキテクチャ上の位置づけ

┌─────────────┐
│ 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: デバイスへの排他アクセス制御用フラグ

型ヒント:

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.pyprintResponse() ラッパーが渡される。

ヘルパーメソッド

_is_overlay_available() -> bool

オーバーレイ機能が利用可能かを安全にチェック。Model が未初期化の場合の AttributeError を回避。

実装:

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 エラーハンドリング:

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"]: メッセージ IDUI でのレスポンスマッピング用)
  • data["message"]: 送信メッセージ

特殊処理:

  • 除外ワード処理:
    • config.USE_EXCLUDE_WORDS が True の場合
    • replaceExclamationsWithRandom(): ![word] を一時的なトークンに置換
    • 翻訳後に restoreText() で復元
    • 最終メッセージから ![...] を削除
  • 同期レスポンス:
    • 他のメッセージ処理と異なり、結果を dict で返却
    • UI が翻訳結果を待機する必要があるため

レスポンス形式:

{
    "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 のみ

設定例:

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. 各マッチを $<hex番号> に置換4096から連番
  3. 置換マップを辞書で返却

用途: 固有名詞や翻訳不要な単語を保護

restoreText(escaped_text: str, escape_dict: dict) -> str

責務: 翻訳後のテキストに元の単語を復元

処理: 正規表現で $<hex番号> を検出し、元の単語に置換(大文字小文字を無視)

removeExclamations(text: str) -> str

責務: 最終メッセージから ![...] マーカーを削除

処理: ![word]word に置換


7. 設定取得・更新メソッドGET/SET

Controller には約200個の設定項目に対する getter/setter が定義されている。以下、代表的なパターンを示す。

パターン1: 単純な設定値

@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: 有効/無効の切り替え

@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: バリデーション付き設定

@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: 依存関係のある設定

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 に設定

エラーハンドリング:

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

マイク音声認識を開始。デバイスアクセスの排他制御を行う。

排他制御:

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):
    • レスポンス data フィールドは 常に None(キーを返さない)
    • delDeeplAuthKey() を呼び出してクリーンアップ

認証失敗時の共通ポリシーPlamo/Gemini/OpenAI/DeepL/Groq/OpenRouter 共通)

  • レスポンス data はキーを含めず None を返す
  • 対応する del*AuthKey() を呼び出し、保存済みキーとモデル選択をクリア

delDeeplAuthKey(*args, **kwargs) -> dict

責務: DeepL API キーを削除

処理:

  1. config.AUTH_KEYS["DeepL_API"] を None に
  2. config.SELECTABLE_TRANSLATION_ENGINE_STATUS["DeepL_API"] を False に
  3. updateTranslationEngineAndEngineList() を呼び出し

16-1. Groq API 認証・モデル管理

setGroqAuthKey(data, *args, **kwargs) -> dict

責務: Groq API キーを設定し、認証を実行

処理:

  1. キー長のバリデーション(gsk で始まり40文字以上
  2. model.authenticationTranslatorGroqAuthKey() で認証
  3. 認証成功時:
    • config.AUTH_KEYS["Groq_API"] に保存
    • config.SELECTABLE_TRANSLATION_ENGINE_STATUS["Groq_API"] を True に
    • config.SELECTABLE_GROQ_MODEL_LIST を取得
    • 未選択の場合は先頭モデルを自動選択
    • model.updateTranslatorGroqClient() でクライアント更新
    • updateTranslationEngineAndEngineList() を呼び出し
  4. 認証失敗時 (status 400):
    • レスポンス data フィールドを None に設定 sensitive data を隠す)
    • delGroqAuthKey() を呼び出してクリーンアップ

API キー検証失敗時の処理:

  • モデルリストをクリア (config.SELECTABLE_GROQ_MODEL_LIST = [])
  • 選択モデルをクリア (config.SELECTED_GROQ_MODEL = None)
  • フロントエンドに通知(レスポンス data は None

delGroqAuthKey(*args, **kwargs) -> dict

責務: Groq API キーを削除

処理:

  1. config.AUTH_KEYS["Groq_API"] を None に
  2. config.SELECTABLE_TRANSLATION_ENGINE_STATUS["Groq_API"] を False に
  3. モデルリストと選択モデルをクリア
  4. updateTranslationEngineAndEngineList() を呼び出し

getGroqAuthKey(*args, **kwargs) -> dict

現在の Groq API キーを取得(マスク処理なし)。

getGroqModelList(*args, **kwargs) -> dict

利用可能な Groq モデルリストを取得。

getGroqModel(*args, **kwargs) -> dict

現在選択中の Groq モデルを取得。

setGroqModel(data, *args, **kwargs) -> dict

責務: 使用する Groq モデルを変更

処理:

  1. モデル名のバリデーション(利用可能リスト内か確認)
  2. model.setTranslatorGroqModel() でモデル設定
  3. model.updateTranslatorGroqClient() でクライアント再生成
  4. config.SELECTED_GROQ_MODEL を更新

16-2. OpenRouter API 認証・モデル管理

setOpenRouterAuthKey(data, *args, **kwargs) -> dict

責務: OpenRouter API キーを設定し、認証を実行

処理:

  1. キー長のバリデーション20文字以上
  2. model.authenticationTranslatorOpenRouterAuthKey() で認証
  3. 認証成功時:
    • config.AUTH_KEYS["OpenRouter_API"] に保存
    • config.SELECTABLE_TRANSLATION_ENGINE_STATUS["OpenRouter_API"] を True に
    • config.SELECTABLE_OPENROUTER_MODEL_LIST を取得
    • 未選択の場合は先頭モデルを自動選択
    • model.updateTranslatorOpenRouterClient() でクライアント更新
    • updateTranslationEngineAndEngineList() を呼び出し
  4. 認証失敗時 (status 400):
    • レスポンス data フィールドを None に設定 sensitive data を隠す)
    • delOpenRouterAuthKey() を呼び出してクリーンアップ

API キー検証失敗時の処理:

  • モデルリストをクリア (config.SELECTABLE_OPENROUTER_MODEL_LIST = [])
  • 選択モデルをクリア (config.SELECTED_OPENROUTER_MODEL = None)
  • フロントエンドに通知(レスポンス data は None

delOpenRouterAuthKey(*args, **kwargs) -> dict

責務: OpenRouter API キーを削除

処理:

  1. config.AUTH_KEYS["OpenRouter_API"] を None に
  2. config.SELECTABLE_TRANSLATION_ENGINE_STATUS["OpenRouter_API"] を False に
  3. モデルリストと選択モデルをクリア
  4. updateTranslationEngineAndEngineList() を呼び出し

getOpenRouterAuthKey(*args, **kwargs) -> dict

現在の OpenRouter API キーを取得(マスク処理なし)。

getOpenRouterModelList(*args, **kwargs) -> dict

利用可能な OpenRouter モデルリストを取得。

getOpenRouterModel(*args, **kwargs) -> dict

現在選択中の OpenRouter モデルを取得。

setOpenRouterModel(data, *args, **kwargs) -> dict

責務: 使用する OpenRouter モデルを変更

処理:

  1. モデル名のバリデーション(利用可能リスト内か確認)
  2. model.setTranslatorOpenRouterModel() でモデル設定
  3. model.updateTranslatorOpenRouterClient() でクライアント再生成
  4. config.SELECTED_OPENROUTER_MODEL を更新

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. 結果を返却

バージョン情報形式:

{
    "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. ログのクリア

removeLog()
printLog("Start Initialization")

2. ネットワーク接続確認

connected_network = isConnectedNetwork()
if connected_network:
    self.connectedNetwork()
else:
    self.disconnectedNetwork()

3. モデルウェイトのダウンロード進捗1/4

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 モデル状態の確認

if (model.checkTranslatorCTranslate2ModelWeight(...) is False or
    model.checkTranscriptionWhisperModelWeight(...) is False):
    self.disableAiModels()
else:
    self.enableAiModels()

5. 翻訳・音声認識エンジンの初期化進捗2/4

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

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

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. デバイス管理の初期化

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 の起動

if config.OVERLAY_SMALL_LOG or config.OVERLAY_LARGE_LOG:
    model.startOverlay()

if config.WEBSOCKET_SERVER:
    if isAvailableWebSocketServer(...):
        model.startWebSocketServer(...)

10. 設定の同期と完了

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. energyFalse の場合
  2. error_device エンドポイントにエラー通知
  3. 処理を継続(他の機能は影響を受けない)

3. ネットワークエラー

検出箇所:

  • 翻訳APIの呼び出し時
  • モデルウェイトのダウンロード時

処理:

  1. 外部API エラー: changeToCTranslate2Process() で CTranslate2 に切り替え
  2. ダウンロードエラー: エラー通知を送信、AI機能を無効化

4. 設定バリデーションエラー

処理:

  • status 400 とエラーメッセージを返却
  • 現在の有効な設定値を data フィールドに含める

例:

{
    "status": 400,
    "result": {
        "message": "Mic energy threshold value is out of range",
        "data": 1000  # 現在の有効な値
    }
}

スレッド安全性

排他制御

デバイスアクセス制御

問題: 複数の機能が同時にデバイスにアクセスすると衝突

解決策: device_access_status フラグによる排他制御

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 は必要に応じて実行(停止処理等)

例:

th_startTranscriptionSendMessage = Thread(target=self.startTranscriptionSendMessage)
th_startTranscriptionSendMessage.daemon = True
th_startTranscriptionSendMessage.start()

パフォーマンス考慮事項

1. 非同期ダウンロード

初期化時: 同期ダウンロード(asynchronous=False

  • UI をブロックして確実にダウンロード完了を待つ

ユーザー操作時: 非同期ダウンロード(asynchronous=True

  • 別スレッドで実行し、進捗バーで通知

2. 並列初期化

CTranslate2 と Whisper のダウンロードを並列実行:

th_download_ctranslate2.start()
th_download_whisper.start()
th_download_ctranslate2.join()
th_download_whisper.join()

3. モデルの遅延ロード

翻訳モデルは setEnableTranslation() が呼ばれるまでロードされない。


依存関係

外部モジュール

from typing import Callable, Any, List, Optional
from time import sleep
from subprocess import Popen
from threading import Thread
import re

内部モジュール

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. 依存性注入

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