chore: auto-commit from dashboard

This commit is contained in:
2026-05-27 05:40:22 +00:00
parent 2a05f7cf49
commit 3dd2ddbd6a
11 changed files with 430 additions and 19 deletions

View File

@@ -21,6 +21,21 @@ TOOLS_DIR = PROJECT_ROOT / "tools"
MEMORY_DIR = PROJECT_ROOT / "memory"
LOGS_DIR = PROJECT_ROOT / "logs"
# ---------------------------------------------------------------------------
# Thread-local channel context — set by router.py before fast_dispatch calls
# ---------------------------------------------------------------------------
_ctx = threading.local()
def set_channel_context(channel_id: str) -> None:
"""Called by router.py before dispatching a fast command."""
_ctx.channel_id = channel_id
def _get_ctx_channel() -> str | None:
return getattr(_ctx, "channel_id", None)
# ---------------------------------------------------------------------------
# Git
@@ -673,6 +688,14 @@ Reminders:
/remind <HH:MM> <text> — Reminder today
/remind <YYYY-MM-DD> <HH:MM> <text> — Reminder on date
Audio:
/audio <text> — TTS pe text
/audio <url> — Extrage articol → audio
/audio rezumat <url> — Rezumat Claude → audio
/audio — Ultimul răspuns Echo → audio
/audio M2 [text|url|gol] — Voce specificată (M1-M5, F1-F5)
/audio ajutor — Ajutor detaliat
Ops:
/logs [N] — Last N log lines (default 20)
/doctor — System diagnostics
@@ -685,6 +708,155 @@ Session:
/model [name] — Show/change model"""
# ---------------------------------------------------------------------------
# Audio / TTS
# ---------------------------------------------------------------------------
_VOICES = {"M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5"}
_AUDIO_PREFIX = "__AUDIO__:"
_MAX_TTS_CHARS = 3000
def cmd_audio(args: list[str]) -> str:
"""TTS via Supertonic. Returnează __AUDIO__:/cale sau text de eroare.
Sintaxa:
/audio → ultimul răspuns Echo → audio
/audio <text> → text explicit → audio
/audio <url> → extrage text principal din URL → audio
/audio rezumat <url> → Claude rezumă URL → audio
/audio M2 [text|url|gol] → voce specificată (M1-M5, F1-F5)
/audio ajutor → ajutor
"""
voice = "M1"
remaining = list(args)
# Detectare voce ca prim token
if remaining and remaining[0].upper() in _VOICES:
voice = remaining[0].upper()
remaining = remaining[1:]
channel_id = _get_ctx_channel()
# Determinare text sursă
text: str | None = None
if not remaining:
from src.last_response_store import get_last
text = get_last(channel_id or "")
if not text:
return "Nu există un răspuns anterior. Folosește: /audio <text>"
elif len(remaining) == 1 and remaining[0].lower() == "ajutor":
return (
"🎙️ /audio — Text-to-Speech local (Supertonic)\n\n"
" /audio <text> — TTS pe text dat\n"
" /audio <url> — extrage articol → audio\n"
" /audio rezumat <url> — rezumat Claude → audio\n"
" /audio — ultimul răspuns Echo → audio\n"
" /audio M2 <...> — voce specifică (M1-M5, F1-F5)\n\n"
"Voci: M1 M2 M3 M4 M5 (masculin) · F1 F2 F3 F4 F5 (feminin)"
)
elif (len(remaining) >= 2
and remaining[0].lower() == "rezumat"
and remaining[1].startswith("http")):
url = remaining[1]
extracted = _extract_url_text(url)
if not extracted:
return f"Nu am putut extrage text din URL: {url}"
text = _claude_summarize(extracted)
if not text:
return "Rezumatul a eșuat. Încearcă /audio <url> pentru extragere directă."
elif len(remaining) == 1 and remaining[0].startswith("http"):
url = remaining[0]
text = _extract_url_text(url)
if not text:
return f"Nu am putut extrage text din URL: {url}"
else:
text = " ".join(remaining)
# Trunchiere text lung
if len(text) > _MAX_TTS_CHARS:
text = text[:_MAX_TTS_CHARS] + "..."
# Apel TTS (import direct din tools/tts.py)
result = _tts_synthesize(text, voice)
if result.get("ok"):
return f"{_AUDIO_PREFIX}{result['path']}"
return f"TTS error: {result.get('error', 'necunoscut')}"
def _tts_synthesize(text: str, voice: str) -> dict:
"""Import tools/tts.py și apelează synthesize()."""
import sys as _sys
_tools_dir = str(TOOLS_DIR)
if _tools_dir not in _sys.path:
_sys.path.insert(0, _tools_dir)
try:
import importlib
import tts as _tts_mod
# Re-import pentru a prinde modificări la hot-reload
importlib.reload(_tts_mod)
return _tts_mod.synthesize(text, voice=voice)
except ImportError as e:
return {"ok": False, "error": f"tools/tts.py nu poate fi importat: {e}"}
except Exception as e:
return {"ok": False, "error": str(e)}
def _extract_url_text(url: str) -> str | None:
"""Extrage textul principal dintr-un URL cu trafilatura."""
try:
import trafilatura
downloaded = trafilatura.fetch_url(url)
if not downloaded:
return None
return trafilatura.extract(downloaded)
except ImportError:
log.warning("trafilatura nu e instalat; folosesc fallback httpx")
return _extract_url_text_fallback(url)
except Exception as e:
log.warning("Extragere URL eșuată pentru %s: %s", url, e)
return None
def _extract_url_text_fallback(url: str) -> str | None:
"""Fallback simplu: fetch + strip HTML."""
try:
import re
import httpx as _httpx
resp = _httpx.get(url, timeout=15, follow_redirects=True)
resp.raise_for_status()
text = re.sub(r"<[^>]+>", " ", resp.text)
text = re.sub(r"\s+", " ", text).strip()
return text[:5000] if text else None
except Exception:
return None
def _claude_summarize(text: str) -> str | None:
"""Rezumă text cu claude -p (one-shot, fără sesiune)."""
prompt = (
"Rezumă în maxim 200 de cuvinte, în română, "
f"păstrând informațiile cheie:\n\n{text[:5000]}"
)
try:
result = subprocess.run(
["claude", "-p", prompt],
capture_output=True, text=True, timeout=90,
cwd=str(PROJECT_ROOT),
)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip()
return None
except Exception as e:
log.warning("Claude summarize eșuat: %s", e)
return None
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
@@ -705,6 +877,7 @@ COMMANDS: dict[str, Callable] = {
"doctor": cmd_doctor,
"heartbeat": cmd_heartbeat,
"help": cmd_help,
"audio": cmd_audio,
}