128 lines
5.1 KiB
Python
128 lines
5.1 KiB
Python
"""Impostazioni dell'app: file JSON in config/, chiavi API nel portachiavi di sistema."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
CONFIG_DIR = BASE_DIR / "config"
|
|
SETTINGS_FILE = CONFIG_DIR / "settings.json"
|
|
DATA_DIR = BASE_DIR / "data"
|
|
KEYRING_SERVICE = "AvatarPy"
|
|
|
|
DEFAULTS: dict[str, Any] = {
|
|
"provider": "anthropic", # anthropic | local | claudecode
|
|
"effort": "medium", # low | medium | high
|
|
"local_base_url": "http://localhost:8000/v1",
|
|
"local_model": "",
|
|
"search_api_key": "",
|
|
"claudecode_model": "sonnet",
|
|
"claudecode_access": "chat", # chat | read | full
|
|
"claudecode_config_dir": "",
|
|
"claudecode_path": "",
|
|
"tts_engine": "kokoro", # kokoro | system
|
|
"kokoro_voice": "if_sara",
|
|
"chatterbox_exaggeration": 0.6, # 0.3 sobria … 0.9 molto enfatica
|
|
"chatterbox_cfg": 0.3, # più basso = più veloce e meno aderente al testo
|
|
"voicebox_profile_id": "",
|
|
"voicebox_engine": "qwen", # qwen | chatterbox | chatterbox_turbo | kokoro | luxtts
|
|
"voicebox_instruct": "", # istruzione di stile per Qwen (es. "parla in modo caloroso e calmo")
|
|
"elevenlabs_voice_id": "",
|
|
"elevenlabs_model": "eleven_flash_v2_5", # rapido; eleven_multilingual_v2 = qualità massima
|
|
"elevenlabs_stability": 0.45,
|
|
"elevenlabs_style": 0.3,
|
|
"chatterbox_ref": "preset:femminile", # preset:femminile | preset:maschile | percorso di un wav da imitare
|
|
"system_voice": "", # "" = automatica
|
|
"stt_model": "mlx-community/whisper-small-mlx",
|
|
"vad_threshold": 0.08,
|
|
"avatar_model": "allegra_2", # nome file (senza .glb) in avatar3d/models
|
|
"telegram_api_id": "",
|
|
"whatsapp_live": False, # avvia il ponte WhatsApp all'apertura
|
|
"whatsapp_annuncia": False, # annuncia a voce i messaggi in arrivo
|
|
"monitor_enabled": False, # monitor Mail/WhatsApp/Telegram con avvisi
|
|
"monitor_annuncia": True, # annuncia a voce gli avvisi
|
|
"monitor_intervallo": 60, # secondi tra un controllo e l'altro
|
|
"monitor_regole": "", # cosa merita attenzione (vuoto = regole predefinite)
|
|
"monitor_escludi": "", # parole/frasi (separate da virgola) che escludono un messaggio dagli avvisi
|
|
}
|
|
|
|
SECRET_KEYS = ("anthropic_api_key", "local_api_key", "telegram_api_hash", "elevenlabs_api_key")
|
|
|
|
|
|
class Settings:
|
|
def __init__(self) -> None:
|
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
self._data: dict[str, Any] = dict(DEFAULTS)
|
|
try:
|
|
self._data.update(json.loads(SETTINGS_FILE.read_text(encoding="utf-8")))
|
|
except Exception:
|
|
pass
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
return self._data.get(key, DEFAULTS.get(key, default))
|
|
|
|
def set(self, key: str, value: Any) -> None:
|
|
self._data[key] = value
|
|
|
|
def update(self, values: dict[str, Any]) -> None:
|
|
for k, v in values.items():
|
|
if k in SECRET_KEYS:
|
|
self.set_secret(k, str(v))
|
|
else:
|
|
self._data[k] = v
|
|
self.save()
|
|
|
|
def save(self) -> None:
|
|
SETTINGS_FILE.write_text(json.dumps(self._data, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
try:
|
|
os.chmod(SETTINGS_FILE, 0o600)
|
|
except OSError:
|
|
pass
|
|
|
|
# ── Segreti ──────────────────────────────────────────────────────────
|
|
def get_secret(self, key: str) -> str:
|
|
env = {"anthropic_api_key": "ANTHROPIC_API_KEY"}.get(key)
|
|
try:
|
|
import keyring
|
|
value = keyring.get_password(KEYRING_SERVICE, key)
|
|
if value:
|
|
return value
|
|
except Exception:
|
|
value = self._data.get(f"_{key}")
|
|
if value:
|
|
return str(value)
|
|
if env and os.environ.get(env):
|
|
return os.environ[env]
|
|
return str(self._data.get(f"_{key}", "") or "")
|
|
|
|
def set_secret(self, key: str, value: str) -> None:
|
|
value = value.strip()
|
|
try:
|
|
import keyring
|
|
if value:
|
|
keyring.set_password(KEYRING_SERVICE, key, value)
|
|
else:
|
|
try:
|
|
keyring.delete_password(KEYRING_SERVICE, key)
|
|
except Exception:
|
|
pass
|
|
self._data.pop(f"_{key}", None)
|
|
except Exception:
|
|
# Portachiavi non disponibile: salva nel file (permessi 600).
|
|
if value:
|
|
self._data[f"_{key}"] = value
|
|
else:
|
|
self._data.pop(f"_{key}", None)
|
|
|
|
@property
|
|
def ready(self) -> bool:
|
|
p = self.get("provider")
|
|
if p == "local":
|
|
return bool(self.get("local_base_url") and self.get("local_model"))
|
|
if p == "claudecode":
|
|
return True
|
|
return bool(self.get_secret("anthropic_api_key"))
|