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
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,3 +34,6 @@ approved-tasks.json
|
||||
dashboard/status.json
|
||||
tools/anaf-monitor/monitor.log
|
||||
models/
|
||||
|
||||
# Backup-uri de stare Baileys — conțin creds.json (chei de sesiune WhatsApp)
|
||||
bridge/whatsapp/auth.bak-*/
|
||||
|
||||
@@ -1,106 +1,187 @@
|
||||
#!/usr/bin/env bash
|
||||
# Descarcă un video (Facebook, YouTube etc.), extrage audio, transcrie cu Whisper.
|
||||
# Usage: ./transcribe_video.sh <URL> [language]
|
||||
# ./transcribe_video.sh <URL> [language] --save-kb
|
||||
#
|
||||
# Usage: ./transcribe_video.sh <URL> [language] [--save-kb] [--bg] [--notify <channel_id>]
|
||||
#
|
||||
# language cod ISO (ro, en, ...) sau "auto" (implicit) pentru detecție automată.
|
||||
# NU forța limba dacă nu ești sigur: `ro` pe audio englezesc face
|
||||
# Whisper să halucineze text incoerent (incident 2026-08-31).
|
||||
# --save-kb scrie notița în memory/kb/<categorie>/ și reindexează.
|
||||
# --bg rulează detașat și întoarce imediat controlul. Obligatoriu când
|
||||
# scriptul e chemat dintr-un turn de chat: turnul are timeout de
|
||||
# 300s (DEFAULT_TIMEOUT, src/claude_session.py) iar un video lung
|
||||
# îl depășește, procesul e omorât la mijloc și userul nu primește
|
||||
# nimic. Cu --bg turnul răspunde imediat, jobul continuă singur.
|
||||
# --notify channel ID Discord unde se raportează finalizarea (util cu --bg).
|
||||
#
|
||||
# Exemple:
|
||||
# ./transcribe_video.sh "https://www.facebook.com/share/v/1EdPt3q2sq/"
|
||||
# ./transcribe_video.sh "https://www.facebook.com/share/r/1akfPJYvTw/" ro --save-kb
|
||||
# ./transcribe_video.sh "https://youtu.be/xyz" ro
|
||||
# ./transcribe_video.sh "https://youtu.be/xyz" auto --save-kb --bg --notify 1471916752119009432
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
URL="${1:-}"
|
||||
LANG="${2:-en}"
|
||||
SAVE_KB=0
|
||||
|
||||
# Parse flags
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "--save-kb" ]]; then
|
||||
SAVE_KB=1
|
||||
fi
|
||||
done
|
||||
|
||||
WORKDIR="/tmp/transcribe_$$"
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
# Whisper trăiește în venv, nu în python-ul de sistem. Scriptul mergea înainte
|
||||
# doar din noroc, când era chemat dintr-un shell cu venv-ul deja activat.
|
||||
PY="$PROJECT_ROOT/.venv/bin/python3"
|
||||
KB_DIR="$PROJECT_ROOT/memory/kb"
|
||||
LOG_DIR="$PROJECT_ROOT/logs/transcribe"
|
||||
|
||||
URL=""
|
||||
LANG="auto"
|
||||
SAVE_KB=0
|
||||
BG=0
|
||||
NOTIFY=""
|
||||
|
||||
# --- parse argumente (poziționale: URL, apoi limba; restul flag-uri) ---
|
||||
POSITIONAL=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--save-kb) SAVE_KB=1; shift ;;
|
||||
--bg) BG=1; shift ;;
|
||||
--notify) NOTIFY="${2:-}"; shift 2 ;;
|
||||
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
|
||||
*) POSITIONAL+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
URL="${POSITIONAL[0]:-}"
|
||||
LANG="${POSITIONAL[1]:-auto}"
|
||||
|
||||
if [[ -z "$URL" ]]; then
|
||||
echo "Usage: $0 <URL> [language (default: en)] [--save-kb]"
|
||||
echo "Usage: $0 <URL> [language|auto] [--save-kb] [--bg] [--notify <channel_id>]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PATH="/home/moltbot/bin:$PATH"
|
||||
|
||||
# --- mod background: re-exec detașat, întoarce imediat ---
|
||||
if [[ "$BG" == "1" ]]; then
|
||||
mkdir -p "$LOG_DIR"
|
||||
JOB_ID="$(date +%Y%m%d-%H%M%S)-$$"
|
||||
JOB_LOG="$LOG_DIR/$JOB_ID.log"
|
||||
ARGS=("$URL" "$LANG")
|
||||
[[ "$SAVE_KB" == "1" ]] && ARGS+=(--save-kb)
|
||||
[[ -n "$NOTIFY" ]] && ARGS+=(--notify "$NOTIFY")
|
||||
setsid nohup "$0" "${ARGS[@]}" > "$JOB_LOG" 2>&1 &
|
||||
echo "→ Job pornit în background: $JOB_ID (pid $!)"
|
||||
echo " Log: $JOB_LOG"
|
||||
[[ -n "$NOTIFY" ]] && echo " Raportez pe canalul $NOTIFY la final."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- curăță workdir-uri orfane (trap-ul EXIT nu rulează la SIGKILL) ---
|
||||
find /tmp -maxdepth 1 -type d -name 'transcribe_*' -mmin +60 -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
WORKDIR="/tmp/transcribe_$$"
|
||||
mkdir -p "$WORKDIR"
|
||||
trap 'rm -rf "$WORKDIR"' EXIT
|
||||
|
||||
notify() {
|
||||
[[ -z "$NOTIFY" ]] && return 0
|
||||
"$PY" "$PROJECT_ROOT/tools/discord_send_file.py" \
|
||||
--channel "$NOTIFY" --text "$1" >/dev/null 2>&1 || true
|
||||
}
|
||||
fail() {
|
||||
echo "Eroare: $1"
|
||||
notify "❌ Transcriere eșuată pentru $URL — $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "→ Obțin informații video..."
|
||||
INFO_JSON=$(yt-dlp "$URL" --dump-json --no-download -q 2>/dev/null || echo "{}")
|
||||
TITLE=$(echo "$INFO_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('title','Unknown'))" 2>/dev/null || echo "Unknown")
|
||||
# Trunchiază titlul la primul '|' sau la 80 caractere (Facebook posts au titlul = descriere)
|
||||
TITLE_SHORT=$(echo "$TITLE" | python3 -c "
|
||||
import sys
|
||||
s = sys.stdin.read().strip()
|
||||
if '|' in s:
|
||||
s = s.split('|')[0].strip()
|
||||
if len(s) > 80:
|
||||
s = s[:77].rstrip() + '...'
|
||||
print(s)
|
||||
")
|
||||
CREATOR=$(echo "$INFO_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('uploader') or d.get('channel') or '')" 2>/dev/null || echo "")
|
||||
DURATION=$(echo "$INFO_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); s=d.get('duration',0); print(f'{s//60}:{s%60:02d}')" 2>/dev/null || echo "?")
|
||||
# JSON-ul merge pe disc, nu printr-o variabilă de shell: descrierile Facebook conțin
|
||||
# caractere de control și emoji care corup variabila (zsh: "character not in range"),
|
||||
# iar `|| echo Unknown` din versiunea veche masca eșecul în loc să-l semnaleze.
|
||||
yt-dlp "$URL" --dump-json --no-download -q > "$WORKDIR/info.json" 2>/dev/null || echo '{}' > "$WORKDIR/info.json"
|
||||
|
||||
# Titlul, creatorul și durata într-o singură trecere.
|
||||
# Facebook împachetează titlul ca "830K views · 15K reactions | Titlul real | Pagina | ...";
|
||||
# luarea oarbă a primului segment producea notițe numite "807k-views-15k-reactions"
|
||||
# (incident 2026-08-31), deci sărim segmentele de statistici și pe cel egal cu numele paginii.
|
||||
eval "$("$PY" - "$WORKDIR/info.json" <<'PYMETA'
|
||||
import json, re, shlex, sys
|
||||
|
||||
try:
|
||||
d = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
except Exception:
|
||||
d = {}
|
||||
|
||||
creator = (d.get("uploader") or d.get("channel") or "").strip()
|
||||
secs = int(d.get("duration") or 0)
|
||||
duration = f"{secs // 60}:{secs % 60:02d}" if secs else "?"
|
||||
|
||||
raw = (d.get("title") or "Unknown").strip()
|
||||
STATS = re.compile(r"^[\d.,]+\s*[KMB]?\s*(views|reactions|comments|shares|likes)\b", re.I)
|
||||
parts = [p.strip() for p in raw.split("|") if p.strip()]
|
||||
title = next(
|
||||
(p for p in parts if not STATS.match(p) and p.lower() != creator.lower() and len(p) > 3),
|
||||
parts[0] if parts else raw,
|
||||
)
|
||||
title = " ".join(title.split())
|
||||
if len(title) > 80:
|
||||
title = title[:77].rstrip() + "..."
|
||||
|
||||
for name, val in (("TITLE_SHORT", title), ("CREATOR", creator), ("DURATION", duration)):
|
||||
print(f"{name}={shlex.quote(val)}")
|
||||
PYMETA
|
||||
)"
|
||||
|
||||
echo "→ Descarc video: $TITLE_SHORT..."
|
||||
yt-dlp "$URL" -o "$WORKDIR/video.%(ext)s" --no-playlist -q
|
||||
yt-dlp "$URL" -o "$WORKDIR/video.%(ext)s" --no-playlist -q || fail "descărcarea a eșuat"
|
||||
|
||||
VIDEO_FILE=$(ls "$WORKDIR"/video.* 2>/dev/null | head -1)
|
||||
if [[ -z "$VIDEO_FILE" ]]; then
|
||||
echo "Eroare: descărcarea a eșuat."
|
||||
exit 1
|
||||
fi
|
||||
[[ -z "$VIDEO_FILE" ]] && fail "descărcarea a eșuat (niciun fișier)"
|
||||
|
||||
echo "→ Extrag audio..."
|
||||
ffmpeg -i "$VIDEO_FILE" -vn -acodec pcm_s16le -ar 16000 -ac 1 "$WORKDIR/audio.wav" -y -loglevel error
|
||||
ffmpeg -i "$VIDEO_FILE" -vn -acodec pcm_s16le -ar 16000 -ac 1 "$WORKDIR/audio.wav" -y -loglevel error \
|
||||
|| fail "extragerea audio a eșuat"
|
||||
|
||||
echo "→ Transcriu cu Whisper (model: small, limbă: $LANG)..."
|
||||
TRANSCRIPT=$(python3 -c "
|
||||
import whisper
|
||||
model = whisper.load_model('small')
|
||||
result = model.transcribe('$WORKDIR/audio.wav', language='$LANG')
|
||||
print(result['text'])
|
||||
" 2>/dev/null)
|
||||
# cpu_threads = core-uri FIZICE; hyperthread-urile încetinesc int8 compute-bound.
|
||||
THREADS=$(lscpu -p=CORE 2>/dev/null | grep -v '^#' | sort -u | wc -l)
|
||||
[[ -z "$THREADS" || "$THREADS" -lt 1 ]] && THREADS=4
|
||||
|
||||
echo "→ Transcriu cu faster-whisper (model: small, int8, ${THREADS} threads, limbă: $LANG)..."
|
||||
# faster-whisper (declarat în requirements.txt) în locul openai-whisper: ~5x realtime
|
||||
# pe acest CPU vs. peste 300s pentru 9 minute de audio — exact ce depășea timeout-ul.
|
||||
"$PY" - "$WORKDIR/audio.wav" "$LANG" "$THREADS" > "$WORKDIR/transcript.txt" 2>"$WORKDIR/whisper.err" <<'PYEOF' || fail "transcrierea a eșuat ($(tail -1 "$WORKDIR/whisper.err" 2>/dev/null))"
|
||||
import sys
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
wav, lang, threads = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
||||
model = WhisperModel("small", device="cpu", compute_type="int8", cpu_threads=threads)
|
||||
segments, info = model.transcribe(wav, language=None if lang == "auto" else lang, vad_filter=True)
|
||||
text = " ".join(s.text.strip() for s in segments)
|
||||
print(text)
|
||||
print(f"[detected_language={info.language}]", file=sys.stderr)
|
||||
PYEOF
|
||||
|
||||
TRANSCRIPT=$(cat "$WORKDIR/transcript.txt")
|
||||
DETECTED=$(grep -oP 'detected_language=\K\w+' "$WORKDIR/whisper.err" 2>/dev/null || echo "$LANG")
|
||||
[[ -z "$TRANSCRIPT" ]] && fail "transcrierea a ieșit goală"
|
||||
|
||||
echo ""
|
||||
echo "=== $TITLE ==="
|
||||
echo "=== $TITLE_SHORT ==="
|
||||
echo "$TRANSCRIPT"
|
||||
echo ""
|
||||
echo "✓ Transcriere completă."
|
||||
echo "✓ Transcriere completă (limbă: $DETECTED)."
|
||||
|
||||
if [[ "$SAVE_KB" == "1" ]]; then
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
|
||||
# Slug din titlu scurt: lowercase, fără diacritice, doar alfanumerice și cratime
|
||||
SLUG=$(echo "$TITLE_SHORT" | python3 -c "
|
||||
SLUG=$(echo "$TITLE_SHORT" | "$PY" -c "
|
||||
import sys, re, unicodedata
|
||||
s = sys.stdin.read().strip()
|
||||
s = unicodedata.normalize('NFD', s)
|
||||
s = ''.join(c for c in s if unicodedata.category(c) != 'Mn')
|
||||
s = s.lower()
|
||||
s = re.sub(r'[^a-z0-9]+', '-', s)
|
||||
s = s.strip('-')[:50]
|
||||
print(s)
|
||||
s = unicodedata.normalize('NFD', sys.stdin.read().strip())
|
||||
s = ''.join(c for c in s if unicodedata.category(c) != 'Mn').lower()
|
||||
print(re.sub(r'[^a-z0-9]+', '-', s).strip('-')[:50])
|
||||
")
|
||||
[[ -z "$SLUG" ]] && SLUG="video-$(date +%H%M%S)"
|
||||
|
||||
# Detectează categoria din URL
|
||||
if echo "$URL" | grep -qi "facebook\.com"; then
|
||||
CATEGORY="facebook"
|
||||
FORMAT="Reel (~${DURATION} min)"
|
||||
CATEGORY="facebook"; FORMAT="Reel (~${DURATION} min)"
|
||||
elif echo "$URL" | grep -qi "youtube\.com\|youtu\.be"; then
|
||||
CATEGORY="youtube"
|
||||
FORMAT="Video (~${DURATION} min)"
|
||||
CATEGORY="youtube"; FORMAT="Video (~${DURATION} min)"
|
||||
else
|
||||
CATEGORY="media"
|
||||
FORMAT="Video (~${DURATION} min)"
|
||||
CATEGORY="media"; FORMAT="Video (~${DURATION} min)"
|
||||
fi
|
||||
|
||||
NOTE_DIR="$KB_DIR/$CATEGORY"
|
||||
@@ -114,6 +195,7 @@ print(s)
|
||||
**Data:** $DATE
|
||||
**Creator:** $CREATOR
|
||||
**Format:** $FORMAT
|
||||
**Limbă:** $DETECTED
|
||||
**Tags:** @coaching
|
||||
|
||||
---
|
||||
@@ -131,9 +213,13 @@ NOTEEOF
|
||||
|
||||
echo ""
|
||||
echo "→ Notiță salvată: $NOTE_FILE"
|
||||
|
||||
echo "→ Reindexez KB..."
|
||||
python3 "$PROJECT_ROOT/tools/update_notes_index.py"
|
||||
|
||||
echo "✓ KB actualizat. Link: /echo/files.html#memory/kb/$CATEGORY/${DATE}_${SLUG}.md"
|
||||
"$PY" "$PROJECT_ROOT/tools/update_notes_index.py" >/dev/null
|
||||
KB_LINK="https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/$CATEGORY/${DATE}_${SLUG}.md"
|
||||
echo "✓ KB actualizat. Link: $KB_LINK"
|
||||
notify "✅ Transcriere gata: **$TITLE_SHORT** (${DURATION} min, $DETECTED)
|
||||
$KB_LINK
|
||||
⚠️ TL;DR-ul e gol — completează-l."
|
||||
else
|
||||
notify "✅ Transcriere gata: **$TITLE_SHORT** (${DURATION} min, $DETECTED). Rulat fără --save-kb."
|
||||
fi
|
||||
|
||||
@@ -63,7 +63,7 @@ Lecții capturate din corectările lui Marius. Citește acest fișier la începu
|
||||
**Data:** 2026-04-29
|
||||
**Context:** Salvam o notiță din Facebook reel în memory/kb/. Am adăugat manual o intrare în index.json cu schema greșită (`id` + `path` în loc de `file`), ceea ce a blocat notes.html pe "Se încarcă..." cu un TypeError în renderNoteCard.
|
||||
**Greșeala:** Am editat index.json direct, cu o schemă diferită față de ce produce update_notes_index.py.
|
||||
**Regula:** Niciodată nu scriei manual în `memory/kb/index.json`. Fluxul corect: (1) creezi fișierul `.md` în `memory/kb/<categorie>/`, (2) rulezi `python3 tools/update_notes_index.py`. Dacă ai nevoie să salvezi o notiță din Facebook/video, folosești `scripts/transcribe_video.sh <URL> <lang> --save-kb` care face totul corect.
|
||||
**Regula:** Niciodată nu scriei manual în `memory/kb/index.json`. Fluxul corect: (1) creezi fișierul `.md` în `memory/kb/<categorie>/`, (2) rulezi `python3 tools/update_notes_index.py`. Dacă ai nevoie să salvezi o notiță din Facebook/video, folosești `scripts/transcribe_video.sh <URL> auto --save-kb --bg --notify <channel_id>` care face totul corect (`--bg` e obligatoriu dintr-un turn de chat — vezi lecția despre timeout-ul de 300s).
|
||||
**Când se aplică:** Orice salvare de notiță în KB (Facebook, YouTube, coaching, insights, orice). Dacă ești tentat să `json.dump` în index.json — stop, rulează scriptul.
|
||||
|
||||
## Verifică că modelul/tool-ul numit chiar are capabilitatea ÎNAINTE de a planifica în jurul lui
|
||||
@@ -86,3 +86,20 @@ Lecții capturate din corectările lui Marius. Citește acest fișier la începu
|
||||
**Greșeala:** Am optimizat pentru „să se audă ceva" fără să întreb ce dimensiune e prioritară pentru user. Vocea clonată E produsul; schimbarea ei e regresia maximă percepută.
|
||||
**Regula:** Când un input parțial invalid ajunge la un sistem cu identitate configurată de user (voce, persona, stil), remediază conținutul ca să treacă (transliterare, sanitizare), nu comuta pe altă identitate. Fallback pe alt engine/voce doar la eșec TEHNIC (server picat), nu la eșec de conținut.
|
||||
**Când se aplică:** Orice pipeline TTS/persona cu fallback. Implementare: `fold_ro_diacritics` în tools/tts.py + push_text în src/voice/tts_stream.py.
|
||||
|
||||
## Munca mai lungă de 300s nu are ce căuta într-un turn de chat — rulează detașat și raportează pe canal
|
||||
**Data:** 2026-08-31
|
||||
**Context:** Marius a trimis două linkuri de video Facebook pe Discord. Niciunul n-a produs un răspuns. În jurnal: `Claude CLI timed out after 300s`, de cinci ori, pe patru încercări diferite. `scripts/transcribe_video.sh` rula în interiorul turnului, iar yt-dlp + ffmpeg + openai-whisper pe 9 minute de audio depășeau constant `DEFAULT_TIMEOUT` (`src/claude_session.py:37`). Procesul era omorât la mijlocul Whisper-ului.
|
||||
**Greșeala:** Am tratat o sarcină cu durată nemărginită (descărcare rețea + inferență ML) ca pe un apel sincron într-un turn conversațional. Efectele în cascadă au fost mai rele decât timeout-ul în sine:
|
||||
- **Retry-uri oarbe pe linkul greșit** — la reîncercare am reprocesat linkul 1 de două ori (două notițe duplicate) și n-am procesat niciodată linkul 2. Când mai multe linkuri eșuează, verifică *care* a eșuat înainte să reiei.
|
||||
- **Documente pe jumătate** — scriptul scrie template-ul cu TL;DR gol, iar rezumatul îl completez eu *după* ce scriptul întoarce. Procesul omorât ⇒ notițe cu TL;DR gol, care arată „gata" în index dar nu sunt.
|
||||
- **Orfani pe disc** — `trap ... EXIT` nu rulează la SIGKILL; rămâneau 33MB în `/tmp/transcribe_$$`.
|
||||
- **Eșec tăcut la nivel de user** — Marius n-a primit nici rezultat, nici eroare acționabilă.
|
||||
**Regula:** Dacă un pas poate depăși ~2-3 minute, nu-l rula în turn. Pornește-l detașat (`setsid nohup`), întoarce imediat un confirm cu job id + cale de log, și raportează finalizarea pe canal (`tools/discord_send_file.py --text`). Turnul confirmă *pornirea*, nu *terminarea*.
|
||||
**Corolare descoperite la fix:**
|
||||
- **Nu forța limba.** Rularea cu `ro` pe audio englezesc a produs transcriere halucinată (română inventată amestecată cu CJK). Implicit e acum `auto`; forțează doar dacă ești sigur.
|
||||
- **Alege motorul înainte de a ridica timeout-ul.** faster-whisper int8 face 554s de audio în 113s (~5x realtime); openai-whisper depășea 300s pe același fișier. `faster-whisper` era deja în `requirements.txt`, openai-whisper nici măcar nu era declarat. Un motor de 5x a fost fix mai bun decât un timeout mai mare.
|
||||
- **Folosește `.venv/bin/python3` explicit în scripturi shell.** Scriptul chema `python3` bare, iar `whisper` nu există în python-ul de sistem — mergea doar din noroc, când era chemat cu venv-ul activat.
|
||||
- **Nu trece JSON mare printr-o variabilă de shell.** Descrierile Facebook conțin caractere de control care corup variabila (`character not in range`); scrie-l pe disc și parsează fișierul o singură dată. `|| echo Unknown` masca eșecul.
|
||||
- **Titlurile Facebook încep cu statistici,** nu cu titlul: `830K views · 15K reactions | Titlul real | Pagina`. `split('|')[0]` producea notițe numite `2026-08-31_807k-views-15k-reactions.md`. Sari segmentele de statistici și pe cel egal cu numele paginii.
|
||||
**Când se aplică:** Orice transcriere video/audio, OCR pe fișiere mari, scraping multi-pagină, build sau inferență ML pornită dintr-un mesaj de chat. Implementare: `scripts/transcribe_video.sh` (`--bg`, `--notify`) + `send_text()` în `tools/discord_send_file.py`.
|
||||
|
||||
@@ -58,13 +58,47 @@ def send_file(channel_id: str, path: str, content: str | None = None, filename:
|
||||
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", required=True, help="Cale către fișierul de trimis")
|
||||
parser.add_argument("--text", default=None, help="Text opțional care însoțește fișierul")
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user