Refine config and runtime flow

This commit is contained in:
Thales Maciel 2026-02-24 14:15:17 -03:00
parent 85e082dd46
commit b3be444625
No known key found for this signature in database
GPG key ID: 33112E6833C34679
16 changed files with 642 additions and 137 deletions

View file

@ -1,16 +1,59 @@
from __future__ import annotations
import json
from dataclasses import dataclass, field
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any
from constants import DEFAULT_CONFIG_PATH
DEFAULT_HOTKEY = "Cmd+m"
DEFAULT_STT_MODEL = "base"
DEFAULT_STT_DEVICE = "cpu"
DEFAULT_INJECTION_BACKEND = "clipboard"
ALLOWED_INJECTION_BACKENDS = {"clipboard", "injection"}
@dataclass
class DaemonConfig:
hotkey: str = DEFAULT_HOTKEY
@dataclass
class RecordingConfig:
input: str | int | None = ""
@dataclass
class SttConfig:
model: str = DEFAULT_STT_MODEL
device: str = DEFAULT_STT_DEVICE
@dataclass
class InjectionConfig:
backend: str = DEFAULT_INJECTION_BACKEND
@dataclass
class AiConfig:
enabled: bool = True
@dataclass
class LoggingConfig:
log_transcript: bool = False
@dataclass
class Config:
daemon: dict = field(default_factory=lambda: {"hotkey": "Cmd+m"})
recording: dict = field(default_factory=lambda: {"input": ""})
stt: dict = field(default_factory=lambda: {"model": "base", "device": "cpu"})
injection: dict = field(default_factory=lambda: {"backend": "clipboard"})
daemon: DaemonConfig = field(default_factory=DaemonConfig)
recording: RecordingConfig = field(default_factory=RecordingConfig)
stt: SttConfig = field(default_factory=SttConfig)
injection: InjectionConfig = field(default_factory=InjectionConfig)
ai: AiConfig = field(default_factory=AiConfig)
logging: LoggingConfig = field(default_factory=LoggingConfig)
def load(path: str | None) -> Config:
@ -18,33 +61,120 @@ def load(path: str | None) -> Config:
p = Path(path) if path else DEFAULT_CONFIG_PATH
if p.exists():
data = json.loads(p.read_text(encoding="utf-8"))
if any(k in data for k in ("daemon", "recording", "stt", "injection")):
for k, v in data.items():
if hasattr(cfg, k):
setattr(cfg, k, v)
else:
cfg.daemon["hotkey"] = data.get("hotkey", cfg.daemon["hotkey"])
cfg.recording["input"] = data.get("input", cfg.recording["input"])
cfg.stt["model"] = data.get("whisper_model", cfg.stt["model"])
cfg.stt["device"] = data.get("whisper_device", cfg.stt["device"])
cfg.injection["backend"] = data.get("injection_backend", cfg.injection["backend"])
if not isinstance(cfg.daemon, dict):
cfg.daemon = {"hotkey": "Cmd+m"}
if not isinstance(cfg.recording, dict):
cfg.recording = {"input": ""}
if not isinstance(cfg.stt, dict):
cfg.stt = {"model": "base", "device": "cpu"}
if not isinstance(cfg.injection, dict):
cfg.injection = {"backend": "clipboard"}
if not isinstance(data, dict):
raise ValueError("config must be a JSON object")
cfg = _from_dict(data, cfg)
validate(cfg)
return cfg
def redacted_dict(cfg: Config) -> dict:
return cfg.__dict__.copy()
def redacted_dict(cfg: Config) -> dict[str, Any]:
return asdict(cfg)
def validate(cfg: Config) -> None:
if not cfg.daemon.get("hotkey"):
hotkey = cfg.daemon.hotkey.strip()
if not hotkey:
raise ValueError("daemon.hotkey cannot be empty")
if isinstance(cfg.recording.input, bool):
raise ValueError("recording.input cannot be boolean")
if not isinstance(cfg.recording.input, (str, int)) and cfg.recording.input is not None:
raise ValueError("recording.input must be string, integer, or null")
model = cfg.stt.model.strip()
if not model:
raise ValueError("stt.model cannot be empty")
device = cfg.stt.device.strip()
if not device:
raise ValueError("stt.device cannot be empty")
backend = cfg.injection.backend.strip().lower()
if backend not in ALLOWED_INJECTION_BACKENDS:
allowed = ", ".join(sorted(ALLOWED_INJECTION_BACKENDS))
raise ValueError(f"injection.backend must be one of: {allowed}")
cfg.injection.backend = backend
if not isinstance(cfg.ai.enabled, bool):
raise ValueError("ai.enabled must be boolean")
if not isinstance(cfg.logging.log_transcript, bool):
raise ValueError("logging.log_transcript must be boolean")
def _from_dict(data: dict[str, Any], cfg: Config) -> Config:
has_sections = any(
key in data for key in ("daemon", "recording", "stt", "injection", "ai", "logging")
)
if has_sections:
daemon = _ensure_dict(data.get("daemon"), "daemon")
recording = _ensure_dict(data.get("recording"), "recording")
stt = _ensure_dict(data.get("stt"), "stt")
injection = _ensure_dict(data.get("injection"), "injection")
ai = _ensure_dict(data.get("ai"), "ai")
logging_cfg = _ensure_dict(data.get("logging"), "logging")
if "hotkey" in daemon:
cfg.daemon.hotkey = _as_nonempty_str(daemon["hotkey"], "daemon.hotkey")
if "input" in recording:
cfg.recording.input = _as_recording_input(recording["input"])
if "model" in stt:
cfg.stt.model = _as_nonempty_str(stt["model"], "stt.model")
if "device" in stt:
cfg.stt.device = _as_nonempty_str(stt["device"], "stt.device")
if "backend" in injection:
cfg.injection.backend = _as_nonempty_str(injection["backend"], "injection.backend")
if "enabled" in ai:
cfg.ai.enabled = _as_bool(ai["enabled"], "ai.enabled")
if "log_transcript" in logging_cfg:
cfg.logging.log_transcript = _as_bool(logging_cfg["log_transcript"], "logging.log_transcript")
return cfg
if "hotkey" in data:
cfg.daemon.hotkey = _as_nonempty_str(data["hotkey"], "hotkey")
if "input" in data:
cfg.recording.input = _as_recording_input(data["input"])
if "whisper_model" in data:
cfg.stt.model = _as_nonempty_str(data["whisper_model"], "whisper_model")
if "whisper_device" in data:
cfg.stt.device = _as_nonempty_str(data["whisper_device"], "whisper_device")
if "injection_backend" in data:
cfg.injection.backend = _as_nonempty_str(data["injection_backend"], "injection_backend")
if "ai_enabled" in data:
cfg.ai.enabled = _as_bool(data["ai_enabled"], "ai_enabled")
if "log_transcript" in data:
cfg.logging.log_transcript = _as_bool(data["log_transcript"], "log_transcript")
return cfg
def _ensure_dict(value: Any, field_name: str) -> dict[str, Any]:
if value is None:
return {}
if not isinstance(value, dict):
raise ValueError(f"{field_name} must be an object")
return value
def _as_nonempty_str(value: Any, field_name: str) -> str:
if not isinstance(value, str):
raise ValueError(f"{field_name} must be a string")
if not value.strip():
raise ValueError(f"{field_name} cannot be empty")
return value
def _as_bool(value: Any, field_name: str) -> bool:
if not isinstance(value, bool):
raise ValueError(f"{field_name} must be boolean")
return value
def _as_recording_input(value: Any) -> str | int | None:
if value is None:
return None
if isinstance(value, bool):
raise ValueError("recording.input cannot be boolean")
if isinstance(value, (str, int)):
return value
raise ValueError("recording.input must be string, integer, or null")