Compare commits

...

4 Commits

Author SHA1 Message Date
44cf0001bb chore: auto-commit from dashboard 2026-05-27 06:12:13 +00:00
574f9be5ea feat(discord): add /audio slash command with voce + text_sau_url params
Adds missing /audio slash command on Discord with:
- voce: optional choices M1-M5 / F1-F5 with descriptions
- text_sau_url: optional text or URL input
- handles __AUDIO__: response by sending WAV as file attachment

Telegram already had /audio fully implemented.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 06:00:54 +00:00
0d2d5b860d chore(tts): schimbă vocea default din M1 în M2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 05:56:55 +00:00
8fe39adc01 fix(tts): trimite lang=ro explicit la Supertonic API
Parametrul `lang` era definit (DEFAULT_LANG = "ro") dar nu era inclus
in request-ul HTTP catre /v1/audio/speech. Adaugat "lang": lang in
body-ul JSON si lang="ro" explicit in _tts_synthesize().

OpenAPI-ul Supertonic confirma ca /v1/audio/speech accepta `lang`
ca parametru optional (OpenAISpeechRequest schema).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 05:47:39 +00:00
7 changed files with 83 additions and 28 deletions

View File

@@ -269,9 +269,9 @@
"prompt": "Heartbeat check. Rulează src/heartbeat.py printr-un scurt raport de status.\nDacă nu e nimic de raportat (email=0, calendar nu are evenimente <2h, kb ok), răspunde doar cu HEARTBEAT_OK și oprește-te — nu trimite mesaj.\nDacă e ceva: raport scurt pe Discord #echo-work.", "prompt": "Heartbeat check. Rulează src/heartbeat.py printr-un scurt raport de status.\nDacă nu e nimic de raportat (email=0, calendar nu are evenimente <2h, kb ok), răspunde doar cu HEARTBEAT_OK și oprește-te — nu trimite mesaj.\nDacă e ceva: raport scurt pe Discord #echo-work.",
"allowed_tools": [], "allowed_tools": [],
"enabled": true, "enabled": true,
"last_run": "2026-05-26T18:00:00.002989+00:00", "last_run": "2026-05-27T06:00:00.002154+00:00",
"last_status": "error", "last_status": "ok",
"next_run": "2026-05-27T06:00:00+00:00" "next_run": "2026-05-27T08:00:00+00:00"
}, },
{ {
"name": "night-execute", "name": "night-execute",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

BIN
image.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -909,6 +909,55 @@ def create_bot(config: Config) -> discord.Client:
f"Error reading logs: {e}", ephemeral=True f"Error reading logs: {e}", ephemeral=True
) )
@tree.command(name="audio", description="TTS: convertește text sau URL în voice note")
@app_commands.describe(
voce="Voce (M1-M5 masculin, F1-F5 feminin; default M2)",
text_sau_url="Text direct, URL articol, sau gol pentru ultimul răspuns Echo",
rezumat="Dacă să facă Claude rezumat înainte de TTS (doar pentru URL)",
)
@app_commands.choices(
voce=[
app_commands.Choice(name="M1 — Masculin 1", value="M1"),
app_commands.Choice(name="M2 — Masculin 2 (default)", value="M2"),
app_commands.Choice(name="M3 — Masculin 3", value="M3"),
app_commands.Choice(name="M4 — Masculin 4", value="M4"),
app_commands.Choice(name="M5 — Masculin 5", value="M5"),
app_commands.Choice(name="F1 — Feminin 1", value="F1"),
app_commands.Choice(name="F2 — Feminin 2", value="F2"),
app_commands.Choice(name="F3 — Feminin 3", value="F3"),
app_commands.Choice(name="F4 — Feminin 4", value="F4"),
app_commands.Choice(name="F5 — Feminin 5", value="F5"),
]
)
async def audio_cmd(
interaction: discord.Interaction,
voce: str | None = None,
text_sau_url: str | None = None,
rezumat: bool = False,
) -> None:
await interaction.response.defer()
args: list[str] = []
if voce:
args.append(voce)
if text_sau_url:
args.extend(text_sau_url.split())
if rezumat:
args.append("rezumat")
result = await asyncio.to_thread(fast_dispatch, "audio", args)
if result and result.startswith("__AUDIO__:"):
wav_path = result[len("__AUDIO__:"):]
try:
await interaction.followup.send(
file=discord.File(wav_path, filename="echo-audio.wav")
)
finally:
try:
os.unlink(wav_path)
except OSError:
pass
else:
await interaction.followup.send(result or "Eroare TTS.")
# --- Ralph commands (autonomous project execution) --- # --- Ralph commands (autonomous project execution) ---
async def _autocomplete_by_status( async def _autocomplete_by_status(

View File

@@ -1135,7 +1135,7 @@ def create_telegram_bot(config: Config, token: str) -> Application:
BotCommand("logs", "Show log lines"), BotCommand("logs", "Show log lines"),
BotCommand("doctor", "Diagnostics"), BotCommand("doctor", "Diagnostics"),
BotCommand("heartbeat", "Health checks"), BotCommand("heartbeat", "Health checks"),
BotCommand("audio", "TTS: text → voice note"), BotCommand("audio", "TTS: text/url → voice note [voce] [rezumat]"),
BotCommand("p", "Ralph: propose new project"), BotCommand("p", "Ralph: propose new project"),
BotCommand("a", "Ralph: approve project for tonight"), BotCommand("a", "Ralph: approve project for tonight"),
BotCommand("l", "Ralph: list projects status"), BotCommand("l", "Ralph: list projects status"),

View File

@@ -691,9 +691,9 @@ Reminders:
Audio: Audio:
/audio <text> — TTS pe text /audio <text> — TTS pe text
/audio <url> — Extrage articol → audio /audio <url> — Extrage articol → audio
/audio rezumat <url> — Rezumat Claude → audio /audio <url> rezumat — Rezumat Claude → audio (flag oriunde)
/audio — Ultimul răspuns Echo → audio /audio — Ultimul răspuns Echo → audio
/audio M2 [text|url|gol] — Voce specificată (M1-M5, F1-F5) /audio M2 [text|url] [rezumat] — Voce specificată (M1-M5, F1-F5)
/audio ajutor — Ajutor detaliat /audio ajutor — Ajutor detaliat
Ops: Ops:
@@ -728,7 +728,7 @@ def cmd_audio(args: list[str]) -> str:
/audio M2 [text|url|gol] → voce specificată (M1-M5, F1-F5) /audio M2 [text|url|gol] → voce specificată (M1-M5, F1-F5)
/audio ajutor → ajutor /audio ajutor → ajutor
""" """
voice = "M1" voice = "M2"
remaining = list(args) remaining = list(args)
# Detectare voce ca prim token # Detectare voce ca prim token
@@ -736,6 +736,12 @@ def cmd_audio(args: list[str]) -> str:
voice = remaining[0].upper() voice = remaining[0].upper()
remaining = remaining[1:] remaining = remaining[1:]
# Detectare flag "rezumat" oriunde în args (indiferent de ordine)
do_summarize = False
if any(t.lower() == "rezumat" for t in remaining):
do_summarize = True
remaining = [t for t in remaining if t.lower() != "rezumat"]
channel_id = _get_ctx_channel() channel_id = _get_ctx_channel()
# Determinare text sursă # Determinare text sursă
@@ -750,30 +756,29 @@ def cmd_audio(args: list[str]) -> str:
elif len(remaining) == 1 and remaining[0].lower() == "ajutor": elif len(remaining) == 1 and remaining[0].lower() == "ajutor":
return ( return (
"🎙️ /audio — Text-to-Speech local (Supertonic)\n\n" "🎙️ /audio — Text-to-Speech local (Supertonic)\n\n"
" /audio <text> — TTS pe text dat\n" " /audio <text> — TTS pe text dat\n"
" /audio <url> — extrage articol → audio\n" " /audio <url> — extrage articol → audio\n"
" /audio rezumat <url> — rezumat Claude → audio\n" " /audio <url> rezumat — rezumat Claude → audio\n"
" /audio — ultimul răspuns Echo → audio\n" " /audio — ultimul răspuns Echo → audio\n"
" /audio M2 <...> — voce specifică (M1-M5, F1-F5)\n\n" " /audio M2 [text|url] [rezumat] — voce specifică (M1-M5, F1-F5)\n\n"
"Flag rezumat: poate fi pus oriunde în comandă\n"
" /audio rezumat <url> ≡ /audio <url> rezumat ≡ /audio M2 <url> rezumat\n\n"
"Voci: M1 M2 M3 M4 M5 (masculin) · F1 F2 F3 F4 F5 (feminin)" "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"): elif len(remaining) == 1 and remaining[0].startswith("http"):
url = remaining[0] url = remaining[0]
text = _extract_url_text(url) if do_summarize:
if not text: extracted = _extract_url_text(url)
return f"Nu am putut extrage text din URL: {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ă."
else:
text = _extract_url_text(url)
if not text:
return f"Nu am putut extrage text din URL: {url}"
else: else:
text = " ".join(remaining) text = " ".join(remaining)
@@ -800,7 +805,7 @@ def _tts_synthesize(text: str, voice: str) -> dict:
import tts as _tts_mod import tts as _tts_mod
# Re-import pentru a prinde modificări la hot-reload # Re-import pentru a prinde modificări la hot-reload
importlib.reload(_tts_mod) importlib.reload(_tts_mod)
return _tts_mod.synthesize(text, voice=voice) return _tts_mod.synthesize(text, voice=voice, lang="ro")
except ImportError as e: except ImportError as e:
return {"ok": False, "error": f"tools/tts.py nu poate fi importat: {e}"} return {"ok": False, "error": f"tools/tts.py nu poate fi importat: {e}"}
except Exception as e: except Exception as e:

View File

@@ -20,7 +20,7 @@ import httpx
SUPERTONIC_URL = "http://127.0.0.1:7788" SUPERTONIC_URL = "http://127.0.0.1:7788"
VOICES = {"M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5"} VOICES = {"M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5"}
DEFAULT_VOICE = "M1" DEFAULT_VOICE = "M2"
DEFAULT_LANG = "ro" DEFAULT_LANG = "ro"
@@ -46,6 +46,7 @@ def synthesize(text: str, voice: str = DEFAULT_VOICE, lang: str = DEFAULT_LANG)
"input": text, "input": text,
"voice": voice, "voice": voice,
"response_format": "wav", "response_format": "wav",
"lang": lang,
}, },
timeout=60.0, timeout=60.0,
) )