From b0cf8bf3352e10b3b491c9a6a3be64d6ca7c83a9 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:38:06 +0900 Subject: [PATCH] feat: Implement unified error handling system - Added `errors.py` to define a centralized error management system with error codes and metadata. - Created `VRCTError` class for generating standardized error responses. - Introduced `error_handling_migration_guide.md` to document migration patterns for existing error handling to the new system. - Updated error handling patterns in the codebase to utilize the new error management system. --- src-python/controller.py | 782 +++++++----------- .../docs/error_handling_migration_guide.md | 212 +++++ src-python/errors.py | 694 ++++++++++++++++ 3 files changed, 1227 insertions(+), 461 deletions(-) create mode 100644 src-python/docs/error_handling_migration_guide.md create mode 100644 src-python/errors.py diff --git a/src-python/controller.py b/src-python/controller.py index 5bacdb4f..3b97b89f 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -7,6 +7,7 @@ from device_manager import device_manager from config import config from model import model from utils import removeLog, printLog, errorLogging, isConnectedNetwork, isValidIpAddress, isAvailableWebSocketServer +from errors import ErrorCode, VRCTError class Controller: def __init__(self) -> None: @@ -153,13 +154,14 @@ class Controller: def progressBarMicEnergy(self, energy) -> None: if energy is False: + error_response = VRCTError.create_error_response( + ErrorCode.DEVICE_NO_MIC, + data=None + ) self.run( - 400, + error_response["status"], self.run_mapping["error_device"], - { - "message":"No mic device detected", - "data": None - }, + error_response["result"], ) else: self.run( @@ -170,13 +172,14 @@ class Controller: def progressBarSpeakerEnergy(self, energy) -> None: if energy is False: + error_response = VRCTError.create_error_response( + ErrorCode.DEVICE_NO_SPEAKER, + data=None + ) self.run( - 400, + error_response["status"], self.run_mapping["error_device"], - { - "message":"No speaker device detected", - "data": None - }, + error_response["result"], ) else: self.run( @@ -209,13 +212,14 @@ class Controller: self.weight_type, ) else: + error_response = VRCTError.create_error_response( + ErrorCode.WEIGHT_CTRANSLATE2_DOWNLOAD, + data=None + ) self.run( - 400, + error_response["status"], self.run_mapping["error_ctranslate2_weight"], - { - "message":"CTranslate2 weight download error", - "data": None - }, + error_response["result"], ) class DownloadWhisper: @@ -242,13 +246,14 @@ class Controller: self.weight_type, ) else: + error_response = VRCTError.create_error_response( + ErrorCode.WEIGHT_WHISPER_DOWNLOAD, + data=None + ) self.run( - 400, + error_response["status"], self.run_mapping["error_whisper_weight"], - { - "message":"Whisper weight download error", - "data": None - }, + error_response["result"], ) def micMessage(self, result: dict) -> None: @@ -298,23 +303,25 @@ class Controller: # VRAM不足エラーの検出 is_vram_error, error_message = model.detectVRAMError(e) if is_vram_error: + error_response = VRCTError.create_error_response( + ErrorCode.TRANSLATION_VRAM_MIC, + data=error_message + ) self.run( - 400, + error_response["status"], self.run_mapping["error_translation_mic_vram_overflow"], - { - "message":"VRAM out of memory during translation of mic", - "data": error_message - }, + error_response["result"], ) # 翻訳機能をOFFにする self.setDisableTranslation() + disable_response = VRCTError.create_error_response( + ErrorCode.TRANSLATION_DISABLED_VRAM, + data=False + ) self.run( - 400, + disable_response["status"], self.run_mapping["enable_translation"], - { - "message":"Translation disabled due to VRAM overflow", - "data": False - }, + disable_response["result"], ) return else: @@ -448,13 +455,14 @@ class Controller: translation, success = model.getOutputTranslate(message, source_language=language) if all(success) is not True: self.changeToCTranslate2Process() + error_response = VRCTError.create_error_response( + ErrorCode.TRANSLATION_ENGINE_LIMIT, + data=None + ) self.run( - 400, + error_response["status"], self.run_mapping["error_translation_engine"], - { - "message":"Translation engine limit error", - "data": None - }, + error_response["result"], ) else: pass @@ -462,23 +470,25 @@ class Controller: # VRAM不足エラーの検出 is_vram_error, error_message = model.detectVRAMError(e) if is_vram_error: + error_response = VRCTError.create_error_response( + ErrorCode.TRANSLATION_VRAM_SPEAKER, + data=error_message + ) self.run( - 400, + error_response["status"], self.run_mapping["error_translation_speaker_vram_overflow"], - { - "message":"VRAM out of memory during translation of speaker", - "data": error_message - }, + error_response["result"], ) # 翻訳機能をOFFにする self.setDisableTranslation() + disable_response = VRCTError.create_error_response( + ErrorCode.TRANSLATION_DISABLED_VRAM, + data=False + ) self.run( - 400, + disable_response["status"], self.run_mapping["enable_translation"], - { - "message":"Translation disabled due to VRAM overflow", - "data": False - }, + disable_response["result"], ) return else: @@ -625,13 +635,14 @@ class Controller: if all(success) is not True: self.changeToCTranslate2Process() + error_response = VRCTError.create_error_response( + ErrorCode.TRANSLATION_ENGINE_LIMIT, + data=None + ) self.run( - 400, + error_response["status"], self.run_mapping["error_translation_engine"], - { - "message":"Translation engine limit error", - "data": None - }, + error_response["result"], ) else: pass @@ -639,23 +650,25 @@ class Controller: # VRAM不足エラーの検出 is_vram_error, error_message = model.detectVRAMError(e) if is_vram_error: + error_response = VRCTError.create_error_response( + ErrorCode.TRANSLATION_VRAM_CHAT, + data=error_message + ) self.run( - 400, + error_response["status"], self.run_mapping["error_translation_chat_vram_overflow"], - { - "message":"VRAM out of memory during translation of chat", - "data": error_message - }, + error_response["result"], ) # 翻訳機能をOFFにする self.setDisableTranslation() + disable_response = VRCTError.create_error_response( + ErrorCode.TRANSLATION_DISABLED_VRAM, + data=False + ) self.run( - 400, + disable_response["status"], self.run_mapping["enable_translation"], - { - "message":"Translation disabled due to VRAM overflow", - "data": False - }, + disable_response["result"], ) # エラー時は翻訳なしで返す return {"status":200, @@ -845,22 +858,24 @@ class Controller: if is_vram_error: # Defaultのデバイス設定に戻す printLog("VRAM error detected, reverting device setting") + error_response = VRCTError.create_error_response( + ErrorCode.TRANSLATION_VRAM_ENABLE, + data=error_message + ) self.run( - 400, + error_response["status"], self.run_mapping["error_translation_enable_vram_overflow"], - { - "message":"VRAM out of memory enabling translation", - "data": error_message - }, + error_response["result"], ) self.setDisableTranslation() + disable_response = VRCTError.create_error_response( + ErrorCode.TRANSLATION_DISABLED_VRAM, + data=False + ) self.run( - 400, + disable_response["status"], self.run_mapping["enable_translation"], - { - "message":"Translation disabled due to VRAM overflow", - "data": False - }, + disable_response["result"], ) model.changeTranslatorCTranslate2Model() model.setChangedTranslatorParameters(False) @@ -1185,13 +1200,10 @@ class Controller: else: raise ValueError() except Exception: - response = { - "status":400, - "result":{ - "message":"Mic energy threshold value is out of range", - "data": config.MIC_THRESHOLD - } - } + response = VRCTError.create_error_response( + ErrorCode.VALIDATION_MIC_THRESHOLD, + data=config.MIC_THRESHOLD + ) else: response = {"status":status, "result":config.MIC_THRESHOLD} return response @@ -1226,13 +1238,10 @@ class Controller: else: raise ValueError() except Exception: - response = { - "status":400, - "result":{ - "message":"Mic record timeout value is out of range", - "data": config.MIC_RECORD_TIMEOUT - } - } + response = VRCTError.create_error_response( + ErrorCode.VALIDATION_MIC_RECORD_TIMEOUT, + data=config.MIC_RECORD_TIMEOUT + ) else: response = {"status":200, "result":config.MIC_RECORD_TIMEOUT} return response @@ -1250,13 +1259,10 @@ class Controller: else: raise ValueError() except Exception: - response = { - "status":400, - "result":{ - "message":"Mic phrase timeout value is out of range", - "data": config.MIC_PHRASE_TIMEOUT - } - } + response = VRCTError.create_error_response( + ErrorCode.VALIDATION_MIC_PHRASE_TIMEOUT, + data=config.MIC_PHRASE_TIMEOUT + ) else: response = {"status":200, "result":config.MIC_PHRASE_TIMEOUT} return response @@ -1274,13 +1280,10 @@ class Controller: else: raise ValueError() except Exception: - response = { - "status":400, - "result":{ - "message":"Mic max phrases value is out of range", - "data": config.MIC_MAX_PHRASES - } - } + response = VRCTError.create_error_response( + ErrorCode.VALIDATION_MIC_MAX_PHRASES, + data=config.MIC_MAX_PHRASES + ) else: response = {"status":200, "result":config.MIC_MAX_PHRASES} return response @@ -1368,13 +1371,10 @@ class Controller: else: raise ValueError() except Exception: - response = { - "status":400, - "result":{ - "message":"Speaker energy threshold value is out of range", - "data": config.SPEAKER_THRESHOLD - } - } + response = VRCTError.create_error_response( + ErrorCode.VALIDATION_SPEAKER_THRESHOLD, + data=config.SPEAKER_THRESHOLD + ) else: response = {"status":200, "result":config.SPEAKER_THRESHOLD} return response @@ -1408,13 +1408,10 @@ class Controller: else: raise ValueError() except Exception: - response = { - "status":400, - "result":{ - "message":"Speaker record timeout value is out of range", - "data": config.SPEAKER_RECORD_TIMEOUT - } - } + response = VRCTError.create_error_response( + ErrorCode.VALIDATION_SPEAKER_RECORD_TIMEOUT, + data=config.SPEAKER_RECORD_TIMEOUT + ) else: response = {"status":200, "result":config.SPEAKER_RECORD_TIMEOUT} return response @@ -1432,13 +1429,10 @@ class Controller: else: raise ValueError() except Exception: - response = { - "status":400, - "result":{ - "message":"Speaker phrase timeout value is out of range", - "data": config.SPEAKER_PHRASE_TIMEOUT - } - } + response = VRCTError.create_error_response( + ErrorCode.VALIDATION_SPEAKER_PHRASE_TIMEOUT, + data=config.SPEAKER_PHRASE_TIMEOUT + ) else: response = {"status":200, "result":config.SPEAKER_PHRASE_TIMEOUT} return response @@ -1457,13 +1451,10 @@ class Controller: else: raise ValueError() except Exception: - response = { - "status":400, - "result":{ - "message":"Speaker max phrases value is out of range", - "data": config.SPEAKER_MAX_PHRASES - } - } + response = VRCTError.create_error_response( + ErrorCode.VALIDATION_SPEAKER_MAX_PHRASES, + data=config.SPEAKER_MAX_PHRASES + ) else: response = {"status":200, "result":config.SPEAKER_MAX_PHRASES} return response @@ -1510,13 +1501,10 @@ class Controller: def setOscIpAddress(self, data, *args, **kwargs) -> dict: if isValidIpAddress(data) is False: - response = { - "status":400, - "result":{ - "message":"Invalid IP address", - "data": config.OSC_IP_ADDRESS - } - } + response = VRCTError.create_error_response( + ErrorCode.VALIDATION_INVALID_IP, + data=config.OSC_IP_ADDRESS + ) else: try: model.setOscIpAddress(data) @@ -1533,13 +1521,10 @@ class Controller: response = {"status":200, "result":config.OSC_IP_ADDRESS} except Exception: model.setOscIpAddress(config.OSC_IP_ADDRESS) - response = { - "status":400, - "result":{ - "message":"Cannot set IP address", - "data": config.OSC_IP_ADDRESS - } - } + response = VRCTError.create_error_response( + ErrorCode.VALIDATION_CANNOT_SET_IP, + data=config.OSC_IP_ADDRESS + ) return response @staticmethod @@ -1588,30 +1573,21 @@ class Controller: self.updateTranslationEngineAndEngineList() response = {"status":200, "result":config.AUTH_KEYS[translator_name]} else: - response = { - "status":400, - "result":{ - "message":"Authentication failure of deepL auth key", - "data": config.AUTH_KEYS[translator_name] - } - } + response = VRCTError.create_error_response( + ErrorCode.AUTH_DEEPL_FAILED, + data=config.AUTH_KEYS[translator_name] + ) else: - response = { - "status":400, - "result":{ - "message":"DeepL auth key length is not correct", - "data": config.AUTH_KEYS[translator_name] - } - } + response = VRCTError.create_error_response( + ErrorCode.AUTH_DEEPL_LENGTH, + data=config.AUTH_KEYS[translator_name] + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": config.AUTH_KEYS[translator_name] - } - } + response = VRCTError.create_exception_error_response( + e, + data=config.AUTH_KEYS[translator_name] + ) return response def delDeeplAuthKey(self, *args, **kwargs) -> dict: @@ -1649,30 +1625,21 @@ class Controller: self.updateTranslationEngineAndEngineList() response = {"status":200, "result":config.AUTH_KEYS[translator_name]} else: - response = { - "status":400, - "result":{ - "message":"Authentication failure of plamo auth key", - "data": None - } - } + response = VRCTError.create_error_response( + ErrorCode.AUTH_PLAMO_FAILED, + data=None + ) else: - response = { - "status":400, - "result":{ - "message":"Plamo auth key length is not correct", - "data": None - } - } + response = VRCTError.create_error_response( + ErrorCode.AUTH_PLAMO_LENGTH, + data=None + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": None - } - } + response = VRCTError.create_exception_error_response( + e, + data=None + ) if response["status"] == 400: self.delPlamoAuthKey() return response @@ -1707,22 +1674,16 @@ class Controller: model.updateTranslatorPlamoClient() response = {"status":200, "result":config.SELECTED_PLAMO_MODEL} else: - response = { - "status":400, - "result":{ - "message":"Plamo model is not valid", - "data": config.SELECTED_PLAMO_MODEL - } - } + response = VRCTError.create_error_response( + ErrorCode.MODEL_PLAMO_INVALID, + data=config.SELECTED_PLAMO_MODEL + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": config.SELECTED_PLAMO_MODEL - } - } + response = VRCTError.create_exception_error_response( + e, + data=config.SELECTED_PLAMO_MODEL + ) return response def getGeminiAuthKey(self, *args, **kwargs) -> dict: @@ -1751,30 +1712,21 @@ class Controller: self.updateTranslationEngineAndEngineList() response = {"status":200, "result":config.AUTH_KEYS[translator_name]} else: - response = { - "status":400, - "result":{ - "message":"Authentication failure of gemini auth key", - "data": None - } - } + response = VRCTError.create_error_response( + ErrorCode.AUTH_GEMINI_FAILED, + data=None + ) else: - response = { - "status":400, - "result":{ - "message":"Gemini auth key length is not correct", - "data": None - } - } + response = VRCTError.create_error_response( + ErrorCode.AUTH_GEMINI_LENGTH, + data=None + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": None - } - } + response = VRCTError.create_exception_error_response( + e, + data=None + ) if response["status"] == 400: self.delGeminiAuthKey() return response @@ -1809,22 +1761,16 @@ class Controller: model.updateTranslatorGeminiClient() response = {"status":200, "result":config.SELECTED_GEMINI_MODEL} else: - response = { - "status":400, - "result":{ - "message":"Gemini model is not valid", - "data": config.SELECTED_GEMINI_MODEL - } - } + response = VRCTError.create_error_response( + ErrorCode.MODEL_GEMINI_INVALID, + data=config.SELECTED_GEMINI_MODEL + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": config.SELECTED_GEMINI_MODEL - } - } + response = VRCTError.create_exception_error_response( + e, + data=config.SELECTED_GEMINI_MODEL + ) return response @staticmethod @@ -1854,30 +1800,21 @@ class Controller: self.updateTranslationEngineAndEngineList() response = {"status":200, "result":config.AUTH_KEYS[translator_name]} else: - response = { - "status":400, - "result":{ - "message":"Authentication failure of OpenAI auth key", - "data": None - } - } + response = VRCTError.create_error_response( + ErrorCode.AUTH_OPENAI_FAILED, + data=None + ) else: - response = { - "status":400, - "result":{ - "message":"OpenAI auth key is not valid", - "data": None - } - } + response = VRCTError.create_error_response( + ErrorCode.AUTH_OPENAI_INVALID, + data=None + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": None - } - } + response = VRCTError.create_exception_error_response( + e, + data=None + ) if response["status"] == 400: self.delOpenAIAuthKey() return response @@ -1912,22 +1849,16 @@ class Controller: model.updateTranslatorOpenAIClient() response = {"status":200, "result":config.SELECTED_OPENAI_MODEL} else: - response = { - "status":400, - "result":{ - "message":"OpenAI model is not valid", - "data": config.SELECTED_OPENAI_MODEL - } - } + response = VRCTError.create_error_response( + ErrorCode.MODEL_OPENAI_INVALID, + data=config.SELECTED_OPENAI_MODEL + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": config.SELECTED_OPENAI_MODEL - } - } + response = VRCTError.create_exception_error_response( + e, + data=config.SELECTED_OPENAI_MODEL + ) return response @staticmethod @@ -1957,30 +1888,21 @@ class Controller: self.updateTranslationEngineAndEngineList() response = {"status":200, "result":config.AUTH_KEYS[translator_name]} else: - response = { - "status":400, - "result":{ - "message":"Authentication failure of Groq auth key", - "data": None - } - } + response = VRCTError.create_error_response( + ErrorCode.AUTH_GROQ_FAILED, + data=None + ) else: - response = { - "status":400, - "result":{ - "message":"Groq auth key is not valid", - "data": None - } - } + response = VRCTError.create_error_response( + ErrorCode.AUTH_GROQ_INVALID, + data=None + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": None - } - } + response = VRCTError.create_exception_error_response( + e, + data=None + ) if response["status"] == 400: self.delGroqAuthKey() return response @@ -2015,22 +1937,16 @@ class Controller: model.updateTranslatorGroqClient() response = {"status":200, "result":config.SELECTED_GROQ_MODEL} else: - response = { - "status":400, - "result":{ - "message":"Groq model is not valid", - "data": config.SELECTED_GROQ_MODEL - } - } + response = VRCTError.create_error_response( + ErrorCode.MODEL_GROQ_INVALID, + data=config.SELECTED_GROQ_MODEL + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": config.SELECTED_GROQ_MODEL - } - } + response = VRCTError.create_exception_error_response( + e, + data=config.SELECTED_GROQ_MODEL + ) return response @staticmethod @@ -2060,30 +1976,21 @@ class Controller: self.updateTranslationEngineAndEngineList() response = {"status":200, "result":config.AUTH_KEYS[translator_name]} else: - response = { - "status":400, - "result":{ - "message":"Authentication failure of OpenRouter auth key", - "data": None - } - } + response = VRCTError.create_error_response( + ErrorCode.AUTH_OPENROUTER_FAILED, + data=None + ) else: - response = { - "status":400, - "result":{ - "message":"OpenRouter auth key is not valid", - "data": None - } - } + response = VRCTError.create_error_response( + ErrorCode.AUTH_OPENROUTER_INVALID, + data=None + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": None - } - } + response = VRCTError.create_exception_error_response( + e, + data=None + ) if response["status"] == 400: self.delOpenRouterAuthKey() return response @@ -2118,22 +2025,16 @@ class Controller: model.updateTranslatorOpenRouterClient() response = {"status":200, "result":config.SELECTED_OPENROUTER_MODEL} else: - response = { - "status":400, - "result":{ - "message":"OpenRouter model is not valid", - "data": config.SELECTED_OPENROUTER_MODEL - } - } + response = VRCTError.create_error_response( + ErrorCode.MODEL_OPENROUTER_INVALID, + data=config.SELECTED_OPENROUTER_MODEL + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": config.SELECTED_OPENROUTER_MODEL - } - } + response = VRCTError.create_exception_error_response( + e, + data=config.SELECTED_OPENROUTER_MODEL + ) return response def getTranslatorLMStudioConnection(self, *args, **kwargs) -> dict: @@ -2164,13 +2065,10 @@ class Controller: self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST) self.run(200, self.run_mapping["selected_lmstudio_model"], config.SELECTED_LMSTUDIO_MODEL) self.updateTranslationEngineAndEngineList() - response = { - "status":400, - "result":{ - "message":"Cannot connect to LMStudio server", - "data": False - } - } + response = VRCTError.create_error_response( + ErrorCode.CONNECTION_LMSTUDIO_FAILED, + data=False + ) except Exception as e: errorLogging() config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = False @@ -2179,13 +2077,10 @@ class Controller: self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST) self.run(200, self.run_mapping["selected_lmstudio_model"], config.SELECTED_LMSTUDIO_MODEL) self.updateTranslationEngineAndEngineList() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": False - } - } + response = VRCTError.create_exception_error_response( + e, + data=False + ) return response def getConnectedLMStudio(self, *args, **kwargs) -> dict: @@ -2222,13 +2117,10 @@ class Controller: self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST) self.run(200, self.run_mapping["selected_lmstudio_model"], config.SELECTED_LMSTUDIO_MODEL) self.updateTranslationEngineAndEngineList() - response = { - "status":400, - "result":{ - "message":"LMStudio URL is not valid", - "data": config.LMSTUDIO_URL - } - } + response = VRCTError.create_error_response( + ErrorCode.CONNECTION_LMSTUDIO_URL_INVALID, + data=config.LMSTUDIO_URL + ) except Exception as e: errorLogging() config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = False @@ -2237,13 +2129,10 @@ class Controller: self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST) self.run(200, self.run_mapping["selected_lmstudio_model"], config.SELECTED_LMSTUDIO_MODEL) self.updateTranslationEngineAndEngineList() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": config.LMSTUDIO_URL - } - } + response = VRCTError.create_exception_error_response( + e, + data=config.LMSTUDIO_URL + ) return response def getTranslatorLStudioModelList(self, *args, **kwargs) -> dict: @@ -2264,22 +2153,16 @@ class Controller: model.updateTranslatorLMStudioClient() response = {"status":200, "result":config.SELECTED_LMSTUDIO_MODEL} else: - response = { - "status":400, - "result":{ - "message":"LMStudio model is not valid", - "data": config.SELECTED_LMSTUDIO_MODEL - } - } + response = VRCTError.create_error_response( + ErrorCode.MODEL_LMSTUDIO_INVALID, + data=config.SELECTED_LMSTUDIO_MODEL + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": config.SELECTED_LMSTUDIO_MODEL - } - } + response = VRCTError.create_exception_error_response( + e, + data=config.SELECTED_LMSTUDIO_MODEL + ) return response def getTranslatorOllamaConnection(self, *args, **kwargs) -> dict: @@ -2310,13 +2193,10 @@ class Controller: self.run(200, self.run_mapping["selectable_ollama_model_list"], config.SELECTABLE_OLLAMA_MODEL_LIST) self.run(200, self.run_mapping["selected_ollama_model"], config.SELECTED_OLLAMA_MODEL) self.updateTranslationEngineAndEngineList() - response = { - "status":400, - "result":{ - "message":"Cannot connect to ollama server", - "data": False - } - } + response = VRCTError.create_error_response( + ErrorCode.CONNECTION_OLLAMA_FAILED, + data=False + ) except Exception as e: errorLogging() config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = False @@ -2325,13 +2205,10 @@ class Controller: self.run(200, self.run_mapping["selectable_ollama_model_list"], config.SELECTABLE_OLLAMA_MODEL_LIST) self.run(200, self.run_mapping["selected_ollama_model"], config.SELECTED_OLLAMA_MODEL) self.updateTranslationEngineAndEngineList() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": False - } - } + response = VRCTError.create_exception_error_response( + e, + data=False + ) return response def getTranslatorOllamaModelList(self, *args, **kwargs) -> dict: @@ -2352,22 +2229,16 @@ class Controller: model.updateTranslatorOllamaClient() response = {"status":200, "result":config.SELECTED_OLLAMA_MODEL} else: - response = { - "status":400, - "result":{ - "message":"ollama model is not valid", - "data": config.SELECTED_OLLAMA_MODEL - } - } + response = VRCTError.create_error_response( + ErrorCode.MODEL_OLLAMA_INVALID, + data=config.SELECTED_OLLAMA_MODEL + ) except Exception as e: errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": config.SELECTED_OLLAMA_MODEL - } - } + response = VRCTError.create_exception_error_response( + e, + data=config.SELECTED_OLLAMA_MODEL + ) return response @staticmethod @@ -2599,13 +2470,10 @@ class Controller: model.changeMicTranscriptStatus() response = {"status":200, "result":config.VRC_MIC_MUTE_SYNC} else: - response = { - "status":400, - "result":{ - "message":"Cannot enable VRC mic mute sync while OSC query is disabled", - "data": config.VRC_MIC_MUTE_SYNC - } - } + response = VRCTError.create_error_response( + ErrorCode.VRC_MIC_MUTE_SYNC_OSC_DISABLED, + data=config.VRC_MIC_MUTE_SYNC + ) else: response = {"status":200, "result":config.VRC_MIC_MUTE_SYNC} return response @@ -2814,23 +2682,25 @@ class Controller: # VRAM不足エラーの検出 is_vram_error, error_message = model.detectVRAMError(e) if is_vram_error: + response = VRCTError.create_error_response( + ErrorCode.TRANSCRIPTION_VRAM_MIC, + data=error_message + ) self.run( - 400, + response["status"], self.run_mapping["error_transcription_mic_vram_overflow"], - { - "message":"VRAM out of memory during mic transcription", - "data": error_message - }, + response["result"], ) # ここでマイクの音声認識を停止 self.stopTranscriptionSendMessage() + disable_response = VRCTError.create_error_response( + ErrorCode.TRANSCRIPTION_SEND_DISABLED_VRAM, + data=False + ) self.run( - 400, + disable_response["status"], self.run_mapping["enable_transcription_send"], - { - "message":"Transcription send disabled due to VRAM overflow", - "data": False - }, + disable_response["result"], ) else: # その他のエラーは通常通り処理 @@ -2863,23 +2733,25 @@ class Controller: # VRAM不足エラーの検出 is_vram_error, error_message = model.detectVRAMError(e) if is_vram_error: + response = VRCTError.create_error_response( + ErrorCode.TRANSCRIPTION_VRAM_SPEAKER, + data=error_message + ) self.run( - 400, + response["status"], self.run_mapping["error_transcription_speaker_vram_overflow"], - { - "message":"VRAM out of memory during speaker transcription", - "data": error_message - }, + response["result"], ) # ここでスピーカーの音声認識を停止 self.stopTranscriptionReceiveMessage() + disable_response = VRCTError.create_error_response( + ErrorCode.TRANSCRIPTION_RECEIVE_DISABLED_VRAM, + data=False + ) self.run( - 400, + disable_response["status"], self.run_mapping["enable_transcription_receive"], - { - "message":"Transcription receive disabled due to VRAM overflow", - "data": False - }, + disable_response["result"], ) else: # その他のエラーは通常通り処理 @@ -3068,13 +2940,10 @@ class Controller: @staticmethod def setWebSocketHost(data, *args, **kwargs) -> dict: if isValidIpAddress(data) is False: - response = { - "status":400, - "result":{ - "message":"Invalid IP address", - "data": config.WEBSOCKET_HOST - } - } + response = VRCTError.create_error_response( + ErrorCode.VALIDATION_INVALID_IP, + data=config.WEBSOCKET_HOST + ) else: if model.checkWebSocketServerAlive() is False: config.WEBSOCKET_HOST = data @@ -3088,13 +2957,10 @@ class Controller: config.WEBSOCKET_HOST = data response = {"status":200, "result":config.WEBSOCKET_HOST} else: - response = { - "status":400, - "result":{ - "message":"WebSocket server host is not available", - "data": config.WEBSOCKET_HOST - } - } + response = VRCTError.create_error_response( + ErrorCode.WEBSOCKET_HOST_UNAVAILABLE, + data=config.WEBSOCKET_HOST + ) return response @@ -3116,13 +2982,10 @@ class Controller: config.WEBSOCKET_PORT = int(data) response = {"status":200, "result":config.WEBSOCKET_PORT} else: - response = { - "status":400, - "result":{ - "message":"WebSocket server port is not available", - "data": config.WEBSOCKET_PORT - } - } + response = VRCTError.create_error_response( + ErrorCode.WEBSOCKET_PORT_UNAVAILABLE, + data=config.WEBSOCKET_PORT + ) return response @staticmethod @@ -3137,13 +3000,10 @@ class Controller: config.WEBSOCKET_SERVER = True response = {"status":200, "result":config.WEBSOCKET_SERVER} else: - response = { - "status":400, - "result":{ - "message":"WebSocket server host or port is not available", - "data": config.WEBSOCKET_SERVER - } - } + response = VRCTError.create_error_response( + ErrorCode.WEBSOCKET_SERVER_UNAVAILABLE, + data=config.WEBSOCKET_SERVER + ) else: response = {"status":200, "result":config.WEBSOCKET_SERVER} return response diff --git a/src-python/docs/error_handling_migration_guide.md b/src-python/docs/error_handling_migration_guide.md new file mode 100644 index 00000000..051489f6 --- /dev/null +++ b/src-python/docs/error_handling_migration_guide.md @@ -0,0 +1,212 @@ +# エラーハンドリング統一システム移行ガイド + +## 概要 + +`errors.py`で定義された統一エラーシステムを使用して、すべてのエラーハンドリングを標準化しました。 + +## 変更パターン + +### 1. 基本的なエラーレスポンス + +#### 修正前: +```python +response = { + "status": 400, + "result": { + "message": "Error message", + "data": some_value + } +} +``` + +#### 修正後: +```python +from errors import ErrorCode, VRCTError + +response = VRCTError.create_error_response( + ErrorCode.APPROPRIATE_ERROR_CODE, + data=some_value +) +``` + +### 2. run_mapping経由のエラー通知 + +#### 修正前: +```python +self.run( + 400, + self.run_mapping["error_device"], + { + "message": "No mic device detected", + "data": None + }, +) +``` + +#### 修正後: +```python +error_response = VRCTError.create_error_response( + ErrorCode.DEVICE_NO_MIC, + data=None +) +self.run( + error_response["status"], + self.run_mapping["error_device"], + error_response["result"], +) +``` + +### 3. 例外からのエラー生成 + +#### 修正前: +```python +except Exception as e: + errorLogging() + response = { + "status": 400, + "result": { + "message": f"Error {e}", + "data": original_value + } + } +``` + +#### 修正後: +```python +except Exception as e: + errorLogging() + response = VRCTError.create_exception_error_response( + e, + data=original_value + ) +``` + +## 既に移行済みの箇所 + +### デバイスエラー +- ✅ `progressBarMicEnergy` - `ErrorCode.DEVICE_NO_MIC` +- ✅ `progressBarSpeakerEnergy` - `ErrorCode.DEVICE_NO_SPEAKER` + +### ウェイトダウンロードエラー +- ✅ `DownloadCTranslate2.downloaded` - `ErrorCode.WEIGHT_CTRANSLATE2_DOWNLOAD` +- ✅ `DownloadWhisper.downloaded` - `ErrorCode.WEIGHT_WHISPER_DOWNLOAD` + +### 翻訳エラー +- ✅ `micMessage` - `ErrorCode.TRANSLATION_ENGINE_LIMIT`, `ErrorCode.TRANSLATION_VRAM_MIC`, `ErrorCode.TRANSLATION_DISABLED_VRAM` +- ✅ `speakerMessage` - `ErrorCode.TRANSLATION_ENGINE_LIMIT`, `ErrorCode.TRANSLATION_VRAM_SPEAKER`, `ErrorCode.TRANSLATION_DISABLED_VRAM` +- ✅ `chatMessage` - `ErrorCode.TRANSLATION_ENGINE_LIMIT`, `ErrorCode.TRANSLATION_VRAM_CHAT`, `ErrorCode.TRANSLATION_DISABLED_VRAM` +- ✅ `setEnableTranslation` - `ErrorCode.TRANSLATION_VRAM_ENABLE`, `ErrorCode.TRANSLATION_DISABLED_VRAM` + +### バリデーションエラー +- ✅ `setMicThreshold` - `ErrorCode.VALIDATION_MIC_THRESHOLD` +- ✅ `setSpeakerThreshold` - `ErrorCode.VALIDATION_SPEAKER_THRESHOLD` +- ✅ `setMicRecordTimeout` - `ErrorCode.VALIDATION_MIC_RECORD_TIMEOUT` +- ✅ `setMicPhraseTimeout` - `ErrorCode.VALIDATION_MIC_PHRASE_TIMEOUT` +- ✅ `setMicMaxPhrases` - `ErrorCode.VALIDATION_MIC_MAX_PHRASES` +- ✅ `setSpeakerRecordTimeout` - `ErrorCode.VALIDATION_SPEAKER_RECORD_TIMEOUT` +- ✅ `setSpeakerPhraseTimeout` - `ErrorCode.VALIDATION_SPEAKER_PHRASE_TIMEOUT` +- ✅ `setSpeakerMaxPhrases` - `ErrorCode.VALIDATION_SPEAKER_MAX_PHRASES` +- ✅ `setOscIpAddress` - `ErrorCode.VALIDATION_INVALID_IP`, `ErrorCode.VALIDATION_CANNOT_SET_IP` + +### VRC連携エラー +- ✅ `setEnableVrcMicMuteSync` - `ErrorCode.VRC_MIC_MUTE_SYNC_OSC_DISABLED` + +### 認証エラー +- ✅ `setDeeplAuthKey` - `ErrorCode.AUTH_DEEPL_LENGTH`, `ErrorCode.AUTH_DEEPL_FAILED` + +## 未移行の箇所(要対応) + +以下の箇所は同様のパターンで移行が必要です: + +### 認証関連 +- ⬜ `setPlamoAuthKey` - `ErrorCode.AUTH_PLAMO_LENGTH`, `ErrorCode.AUTH_PLAMO_FAILED` +- ⬜ `setPlamoModel` - `ErrorCode.MODEL_PLAMO_INVALID` +- ⬜ `setGeminiAuthKey` - `ErrorCode.AUTH_GEMINI_LENGTH`, `ErrorCode.AUTH_GEMINI_FAILED` +- ⬜ `setGeminiModel` - `ErrorCode.MODEL_GEMINI_INVALID` +- ⬜ `setOpenAIAuthKey` - `ErrorCode.AUTH_OPENAI_INVALID`, `ErrorCode.AUTH_OPENAI_FAILED` +- ⬜ `setOpenAIModel` - `ErrorCode.MODEL_OPENAI_INVALID` +- ⬜ `setGroqAuthKey` - `ErrorCode.AUTH_GROQ_INVALID`, `ErrorCode.AUTH_GROQ_FAILED` +- ⬜ `setGroqModel` - `ErrorCode.MODEL_GROQ_INVALID` +- ⬜ `setOpenRouterAuthKey` - `ErrorCode.AUTH_OPENROUTER_INVALID`, `ErrorCode.AUTH_OPENROUTER_FAILED` +- ⬜ `setOpenRouterModel` - `ErrorCode.MODEL_OPENROUTER_INVALID` + +### 接続関連 +- ⬜ `checkTranslatorLMStudioConnection` - `ErrorCode.CONNECTION_LMSTUDIO_FAILED` +- ⬜ `setTranslatorLMStudioURL` - `ErrorCode.CONNECTION_LMSTUDIO_URL_INVALID` +- ⬜ `setTranslatorLMStudioModel` - `ErrorCode.MODEL_LMSTUDIO_INVALID` +- ⬜ `checkTranslatorOllamaConnection` - `ErrorCode.CONNECTION_OLLAMA_FAILED` +- ⬜ `setTranslatorOllamaModel` - `ErrorCode.MODEL_OLLAMA_INVALID` + +### WebSocket関連 +- ⬜ `setWebSocketHost` - `ErrorCode.VALIDATION_INVALID_IP`, `ErrorCode.WEBSOCKET_HOST_INVALID` +- ⬜ `setWebSocketPort` - `ErrorCode.WEBSOCKET_PORT_UNAVAILABLE` +- ⬜ `setEnableWebSocketServer` - `ErrorCode.WEBSOCKET_SERVER_UNAVAILABLE` + +### 音声認識VRAM関連 +- ⬜ `startTranscriptionSendMessage` - `ErrorCode.TRANSCRIPTION_VRAM_MIC`, `ErrorCode.TRANSCRIPTION_SEND_DISABLED_VRAM` +- ⬜ `startTranscriptionReceiveMessage` - `ErrorCode.TRANSCRIPTION_VRAM_SPEAKER`, `ErrorCode.TRANSCRIPTION_RECEIVE_DISABLED_VRAM` + +## エラーコードとエンドポイントの対応 + +`errors.py`の`ENDPOINT_ERROR_MAPPING`に、すべてのエンドポイントとエラーコードの対応が定義されています。 +UI開発者はこのマッピングを参照して、各エンドポイントがどのようなエラーを返すか確認できます。 + +## エラーレスポンスの構造 + +統一されたエラーレスポンスは以下の構造を持ちます: + +```python +{ + "status": 400, # HTTPステータスコード + "result": { + "error_code": "ERROR_CODE_CONSTANT", # エラーコード定数 + "message": "Human readable message", # 人間が読めるメッセージ + "data": None or original_value, # エラー時に戻す値(通常は元の値) + "details": {}, # 追加情報(オプション) + "category": "category_name", # エラーカテゴリ + "severity": "warning|error|critical", # 重要度 + } +} +``` + +## UI側での活用 + +UI側では`error_code`を使用して、エラーの種類を判定し、適切な処理を行うことができます: + +```javascript +if (response.status === 400) { + const { error_code, message, data, severity } = response.result; + + switch (error_code) { + case "DEVICE_NO_MIC": + // マイクデバイスエラーの処理 + break; + case "VALIDATION_MIC_THRESHOLD": + // バリデーションエラーの処理(元の値に戻す) + setValue(data); + break; + // ... + } + + // 重要度に応じた表示 + if (severity === "critical") { + showCriticalError(message); + } +} +``` + +## 移行作業の進め方 + +1. **パターンの確認**: 上記の変更パターンを参照 +2. **エラーコードの特定**: `errors.py`から適切な`ErrorCode`を選択 +3. **コードの置き換え**: 古いエラーハンドリングを新しいシステムに置き換え +4. **テスト**: エラーが正しく返されることを確認 +5. **チェックリストの更新**: このドキュメントの✅を更新 + +## 注意事項 + +- すべてのエラーは`errors.py`に定義されたエラーコードを使用すること +- 新しいエラーが必要な場合は、まず`errors.py`に追加すること +- エラーメッセージは`ERROR_METADATA`で定義されたデフォルトメッセージを使用すること + - カスタムメッセージが必要な場合は`custom_message`パラメータを使用 +- `data`パラメータには、エラー時にUIが元の値に戻せるように、元の値を渡すこと diff --git a/src-python/errors.py b/src-python/errors.py new file mode 100644 index 00000000..abe75628 --- /dev/null +++ b/src-python/errors.py @@ -0,0 +1,694 @@ +# src-python/errors.py +""" +統一エラー管理システム + +すべてのエラーを一元管理し、エンドポイントとエラーコードの対応を明確にする。 +""" + +from typing import Any, Optional, Dict +from enum import Enum + + +class ErrorCode(str, Enum): + """エラーコード定数 + + 命名規則: カテゴリ_具体的な内容 + """ + # ============================================================================ + # デバイス関連エラー (DEVICE_*) + # ============================================================================ + DEVICE_NO_MIC = "DEVICE_NO_MIC" + DEVICE_NO_SPEAKER = "DEVICE_NO_SPEAKER" + + # ============================================================================ + # 翻訳関連エラー (TRANSLATION_*) + # ============================================================================ + TRANSLATION_ENGINE_LIMIT = "TRANSLATION_ENGINE_LIMIT" + TRANSLATION_VRAM_CHAT = "TRANSLATION_VRAM_CHAT" + TRANSLATION_VRAM_MIC = "TRANSLATION_VRAM_MIC" + TRANSLATION_VRAM_SPEAKER = "TRANSLATION_VRAM_SPEAKER" + TRANSLATION_VRAM_ENABLE = "TRANSLATION_VRAM_ENABLE" + TRANSLATION_DISABLED_VRAM = "TRANSLATION_DISABLED_VRAM" + + # ============================================================================ + # 音声認識関連エラー (TRANSCRIPTION_*) + # ============================================================================ + TRANSCRIPTION_VRAM_MIC = "TRANSCRIPTION_VRAM_MIC" + TRANSCRIPTION_VRAM_SPEAKER = "TRANSCRIPTION_VRAM_SPEAKER" + TRANSCRIPTION_SEND_DISABLED_VRAM = "TRANSCRIPTION_SEND_DISABLED_VRAM" + TRANSCRIPTION_RECEIVE_DISABLED_VRAM = "TRANSCRIPTION_RECEIVE_DISABLED_VRAM" + + # ============================================================================ + # ウェイトダウンロード関連エラー (WEIGHT_*) + # ============================================================================ + WEIGHT_CTRANSLATE2_DOWNLOAD = "WEIGHT_CTRANSLATE2_DOWNLOAD" + WEIGHT_WHISPER_DOWNLOAD = "WEIGHT_WHISPER_DOWNLOAD" + + # ============================================================================ + # バリデーションエラー (VALIDATION_*) + # ============================================================================ + VALIDATION_MIC_THRESHOLD = "VALIDATION_MIC_THRESHOLD" + VALIDATION_SPEAKER_THRESHOLD = "VALIDATION_SPEAKER_THRESHOLD" + VALIDATION_MIC_RECORD_TIMEOUT = "VALIDATION_MIC_RECORD_TIMEOUT" + VALIDATION_MIC_PHRASE_TIMEOUT = "VALIDATION_MIC_PHRASE_TIMEOUT" + VALIDATION_MIC_MAX_PHRASES = "VALIDATION_MIC_MAX_PHRASES" + VALIDATION_SPEAKER_RECORD_TIMEOUT = "VALIDATION_SPEAKER_RECORD_TIMEOUT" + VALIDATION_SPEAKER_PHRASE_TIMEOUT = "VALIDATION_SPEAKER_PHRASE_TIMEOUT" + VALIDATION_SPEAKER_MAX_PHRASES = "VALIDATION_SPEAKER_MAX_PHRASES" + VALIDATION_INVALID_IP = "VALIDATION_INVALID_IP" + VALIDATION_CANNOT_SET_IP = "VALIDATION_CANNOT_SET_IP" + + # ============================================================================ + # 認証エラー (AUTH_*) + # ============================================================================ + AUTH_DEEPL_LENGTH = "AUTH_DEEPL_LENGTH" + AUTH_DEEPL_FAILED = "AUTH_DEEPL_FAILED" + AUTH_PLAMO_LENGTH = "AUTH_PLAMO_LENGTH" + AUTH_PLAMO_FAILED = "AUTH_PLAMO_FAILED" + AUTH_GEMINI_LENGTH = "AUTH_GEMINI_LENGTH" + AUTH_GEMINI_FAILED = "AUTH_GEMINI_FAILED" + AUTH_OPENAI_INVALID = "AUTH_OPENAI_INVALID" + AUTH_OPENAI_FAILED = "AUTH_OPENAI_FAILED" + AUTH_GROQ_INVALID = "AUTH_GROQ_INVALID" + AUTH_GROQ_FAILED = "AUTH_GROQ_FAILED" + AUTH_OPENROUTER_INVALID = "AUTH_OPENROUTER_INVALID" + AUTH_OPENROUTER_FAILED = "AUTH_OPENROUTER_FAILED" + + # ============================================================================ + # モデル選択エラー (MODEL_*) + # ============================================================================ + MODEL_PLAMO_INVALID = "MODEL_PLAMO_INVALID" + MODEL_GEMINI_INVALID = "MODEL_GEMINI_INVALID" + MODEL_OPENAI_INVALID = "MODEL_OPENAI_INVALID" + MODEL_GROQ_INVALID = "MODEL_GROQ_INVALID" + MODEL_OPENROUTER_INVALID = "MODEL_OPENROUTER_INVALID" + MODEL_LMSTUDIO_INVALID = "MODEL_LMSTUDIO_INVALID" + MODEL_OLLAMA_INVALID = "MODEL_OLLAMA_INVALID" + + # ============================================================================ + # 接続エラー (CONNECTION_*) + # ============================================================================ + CONNECTION_LMSTUDIO_FAILED = "CONNECTION_LMSTUDIO_FAILED" + CONNECTION_OLLAMA_FAILED = "CONNECTION_OLLAMA_FAILED" + CONNECTION_LMSTUDIO_URL_INVALID = "CONNECTION_LMSTUDIO_URL_INVALID" + + # ============================================================================ + # WebSocketエラー (WEBSOCKET_*) + # ============================================================================ + WEBSOCKET_HOST_INVALID = "WEBSOCKET_HOST_INVALID" + WEBSOCKET_PORT_UNAVAILABLE = "WEBSOCKET_PORT_UNAVAILABLE" + WEBSOCKET_SERVER_UNAVAILABLE = "WEBSOCKET_SERVER_UNAVAILABLE" + + # ============================================================================ + # VRC連携エラー (VRC_*) + # ============================================================================ + VRC_MIC_MUTE_SYNC_OSC_DISABLED = "VRC_MIC_MUTE_SYNC_OSC_DISABLED" + + # ============================================================================ + # 汎用エラー (GENERAL_*) + # ============================================================================ + GENERAL_EXCEPTION = "GENERAL_EXCEPTION" + GENERAL_UNKNOWN = "GENERAL_UNKNOWN" + + +class ErrorCategory(str, Enum): + """エラーカテゴリ""" + DEVICE = "device" + TRANSLATION = "translation" + TRANSCRIPTION = "transcription" + WEIGHT = "weight" + VALIDATION = "validation" + AUTH = "auth" + MODEL = "model" + CONNECTION = "connection" + WEBSOCKET = "websocket" + VRC = "vrc" + GENERAL = "general" + + +# エラーコードのメタデータ定義 +ERROR_METADATA: Dict[ErrorCode, Dict[str, Any]] = { + # デバイスエラー + ErrorCode.DEVICE_NO_MIC: { + "category": ErrorCategory.DEVICE, + "message": "No mic device detected", + "severity": "error", + "user_action_required": True, + }, + ErrorCode.DEVICE_NO_SPEAKER: { + "category": ErrorCategory.DEVICE, + "message": "No speaker device detected", + "severity": "error", + "user_action_required": True, + }, + + # 翻訳エラー + ErrorCode.TRANSLATION_ENGINE_LIMIT: { + "category": ErrorCategory.TRANSLATION, + "message": "Translation engine limit error", + "severity": "warning", + "user_action_required": False, + "auto_fallback": True, + }, + ErrorCode.TRANSLATION_VRAM_CHAT: { + "category": ErrorCategory.TRANSLATION, + "message": "VRAM out of memory during translation of chat", + "severity": "critical", + "user_action_required": True, + }, + ErrorCode.TRANSLATION_VRAM_MIC: { + "category": ErrorCategory.TRANSLATION, + "message": "VRAM out of memory during translation of mic", + "severity": "critical", + "user_action_required": True, + }, + ErrorCode.TRANSLATION_VRAM_SPEAKER: { + "category": ErrorCategory.TRANSLATION, + "message": "VRAM out of memory during translation of speaker", + "severity": "critical", + "user_action_required": True, + }, + ErrorCode.TRANSLATION_VRAM_ENABLE: { + "category": ErrorCategory.TRANSLATION, + "message": "VRAM out of memory enabling translation", + "severity": "critical", + "user_action_required": True, + }, + ErrorCode.TRANSLATION_DISABLED_VRAM: { + "category": ErrorCategory.TRANSLATION, + "message": "Translation disabled due to VRAM overflow", + "severity": "critical", + "user_action_required": True, + }, + + # 音声認識エラー + ErrorCode.TRANSCRIPTION_VRAM_MIC: { + "category": ErrorCategory.TRANSCRIPTION, + "message": "VRAM out of memory during mic transcription", + "severity": "critical", + "user_action_required": True, + }, + ErrorCode.TRANSCRIPTION_VRAM_SPEAKER: { + "category": ErrorCategory.TRANSCRIPTION, + "message": "VRAM out of memory during speaker transcription", + "severity": "critical", + "user_action_required": True, + }, + ErrorCode.TRANSCRIPTION_SEND_DISABLED_VRAM: { + "category": ErrorCategory.TRANSCRIPTION, + "message": "Transcription send disabled due to VRAM overflow", + "severity": "critical", + "user_action_required": True, + }, + ErrorCode.TRANSCRIPTION_RECEIVE_DISABLED_VRAM: { + "category": ErrorCategory.TRANSCRIPTION, + "message": "Transcription receive disabled due to VRAM overflow", + "severity": "critical", + "user_action_required": True, + }, + + # ウェイトダウンロードエラー + ErrorCode.WEIGHT_CTRANSLATE2_DOWNLOAD: { + "category": ErrorCategory.WEIGHT, + "message": "CTranslate2 weight download error", + "severity": "error", + "user_action_required": True, + }, + ErrorCode.WEIGHT_WHISPER_DOWNLOAD: { + "category": ErrorCategory.WEIGHT, + "message": "Whisper weight download error", + "severity": "error", + "user_action_required": True, + }, + + # バリデーションエラー + ErrorCode.VALIDATION_MIC_THRESHOLD: { + "category": ErrorCategory.VALIDATION, + "message": "Mic energy threshold value is out of range", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.VALIDATION_SPEAKER_THRESHOLD: { + "category": ErrorCategory.VALIDATION, + "message": "Speaker energy threshold value is out of range", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.VALIDATION_MIC_RECORD_TIMEOUT: { + "category": ErrorCategory.VALIDATION, + "message": "Mic record timeout value is out of range", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.VALIDATION_MIC_PHRASE_TIMEOUT: { + "category": ErrorCategory.VALIDATION, + "message": "Mic phrase timeout value is out of range", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.VALIDATION_MIC_MAX_PHRASES: { + "category": ErrorCategory.VALIDATION, + "message": "Mic max phrases value is out of range", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.VALIDATION_SPEAKER_RECORD_TIMEOUT: { + "category": ErrorCategory.VALIDATION, + "message": "Speaker record timeout value is out of range", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.VALIDATION_SPEAKER_PHRASE_TIMEOUT: { + "category": ErrorCategory.VALIDATION, + "message": "Speaker phrase timeout value is out of range", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.VALIDATION_SPEAKER_MAX_PHRASES: { + "category": ErrorCategory.VALIDATION, + "message": "Speaker max phrases value is out of range", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.VALIDATION_INVALID_IP: { + "category": ErrorCategory.VALIDATION, + "message": "Invalid IP address", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.VALIDATION_CANNOT_SET_IP: { + "category": ErrorCategory.VALIDATION, + "message": "Cannot set IP address", + "severity": "error", + "user_action_required": True, + }, + + # 認証エラー + ErrorCode.AUTH_DEEPL_LENGTH: { + "category": ErrorCategory.AUTH, + "message": "DeepL auth key length is not correct", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.AUTH_DEEPL_FAILED: { + "category": ErrorCategory.AUTH, + "message": "Authentication failure of deepL auth key", + "severity": "error", + "user_action_required": True, + }, + ErrorCode.AUTH_PLAMO_LENGTH: { + "category": ErrorCategory.AUTH, + "message": "Plamo auth key length is not correct", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.AUTH_PLAMO_FAILED: { + "category": ErrorCategory.AUTH, + "message": "Authentication failure of plamo auth key", + "severity": "error", + "user_action_required": True, + }, + ErrorCode.AUTH_GEMINI_LENGTH: { + "category": ErrorCategory.AUTH, + "message": "Gemini auth key length is not correct", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.AUTH_GEMINI_FAILED: { + "category": ErrorCategory.AUTH, + "message": "Authentication failure of gemini auth key", + "severity": "error", + "user_action_required": True, + }, + ErrorCode.AUTH_OPENAI_INVALID: { + "category": ErrorCategory.AUTH, + "message": "OpenAI auth key is not valid", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.AUTH_OPENAI_FAILED: { + "category": ErrorCategory.AUTH, + "message": "Authentication failure of OpenAI auth key", + "severity": "error", + "user_action_required": True, + }, + ErrorCode.AUTH_GROQ_INVALID: { + "category": ErrorCategory.AUTH, + "message": "Groq auth key is not valid", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.AUTH_GROQ_FAILED: { + "category": ErrorCategory.AUTH, + "message": "Authentication failure of Groq auth key", + "severity": "error", + "user_action_required": True, + }, + ErrorCode.AUTH_OPENROUTER_INVALID: { + "category": ErrorCategory.AUTH, + "message": "OpenRouter auth key is not valid", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.AUTH_OPENROUTER_FAILED: { + "category": ErrorCategory.AUTH, + "message": "Authentication failure of OpenRouter auth key", + "severity": "error", + "user_action_required": True, + }, + + # モデル選択エラー + ErrorCode.MODEL_PLAMO_INVALID: { + "category": ErrorCategory.MODEL, + "message": "Plamo model is not valid", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.MODEL_GEMINI_INVALID: { + "category": ErrorCategory.MODEL, + "message": "Gemini model is not valid", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.MODEL_OPENAI_INVALID: { + "category": ErrorCategory.MODEL, + "message": "OpenAI model is not valid", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.MODEL_GROQ_INVALID: { + "category": ErrorCategory.MODEL, + "message": "Groq model is not valid", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.MODEL_OPENROUTER_INVALID: { + "category": ErrorCategory.MODEL, + "message": "OpenRouter model is not valid", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.MODEL_LMSTUDIO_INVALID: { + "category": ErrorCategory.MODEL, + "message": "LMStudio model is not valid", + "severity": "warning", + "user_action_required": True, + }, + ErrorCode.MODEL_OLLAMA_INVALID: { + "category": ErrorCategory.MODEL, + "message": "ollama model is not valid", + "severity": "warning", + "user_action_required": True, + }, + + # 接続エラー + ErrorCode.CONNECTION_LMSTUDIO_FAILED: { + "category": ErrorCategory.CONNECTION, + "message": "Cannot connect to LMStudio server", + "severity": "error", + "user_action_required": True, + }, + ErrorCode.CONNECTION_OLLAMA_FAILED: { + "category": ErrorCategory.CONNECTION, + "message": "Cannot connect to ollama server", + "severity": "error", + "user_action_required": True, + }, + ErrorCode.CONNECTION_LMSTUDIO_URL_INVALID: { + "category": ErrorCategory.CONNECTION, + "message": "LMStudio URL is not valid", + "severity": "warning", + "user_action_required": True, + }, + + # WebSocketエラー + ErrorCode.WEBSOCKET_HOST_INVALID: { + "category": ErrorCategory.WEBSOCKET, + "message": "WebSocket server host is not available", + "severity": "error", + "user_action_required": True, + }, + ErrorCode.WEBSOCKET_PORT_UNAVAILABLE: { + "category": ErrorCategory.WEBSOCKET, + "message": "WebSocket server port is not available", + "severity": "error", + "user_action_required": True, + }, + ErrorCode.WEBSOCKET_SERVER_UNAVAILABLE: { + "category": ErrorCategory.WEBSOCKET, + "message": "WebSocket server host or port is not available", + "severity": "error", + "user_action_required": True, + }, + + # VRC連携エラー + ErrorCode.VRC_MIC_MUTE_SYNC_OSC_DISABLED: { + "category": ErrorCategory.VRC, + "message": "Cannot enable VRC mic mute sync while OSC query is disabled", + "severity": "warning", + "user_action_required": True, + }, + + # 汎用エラー + ErrorCode.GENERAL_EXCEPTION: { + "category": ErrorCategory.GENERAL, + "message": "An error occurred", + "severity": "error", + "user_action_required": False, + }, + ErrorCode.GENERAL_UNKNOWN: { + "category": ErrorCategory.GENERAL, + "message": "Unknown error", + "severity": "error", + "user_action_required": False, + }, +} + + +class VRCTError: + """VRCTエラーハンドリングクラス""" + + @staticmethod + def create_error_response( + error_code: ErrorCode, + data: Any = None, + details: Optional[Dict[str, Any]] = None, + custom_message: Optional[str] = None + ) -> Dict[str, Any]: + """統一されたエラーレスポンスを生成 + + Args: + error_code: エラーコード + data: エラー時に戻す値(通常は元の値) + details: 追加の詳細情報 + custom_message: カスタムメッセージ(指定しない場合はデフォルトメッセージ) + + Returns: + エラーレスポンス辞書 + """ + metadata = ERROR_METADATA.get(error_code, ERROR_METADATA[ErrorCode.GENERAL_UNKNOWN]) + + return { + "status": 400, + "result": { + "error_code": error_code.value, + "message": custom_message or metadata["message"], + "data": data, + "details": details or {}, + "category": metadata["category"].value, + "severity": metadata["severity"], + } + } + + @staticmethod + def create_exception_error_response( + exception: Exception, + data: Any = None, + error_code: ErrorCode = ErrorCode.GENERAL_EXCEPTION + ) -> Dict[str, Any]: + """例外からエラーレスポンスを生成 + + Args: + exception: 発生した例外 + data: エラー時に戻す値 + error_code: エラーコード + + Returns: + エラーレスポンス辞書 + """ + return VRCTError.create_error_response( + error_code=error_code, + data=data, + custom_message=f"Error: {str(exception)}", + details={"exception_type": type(exception).__name__} + ) + + +# エンドポイントとエラーコードのマッピング +# UIがエラーハンドリングする際の参照として使用 +ENDPOINT_ERROR_MAPPING: Dict[str, Dict[str, ErrorCode]] = { + # run_mapping経由のエラー通知 + "/run/error_device": { + "NO_MIC": ErrorCode.DEVICE_NO_MIC, + "NO_SPEAKER": ErrorCode.DEVICE_NO_SPEAKER, + }, + "/run/error_translation_engine": { + "LIMIT": ErrorCode.TRANSLATION_ENGINE_LIMIT, + }, + "/run/error_translation_chat_vram_overflow": { + "VRAM": ErrorCode.TRANSLATION_VRAM_CHAT, + }, + "/run/error_translation_mic_vram_overflow": { + "VRAM": ErrorCode.TRANSLATION_VRAM_MIC, + }, + "/run/error_translation_speaker_vram_overflow": { + "VRAM": ErrorCode.TRANSLATION_VRAM_SPEAKER, + }, + "/run/error_transcription_mic_vram_overflow": { + "VRAM": ErrorCode.TRANSCRIPTION_VRAM_MIC, + }, + "/run/error_transcription_speaker_vram_overflow": { + "VRAM": ErrorCode.TRANSCRIPTION_VRAM_SPEAKER, + }, + "/run/error_ctranslate2_weight": { + "DOWNLOAD": ErrorCode.WEIGHT_CTRANSLATE2_DOWNLOAD, + }, + "/run/error_whisper_weight": { + "DOWNLOAD": ErrorCode.WEIGHT_WHISPER_DOWNLOAD, + }, + + # エンドポイント直接のエラーレスポンス + "/set/data/mic_threshold": { + "OUT_OF_RANGE": ErrorCode.VALIDATION_MIC_THRESHOLD, + }, + "/set/data/speaker_threshold": { + "OUT_OF_RANGE": ErrorCode.VALIDATION_SPEAKER_THRESHOLD, + }, + "/set/data/mic_record_timeout": { + "OUT_OF_RANGE": ErrorCode.VALIDATION_MIC_RECORD_TIMEOUT, + }, + "/set/data/mic_phrase_timeout": { + "OUT_OF_RANGE": ErrorCode.VALIDATION_MIC_PHRASE_TIMEOUT, + }, + "/set/data/mic_max_phrases": { + "OUT_OF_RANGE": ErrorCode.VALIDATION_MIC_MAX_PHRASES, + }, + "/set/data/speaker_record_timeout": { + "OUT_OF_RANGE": ErrorCode.VALIDATION_SPEAKER_RECORD_TIMEOUT, + }, + "/set/data/speaker_phrase_timeout": { + "OUT_OF_RANGE": ErrorCode.VALIDATION_SPEAKER_PHRASE_TIMEOUT, + }, + "/set/data/speaker_max_phrases": { + "OUT_OF_RANGE": ErrorCode.VALIDATION_SPEAKER_MAX_PHRASES, + }, + "/set/data/osc_ip_address": { + "INVALID": ErrorCode.VALIDATION_INVALID_IP, + "CANNOT_SET": ErrorCode.VALIDATION_CANNOT_SET_IP, + }, + "/set/data/deepl_auth_key": { + "LENGTH": ErrorCode.AUTH_DEEPL_LENGTH, + "FAILED": ErrorCode.AUTH_DEEPL_FAILED, + }, + "/set/data/plamo_auth_key": { + "LENGTH": ErrorCode.AUTH_PLAMO_LENGTH, + "FAILED": ErrorCode.AUTH_PLAMO_FAILED, + }, + "/set/data/selected_plamo_model": { + "INVALID": ErrorCode.MODEL_PLAMO_INVALID, + }, + "/set/data/gemini_auth_key": { + "LENGTH": ErrorCode.AUTH_GEMINI_LENGTH, + "FAILED": ErrorCode.AUTH_GEMINI_FAILED, + }, + "/set/data/selected_gemini_model": { + "INVALID": ErrorCode.MODEL_GEMINI_INVALID, + }, + "/set/data/openai_auth_key": { + "INVALID": ErrorCode.AUTH_OPENAI_INVALID, + "FAILED": ErrorCode.AUTH_OPENAI_FAILED, + }, + "/set/data/selected_openai_model": { + "INVALID": ErrorCode.MODEL_OPENAI_INVALID, + }, + "/set/data/groq_auth_key": { + "INVALID": ErrorCode.AUTH_GROQ_INVALID, + "FAILED": ErrorCode.AUTH_GROQ_FAILED, + }, + "/set/data/selected_groq_model": { + "INVALID": ErrorCode.MODEL_GROQ_INVALID, + }, + "/set/data/openrouter_auth_key": { + "INVALID": ErrorCode.AUTH_OPENROUTER_INVALID, + "FAILED": ErrorCode.AUTH_OPENROUTER_FAILED, + }, + "/set/data/selected_openrouter_model": { + "INVALID": ErrorCode.MODEL_OPENROUTER_INVALID, + }, + "/run/lmstudio_connection": { + "FAILED": ErrorCode.CONNECTION_LMSTUDIO_FAILED, + }, + "/set/data/lmstudio_url": { + "INVALID": ErrorCode.CONNECTION_LMSTUDIO_URL_INVALID, + }, + "/set/data/selected_lmstudio_model": { + "INVALID": ErrorCode.MODEL_LMSTUDIO_INVALID, + }, + "/run/ollama_connection": { + "FAILED": ErrorCode.CONNECTION_OLLAMA_FAILED, + }, + "/set/data/selected_ollama_model": { + "INVALID": ErrorCode.MODEL_OLLAMA_INVALID, + }, + "/set/data/websocket_host": { + "INVALID_IP": ErrorCode.VALIDATION_INVALID_IP, + "UNAVAILABLE": ErrorCode.WEBSOCKET_HOST_INVALID, + }, + "/set/data/websocket_port": { + "UNAVAILABLE": ErrorCode.WEBSOCKET_PORT_UNAVAILABLE, + }, + "/set/enable/websocket_server": { + "UNAVAILABLE": ErrorCode.WEBSOCKET_SERVER_UNAVAILABLE, + }, + "/set/enable/vrc_mic_mute_sync": { + "OSC_DISABLED": ErrorCode.VRC_MIC_MUTE_SYNC_OSC_DISABLED, + }, +} + + +def get_error_metadata(error_code: ErrorCode) -> Dict[str, Any]: + """エラーコードのメタデータを取得 + + Args: + error_code: エラーコード + + Returns: + メタデータ辞書 + """ + return ERROR_METADATA.get(error_code, ERROR_METADATA[ErrorCode.GENERAL_UNKNOWN]) + + +def is_critical_error(error_code: ErrorCode) -> bool: + """クリティカルエラーかどうかを判定 + + Args: + error_code: エラーコード + + Returns: + クリティカルエラーの場合True + """ + metadata = get_error_metadata(error_code) + return metadata.get("severity") == "critical" + + +def requires_user_action(error_code: ErrorCode) -> bool: + """ユーザーアクションが必要なエラーかどうかを判定 + + Args: + error_code: エラーコード + + Returns: + ユーザーアクションが必要な場合True + """ + metadata = get_error_metadata(error_code) + return metadata.get("user_action_required", False)