419 lines
26 KiB
Python
419 lines
26 KiB
Python
"""Finestra impostazioni: motore, chiavi, voce e riconoscimento vocale."""
|
||
from __future__ import annotations
|
||
|
||
import threading
|
||
|
||
from PyQt6.QtCore import Qt, pyqtSignal
|
||
from PyQt6.QtWidgets import (QCheckBox, QComboBox, QDialog, QFormLayout, QHBoxLayout, QLabel, QLineEdit,
|
||
QPushButton, QStackedWidget, QVBoxLayout, QWidget, QDoubleSpinBox)
|
||
|
||
from .engines.claude_code import check_claude_code
|
||
from .engines.openai_compat import list_models
|
||
from .settings import Settings
|
||
from .tts import KOKORO_VOICES, SystemVoice
|
||
|
||
STYLE = """
|
||
QDialog { background: #030a10; color: #cfe8ff; }
|
||
QLabel { color: #8fb8d8; font-family: 'Menlo'; font-size: 14px; }
|
||
QLineEdit, QComboBox, QDoubleSpinBox { background: #000d12; color: #e6f4ff; border: 1px solid #12354a; border-radius: 3px; padding: 4px 6px; font-family: 'Menlo'; font-size: 14px; }
|
||
QLineEdit:focus, QComboBox:focus { border: 1px solid #3fd0ff; }
|
||
QPushButton { background: #05202c; color: #9fdfff; border: 1px solid #12506a; border-radius: 3px; padding: 5px 12px; font-family: 'Menlo'; font-size: 14px; }
|
||
QPushButton:hover { border-color: #3fd0ff; color: #ffffff; }
|
||
QPushButton#primary { background: #0a4a66; color: #ffffff; }
|
||
"""
|
||
|
||
PROVIDERS = [("anthropic", "Claude (Anthropic, cloud)"), ("local", "Server locale compatibile OpenAI (vLLM, Ollama…)"),
|
||
("claudecode", "Claude Code (il tuo accesso, nessuna chiave)")]
|
||
EFFORTS = [("low", "Veloce"), ("medium", "Bilanciata"), ("high", "Approfondita")]
|
||
ACCESS = [("chat", "Solo conversazione e ricerca web"), ("read", "Leggere file e cercare nelle cartelle"),
|
||
("full", "Completo: modifica file ed esegue comandi senza chiedere")]
|
||
CC_MODELS = [("sonnet", "Sonnet (veloce, consigliato)"), ("opus", "Opus (più capace)"), ("haiku", "Haiku (il più rapido)")]
|
||
STT_MODELS = [("mlx-community/whisper-base-mlx", "Whisper base (leggero)"), ("mlx-community/whisper-small-mlx", "Whisper small (consigliato)"),
|
||
("mlx-community/whisper-large-v3-turbo", "Whisper large v3 turbo (preciso, pesante)")]
|
||
|
||
|
||
def _combo(items, current) -> QComboBox:
|
||
c = QComboBox()
|
||
for value, label in items:
|
||
c.addItem(label, value)
|
||
idx = c.findData(current)
|
||
c.setCurrentIndex(idx if idx >= 0 else 0)
|
||
return c
|
||
|
||
|
||
class SettingsDialog(QDialog):
|
||
_async = pyqtSignal(str, object)
|
||
|
||
def __init__(self, settings: Settings, on_saved, parent=None) -> None:
|
||
super().__init__(parent)
|
||
self.settings, self.on_saved = settings, on_saved
|
||
self.setWindowTitle("Motore & Voce")
|
||
self.setStyleSheet(STYLE)
|
||
self.setMinimumWidth(620)
|
||
s = settings
|
||
root = QVBoxLayout(self)
|
||
|
||
form = QFormLayout()
|
||
self.provider = _combo(PROVIDERS, s.get("provider"))
|
||
form.addRow("Motore", self.provider)
|
||
self.effort = _combo(EFFORTS, s.get("effort"))
|
||
form.addRow("Profondità di ragionamento (Claude e Claude Code)", self.effort)
|
||
root.addLayout(form)
|
||
|
||
self.stack = QStackedWidget()
|
||
# Anthropic
|
||
w = QWidget(); f = QFormLayout(w)
|
||
self.api_key = QLineEdit(); self.api_key.setEchoMode(QLineEdit.EchoMode.Password)
|
||
self.api_key.setPlaceholderText("•••••• (salvata, lascia vuoto per non cambiarla)" if s.get_secret("anthropic_api_key") else "sk-ant-…")
|
||
f.addRow("Chiave API Anthropic", self.api_key)
|
||
f.addRow("", QLabel("Creala su console.anthropic.com. Viene salvata nel portachiavi di macOS."))
|
||
self.stack.addWidget(w)
|
||
# Locale
|
||
w = QWidget(); f = QFormLayout(w)
|
||
self.local_url = QLineEdit(s.get("local_base_url")); f.addRow("Indirizzo del server", self.local_url)
|
||
self.local_key = QLineEdit(); self.local_key.setEchoMode(QLineEdit.EchoMode.Password)
|
||
self.local_key.setPlaceholderText("opzionale"); f.addRow("Chiave API del server", self.local_key)
|
||
row = QHBoxLayout(); self.local_model = QComboBox(); self.local_model.setEditable(True)
|
||
self.local_model.setEditText(s.get("local_model") or ""); row.addWidget(self.local_model, 1)
|
||
b = QPushButton("Rileva"); b.clicked.connect(self._detect_models); row.addWidget(b)
|
||
f.addRow("Modello", row)
|
||
self.local_hint = QLabel("Premi \"Rileva\" per leggere i modelli dal server."); f.addRow("", self.local_hint)
|
||
self.search_key = QLineEdit(s.get("search_api_key")); self.search_key.setEchoMode(QLineEdit.EchoMode.Password)
|
||
self.search_key.setPlaceholderText("opzionale: senza chiave la ricerca web resta spenta")
|
||
f.addRow("Chiave Brave Search", self.search_key)
|
||
self.stack.addWidget(w)
|
||
# Claude Code
|
||
w = QWidget(); f = QFormLayout(w)
|
||
self.cc_model = _combo(CC_MODELS, s.get("claudecode_model")); f.addRow("Modello", self.cc_model)
|
||
self.cc_access = _combo(ACCESS, s.get("claudecode_access")); f.addRow("Cosa può fare sul Mac", self.cc_access)
|
||
row = QHBoxLayout(); self.cc_config = QComboBox(); self.cc_config.setEditable(True)
|
||
self.cc_config.setEditText(s.get("claudecode_config_dir") or ""); row.addWidget(self.cc_config, 1)
|
||
b = QPushButton("Verifica"); b.clicked.connect(self._check_cc); row.addWidget(b)
|
||
f.addRow("Profilo (cartella di configurazione)", row)
|
||
self.cc_hint = QLabel(""); self.cc_hint.setWordWrap(True); f.addRow("", self.cc_hint)
|
||
self.cc_path = QLineEdit(s.get("claudecode_path")); self.cc_path.setPlaceholderText("vuoto = automatico")
|
||
f.addRow("Percorso del comando claude", self.cc_path)
|
||
self.stack.addWidget(w)
|
||
root.addWidget(self.stack)
|
||
self.provider.currentIndexChanged.connect(self.stack.setCurrentIndex)
|
||
self.stack.setCurrentIndex(self.provider.currentIndex())
|
||
|
||
form2 = QFormLayout()
|
||
self.tts_engine = _combo([("kokoro", "Kokoro, voce neurale in locale"), ("elevenlabs", "ElevenLabs, espressiva nel cloud (chiave API)"), ("chatterbox", "Chatterbox, espressiva in locale (lenta)"), ("system", "Voce di sistema (macOS)")], s.get("tts_engine"))
|
||
form2.addRow("Motore voce", self.tts_engine)
|
||
self.kokoro_voice = _combo(list(KOKORO_VOICES.items()), s.get("kokoro_voice")); form2.addRow("Voce Kokoro", self.kokoro_voice)
|
||
self.system_voice = _combo([("", "Automatica (Alice)")] + [(v, v) for v in SystemVoice.list_voices()], s.get("system_voice"))
|
||
form2.addRow("Voce di sistema", self.system_voice)
|
||
row = QHBoxLayout()
|
||
self.cb_exag = QDoubleSpinBox(); self.cb_exag.setRange(0.2, 1.0); self.cb_exag.setSingleStep(0.1); self.cb_exag.setValue(float(s.get("chatterbox_exaggeration", 0.6))); row.addWidget(QLabel("enfasi")); row.addWidget(self.cb_exag)
|
||
self.cb_cfg = QDoubleSpinBox(); self.cb_cfg.setRange(0.0, 1.0); self.cb_cfg.setSingleStep(0.1); self.cb_cfg.setValue(float(s.get("chatterbox_cfg", 0.3))); row.addWidget(QLabel("aderenza (0 = più veloce)")); row.addWidget(self.cb_cfg)
|
||
form2.addRow("Chatterbox", row)
|
||
row = QHBoxLayout()
|
||
cur_ref = s.get("chatterbox_ref") or ""
|
||
self.cb_voice = _combo([("", "Predefinita del modello"), ("preset:femminile", "Femminile (timbro di Sara)"), ("preset:maschile", "Maschile (timbro di Nicola)"), ("custom", "Personalizzata: file wav")],
|
||
cur_ref if cur_ref in ("", "preset:femminile", "preset:maschile") else "custom")
|
||
row.addWidget(QLabel("voce")); row.addWidget(self.cb_voice)
|
||
self.cb_ref = QLineEdit(cur_ref if cur_ref.startswith("/") or cur_ref.startswith("~") else ""); self.cb_ref.setPlaceholderText("percorso di un wav di 10 secondi con la voce da imitare"); row.addWidget(self.cb_ref, 1)
|
||
form2.addRow("Voce Chatterbox", row)
|
||
row = QHBoxLayout()
|
||
self.el_key = QLineEdit(); self.el_key.setEchoMode(QLineEdit.EchoMode.Password)
|
||
self.el_key.setPlaceholderText("•••••• (salvata)" if s.get_secret("elevenlabs_api_key") else "chiave API da elevenlabs.io"); row.addWidget(self.el_key, 1)
|
||
self.el_voice = QComboBox(); self.el_voice.setMinimumWidth(220); row.addWidget(self.el_voice, 1)
|
||
b = QPushButton("Carica voci"); b.clicked.connect(self._el_voices); row.addWidget(b)
|
||
form2.addRow("ElevenLabs", row)
|
||
row = QHBoxLayout()
|
||
self.el_model = _combo([("eleven_flash_v2_5", "Flash v2.5 (rapido)"), ("eleven_multilingual_v2", "Multilingual v2 (qualità)"), ("eleven_v3", "v3 (più espressivo, sperimentale)")], s.get("elevenlabs_model")); row.addWidget(QLabel("modello")); row.addWidget(self.el_model)
|
||
self.el_stab = QDoubleSpinBox(); self.el_stab.setRange(0.0, 1.0); self.el_stab.setSingleStep(0.05); self.el_stab.setValue(float(s.get("elevenlabs_stability", 0.45))); row.addWidget(QLabel("stabilità")); row.addWidget(self.el_stab)
|
||
self.el_style = QDoubleSpinBox(); self.el_style.setRange(0.0, 1.0); self.el_style.setSingleStep(0.05); self.el_style.setValue(float(s.get("elevenlabs_style", 0.3))); row.addWidget(QLabel("stile")); row.addWidget(self.el_style)
|
||
row.addStretch()
|
||
form2.addRow("", row)
|
||
self._el_current = str(s.get("elevenlabs_voice_id") or "")
|
||
self.el_voice.addItem("(carica le voci con la chiave)" if not self._el_current else f"voce salvata: {self._el_current[:10]}…", self._el_current)
|
||
self.stt_model = _combo(STT_MODELS, s.get("stt_model")); form2.addRow("Riconoscimento vocale", self.stt_model)
|
||
from .avatar3d import list_models
|
||
models = list_models()
|
||
self.avatar_model = _combo([(m, m.replace("_", " ").title()) for m in models] or [("", "nessun modello")], s.get("avatar_model"))
|
||
form2.addRow("Avatar 3D (file in avatar3d/models)", self.avatar_model)
|
||
self.vad = QDoubleSpinBox(); self.vad.setRange(0.02, 0.5); self.vad.setSingleStep(0.01); self.vad.setValue(float(s.get("vad_threshold", 0.08)))
|
||
form2.addRow("Sensibilità microfono (più basso = più sensibile)", self.vad)
|
||
root.addLayout(form2)
|
||
|
||
# ── Telegram ──────────────────────────────────────────────────────
|
||
form3 = QFormLayout()
|
||
form3.addRow(QLabel("Telegram (account personale): credenziali da my.telegram.org > API development tools"))
|
||
row = QHBoxLayout()
|
||
self.tg_id = QLineEdit(str(s.get("telegram_api_id") or "")); self.tg_id.setPlaceholderText("api id"); row.addWidget(self.tg_id)
|
||
self.tg_hash = QLineEdit(); self.tg_hash.setEchoMode(QLineEdit.EchoMode.Password)
|
||
self.tg_hash.setPlaceholderText("•••••• (salvato)" if s.get_secret("telegram_api_hash") else "api hash"); row.addWidget(self.tg_hash, 1)
|
||
form3.addRow("Credenziali", row)
|
||
row = QHBoxLayout()
|
||
self.tg_phone = QLineEdit(); self.tg_phone.setPlaceholderText("+39…"); row.addWidget(self.tg_phone)
|
||
b = QPushButton("Invia codice"); b.clicked.connect(self._tg_send_code); row.addWidget(b)
|
||
self.tg_code = QLineEdit(); self.tg_code.setPlaceholderText("codice"); row.addWidget(self.tg_code)
|
||
self.tg_pwd = QLineEdit(); self.tg_pwd.setEchoMode(QLineEdit.EchoMode.Password); self.tg_pwd.setPlaceholderText("password 2FA (se attiva)"); row.addWidget(self.tg_pwd)
|
||
b = QPushButton("Accedi"); b.clicked.connect(self._tg_sign_in); row.addWidget(b)
|
||
b = QPushButton("Esci"); b.clicked.connect(self._tg_logout); row.addWidget(b)
|
||
form3.addRow("Accesso", row)
|
||
row = QHBoxLayout()
|
||
b = QPushButton("Accesso con QR (senza codice)"); b.clicked.connect(self._tg_qr); row.addWidget(b)
|
||
self.tg_qr_label = QLabel(); self.tg_qr_label.setFixedSize(220, 220); self.tg_qr_label.setScaledContents(True); row.addWidget(self.tg_qr_label); row.addStretch()
|
||
form3.addRow("Alternativa", row)
|
||
self.tg_hint = QLabel("…"); self.tg_hint.setWordWrap(True); form3.addRow("", self.tg_hint)
|
||
root.addLayout(form3)
|
||
threading.Thread(target=lambda: self._async.emit("tg", self._tg_status()), daemon=True).start()
|
||
|
||
# ── WhatsApp (dispositivo collegato, Baileys) ─────────────────────
|
||
form4 = QFormLayout()
|
||
form4.addRow(QLabel("WhatsApp in tempo reale (client non ufficiale: possibile blocco del numero, a tuo rischio)"))
|
||
row = QHBoxLayout()
|
||
self.wa_live = QCheckBox("Attivo all'avvio"); self.wa_live.setChecked(bool(s.get("whatsapp_live"))); row.addWidget(self.wa_live)
|
||
self.wa_annuncia = QCheckBox("Annuncia a voce i messaggi in arrivo"); self.wa_annuncia.setChecked(bool(s.get("whatsapp_annuncia"))); row.addWidget(self.wa_annuncia)
|
||
b = QPushButton("Collega (QR)"); b.clicked.connect(self._wa_link); row.addWidget(b)
|
||
b = QPushButton("Scollega"); b.clicked.connect(self._wa_logout); row.addWidget(b)
|
||
row.addStretch()
|
||
form4.addRow("Stato", row)
|
||
row = QHBoxLayout()
|
||
self.wa_qr_label = QLabel(); self.wa_qr_label.setFixedSize(220, 220); self.wa_qr_label.setScaledContents(True); row.addWidget(self.wa_qr_label)
|
||
self.wa_hint = QLabel("…"); self.wa_hint.setWordWrap(True); row.addWidget(self.wa_hint, 1)
|
||
form4.addRow("", row)
|
||
root.addLayout(form4)
|
||
threading.Thread(target=lambda: self._async.emit("wa", (self._wa_status(), None)), daemon=True).start()
|
||
|
||
# ── Monitor avvisi ────────────────────────────────────────────────
|
||
form5 = QFormLayout()
|
||
row = QHBoxLayout()
|
||
self.mon_on = QCheckBox("Controlla Mail, WhatsApp e Telegram"); self.mon_on.setChecked(bool(s.get("monitor_enabled"))); row.addWidget(self.mon_on)
|
||
self.mon_say = QCheckBox("Annuncia a voce gli avvisi"); self.mon_say.setChecked(bool(s.get("monitor_annuncia"))); row.addWidget(self.mon_say)
|
||
self.mon_int = _combo([(30, "ogni 30 s"), (60, "ogni minuto"), (300, "ogni 5 minuti"), (900, "ogni 15 minuti")], int(s.get("monitor_intervallo") or 60)); row.addWidget(self.mon_int)
|
||
row.addStretch()
|
||
form5.addRow("Monitor", row)
|
||
from .monitor import DEFAULT_RULES
|
||
self.mon_rules = QLineEdit(s.get("monitor_regole") or ""); self.mon_rules.setPlaceholderText(DEFAULT_RULES[:110] + "…")
|
||
form5.addRow("Cosa merita un avviso", self.mon_rules)
|
||
self.mon_excl = QLineEdit(s.get("monitor_escludi") or ""); self.mon_excl.setPlaceholderText("es. newsletter, offerta, gruppo Calcetto, Amazon — parole separate da virgola, cercate in mittente, chat e testo")
|
||
form5.addRow("Escludi dagli avvisi", self.mon_excl)
|
||
root.addLayout(form5)
|
||
|
||
btns = QHBoxLayout(); btns.addStretch()
|
||
cancel = QPushButton("Annulla"); cancel.clicked.connect(self.reject); btns.addWidget(cancel)
|
||
save = QPushButton("Salva"); save.setObjectName("primary"); save.clicked.connect(self._save); btns.addWidget(save)
|
||
root.addLayout(btns)
|
||
self._async.connect(self._on_async)
|
||
self._check_cc()
|
||
|
||
# ── Azioni ────────────────────────────────────────────────────────────
|
||
def _detect_models(self) -> None:
|
||
url, key = self.local_url.text().strip().rstrip("/"), self.local_key.text().strip() or self.settings.get_secret("local_api_key")
|
||
self.local_hint.setText("Interrogo il server…")
|
||
def work():
|
||
try:
|
||
self._async.emit("models", list_models(url, key))
|
||
except Exception as err:
|
||
self._async.emit("models_error", str(err)[:160])
|
||
threading.Thread(target=work, daemon=True).start()
|
||
|
||
def _check_cc(self) -> None:
|
||
self.cc_hint.setText("Verifico…")
|
||
path, cfg = self.cc_path.text().strip(), self.cc_config.currentText().strip()
|
||
threading.Thread(target=lambda: self._async.emit("cc", check_claude_code(path, cfg)), daemon=True).start()
|
||
|
||
# ── Telegram ──────────────────────────────────────────────────────────
|
||
def _tg_save_creds(self) -> None:
|
||
vals = {"telegram_api_id": self.tg_id.text().strip()}
|
||
if self.tg_hash.text().strip():
|
||
vals["telegram_api_hash"] = self.tg_hash.text().strip()
|
||
self.settings.update(vals)
|
||
|
||
def _tg_status(self) -> str:
|
||
try:
|
||
from .telegram_client import status
|
||
return status()
|
||
except Exception as err:
|
||
return f"Errore: {err}"
|
||
|
||
def _tg_send_code(self) -> None:
|
||
self._tg_save_creds()
|
||
phone = self.tg_phone.text().strip()
|
||
self.tg_hint.setText("Invio il codice…")
|
||
def work():
|
||
try:
|
||
from .telegram_client import send_code
|
||
self._async.emit("tg", send_code(phone))
|
||
except Exception as err:
|
||
self._async.emit("tg", f"Errore: {err}")
|
||
threading.Thread(target=work, daemon=True).start()
|
||
|
||
def _tg_sign_in(self) -> None:
|
||
self._tg_save_creds()
|
||
code, pwd = self.tg_code.text().strip(), self.tg_pwd.text()
|
||
self.tg_hint.setText("Accedo…")
|
||
def work():
|
||
try:
|
||
from .telegram_client import sign_in
|
||
self._async.emit("tg", sign_in(code, pwd))
|
||
except Exception as err:
|
||
self._async.emit("tg", f"Errore: {err}")
|
||
threading.Thread(target=work, daemon=True).start()
|
||
|
||
def _tg_qr(self) -> None:
|
||
self._tg_save_creds()
|
||
self.tg_hint.setText("Genero il codice QR…")
|
||
def work():
|
||
try:
|
||
from .telegram_client import qr_login, _qr_state
|
||
_qr_state["password"] = self.tg_pwd.text()
|
||
qr_login(lambda msg, png: self._async.emit("tg_qr", (msg, png)))
|
||
except Exception as err:
|
||
self._async.emit("tg", f"Errore: {err}")
|
||
threading.Thread(target=work, daemon=True).start()
|
||
|
||
def _tg_logout(self) -> None:
|
||
def work():
|
||
try:
|
||
from .telegram_client import logout
|
||
self._async.emit("tg", logout())
|
||
except Exception as err:
|
||
self._async.emit("tg", f"Errore: {err}")
|
||
threading.Thread(target=work, daemon=True).start()
|
||
|
||
# ── WhatsApp ──────────────────────────────────────────────────────────
|
||
def _wa_status(self) -> str:
|
||
try:
|
||
from . import whatsapp_bridge as wb
|
||
return wb.status_text()
|
||
except Exception as err:
|
||
return f"Errore: {err}"
|
||
|
||
def _wa_link(self) -> None:
|
||
self.wa_hint.setText("Avvio il ponte e genero il QR…")
|
||
def work():
|
||
try:
|
||
from . import whatsapp_bridge as wb
|
||
import time as _t
|
||
msg = wb.start()
|
||
self._async.emit("wa", (msg, None))
|
||
for _ in range(120): # fino a 2 minuti: aggiorna QR e stato
|
||
_t.sleep(1)
|
||
st = wb.status()
|
||
if st.get("connection") == "open":
|
||
self._async.emit("wa", (wb.status_text() + " Sul telefono: WhatsApp › Dispositivi collegati.", None)); return
|
||
if st.get("qr"):
|
||
self._async.emit("wa", ("Inquadra il QR dal telefono: WhatsApp › Impostazioni › Dispositivi collegati › Collega un dispositivo.", wb.qr_png()))
|
||
self._async.emit("wa", ("Tempo scaduto: premi di nuovo «Collega (QR)».", None))
|
||
except Exception as err:
|
||
self._async.emit("wa", (f"Errore: {err}", None))
|
||
threading.Thread(target=work, daemon=True).start()
|
||
|
||
def _wa_logout(self) -> None:
|
||
def work():
|
||
try:
|
||
from . import whatsapp_bridge as wb
|
||
self._async.emit("wa", (wb.logout(), None))
|
||
except Exception as err:
|
||
self._async.emit("wa", (f"Errore: {err}", None))
|
||
threading.Thread(target=work, daemon=True).start()
|
||
|
||
def _el_voices(self) -> None:
|
||
key = self.el_key.text().strip() or self.settings.get_secret("elevenlabs_api_key")
|
||
if not key:
|
||
self.el_voice.clear(); self.el_voice.addItem("inserisci prima la chiave API", ""); return
|
||
if self.el_key.text().strip():
|
||
self.settings.update({"elevenlabs_api_key": key})
|
||
def work():
|
||
try:
|
||
from .tts import ElevenLabsVoice
|
||
self._async.emit("el", ElevenLabsVoice.list_voices(key))
|
||
except Exception as err:
|
||
self._async.emit("el_error", str(err)[:120])
|
||
threading.Thread(target=work, daemon=True).start()
|
||
|
||
def _on_async(self, kind: str, payload) -> None:
|
||
if kind == "el":
|
||
self.el_voice.clear()
|
||
for vid, label in payload:
|
||
self.el_voice.addItem(label, vid)
|
||
idx = self.el_voice.findData(self._el_current)
|
||
self.el_voice.setCurrentIndex(idx if idx >= 0 else 0)
|
||
return
|
||
if kind == "el_error":
|
||
self.el_voice.clear(); self.el_voice.addItem(f"errore: {payload}", ""); return
|
||
if kind == "wa":
|
||
msg, png = payload
|
||
self.wa_hint.setText(str(msg))
|
||
if png:
|
||
from PyQt6.QtGui import QPixmap
|
||
pm = QPixmap(); pm.loadFromData(png); self.wa_qr_label.setPixmap(pm)
|
||
else:
|
||
self.wa_qr_label.clear()
|
||
return
|
||
if kind == "tg":
|
||
self.tg_hint.setText(str(payload))
|
||
return
|
||
if kind == "tg_qr":
|
||
msg, png = payload
|
||
self.tg_hint.setText(str(msg))
|
||
if png:
|
||
from PyQt6.QtGui import QPixmap
|
||
pm = QPixmap(); pm.loadFromData(png); self.tg_qr_label.setPixmap(pm)
|
||
else:
|
||
self.tg_qr_label.clear()
|
||
return
|
||
if kind == "models":
|
||
current = self.local_model.currentText().strip()
|
||
self.local_model.clear(); self.local_model.addItems(payload)
|
||
if current in payload:
|
||
self.local_model.setCurrentText(current)
|
||
self.local_hint.setText(f"Trovati {len(payload)} modelli: {', '.join(payload)}" if payload else "Il server risponde ma non espone modelli.")
|
||
elif kind == "models_error":
|
||
self.local_hint.setText(f"Server non raggiungibile: {payload}")
|
||
elif kind == "cc":
|
||
st = payload
|
||
current = self.cc_config.currentText().strip()
|
||
self.cc_config.clear(); self.cc_config.addItems(st.get("profiles", [])); self.cc_config.setEditText(current)
|
||
if not st.get("binary"):
|
||
self.cc_hint.setText("Comando claude non trovato. Installa Claude Code o indica il percorso.")
|
||
elif st.get("error"):
|
||
self.cc_hint.setText(f"Profilo {st['configDir']}: {st['error']}")
|
||
elif st.get("loggedIn"):
|
||
self.cc_hint.setText(f"Profilo {st['configDir']}: collegato come {st.get('email') or 'account sconosciuto'}. "
|
||
f"Con un errore 401 esegui: CLAUDE_CONFIG_DIR={st['configDir']} claude auth login")
|
||
else:
|
||
self.cc_hint.setText(f"Profilo {st['configDir']}: nessun accesso. Esegui: CLAUDE_CONFIG_DIR={st['configDir']} claude auth login")
|
||
|
||
def _save(self) -> None:
|
||
values = {
|
||
"provider": self.provider.currentData(), "effort": self.effort.currentData(),
|
||
"local_base_url": self.local_url.text().strip().rstrip("/"), "local_model": self.local_model.currentText().strip(),
|
||
"search_api_key": self.search_key.text().strip(),
|
||
"claudecode_model": self.cc_model.currentData(), "claudecode_access": self.cc_access.currentData(),
|
||
"claudecode_config_dir": self.cc_config.currentText().strip(), "claudecode_path": self.cc_path.text().strip(),
|
||
"tts_engine": self.tts_engine.currentData(), "kokoro_voice": self.kokoro_voice.currentData(),
|
||
"system_voice": self.system_voice.currentData(), "stt_model": self.stt_model.currentData(),
|
||
"elevenlabs_voice_id": self.el_voice.currentData() or "", "elevenlabs_model": self.el_model.currentData(),
|
||
"elevenlabs_stability": float(self.el_stab.value()), "elevenlabs_style": float(self.el_style.value()),
|
||
"chatterbox_exaggeration": float(self.cb_exag.value()), "chatterbox_cfg": float(self.cb_cfg.value()),
|
||
"chatterbox_ref": (self.cb_ref.text().strip() if self.cb_voice.currentData() == "custom" else self.cb_voice.currentData()),
|
||
"vad_threshold": float(self.vad.value()),
|
||
"avatar_model": self.avatar_model.currentData() or "",
|
||
"telegram_api_id": self.tg_id.text().strip(),
|
||
"whatsapp_live": self.wa_live.isChecked(),
|
||
"whatsapp_annuncia": self.wa_annuncia.isChecked(),
|
||
"monitor_enabled": self.mon_on.isChecked(),
|
||
"monitor_annuncia": self.mon_say.isChecked(),
|
||
"monitor_intervallo": int(self.mon_int.currentData() or 60),
|
||
"monitor_regole": self.mon_rules.text().strip(),
|
||
"monitor_escludi": self.mon_excl.text().strip(),
|
||
}
|
||
if self.tg_hash.text().strip():
|
||
values["telegram_api_hash"] = self.tg_hash.text().strip()
|
||
if self.api_key.text().strip():
|
||
values["anthropic_api_key"] = self.api_key.text().strip()
|
||
if self.local_key.text().strip():
|
||
values["local_api_key"] = self.local_key.text().strip()
|
||
if self.el_key.text().strip():
|
||
values["elevenlabs_api_key"] = self.el_key.text().strip()
|
||
self.settings.update(values)
|
||
self.accept()
|
||
if self.on_saved:
|
||
self.on_saved()
|