Files
VRCT/src-python/config.py

1089 lines
48 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sys
import copy
from os import path as os_path, makedirs as os_makedirs
from json import load as json_load
from json import dump as json_dump
import threading
from typing import Optional, Dict, Any
import torch
# Guard optional, potentially heavy or platform-specific imports so importing
# config.py doesn't raise in environments missing those packages.
try:
from device_manager import device_manager
except Exception: # pragma: no cover - optional runtime
device_manager = None # type: ignore
try:
from models.translation.translation_languages import translation_lang, loadTranslationLanguages
except Exception: # pragma: no cover - optional runtime
translation_lang = {} # type: ignore
def loadTranslationLanguages(path: str, force: bool = False) -> Dict[str, Any]:
return {}
try:
from models.translation.translation_utils import ctranslate2_weights
except Exception: # pragma: no cover - optional runtime
ctranslate2_weights = {} # type: ignore
try:
from models.transcription.transcription_languages import transcription_lang
except Exception: # pragma: no cover - optional runtime
transcription_lang = {} # type: ignore
try:
from models.transcription.transcription_whisper import _MODELS as whisper_models
except Exception: # pragma: no cover - optional runtime
whisper_models = {} # type: ignore
from utils import errorLogging, validateDictStructure, getComputeDeviceList
json_serializable_vars = {}
def json_serializable(var_name):
def decorator(func):
json_serializable_vars[var_name] = func
return func
return decorator
# Auto-register descriptors for serialization
def _auto_register_descriptors():
"""Automatically register ManagedProperty and ValidatedProperty descriptors
for JSON serialization, reducing boilerplate _json_XXX methods.
"""
for name, obj in Config.__dict__.items():
if isinstance(obj, (ManagedProperty, ValidatedProperty)):
# Only register if serialize=True and not already manually registered
if obj.serialize and name not in json_serializable_vars:
# Create closure to capture current name
def make_serializer(attr_name):
@json_serializable(attr_name)
def _auto_serialize(self):
return getattr(self, attr_name)
return _auto_serialize
make_serializer(name)
# Wrapper classes for mutable types that auto-save on modification
class ManagedDict(dict):
"""Dict wrapper that saves changes back to config."""
def __init__(self, instance, property_name, immediate_save):
self._instance = instance
self._property_name = property_name
self._immediate_save = immediate_save
self._internal_name = f"_{property_name}"
# Initialize from internal storage
super().__init__(getattr(instance, self._internal_name))
def _get_internal(self):
"""Get reference to internal storage."""
return getattr(self._instance, self._internal_name)
def _save(self):
"""Save current state back to config and sync internal storage."""
try:
# Update internal storage directly
internal_dict = self._get_internal()
internal_dict.clear()
internal_dict.update(dict.items(self))
# Trigger config save only if the corresponding property is serializable
descriptor = getattr(type(self._instance), self._property_name, None)
if getattr(descriptor, "serialize", True):
self._instance.saveConfig(self._property_name, dict(self), immediate_save=self._immediate_save)
except Exception:
pass
def __getitem__(self, key):
# Always read from internal storage to get latest value
return self._get_internal()[key]
def __setitem__(self, key, value):
super().__setitem__(key, value)
self._save()
def __delitem__(self, key):
super().__delitem__(key)
self._save()
def __contains__(self, key):
return key in self._get_internal()
def get(self, key, default=None):
return self._get_internal().get(key, default)
def keys(self):
return self._get_internal().keys()
def values(self):
return self._get_internal().values()
def items(self):
return self._get_internal().items()
def update(self, *args, **kwargs):
super().update(*args, **kwargs)
self._save()
def pop(self, *args):
result = super().pop(*args)
self._save()
return result
def popitem(self):
result = super().popitem()
self._save()
return result
def clear(self):
super().clear()
self._save()
def setdefault(self, key, default=None):
result = super().setdefault(key, default)
self._save()
return result
class ManagedList(list):
"""List wrapper that saves changes back to config."""
def __init__(self, instance, property_name, immediate_save):
self._instance = instance
self._property_name = property_name
self._immediate_save = immediate_save
self._internal_name = f"_{property_name}"
# Initialize from internal storage
super().__init__(getattr(instance, self._internal_name))
def _get_internal(self):
"""Get reference to internal storage."""
return getattr(self._instance, self._internal_name)
def _save(self):
"""Save current state back to config and sync internal storage."""
try:
# Update internal storage directly
internal_list = self._get_internal()
internal_list.clear()
internal_list.extend(list.__iter__(self))
# Trigger config save only if the corresponding property is serializable
descriptor = getattr(type(self._instance), self._property_name, None)
if getattr(descriptor, "serialize", True):
self._instance.saveConfig(self._property_name, list(self), immediate_save=self._immediate_save)
except Exception:
pass
def __getitem__(self, index):
# Always read from internal storage to get latest value
return self._get_internal()[index]
def __setitem__(self, index, value):
super().__setitem__(index, value)
self._save()
def __delitem__(self, index):
super().__delitem__(index)
self._save()
def __len__(self):
return len(self._get_internal())
def __contains__(self, value):
return value in self._get_internal()
def __iter__(self):
return iter(self._get_internal())
def append(self, value):
super().append(value)
self._save()
def extend(self, iterable):
super().extend(iterable)
self._save()
def insert(self, index, value):
super().insert(index, value)
self._save()
def remove(self, value):
super().remove(value)
self._save()
def pop(self, index=-1):
result = super().pop(index)
self._save()
return result
def clear(self):
super().clear()
self._save()
def sort(self, *args, **kwargs):
super().sort(*args, **kwargs)
self._save()
def reverse(self):
super().reverse()
self._save()
# Descriptor for simple managed config properties to reduce repetitive getters/setters.
# It performs optional type validation, optional allowed-values check, and calls
# instance.saveConfig(...) on successful set.
class ManagedProperty:
def __init__(self, name: str, type_: type = None, allowed=None, immediate_save: bool = False, serialize: bool = True, readonly: bool = False, mutable_tracking: bool = False):
self.name = name
self.type_ = type_
self.allowed = allowed
self.immediate_save = immediate_save
self.serialize = serialize
self.readonly = readonly
self.mutable_tracking = mutable_tracking
self.private_name = f"_{name}"
self.wrapper_cache_name = f"_wrapper_{name}"
def __get__(self, instance, owner):
if instance is None:
return self
stored = getattr(instance, self.private_name)
# If mutable_tracking is enabled, return cached wrapper or create new one
if self.mutable_tracking and isinstance(stored, dict):
wrapper = getattr(instance, self.wrapper_cache_name, None)
if wrapper is None or not isinstance(wrapper, ManagedDict):
wrapper = ManagedDict(instance, self.name, self.immediate_save)
setattr(instance, self.wrapper_cache_name, wrapper)
# Wrapper automatically syncs with internal storage on access
return wrapper
elif self.mutable_tracking and isinstance(stored, list):
wrapper = getattr(instance, self.wrapper_cache_name, None)
if wrapper is None or not isinstance(wrapper, ManagedList):
wrapper = ManagedList(instance, self.name, self.immediate_save)
setattr(instance, self.wrapper_cache_name, wrapper)
# Wrapper automatically syncs with internal storage on access
return wrapper
# Return deep copy for mutable types to prevent external modification
if isinstance(stored, (dict, list)):
return copy.deepcopy(stored)
return stored
def __set__(self, instance, value):
# Prevent modification of read-only properties
if self.readonly:
raise AttributeError(f"Cannot set read-only property '{self.name}'")
# Type check if requestedNoneは常に許可
if self.type_ is not None and value is not None and not isinstance(value, self.type_):
return
# Allowed-values check: can be an iterable or a callable
if self.allowed is not None:
if callable(self.allowed):
try:
ok = self.allowed(value, instance)
except Exception:
ok = False
if not ok:
return
else:
if value not in self.allowed:
return
# Deep copy mutable types to prevent external reference issues
if isinstance(value, (dict, list)):
value = copy.deepcopy(value)
setattr(instance, self.private_name, value)
# Persist change
try:
if self.serialize:
instance.saveConfig(self.name, value, immediate_save=self.immediate_save)
except Exception:
# Keep setter robust during import-time initialization
pass
class ValidatedProperty:
"""Descriptor for complex validated properties.
validator(value, instance) -> normalized_value | None
If returns None (or raises), value is ignored.
"""
def __init__(self, name: str, validator, immediate_save: bool = False, serialize: bool = True):
self.name = name
self.validator = validator
self.immediate_save = immediate_save
self.serialize = serialize
self.private_name = f"_{name}"
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.private_name)
def __set__(self, instance, value):
try:
normalized = self.validator(value, instance)
except Exception:
return
if normalized is None:
return
setattr(instance, self.private_name, normalized)
try:
if self.serialize:
instance.saveConfig(self.name, normalized, immediate_save=self.immediate_save)
except Exception:
pass
# ============================================================================
# Validator Functions for ValidatedProperty
# ============================================================================
def _main_window_geometry_validator(val, inst):
if not (isinstance(val, dict) and set(val.keys()) == set(inst.MAIN_WINDOW_GEOMETRY.keys())):
return None
new = {}
for key, value in val.items():
if isinstance(value, int):
new[key] = value
else:
new[key] = inst.MAIN_WINDOW_GEOMETRY[key]
return new
def _selected_transcription_compute_type_validator(val, inst):
if not isinstance(val, str):
return None
compute_types = inst.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE.get("compute_types", [])
if val in compute_types:
return val
return None
def _overlay_small_validator(val, inst):
if not (isinstance(val, dict) and set(val.keys()) == set(inst.OVERLAY_SMALL_LOG_SETTINGS.keys())):
return None
base = inst.OVERLAY_SMALL_LOG_SETTINGS
new = dict(base)
for key, v in val.items():
if key == 'tracker' and isinstance(v, str) and v in ['HMD', 'LeftHand', 'RightHand']:
new[key] = v
elif key in ['x_pos','y_pos','z_pos','x_rotation','y_rotation','z_rotation'] and isinstance(v,(int,float)):
new[key] = float(v)
elif key in ['display_duration','fadeout_duration'] and isinstance(v,int):
new[key] = v
elif key in ['opacity','ui_scaling'] and isinstance(v,(int,float)):
new[key] = float(v)
return new
def _overlay_large_validator(val, inst):
if not (isinstance(val, dict) and set(val.keys()) == set(inst.OVERLAY_LARGE_LOG_SETTINGS.keys())):
return None
base = inst.OVERLAY_LARGE_LOG_SETTINGS
new = dict(base)
for key, v in val.items():
if key == 'tracker' and isinstance(v, str) and v in ['HMD', 'LeftHand', 'RightHand']:
new[key] = v
elif key in ['x_pos','y_pos','z_pos','x_rotation','y_rotation','z_rotation'] and isinstance(v,(int,float)):
new[key] = float(v)
elif key in ['display_duration','fadeout_duration'] and isinstance(v,int):
new[key] = v
elif key in ['opacity','ui_scaling'] and isinstance(v,(int,float)):
new[key] = float(v)
return new
def _format_validator_send(val, inst):
valid_parts = {
"message": {"prefix": str, "suffix": str},
"separator": str,
"translation": {"prefix": str, "separator": str, "suffix": str},
"translation_first": bool
}
if not isinstance(val, dict):
return None
return val if validateDictStructure(val, valid_parts) else None
def _format_validator_received(val, inst):
valid_parts = {
"message": {"prefix": str, "suffix": str},
"separator": str,
"translation": {"prefix": str, "separator": str, "suffix": str},
"translation_first": bool
}
if not isinstance(val, dict):
return None
return val if validateDictStructure(val, valid_parts) else None
def _mic_word_filter_validator(val, inst):
if not isinstance(val, list):
return None
seen = set()
result = []
for item in val:
if isinstance(item, str) and item not in seen:
seen.add(item)
result.append(item)
return result
def _plugins_status_validator(val, inst):
if not isinstance(val, list):
return None
if not all(isinstance(item, dict) for item in val):
return None
return [dict(item) for item in val]
def _selected_translation_engines_validator(val, inst):
if not isinstance(val, dict):
return None
old_value = inst.SELECTED_TRANSLATION_ENGINES
new = {}
for k, v in val.items():
if v in inst.SELECTABLE_TRANSLATION_ENGINE_LIST:
new[k] = v
else:
new[k] = old_value.get(k)
return new
def _selected_your_languages_validator(val, inst):
if not isinstance(val, dict):
return None
old = inst.SELECTED_YOUR_LANGUAGES
new = {}
for k0, v0 in val.items():
new[k0] = {}
for k1, v1 in v0.items():
language = v1.get("language")
country = v1.get("country")
enable = v1.get("enable")
if (language not in list(transcription_lang.keys()) or
country not in list(transcription_lang.get(language, {}).keys()) or
not isinstance(enable, bool)):
new[k0][k1] = old.get(k0, {}).get(k1)
else:
new[k0][k1] = {"language": language, "country": country, "enable": enable}
return new
def _selected_target_languages_validator(val, inst):
if not isinstance(val, dict):
return None
old = inst.SELECTED_TARGET_LANGUAGES
new = {}
for k0, v0 in val.items():
new[k0] = {}
for k1, v1 in v0.items():
language = v1.get("language")
country = v1.get("country")
enable = v1.get("enable")
if (language not in list(transcription_lang.keys()) or
country not in list(transcription_lang.get(language, {}).keys()) or
not isinstance(enable, bool)):
new[k0][k1] = old.get(k0, {}).get(k1)
else:
new[k0][k1] = {"language": language, "country": country, "enable": enable}
return new
def _selected_translation_compute_type_validator(val, inst):
if not isinstance(val, str):
return None
compute_types = inst.SELECTED_TRANSLATION_COMPUTE_DEVICE.get("compute_types", [])
if val in compute_types:
return val
return None
def _mic_host_validator(val, inst):
if device_manager is None:
return None
if not isinstance(val, str):
return None
hosts = list(device_manager.getMicDevices().keys())
return val if val in hosts else None
def _mic_device_validator(val, inst):
if device_manager is None:
return None
if not isinstance(val, str):
return None
try:
devices = device_manager.getMicDevices().get(inst.SELECTED_MIC_HOST, [])
names = [d.get('name') for d in devices]
return val if val in names else None
except Exception:
return None
def _speaker_device_validator(val, inst):
if device_manager is None:
return None
if not isinstance(val, str):
return None
try:
names = [d.get('name') for d in device_manager.getSpeakerDevices()]
return val if val in names else None
except Exception:
return None
def _compute_device_validator(val, inst):
if not isinstance(val, dict):
return None
for dev in inst.SELECTABLE_COMPUTE_DEVICE_LIST:
if dev == val:
return copy.deepcopy(val)
return None
def _allowed_in_populated(list_attr_name: str):
def _inner(value, inst):
try:
lst = getattr(inst, list_attr_name)
except Exception:
return True # インスタンス状態取得失敗時も弾かない
if not lst: # 空/未初期化
return True
if value is None:
return True
return value in lst
return _inner
class Config:
"""Application configuration singleton.
Responsibilities:
- expose read-only and read-write configuration via properties
- persist selected values to JSON with debounce
Implementation notes: initialization may depend on optional subsystems; any
exceptions during init/load are captured and logged to avoid import-time
crashes.
"""
_instance = None
_config_data: Dict[str, Any] = {}
_timer: Optional[threading.Timer] = None
_debounce_time: int = 2
def __new__(cls):
if cls._instance is None:
cls._instance = super(Config, cls).__new__(cls)
try:
cls._instance.init_config()
except Exception:
errorLogging()
try:
cls._instance.load_config()
except Exception:
errorLogging()
return cls._instance
def saveConfigToFile(self) -> None:
# 永続化対象を descriptor 情報 (json_serializable_vars) から再構成
filtered = {}
for var_name, var_func in json_serializable_vars.items():
try:
filtered[var_name] = var_func(self)
except Exception:
pass
self._config_data = filtered
with open(self.PATH_CONFIG, "w", encoding="utf-8") as fp:
json_dump(filtered, fp, indent=4, ensure_ascii=False)
def saveConfig(self, key: str, value: Any, immediate_save: bool = False) -> None:
self._config_data[key] = value
if isinstance(self._timer, threading.Timer) and self._timer.is_alive():
self._timer.cancel()
if immediate_save:
self.saveConfigToFile()
else:
self._timer = threading.Timer(self._debounce_time, self.saveConfigToFile)
self._timer.daemon = True
self._timer.start()
# Read Only
VERSION = ManagedProperty('VERSION', readonly=True, serialize=False)
PATH_LOCAL = ManagedProperty('PATH_LOCAL', readonly=True, serialize=False)
PATH_CONFIG = ManagedProperty('PATH_CONFIG', readonly=True, serialize=False)
PATH_LOGS = ManagedProperty('PATH_LOGS', readonly=True, serialize=False)
GITHUB_URL = ManagedProperty('GITHUB_URL', readonly=True, serialize=False)
UPDATER_URL = ManagedProperty('UPDATER_URL', readonly=True, serialize=False)
MAX_MIC_THRESHOLD = ManagedProperty('MAX_MIC_THRESHOLD', readonly=True, serialize=False)
MAX_SPEAKER_THRESHOLD = ManagedProperty('MAX_SPEAKER_THRESHOLD', readonly=True, serialize=False)
WATCHDOG_TIMEOUT = ManagedProperty('WATCHDOG_TIMEOUT', readonly=True, serialize=False)
WATCHDOG_INTERVAL = ManagedProperty('WATCHDOG_INTERVAL', readonly=True, serialize=False)
SELECTABLE_TAB_NO_LIST = ManagedProperty('SELECTABLE_TAB_NO_LIST', readonly=True, serialize=False)
SELECTED_TAB_TARGET_LANGUAGES_NO_LIST = ManagedProperty('SELECTED_TAB_TARGET_LANGUAGES_NO_LIST', readonly=True, serialize=False)
SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST = ManagedProperty('SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST', readonly=True, serialize=False)
SELECTABLE_WHISPER_WEIGHT_TYPE_LIST = ManagedProperty('SELECTABLE_WHISPER_WEIGHT_TYPE_LIST', readonly=True, serialize=False)
SELECTABLE_TRANSLATION_ENGINE_LIST = ManagedProperty('SELECTABLE_TRANSLATION_ENGINE_LIST', readonly=True, serialize=False)
SELECTABLE_TRANSCRIPTION_ENGINE_LIST = ManagedProperty('SELECTABLE_TRANSCRIPTION_ENGINE_LIST', readonly=True, serialize=False)
SELECTABLE_UI_LANGUAGE_LIST = ManagedProperty('SELECTABLE_UI_LANGUAGE_LIST', readonly=True, serialize=False)
COMPUTE_MODE = ManagedProperty('COMPUTE_MODE', readonly=True, serialize=False)
SELECTABLE_COMPUTE_DEVICE_LIST = ManagedProperty('SELECTABLE_COMPUTE_DEVICE_LIST', readonly=True, serialize=False)
SEND_MESSAGE_BUTTON_TYPE_LIST = ManagedProperty('SEND_MESSAGE_BUTTON_TYPE_LIST', readonly=True, serialize=False)
# Read Write
# --- Simple boolean flags (managed by descriptor) ---
ENABLE_TRANSLATION = ManagedProperty('ENABLE_TRANSLATION', type_=bool, serialize=False)
ENABLE_TRANSCRIPTION_SEND = ManagedProperty('ENABLE_TRANSCRIPTION_SEND', type_=bool, serialize=False)
ENABLE_TRANSCRIPTION_RECEIVE = ManagedProperty('ENABLE_TRANSCRIPTION_RECEIVE', type_=bool, serialize=False)
ENABLE_FOREGROUND = ManagedProperty('ENABLE_FOREGROUND', type_=bool, serialize=False)
ENABLE_CHECK_ENERGY_SEND = ManagedProperty('ENABLE_CHECK_ENERGY_SEND', type_=bool, serialize=False)
ENABLE_CHECK_ENERGY_RECEIVE = ManagedProperty('ENABLE_CHECK_ENERGY_RECEIVE', type_=bool, serialize=False)
# --- Selectable dict/list properties (managed by descriptor, not serialized) ---
# These are dynamically generated in init_config() based on installed packages/APIs
SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT = ManagedProperty('SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT', type_=dict, serialize=False, mutable_tracking=True)
SELECTABLE_WHISPER_WEIGHT_TYPE_DICT = ManagedProperty('SELECTABLE_WHISPER_WEIGHT_TYPE_DICT', type_=dict, serialize=False, mutable_tracking=True)
SELECTABLE_TRANSLATION_ENGINE_STATUS = ManagedProperty('SELECTABLE_TRANSLATION_ENGINE_STATUS', type_=dict, serialize=False, mutable_tracking=True)
SELECTABLE_TRANSCRIPTION_ENGINE_STATUS = ManagedProperty('SELECTABLE_TRANSCRIPTION_ENGINE_STATUS', type_=dict, serialize=False, mutable_tracking=True)
SELECTABLE_PLAMO_MODEL_LIST = ManagedProperty('SELECTABLE_PLAMO_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True)
SELECTABLE_GEMINI_MODEL_LIST = ManagedProperty('SELECTABLE_GEMINI_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True)
SELECTABLE_OPENAI_MODEL_LIST = ManagedProperty('SELECTABLE_OPENAI_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True)
SELECTABLE_GROQ_MODEL_LIST = ManagedProperty('SELECTABLE_GROQ_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True)
SELECTABLE_OPENROUTER_MODEL_LIST = ManagedProperty('SELECTABLE_OPENROUTER_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True)
SELECTABLE_LMSTUDIO_MODEL_LIST = ManagedProperty('SELECTABLE_LMSTUDIO_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True)
SELECTABLE_OLLAMA_MODEL_LIST = ManagedProperty('SELECTABLE_OLLAMA_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True)
# --- Save Json Data (ManagedProperty-based) ---
# More simple boolean flags replaced with ManagedProperty
CONVERT_MESSAGE_TO_ROMAJI = ManagedProperty('CONVERT_MESSAGE_TO_ROMAJI', type_=bool)
CONVERT_MESSAGE_TO_HIRAGANA = ManagedProperty('CONVERT_MESSAGE_TO_HIRAGANA', type_=bool)
MAIN_WINDOW_SIDEBAR_COMPACT_MODE = ManagedProperty('MAIN_WINDOW_SIDEBAR_COMPACT_MODE', type_=bool)
## Config Window
TRANSPARENCY = ManagedProperty('TRANSPARENCY', type_=int)
UI_SCALING = ManagedProperty('UI_SCALING', type_=int)
TEXTBOX_UI_SCALING = ManagedProperty('TEXTBOX_UI_SCALING', type_=int)
MESSAGE_BOX_RATIO = ManagedProperty('MESSAGE_BOX_RATIO', type_=(int, float), immediate_save=True)
SEND_MESSAGE_BUTTON_TYPE = ManagedProperty('SEND_MESSAGE_BUTTON_TYPE', type_=str, allowed=lambda v, inst: v in inst.SEND_MESSAGE_BUTTON_TYPE_LIST)
SHOW_RESEND_BUTTON = ManagedProperty('SHOW_RESEND_BUTTON', type_=bool)
FONT_FAMILY = ManagedProperty('FONT_FAMILY', type_=str)
UI_LANGUAGE = ManagedProperty('UI_LANGUAGE', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_UI_LANGUAGE_LIST)
MAIN_WINDOW_GEOMETRY = ValidatedProperty('MAIN_WINDOW_GEOMETRY', _main_window_geometry_validator, immediate_save=True)
# --- Mic-related simple properties ---
MIC_THRESHOLD = ManagedProperty('MIC_THRESHOLD', type_=int)
MIC_AUTOMATIC_THRESHOLD = ManagedProperty('MIC_AUTOMATIC_THRESHOLD', type_=bool)
MIC_RECORD_TIMEOUT = ManagedProperty('MIC_RECORD_TIMEOUT', type_=int)
MIC_PHRASE_TIMEOUT = ManagedProperty('MIC_PHRASE_TIMEOUT', type_=int)
MIC_MAX_PHRASES = ManagedProperty('MIC_MAX_PHRASES', type_=int)
MIC_AVG_LOGPROB = ManagedProperty('MIC_AVG_LOGPROB', type_=(int, float))
MIC_NO_SPEECH_PROB = ManagedProperty('MIC_NO_SPEECH_PROB', type_=(int, float))
MIC_NO_REPEAT_NGRAM_SIZE = ManagedProperty('MIC_NO_REPEAT_NGRAM_SIZE', type_=int)
MIC_VAD_FILTER = ManagedProperty('MIC_VAD_FILTER', type_=bool)
MIC_VAD_PARAMETERS = ManagedProperty('MIC_VAD_PARAMETERS', type_=dict, mutable_tracking=True)
HOTKEYS = ValidatedProperty('HOTKEYS',
validator=lambda val, inst: (
{k: (v if (isinstance(v, list) or v is None) else inst.HOTKEYS.get(k))
for k, v in val.items()} if isinstance(val, dict) and set(val.keys()) == set(inst.HOTKEYS.keys()) else None
),
immediate_save=True
)
# --- Speaker-related simple properties ---
SPEAKER_THRESHOLD = ManagedProperty('SPEAKER_THRESHOLD', type_=int)
SPEAKER_AUTOMATIC_THRESHOLD = ManagedProperty('SPEAKER_AUTOMATIC_THRESHOLD', type_=bool)
SPEAKER_RECORD_TIMEOUT = ManagedProperty('SPEAKER_RECORD_TIMEOUT', type_=int)
SPEAKER_PHRASE_TIMEOUT = ManagedProperty('SPEAKER_PHRASE_TIMEOUT', type_=int)
SPEAKER_MAX_PHRASES = ManagedProperty('SPEAKER_MAX_PHRASES', type_=int)
SPEAKER_AVG_LOGPROB = ManagedProperty('SPEAKER_AVG_LOGPROB', type_=(int, float))
SPEAKER_NO_SPEECH_PROB = ManagedProperty('SPEAKER_NO_SPEECH_PROB', type_=(int, float))
SPEAKER_NO_REPEAT_NGRAM_SIZE = ManagedProperty('SPEAKER_NO_REPEAT_NGRAM_SIZE', type_=int)
SPEAKER_VAD_FILTER = ManagedProperty('SPEAKER_VAD_FILTER', type_=bool)
SPEAKER_VAD_PARAMETERS = ManagedProperty('SPEAKER_VAD_PARAMETERS', type_=dict, mutable_tracking=True)
# --- Auth and API settings ---
AUTH_KEYS = ValidatedProperty('AUTH_KEYS',
validator=lambda val, inst: (
{k: (v if isinstance(v, str) else inst.AUTH_KEYS.get(k)) for k, v in val.items()}
if isinstance(val, dict) and set(val.keys()) == set(inst.AUTH_KEYS.keys()) else None
)
)
LMSTUDIO_URL = ManagedProperty('LMSTUDIO_URL', type_=str)
# --- Transcription settings ---
SELECTED_TRANSCRIPTION_COMPUTE_TYPE = ValidatedProperty('SELECTED_TRANSCRIPTION_COMPUTE_TYPE', _selected_transcription_compute_type_validator)
# --- Overlay settings ---
OVERLAY_SMALL_LOG_SETTINGS = ValidatedProperty('OVERLAY_SMALL_LOG_SETTINGS', _overlay_small_validator)
OVERLAY_LARGE_LOG_SETTINGS = ValidatedProperty('OVERLAY_LARGE_LOG_SETTINGS', _overlay_large_validator)
# --- Message format settings ---
SEND_MESSAGE_FORMAT_PARTS = ValidatedProperty('SEND_MESSAGE_FORMAT_PARTS', _format_validator_send)
RECEIVED_MESSAGE_FORMAT_PARTS = ValidatedProperty('RECEIVED_MESSAGE_FORMAT_PARTS', _format_validator_received)
# Convert remaining simple properties to ManagedProperty to reduce repetition
WEBSOCKET_SERVER = ManagedProperty('WEBSOCKET_SERVER', type_=bool)
OSC_IP_ADDRESS = ManagedProperty('OSC_IP_ADDRESS', type_=str)
OSC_PORT = ManagedProperty('OSC_PORT', type_=int)
AUTO_CLEAR_MESSAGE_BOX = ManagedProperty('AUTO_CLEAR_MESSAGE_BOX', type_=bool)
SEND_ONLY_TRANSLATED_MESSAGES = ManagedProperty('SEND_ONLY_TRANSLATED_MESSAGES', type_=bool)
OVERLAY_SMALL_LOG = ManagedProperty('OVERLAY_SMALL_LOG', type_=bool)
OVERLAY_LARGE_LOG = ManagedProperty('OVERLAY_LARGE_LOG', type_=bool)
OVERLAY_SHOW_ONLY_TRANSLATED_MESSAGES = ManagedProperty('OVERLAY_SHOW_ONLY_TRANSLATED_MESSAGES', type_=bool)
SEND_MESSAGE_TO_VRC = ManagedProperty('SEND_MESSAGE_TO_VRC', type_=bool)
SEND_RECEIVED_MESSAGE_TO_VRC = ManagedProperty('SEND_RECEIVED_MESSAGE_TO_VRC', type_=bool)
LOGGER_FEATURE = ManagedProperty('LOGGER_FEATURE', type_=bool)
VRC_MIC_MUTE_SYNC = ManagedProperty('VRC_MIC_MUTE_SYNC', type_=bool)
NOTIFICATION_VRC_SFX = ManagedProperty('NOTIFICATION_VRC_SFX', type_=bool)
WEBSOCKET_HOST = ManagedProperty('WEBSOCKET_HOST', type_=str)
WEBSOCKET_PORT = ManagedProperty('WEBSOCKET_PORT', type_=int)
# --- Telemetry Settings ---
ENABLE_TELEMETRY = ManagedProperty('ENABLE_TELEMETRY', type_=bool)
# --- Selection properties with validation (ManagedProperty) ---
SELECTED_TAB_NO = ManagedProperty('SELECTED_TAB_NO', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_TAB_NO_LIST)
SELECTED_TRANSCRIPTION_ENGINE = ManagedProperty('SELECTED_TRANSCRIPTION_ENGINE', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_TRANSCRIPTION_ENGINE_LIST)
USE_EXCLUDE_WORDS = ManagedProperty('USE_EXCLUDE_WORDS', type_=bool)
CTRANSLATE2_WEIGHT_TYPE = ManagedProperty('CTRANSLATE2_WEIGHT_TYPE', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST)
WHISPER_WEIGHT_TYPE = ManagedProperty('WHISPER_WEIGHT_TYPE', type_=str, allowed=lambda v, inst: v in inst.SELECTABLE_WHISPER_WEIGHT_TYPE_LIST)
SELECTED_PLAMO_MODEL = ManagedProperty('SELECTED_PLAMO_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_PLAMO_MODEL_LIST'))
SELECTED_GEMINI_MODEL = ManagedProperty('SELECTED_GEMINI_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_GEMINI_MODEL_LIST'))
SELECTED_OPENAI_MODEL = ManagedProperty('SELECTED_OPENAI_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_OPENAI_MODEL_LIST'))
SELECTED_GROQ_MODEL = ManagedProperty('SELECTED_GROQ_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_GROQ_MODEL_LIST'))
SELECTED_OPENROUTER_MODEL = ManagedProperty('SELECTED_OPENROUTER_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_OPENROUTER_MODEL_LIST'))
SELECTED_LMSTUDIO_MODEL = ManagedProperty('SELECTED_LMSTUDIO_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_LMSTUDIO_MODEL_LIST'))
SELECTED_OLLAMA_MODEL = ManagedProperty('SELECTED_OLLAMA_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_OLLAMA_MODEL_LIST'))
# --- Translation and language settings ---
MIC_WORD_FILTER = ValidatedProperty('MIC_WORD_FILTER', _mic_word_filter_validator)
PLUGINS_STATUS = ValidatedProperty('PLUGINS_STATUS', _plugins_status_validator, immediate_save=True)
SELECTED_TRANSLATION_ENGINES = ValidatedProperty('SELECTED_TRANSLATION_ENGINES', _selected_translation_engines_validator)
SELECTED_YOUR_LANGUAGES = ValidatedProperty('SELECTED_YOUR_LANGUAGES', _selected_your_languages_validator)
SELECTED_TARGET_LANGUAGES = ValidatedProperty('SELECTED_TARGET_LANGUAGES', _selected_target_languages_validator)
SELECTED_TRANSLATION_COMPUTE_TYPE = ValidatedProperty('SELECTED_TRANSLATION_COMPUTE_TYPE', _selected_translation_compute_type_validator)
# --- Device settings ---
AUTO_MIC_SELECT = ManagedProperty('AUTO_MIC_SELECT', type_=bool)
AUTO_SPEAKER_SELECT = ManagedProperty('AUTO_SPEAKER_SELECT', type_=bool)
SELECTED_MIC_HOST = ValidatedProperty('SELECTED_MIC_HOST', _mic_host_validator)
SELECTED_MIC_DEVICE = ValidatedProperty('SELECTED_MIC_DEVICE', _mic_device_validator)
SELECTED_SPEAKER_DEVICE = ValidatedProperty('SELECTED_SPEAKER_DEVICE', _speaker_device_validator)
SELECTED_TRANSLATION_COMPUTE_DEVICE = ValidatedProperty('SELECTED_TRANSLATION_COMPUTE_DEVICE', _compute_device_validator)
SELECTED_TRANSCRIPTION_COMPUTE_DEVICE = ValidatedProperty('SELECTED_TRANSCRIPTION_COMPUTE_DEVICE', _compute_device_validator)
def init_config(self):
# Read Only
self._VERSION = "3.3.2"
if getattr(sys, 'frozen', False):
self._PATH_LOCAL = os_path.dirname(sys.executable)
else:
self._PATH_LOCAL = os_path.dirname(os_path.abspath(__file__))
self._PATH_CONFIG = os_path.join(self._PATH_LOCAL, "config.json")
self._PATH_LOGS = os_path.join(self._PATH_LOCAL, "logs")
os_makedirs(self._PATH_LOGS, exist_ok=True)
self._GITHUB_URL = "https://api.github.com/repos/misyaguziya/VRCT/releases/latest"
self._UPDATER_URL = "https://api.github.com/repos/misyaguziya/VRCT_updater/releases/latest"
self._MAX_MIC_THRESHOLD = 2000
self._MAX_SPEAKER_THRESHOLD = 4000
self._WATCHDOG_TIMEOUT = 60
self._WATCHDOG_INTERVAL = 20
self._SELECTABLE_TAB_NO_LIST = ["1", "2", "3"]
# these external mappings may be empty dicts if the optional modules failed to import
self._SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST = getattr(ctranslate2_weights, 'keys', lambda: [])()
self._SELECTABLE_WHISPER_WEIGHT_TYPE_LIST = getattr(whisper_models, 'keys', lambda: [])()
translation_lang = loadTranslationLanguages(self.PATH_LOCAL)
self._SELECTABLE_TRANSLATION_ENGINE_LIST = getattr(translation_lang, 'keys', lambda: [])()
try:
# transcription_lang is nested dict; attempt to extract keys defensively
first_key = next(iter(transcription_lang))
self._SELECTABLE_TRANSCRIPTION_ENGINE_LIST = list(transcription_lang[first_key].values())[0].keys()
except Exception:
self._SELECTABLE_TRANSCRIPTION_ENGINE_LIST = []
self._SELECTABLE_UI_LANGUAGE_LIST = ["en", "ja", "ko", "zh-Hant", "zh-Hans"]
self._COMPUTE_MODE = "cuda" if torch.cuda.is_available() else "cpu"
self._SELECTABLE_COMPUTE_DEVICE_LIST = getComputeDeviceList()
self._SEND_MESSAGE_BUTTON_TYPE_LIST = ["show", "hide", "show_and_disable_enter_key"]
# Read Write
self._ENABLE_TRANSLATION = False
self._ENABLE_TRANSCRIPTION_SEND = False
self._ENABLE_TRANSCRIPTION_RECEIVE = False
self._ENABLE_FOREGROUND = False
self._ENABLE_CHECK_ENERGY_SEND = False
self._ENABLE_CHECK_ENERGY_RECEIVE = False
self._SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT = {}
for weight_type in self.SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST:
self._SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT[weight_type] = False
self._SELECTABLE_WHISPER_WEIGHT_TYPE_DICT = {}
for weight_type in self.SELECTABLE_WHISPER_WEIGHT_TYPE_LIST:
self._SELECTABLE_WHISPER_WEIGHT_TYPE_DICT[weight_type] = False
self._SELECTABLE_TRANSLATION_ENGINE_STATUS = {}
for engine in self.SELECTABLE_TRANSLATION_ENGINE_LIST:
self._SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
self._SELECTABLE_TRANSCRIPTION_ENGINE_STATUS = {}
for engine in self.SELECTABLE_TRANSCRIPTION_ENGINE_LIST:
self._SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = False
self._SELECTABLE_PLAMO_MODEL_LIST = []
self._SELECTABLE_GEMINI_MODEL_LIST = []
self._SELECTABLE_OPENAI_MODEL_LIST = []
self._SELECTABLE_GROQ_MODEL_LIST = []
self._SELECTABLE_OPENROUTER_MODEL_LIST = []
self._SELECTABLE_LMSTUDIO_MODEL_LIST = []
self._SELECTABLE_OLLAMA_MODEL_LIST = []
# Save Json Data
## Main Window
self._SELECTED_TAB_NO = "1"
self._SELECTED_TRANSLATION_ENGINES = {}
for tab_no in self.SELECTABLE_TAB_NO_LIST:
self._SELECTED_TRANSLATION_ENGINES[tab_no] = "CTranslate2"
self._SELECTED_YOUR_LANGUAGES = {}
for tab_no in self.SELECTABLE_TAB_NO_LIST:
self._SELECTED_YOUR_LANGUAGES[tab_no] = {
"1": {
"language": "Japanese",
"country": "Japan",
"enable": True,
},
}
self._SELECTED_TARGET_LANGUAGES = {}
self._SELECTED_TAB_TARGET_LANGUAGES_NO_LIST = ["1", "2", "3"]
for tab_no in self.SELECTABLE_TAB_NO_LIST:
for tab_target_lang_no in self.SELECTED_TAB_TARGET_LANGUAGES_NO_LIST:
if tab_no not in self.SELECTED_TARGET_LANGUAGES:
self._SELECTED_TARGET_LANGUAGES[tab_no] = {}
if tab_target_lang_no not in self.SELECTED_TARGET_LANGUAGES[tab_no]:
self._SELECTED_TARGET_LANGUAGES[tab_no][tab_target_lang_no] = {
"language": "English",
"country": "United States",
"enable": True if tab_target_lang_no == self.SELECTED_TAB_TARGET_LANGUAGES_NO_LIST[0] else False,
}
self._SELECTED_TRANSCRIPTION_ENGINE = "Google"
self._CONVERT_MESSAGE_TO_ROMAJI = False
self._CONVERT_MESSAGE_TO_HIRAGANA = False
self._MAIN_WINDOW_SIDEBAR_COMPACT_MODE = False
## Config Window
self._TRANSPARENCY = 100
self._UI_SCALING = 100
self._TEXTBOX_UI_SCALING = 100
self._MESSAGE_BOX_RATIO = 10
self._SEND_MESSAGE_BUTTON_TYPE = "show"
self._SHOW_RESEND_BUTTON = False
self._FONT_FAMILY = "Yu Gothic UI"
self._UI_LANGUAGE = "en"
self._MAIN_WINDOW_GEOMETRY = {
"x_pos": 0,
"y_pos": 0,
"width": 870,
"height": 654,
}
self._AUTO_MIC_SELECT = True
# device_manager may be unavailable or not initialized; use safe defaults
try:
if device_manager is not None:
# getDefaultMicDevice performs lazy init/update if needed
dm_def = device_manager.getDefaultMicDevice()
self._SELECTED_MIC_HOST = dm_def.get("host", {}).get("name", "NoHost")
self._SELECTED_MIC_DEVICE = dm_def.get("device", {}).get("name", "NoDevice")
else:
self._SELECTED_MIC_HOST = "NoHost"
self._SELECTED_MIC_DEVICE = "NoDevice"
except Exception:
errorLogging()
self._SELECTED_MIC_HOST = "NoHost"
self._SELECTED_MIC_DEVICE = "NoDevice"
self._MIC_THRESHOLD = 300
self._MIC_AUTOMATIC_THRESHOLD = False
self._MIC_RECORD_TIMEOUT = 3
self._MIC_PHRASE_TIMEOUT = 3
self._MIC_MAX_PHRASES = 10
self._MIC_WORD_FILTER = []
self._HOTKEYS = {
"toggle_vrct_visibility": None,
"toggle_translation": None,
"toggle_transcription_send": None,
"toggle_transcription_receive": None,
}
self._PLUGINS_STATUS = []
self._MIC_AVG_LOGPROB = -0.8
self._MIC_NO_SPEECH_PROB = 0.6
self._MIC_NO_REPEAT_NGRAM_SIZE = 0
self._MIC_VAD_FILTER = False
self._MIC_VAD_PARAMETERS = {
"threshold": 0.5,
"neg_threshold": None,
"min_speech_duration_ms": 0,
"max_speech_duration_s": float("inf"),
"min_silence_duration_ms": 2000,
"speech_pad_ms": 400,
}
self._AUTO_SPEAKER_SELECT = True
try:
if device_manager is not None:
sp_def = device_manager.getDefaultSpeakerDevice()
self._SELECTED_SPEAKER_DEVICE = sp_def.get("device", {}).get("name", "NoDevice")
else:
self._SELECTED_SPEAKER_DEVICE = "NoDevice"
except Exception:
errorLogging()
self._SELECTED_SPEAKER_DEVICE = "NoDevice"
self._SPEAKER_THRESHOLD = 300
self._SPEAKER_AUTOMATIC_THRESHOLD = False
self._SPEAKER_RECORD_TIMEOUT = 3
self._SPEAKER_PHRASE_TIMEOUT = 3
self._SPEAKER_MAX_PHRASES = 10
self._SPEAKER_AVG_LOGPROB = -0.8
self._SPEAKER_NO_SPEECH_PROB = 0.6
self._SPEAKER_NO_REPEAT_NGRAM_SIZE = 0
self._SPEAKER_VAD_FILTER = False
self._SPEAKER_VAD_PARAMETERS = {
"threshold": 0.5,
"neg_threshold": None,
"min_speech_duration_ms": 0,
"max_speech_duration_s": float("inf"),
"min_silence_duration_ms": 2000,
"speech_pad_ms": 400,
}
self._OSC_IP_ADDRESS = "127.0.0.1"
self._OSC_PORT = 9000
self._AUTH_KEYS = {
"DeepL_API": None,
"Plamo_API": None,
"Gemini_API": None,
"OpenAI_API": None,
"Groq_API": None,
"OpenRouter_API": None,
}
self._USE_EXCLUDE_WORDS = True
self._SELECTED_TRANSLATION_COMPUTE_DEVICE = copy.deepcopy(self.SELECTABLE_COMPUTE_DEVICE_LIST[0])
self._SELECTED_TRANSCRIPTION_COMPUTE_DEVICE = copy.deepcopy(self.SELECTABLE_COMPUTE_DEVICE_LIST[0])
self._CTRANSLATE2_WEIGHT_TYPE = "m2m100_418M-ct2-int8"
self._SELECTED_PLAMO_MODEL = None
self._SELECTED_GEMINI_MODEL = None
self._SELECTED_OPENAI_MODEL = None
self._SELECTED_GROQ_MODEL = None
self._SELECTED_OPENROUTER_MODEL = None
self._LMSTUDIO_URL = "http://127.0.0.1:1234/v1"
self._SELECTED_LMSTUDIO_MODEL = None
self._SELECTED_OLLAMA_MODEL = None
self._SELECTED_TRANSLATION_COMPUTE_TYPE = "auto"
self._WHISPER_WEIGHT_TYPE = "base"
self._SELECTED_TRANSCRIPTION_COMPUTE_TYPE = "auto"
self._AUTO_CLEAR_MESSAGE_BOX = True
self._SEND_ONLY_TRANSLATED_MESSAGES = False
self._OVERLAY_SMALL_LOG = False
self._OVERLAY_SMALL_LOG_SETTINGS = {
"x_pos": 0.0,
"y_pos": 0.0,
"z_pos": 0.0,
"x_rotation": 0.0,
"y_rotation": 0.0,
"z_rotation": 0.0,
"display_duration": 5,
"fadeout_duration": 2,
"opacity": 1.0,
"ui_scaling": 1.0,
"tracker": "HMD",
}
self._OVERLAY_LARGE_LOG = False
self._OVERLAY_LARGE_LOG_SETTINGS = {
"x_pos": 0.0,
"y_pos": 0.0,
"z_pos": 0.0,
"x_rotation": 0.0,
"y_rotation": 0.0,
"z_rotation": 0.0,
"display_duration": 5,
"fadeout_duration": 2,
"opacity": 1.0,
"ui_scaling": 1.0,
"tracker": "LeftHand",
}
self._OVERLAY_SHOW_ONLY_TRANSLATED_MESSAGES = False
self._SEND_MESSAGE_TO_VRC = True
self._SEND_RECEIVED_MESSAGE_TO_VRC = False
self._LOGGER_FEATURE = False
self._VRC_MIC_MUTE_SYNC = False
self._NOTIFICATION_VRC_SFX = True
self._SEND_MESSAGE_FORMAT_PARTS = {
"message": {
"prefix": "",
"suffix": ""
},
"separator": "\n",
"translation": {
"prefix": "",
"separator": "\n",
"suffix": ""
},
"translation_first": False,
}
self._RECEIVED_MESSAGE_FORMAT_PARTS = {
"message": {
"prefix": "",
"suffix": ""
},
"separator": "\n",
"translation": {
"prefix": "",
"separator": "\n",
"suffix": ""
},
"translation_first": False,
}
self._WEBSOCKET_SERVER = False
self._WEBSOCKET_HOST = "127.0.0.1"
self._WEBSOCKET_PORT = 2231
## Telemetry
self._ENABLE_TELEMETRY = True # デフォルト有効
def load_config(self):
if os_path.isfile(self.PATH_CONFIG) is not False:
with open(self.PATH_CONFIG, 'r', encoding="utf-8") as fp:
if fp.readable() and fp.seek(0, 2) > 0:
fp.seek(0)
self._config_data = json_load(fp)
for key, value in self._config_data.items():
# 読み込み時: serialize=True かつ readonlyでない Descriptor のみ反映。
# 未知キーDescriptorなしは無視して注入を防止。
try:
descriptor = getattr(type(self), key, None)
if isinstance(descriptor, ManagedProperty):
if descriptor.readonly or not descriptor.serialize:
continue
setattr(self, key, value)
elif isinstance(descriptor, ValidatedProperty):
if not descriptor.serialize:
continue
setattr(self, key, value)
else:
# 不明キーは破棄(古い/不要/改竄の可能性)
continue
except Exception:
errorLogging()
self.saveConfigToFile()
def revalidate_selected_models(self):
pairs = [
('SELECTED_PLAMO_MODEL', 'SELECTABLE_PLAMO_MODEL_LIST'),
('SELECTED_GEMINI_MODEL', 'SELECTABLE_GEMINI_MODEL_LIST'),
('SELECTED_OPENAI_MODEL', 'SELECTABLE_OPENAI_MODEL_LIST'),
('SELECTED_GROQ_MODEL', 'SELECTABLE_GROQ_MODEL_LIST'),
('SELECTED_OPENROUTER_MODEL', 'SELECTABLE_OPENROUTER_MODEL_LIST'),
('SELECTED_LMSTUDIO_MODEL', 'SELECTABLE_LMSTUDIO_MODEL_LIST'),
('SELECTED_OLLAMA_MODEL', 'SELECTABLE_OLLAMA_MODEL_LIST'),
]
for sel_attr, list_attr in pairs:
try:
current = getattr(self, sel_attr)
lst = getattr(self, list_attr)
if lst and current is not None and current not in lst:
if len(lst) > 0:
setattr(self, sel_attr, lst[0])
else:
setattr(self, sel_attr, None)
except Exception:
errorLogging()
# Auto-register all descriptors after Config class definition
_auto_register_descriptors()
config = Config()
if __name__ == "__main__":
print("Test config.py")
for key, value in config._config_data.items():
print(f"{key}: {value}")