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
105 lines
4.3 KiB
Python
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)
|