Files
echo-core/tools/discord_send_file.py
Marius Mutu cb2eedac86 fix(transcribe): rulare detașată, ca joburile video să nu mai moară la timeout
Două linkuri Facebook trimise pe Discord n-au produs niciun răspuns: scriptul
rula în interiorul turnului de chat, iar yt-dlp + ffmpeg + whisper depășeau
constant DEFAULT_TIMEOUT (300s, src/claude_session.py:37). Procesul era omorât
la mijlocul transcrierii, de cinci ori pe patru încercări.

Efecte în cascadă, toate reparate aici:
- retry-uri oarbe: linkul 1 reprocesat de două ori (notițe duplicate), linkul 2
  niciodată atins
- notițe cu TL;DR gol — arată „gata" în index dar nu sunt
- orfani de 33MB în /tmp (trap EXIT nu rulează la SIGKILL)
- userul nu primea nici rezultat, nici eroare acționabilă

Schimbări:
- --bg (setsid nohup) + --notify <channel_id>; turnul confirmă pornirea, nu
  terminarea. send_text() nou în tools/discord_send_file.py, pentru că un job
  detașat supraviețuiește turnului și nu mai poate raporta prin on_text.
- faster-whisper int8 în locul openai-whisper: 554s audio în 113s (~5x realtime)
  vs. peste 300s. Era deja în requirements.txt; openai-whisper nici măcar nu era
  declarat. cpu_threads pe core-uri fizice, nu logice.
- limba implicită „auto": rularea forțată cu `ro` pe audio englezesc producea
  transcriere halucinată (română inventată amestecată cu CJK)
- .venv/bin/python3 explicit; whisper nu există în python-ul de sistem, scriptul
  mergea doar când era chemat cu venv-ul activat
- metadata yt-dlp pe disc, parsată o dată: descrierile Facebook conțin caractere
  de control care corup variabila de shell, iar `|| echo Unknown` masca eșecul
- titlul sare segmentele de statistici: Facebook emite „830K views · 15K
  reactions | Titlul real | Pagina", iar split('|')[0] producea notițe numite
  2026-08-31_807k-views-15k-reactions.md
- curăță workdir-urile orfane mai vechi de 60 min la pornire

.gitignore: bridge/whatsapp/auth.bak-*/ — backup-urile Baileys conțin creds.json
(chei de sesiune vii), la fel ca auth/ care era deja ignorat.

Verificat end-to-end pe ambele linkuri: titlu corect, limbă en auto-detectată,
notiță + reindex + link, trap curăță /tmp, zero orfani.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188YmDTUAzVvZDVh8JL8xSa
2026-08-31 21:52:30 +00:00

105 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Trimite un fișier (audio, imagine, etc.) direct într-un canal Discord.
De ce există: `__AUDIO__:<path>` (folosit de fast_commands.py) e interceptat
de discord_bot.py DOAR când niciun bloc de text n-a fost deja streamat pe
canal în turul curent. Într-un răspuns conversațional normal (acest proces,
`claude_session.py`), fiecare bloc de text pe care Claude îl produce e
trimis live prin `on_text` înainte ca răspunsul final să fie evaluat —
deci prefixul `__AUDIO__:` ajunge mereu trimis ca text brut, nu convertit
în attachment. Singura cale de a atașa un fișier dintr-un răspuns Claude
obișnuit e apelul direct la REST API-ul Discord, cu tokenul botului.
CLI:
python3 tools/discord_send_file.py --channel <id> --file /path/audio.wav [--text "mesaj"]
Modul:
from tools.discord_send_file import send_file
send_file(channel_id, "/path/audio.wav", content="mesaj opțional")
Channel ID: dacă nu-l știi din conversație, citește `sessions/active.json`
(cheia e channel_id-ul; de obicei există o singură sesiune activă).
"""
import argparse
import mimetypes
import sys
import httpx
sys.path.insert(0, "/home/moltbot/echo-core")
from src.credential_store import get_secret
def send_file(channel_id: str, path: str, content: str | None = None, filename: str | None = None) -> dict:
"""Trimite un fișier direct pe canalul Discord dat, via REST API.
Returns: {"ok": True, "message_id": "..."} sau {"ok": False, "error": "..."}
"""
token = get_secret("discord_token")
if not token:
return {"ok": False, "error": "discord_token lipsește din keyring (echo-core)."}
filename = filename or path.rsplit("/", 1)[-1]
mime = mimetypes.guess_type(filename)[0] or "application/octet-stream"
try:
with httpx.Client(timeout=30) as client, open(path, "rb") as f:
resp = client.post(
f"https://discord.com/api/v10/channels/{channel_id}/messages",
headers={"Authorization": f"Bot {token}"},
files={"file": (filename, f, mime)},
data={"content": content} if content else {},
)
if resp.status_code not in (200, 201):
return {"ok": False, "error": f"HTTP {resp.status_code}: {resp.text[:300]}"}
return {"ok": True, "message_id": resp.json().get("id")}
except Exception as e:
return {"ok": False, "error": str(e)}
def send_text(channel_id: str, content: str) -> dict:
"""Trimite un mesaj text pe canalul Discord dat, via REST API.
De ce există pe lângă `send_file`: joburile lungi rulate detașat (ex.
`scripts/transcribe_video.sh --bg`) supraviețuiesc turnului de chat care
le-a pornit, deci nu mai au cum să raporteze prin `on_text`. Au nevoie de
o cale directă către canal, exact ca attachment-urile.
Returns: {"ok": True, "message_id": "..."} sau {"ok": False, "error": "..."}
"""
token = get_secret("discord_token")
if not token:
return {"ok": False, "error": "discord_token lipsește din keyring (echo-core)."}
try:
with httpx.Client(timeout=30) as client:
resp = client.post(
f"https://discord.com/api/v10/channels/{channel_id}/messages",
headers={"Authorization": f"Bot {token}"},
json={"content": content[:2000]},
)
if resp.status_code not in (200, 201):
return {"ok": False, "error": f"HTTP {resp.status_code}: {resp.text[:300]}"}
return {"ok": True, "message_id": resp.json().get("id")}
except Exception as e:
return {"ok": False, "error": str(e)}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Trimite un fișier direct pe un canal Discord")
parser.add_argument("--channel", required=True, help="Channel ID Discord")
parser.add_argument("--file", default=None, help="Cale către fișierul de trimis (opțional)")
parser.add_argument("--text", default=None, help="Text; singur = mesaj simplu, cu --file = caption")
args = parser.parse_args()
if not args.file and not args.text:
parser.error("dă cel puțin --file sau --text")
if args.file:
result = send_file(args.channel, args.file, content=args.text)
else:
result = send_text(args.channel, args.text)
print(result)
sys.exit(0 if result.get("ok") else 1)