Use base URL for chat completions
This commit is contained in:
parent
a0c3b02ab1
commit
ad66a0d3cb
4 changed files with 320 additions and 493 deletions
194
src/config.py
194
src/config.py
|
|
@ -10,43 +10,21 @@ def _parse_bool(val: str) -> bool:
|
|||
|
||||
@dataclass
|
||||
class Config:
|
||||
hotkey: str = "Cmd+m"
|
||||
edit_hotkey: str = "Cmd+n"
|
||||
ffmpeg_input: str = "pulse:default"
|
||||
ffmpeg_path: str = ""
|
||||
|
||||
whisper_model: str = "base"
|
||||
whisper_lang: str = "en"
|
||||
whisper_device: str = "cpu"
|
||||
|
||||
record_timeout_sec: int = 120
|
||||
edit_record_timeout_sec: int = 120
|
||||
|
||||
injection_backend: str = "clipboard"
|
||||
edit_injection_backend: str = "clipboard"
|
||||
|
||||
ai_enabled: bool = False
|
||||
ai_model: str = "llama3.2:3b"
|
||||
ai_temperature: float = 0.0
|
||||
ai_system_prompt_file: str = ""
|
||||
ai_base_url: str = "http://localhost:11434/v1/chat/completions"
|
||||
ai_api_key: str = ""
|
||||
ai_timeout_sec: int = 20
|
||||
edit_ai_enabled: bool = True
|
||||
edit_ai_temperature: float = 0.0
|
||||
edit_ai_system_prompt_file: str = ""
|
||||
edit_window: dict = field(default_factory=lambda: {"width": 800, "height": 400})
|
||||
|
||||
context_capture: dict = field(default_factory=lambda: {"provider": "i3ipc", "on_focus_change": "abort"})
|
||||
context_rules: list[dict] = field(default_factory=list)
|
||||
|
||||
languages: dict = field(
|
||||
daemon: dict = field(default_factory=lambda: {"hotkey": "Cmd+m"})
|
||||
recording: dict = field(default_factory=lambda: {"input": "pulse:default"})
|
||||
transcribing: dict = field(default_factory=lambda: {"model": "base", "device": "cpu"})
|
||||
injection: dict = field(default_factory=lambda: {"backend": "clipboard"})
|
||||
ai_cleanup: dict = field(
|
||||
default_factory=lambda: {
|
||||
"en": {"code": "en", "hotkey": "Cmd+m", "label": "English"},
|
||||
"ptBR": {"code": "pt-BR", "hotkey": "Cmd+b", "label": "Português (Brasil)"},
|
||||
"enabled": False,
|
||||
"model": "llama3.2:3b",
|
||||
"temperature": 0.0,
|
||||
"base_url": "http://localhost:11434",
|
||||
"api_key": "",
|
||||
}
|
||||
)
|
||||
edit_language_detection: dict = field(default_factory=lambda: {"enabled": True, "provider": "langdetect", "fallback_code": "en"})
|
||||
|
||||
|
||||
|
||||
|
||||
def default_path() -> Path:
|
||||
|
|
@ -58,112 +36,92 @@ def load(path: str | None) -> Config:
|
|||
p = Path(path) if path else default_path()
|
||||
if p.exists():
|
||||
data = json.loads(p.read_text(encoding="utf-8"))
|
||||
for k, v in data.items():
|
||||
if hasattr(cfg, k):
|
||||
setattr(cfg, k, v)
|
||||
if any(k in data for k in ("daemon", "recording", "transcribing", "injection", "ai_cleanup", "ai")):
|
||||
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("ffmpeg_input", cfg.recording["input"])
|
||||
cfg.transcribing["model"] = data.get("whisper_model", cfg.transcribing["model"])
|
||||
cfg.transcribing["device"] = data.get("whisper_device", cfg.transcribing["device"])
|
||||
cfg.injection["backend"] = data.get("injection_backend", cfg.injection["backend"])
|
||||
cfg.ai_cleanup["enabled"] = data.get("ai_enabled", cfg.ai_cleanup["enabled"])
|
||||
cfg.ai_cleanup["model"] = data.get("ai_model", cfg.ai_cleanup["model"])
|
||||
cfg.ai_cleanup["temperature"] = data.get("ai_temperature", cfg.ai_cleanup["temperature"])
|
||||
cfg.ai_cleanup["base_url"] = data.get("ai_base_url", cfg.ai_cleanup["base_url"])
|
||||
cfg.ai_cleanup["api_key"] = data.get("ai_api_key", cfg.ai_cleanup["api_key"])
|
||||
|
||||
if not isinstance(cfg.context_capture, dict):
|
||||
cfg.context_capture = {"provider": "i3ipc", "on_focus_change": "abort"}
|
||||
if not isinstance(cfg.context_rules, list):
|
||||
cfg.context_rules = []
|
||||
if not isinstance(cfg.daemon, dict):
|
||||
cfg.daemon = {"hotkey": "Cmd+m"}
|
||||
if not isinstance(cfg.recording, dict):
|
||||
cfg.recording = {"input": "pulse:default"}
|
||||
if not isinstance(cfg.transcribing, dict):
|
||||
cfg.transcribing = {"model": "base", "device": "cpu"}
|
||||
if not isinstance(cfg.injection, dict):
|
||||
cfg.injection = {"backend": "clipboard"}
|
||||
if not isinstance(cfg.ai_cleanup, dict):
|
||||
cfg.ai_cleanup = {
|
||||
"enabled": False,
|
||||
"model": "llama3.2:3b",
|
||||
"temperature": 0.0,
|
||||
"base_url": "http://localhost:11434",
|
||||
"api_key": "",
|
||||
}
|
||||
if isinstance(getattr(cfg, "ai", None), dict) and not cfg.ai_cleanup:
|
||||
cfg.ai_cleanup = cfg.ai
|
||||
if hasattr(cfg, "ai"):
|
||||
try:
|
||||
delattr(cfg, "ai")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# env overrides
|
||||
if os.getenv("WHISPER_MODEL"):
|
||||
cfg.whisper_model = os.environ["WHISPER_MODEL"]
|
||||
if os.getenv("WHISPER_LANG"):
|
||||
cfg.whisper_lang = os.environ["WHISPER_LANG"]
|
||||
cfg.transcribing["model"] = os.environ["WHISPER_MODEL"]
|
||||
if os.getenv("WHISPER_DEVICE"):
|
||||
cfg.whisper_device = os.environ["WHISPER_DEVICE"]
|
||||
cfg.transcribing["device"] = os.environ["WHISPER_DEVICE"]
|
||||
if os.getenv("WHISPER_FFMPEG_IN"):
|
||||
cfg.ffmpeg_input = os.environ["WHISPER_FFMPEG_IN"]
|
||||
cfg.recording["input"] = os.environ["WHISPER_FFMPEG_IN"]
|
||||
|
||||
if os.getenv("LEL_FFMPEG_PATH"):
|
||||
cfg.ffmpeg_path = os.environ["LEL_FFMPEG_PATH"]
|
||||
if os.getenv("LEL_RECORD_TIMEOUT_SEC"):
|
||||
cfg.record_timeout_sec = int(os.environ["LEL_RECORD_TIMEOUT_SEC"])
|
||||
if os.getenv("LEL_EDIT_RECORD_TIMEOUT_SEC"):
|
||||
cfg.edit_record_timeout_sec = int(os.environ["LEL_EDIT_RECORD_TIMEOUT_SEC"])
|
||||
if os.getenv("LEL_HOTKEY"):
|
||||
cfg.hotkey = os.environ["LEL_HOTKEY"]
|
||||
if os.getenv("LEL_EDIT_HOTKEY"):
|
||||
cfg.edit_hotkey = os.environ["LEL_EDIT_HOTKEY"]
|
||||
cfg.daemon["hotkey"] = os.environ["LEL_HOTKEY"]
|
||||
if os.getenv("LEL_INJECTION_BACKEND"):
|
||||
cfg.injection_backend = os.environ["LEL_INJECTION_BACKEND"]
|
||||
if os.getenv("LEL_EDIT_INJECTION_BACKEND"):
|
||||
cfg.edit_injection_backend = os.environ["LEL_EDIT_INJECTION_BACKEND"]
|
||||
cfg.injection["backend"] = os.environ["LEL_INJECTION_BACKEND"]
|
||||
|
||||
if os.getenv("LEL_AI_CLEANUP_ENABLED"):
|
||||
cfg.ai_cleanup["enabled"] = _parse_bool(os.environ["LEL_AI_CLEANUP_ENABLED"])
|
||||
if os.getenv("LEL_AI_CLEANUP_MODEL"):
|
||||
cfg.ai_cleanup["model"] = os.environ["LEL_AI_CLEANUP_MODEL"]
|
||||
if os.getenv("LEL_AI_CLEANUP_TEMPERATURE"):
|
||||
cfg.ai_cleanup["temperature"] = float(os.environ["LEL_AI_CLEANUP_TEMPERATURE"])
|
||||
if os.getenv("LEL_AI_CLEANUP_BASE_URL"):
|
||||
cfg.ai_cleanup["base_url"] = os.environ["LEL_AI_CLEANUP_BASE_URL"]
|
||||
if os.getenv("LEL_AI_CLEANUP_API_KEY"):
|
||||
cfg.ai_cleanup["api_key"] = os.environ["LEL_AI_CLEANUP_API_KEY"]
|
||||
|
||||
if os.getenv("LEL_AI_ENABLED"):
|
||||
cfg.ai_enabled = _parse_bool(os.environ["LEL_AI_ENABLED"])
|
||||
cfg.ai_cleanup["enabled"] = _parse_bool(os.environ["LEL_AI_ENABLED"])
|
||||
if os.getenv("LEL_AI_MODEL"):
|
||||
cfg.ai_model = os.environ["LEL_AI_MODEL"]
|
||||
cfg.ai_cleanup["model"] = os.environ["LEL_AI_MODEL"]
|
||||
if os.getenv("LEL_AI_TEMPERATURE"):
|
||||
cfg.ai_temperature = float(os.environ["LEL_AI_TEMPERATURE"])
|
||||
if os.getenv("LEL_AI_SYSTEM_PROMPT_FILE"):
|
||||
cfg.ai_system_prompt_file = os.environ["LEL_AI_SYSTEM_PROMPT_FILE"]
|
||||
cfg.ai_cleanup["temperature"] = float(os.environ["LEL_AI_TEMPERATURE"])
|
||||
if os.getenv("LEL_AI_BASE_URL"):
|
||||
cfg.ai_base_url = os.environ["LEL_AI_BASE_URL"]
|
||||
cfg.ai_cleanup["base_url"] = os.environ["LEL_AI_BASE_URL"]
|
||||
if os.getenv("LEL_AI_API_KEY"):
|
||||
cfg.ai_api_key = os.environ["LEL_AI_API_KEY"]
|
||||
if os.getenv("LEL_AI_TIMEOUT_SEC"):
|
||||
cfg.ai_timeout_sec = int(os.environ["LEL_AI_TIMEOUT_SEC"])
|
||||
if os.getenv("LEL_EDIT_AI_ENABLED"):
|
||||
cfg.edit_ai_enabled = _parse_bool(os.environ["LEL_EDIT_AI_ENABLED"])
|
||||
if os.getenv("LEL_EDIT_AI_TEMPERATURE"):
|
||||
cfg.edit_ai_temperature = float(os.environ["LEL_EDIT_AI_TEMPERATURE"])
|
||||
if os.getenv("LEL_EDIT_AI_SYSTEM_PROMPT_FILE"):
|
||||
cfg.edit_ai_system_prompt_file = os.environ["LEL_EDIT_AI_SYSTEM_PROMPT_FILE"]
|
||||
|
||||
if os.getenv("LEL_LANGUAGES_JSON"):
|
||||
cfg.languages = json.loads(os.environ["LEL_LANGUAGES_JSON"])
|
||||
if os.getenv("LEL_EDIT_LANG_FALLBACK"):
|
||||
cfg.edit_language_detection["fallback_code"] = os.environ["LEL_EDIT_LANG_FALLBACK"]
|
||||
|
||||
if os.getenv("LEL_CONTEXT_PROVIDER"):
|
||||
cfg.context_capture["provider"] = os.environ["LEL_CONTEXT_PROVIDER"]
|
||||
if os.getenv("LEL_CONTEXT_ON_FOCUS_CHANGE"):
|
||||
cfg.context_capture["on_focus_change"] = os.environ["LEL_CONTEXT_ON_FOCUS_CHANGE"]
|
||||
|
||||
cfg.ai_cleanup["api_key"] = os.environ["LEL_AI_API_KEY"]
|
||||
validate(cfg)
|
||||
return cfg
|
||||
|
||||
|
||||
def redacted_dict(cfg: Config) -> dict:
|
||||
d = cfg.__dict__.copy()
|
||||
d["ai_api_key"] = ""
|
||||
if isinstance(d.get("ai_cleanup"), dict):
|
||||
d["ai_cleanup"] = d["ai_cleanup"].copy()
|
||||
d["ai_cleanup"]["api_key"] = ""
|
||||
return d
|
||||
|
||||
|
||||
def validate(cfg: Config) -> None:
|
||||
if not cfg.hotkey:
|
||||
raise ValueError("hotkey cannot be empty")
|
||||
if not cfg.edit_hotkey:
|
||||
raise ValueError("edit_hotkey cannot be empty")
|
||||
if cfg.record_timeout_sec <= 0:
|
||||
raise ValueError("record_timeout_sec must be > 0")
|
||||
if cfg.edit_record_timeout_sec <= 0:
|
||||
raise ValueError("edit_record_timeout_sec must be > 0")
|
||||
if cfg.context_capture.get("provider") not in {"i3ipc"}:
|
||||
raise ValueError("context_capture.provider must be i3ipc")
|
||||
if cfg.context_capture.get("on_focus_change") not in {"abort"}:
|
||||
raise ValueError("context_capture.on_focus_change must be abort")
|
||||
if not isinstance(cfg.context_rules, list):
|
||||
cfg.context_rules = []
|
||||
if not isinstance(cfg.edit_window, dict):
|
||||
cfg.edit_window = {"width": 800, "height": 400}
|
||||
if not isinstance(cfg.languages, dict) or not cfg.languages:
|
||||
raise ValueError("languages must be a non-empty map")
|
||||
seen_hotkeys = set()
|
||||
for name, info in cfg.languages.items():
|
||||
if not isinstance(info, dict):
|
||||
raise ValueError(f"languages[{name}] must be an object")
|
||||
code = info.get("code")
|
||||
hotkey = info.get("hotkey")
|
||||
if not code or not hotkey:
|
||||
raise ValueError(f"languages[{name}] must include code and hotkey")
|
||||
if hotkey in seen_hotkeys:
|
||||
raise ValueError(f"duplicate hotkey in languages: {hotkey}")
|
||||
seen_hotkeys.add(hotkey)
|
||||
if not isinstance(cfg.edit_language_detection, dict):
|
||||
cfg.edit_language_detection = {"enabled": True, "provider": "langdetect", "fallback_code": "en"}
|
||||
if cfg.edit_language_detection.get("provider") not in {"langdetect"}:
|
||||
raise ValueError("edit_language_detection.provider must be langdetect")
|
||||
if not cfg.daemon.get("hotkey"):
|
||||
raise ValueError("daemon.hotkey cannot be empty")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue