Update TOOLS.md, cron jobs, KB index; add discord file sender + pocket-tts plan
Pre-existing work committed before starting Ralph self-improvement run on ralph/echo-improve branch, so that branch's diff stays isolated to the pocket-tts integration. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,9 +11,9 @@
|
|||||||
"report_on": "changes",
|
"report_on": "changes",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"last_run": "2026-07-10T10:00:00.001575+00:00",
|
"last_run": "2026-07-10T16:00:00.002193+00:00",
|
||||||
"last_status": "ok",
|
"last_status": "ok",
|
||||||
"next_run": "2026-07-10T16:00:00+00:00"
|
"next_run": "2026-07-11T10:00:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "security-audit-daily",
|
"name": "security-audit-daily",
|
||||||
@@ -43,9 +43,9 @@
|
|||||||
"report_on": "never",
|
"report_on": "never",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"last_run": "2026-07-10T03:30:00.002026+00:00",
|
"last_run": "2026-07-11T03:30:00.002049+00:00",
|
||||||
"last_status": "ok",
|
"last_status": "ok",
|
||||||
"next_run": "2026-07-11T03:30:00+00:00"
|
"next_run": "2026-07-12T03:30:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "archive-tasks-daily",
|
"name": "archive-tasks-daily",
|
||||||
@@ -59,9 +59,9 @@
|
|||||||
"report_on": "changes",
|
"report_on": "changes",
|
||||||
"timeout": 60,
|
"timeout": 60,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"last_run": "2026-07-10T03:00:00.002135+00:00",
|
"last_run": "2026-07-11T03:00:00.001905+00:00",
|
||||||
"last_status": "ok",
|
"last_status": "ok",
|
||||||
"next_run": "2026-07-11T03:00:00+00:00"
|
"next_run": "2026-07-12T03:00:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "backup-config",
|
"name": "backup-config",
|
||||||
@@ -75,9 +75,9 @@
|
|||||||
"report_on": "never",
|
"report_on": "never",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"last_run": "2026-07-10T02:00:00.002082+00:00",
|
"last_run": "2026-07-11T02:00:00.002411+00:00",
|
||||||
"last_status": "ok",
|
"last_status": "ok",
|
||||||
"next_run": "2026-07-11T02:00:00+00:00"
|
"next_run": "2026-07-12T02:00:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "insights-extract",
|
"name": "insights-extract",
|
||||||
@@ -243,9 +243,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-07-10T14:00:00.002633+00:00",
|
"last_run": "2026-07-11T06:00:00.002630+00:00",
|
||||||
"last_status": "ok",
|
"last_status": "ok",
|
||||||
"next_run": "2026-07-10T18:00:00+00:00"
|
"next_run": "2026-07-11T10:00:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "night-execute",
|
"name": "night-execute",
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
{
|
{
|
||||||
"notes": [
|
"notes": [
|
||||||
|
{
|
||||||
|
"file": "notes-data/projects/pocket-tts-integration-handoff-prompt.md",
|
||||||
|
"title": "pocket-tts-integration-handoff-prompt",
|
||||||
|
"date": "2026-07-11",
|
||||||
|
"tags": [],
|
||||||
|
"domains": [],
|
||||||
|
"types": [],
|
||||||
|
"category": "projects",
|
||||||
|
"project": null,
|
||||||
|
"subdir": null,
|
||||||
|
"video": "",
|
||||||
|
"tldr": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"file": "notes-data/youtube/2026-07-10_opencode-token-usage-96-percent.md",
|
"file": "notes-data/youtube/2026-07-10_opencode-token-usage-96-percent.md",
|
||||||
"title": "I Cut My OpenCode Token Usage by 96% - Here's How",
|
"title": "I Cut My OpenCode Token Usage by 96% - Here's How",
|
||||||
@@ -9727,8 +9740,8 @@
|
|||||||
"title": "Proiect: Vending Master - Integrare Website → ROA",
|
"title": "Proiect: Vending Master - Integrare Website → ROA",
|
||||||
"date": "2026-01-30",
|
"date": "2026-01-30",
|
||||||
"tags": [
|
"tags": [
|
||||||
"vending-master",
|
"integrare",
|
||||||
"integrare"
|
"vending-master"
|
||||||
],
|
],
|
||||||
"domains": [
|
"domains": [
|
||||||
"work"
|
"work"
|
||||||
@@ -10208,7 +10221,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"stats": {
|
"stats": {
|
||||||
"total": 590,
|
"total": 591,
|
||||||
"by_domain": {
|
"by_domain": {
|
||||||
"work": 202,
|
"work": 202,
|
||||||
"health": 104,
|
"health": 104,
|
||||||
@@ -10225,7 +10238,7 @@
|
|||||||
"facebook": 11,
|
"facebook": 11,
|
||||||
"health": 6,
|
"health": 6,
|
||||||
"insights": 46,
|
"insights": 46,
|
||||||
"projects": 234,
|
"projects": 235,
|
||||||
"reflectii": 3,
|
"reflectii": 3,
|
||||||
"retete": 1,
|
"retete": 1,
|
||||||
"tools": 7,
|
"tools": 7,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
- **[facebook/](facebook/index.md)** — 11 note
|
- **[facebook/](facebook/index.md)** — 11 note
|
||||||
- **[health/](health/index.md)** — 6 note
|
- **[health/](health/index.md)** — 6 note
|
||||||
- **[insights/](insights/index.md)** — 46 note
|
- **[insights/](insights/index.md)** — 46 note
|
||||||
- **[projects/](projects/index.md)** — 234 note
|
- **[projects/](projects/index.md)** — 235 note
|
||||||
- **[reflectii/](reflectii/index.md)** — 3 note
|
- **[reflectii/](reflectii/index.md)** — 3 note
|
||||||
- **[retete/](retete/index.md)** — 1 note
|
- **[retete/](retete/index.md)** — 1 note
|
||||||
- **[tools/](tools/index.md)** — 7 note
|
- **[tools/](tools/index.md)** — 7 note
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Index — projects/
|
# Index — projects/
|
||||||
|
|
||||||
> 234 note. Citește acest index întâi; deschide doar fișierele relevante.
|
> 235 note. Citește acest index întâi; deschide doar fișierele relevante.
|
||||||
|
|
||||||
- **[Feature: PDF Download Button in Files Dashboard](FEATURE-files-pdf-download.md)**
|
- **[Feature: PDF Download Button in Files Dashboard](FEATURE-files-pdf-download.md)**
|
||||||
- User is comfortable with multi-session handoff (can track progress across sub-agents)
|
- User is comfortable with multi-session handoff (can track progress across sub-agents)
|
||||||
@@ -356,6 +356,7 @@
|
|||||||
Marc e la stabilitate financiară. Ședința lucrează pe mila față de angajați — momentele în care "lași de la tine" și eviți limite ferme. Pri
|
Marc e la stabilitate financiară. Ședința lucrează pe mila față de angajați — momentele în care "lași de la tine" și eviți limite ferme. Pri
|
||||||
- **[Monica Ion - Povestea lui Marc - Episodul 9: Anxietatea, frica de control și pierdere](monica-ion/youtube/monica-ion-povestea-lui-marc-ep9-anxietatea.md)** `@growth @work @sprijin`
|
- **[Monica Ion - Povestea lui Marc - Episodul 9: Anxietatea, frica de control și pierdere](monica-ion/youtube/monica-ion-povestea-lui-marc-ep9-anxietatea.md)** `@growth @work @sprijin`
|
||||||
Marc revine la ședință devastat: un proiect european (30% din cifra de afaceri) a fost înghețat, clientul nu mai finanțează. Monica lucrează
|
Marc revine la ședință devastat: un proiect european (30% din cifra de afaceri) a fost înghețat, clientul nu mai finanțează. Monica lucrează
|
||||||
|
- **[pocket-tts-integration-handoff-prompt](pocket-tts-integration-handoff-prompt.md)**
|
||||||
- **[Proiect: Import Bonuri Fiscale via Telegram/WhatsApp → ROA](roa2web-telegram-import/README.md)** `@work`
|
- **[Proiect: Import Bonuri Fiscale via Telegram/WhatsApp → ROA](roa2web-telegram-import/README.md)** `@work`
|
||||||
Sistem pentru importul bonurilor fiscale de achiziție din Telegram/WhatsApp în contabilitatea ROA. OCR prin Doctr (cost zero, local). Integr
|
Sistem pentru importul bonurilor fiscale de achiziție din Telegram/WhatsApp în contabilitatea ROA. OCR prin Doctr (cost zero, local). Integr
|
||||||
- **[Flux Contabil - Import Bonuri Fiscale Achiziție](roa2web-telegram-import/flux-contabil.md)**
|
- **[Flux Contabil - Import Bonuri Fiscale Achiziție](roa2web-telegram-import/flux-contabil.md)**
|
||||||
@@ -450,5 +451,5 @@
|
|||||||
- **[Episodul 20 - Ce au în comun traderii profitabili](trading-basics/_duplicates/21-episodul-20-ce-au-n-comun-traderii-profitabili.md)**
|
- **[Episodul 20 - Ce au în comun traderii profitabili](trading-basics/_duplicates/21-episodul-20-ce-au-n-comun-traderii-profitabili.md)**
|
||||||
**Pentru conținut complet structurat (concepte, quote-uri, aplicații practice), consultă Episodul 9.**
|
**Pentru conținut complet structurat (concepte, quote-uri, aplicații practice), consultă Episodul 9.**
|
||||||
- **[22-episodul-19-motivul-cheie-f-r-de-care-nu-func-ioneaz-nicio-strategie-n-trading-](trading-basics/_duplicates/22-episodul-19-motivul-cheie-f-r-de-care-nu-func-ioneaz-nicio-strategie-n-trading-.md)**
|
- **[22-episodul-19-motivul-cheie-f-r-de-care-nu-func-ioneaz-nicio-strategie-n-trading-](trading-basics/_duplicates/22-episodul-19-motivul-cheie-f-r-de-care-nu-func-ioneaz-nicio-strategie-n-trading-.md)**
|
||||||
- **[Proiect: Vending Master - Integrare Website → ROA](vending-master/README.md)** `@work #vending-master #integrare`
|
- **[Proiect: Vending Master - Integrare Website → ROA](vending-master/README.md)** `@work #integrare #vending-master`
|
||||||
[conversations/2026-01-30-conversatie-completa.md](https://moltbot.tailf7372d.ts.net/echo/files.html#conversations/2026-01-30-conversatie-co
|
[conversations/2026-01-30-conversatie-completa.md](https://moltbot.tailf7372d.ts.net/echo/files.html#conversations/2026-01-30-conversatie-co
|
||||||
|
|||||||
54
memory/kb/projects/pocket-tts-integration-handoff-prompt.md
Normal file
54
memory/kb/projects/pocket-tts-integration-handoff-prompt.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
Vreau să integrezi pocket-tts (Kyutai) ca motor TTS nou în echo-core, alături de Supertonic (motorul actual). Task arhitectural, nu un fix simplu — **intră în plan mode înainte să atingi cod**, și trece planul prin `/plan-eng-review` (arhitectură, edge cases, cum ating fișierele existente fără să stric fluxul live) înainte de implementare. Nu implementa nimic până planul nu e aprobat.
|
||||||
|
|
||||||
|
## Context — ce există deja
|
||||||
|
|
||||||
|
- **Motor actual:** Supertonic, rulează ca serviciu systemd separat (`supertonic-tts.service`), HTTP pe `127.0.0.1:7788`. Wrapper: `tools/tts.py` (`synthesize(text, voice, lang)`).
|
||||||
|
- **Comandă on-demand:** `/audio` (Discord slash command, `src/adapters/discord_bot.py` ~L911-995) — parametri `voce` (M1-M5/F1-F5), `text_sau_url`, `rezumat`. Apelează `fast_dispatch("audio", ...)` din `src/fast_commands.py`, care întoarce `__AUDIO__:<path>` — interceptat de discord_bot.py și trimis ca attachment.
|
||||||
|
- **Mod live:** Discord Voice (`/voice join`), `src/voice/pipeline.py` + `src/voice/tts_stream.py` — streaming TTS pe clauze (`clause_segments()`), buget de latență strict (~5s perceput end-to-end, filler audio după 3s fără răspuns). Folosește `tools/tts.py` intern.
|
||||||
|
- **Credențiale:** keyring, serviciu `"echo-core"` (`src/credential_store.py`). Există deja `discord_token` și `hf_token` (HuggingFace, pentru modelul gated pocket-tts cu voice cloning — vezi mai jos).
|
||||||
|
|
||||||
|
## Research + teste deja făcute (nu le repeta)
|
||||||
|
|
||||||
|
Am evaluat deja pocket-tts manual într-o sesiune anterioară (vezi memory: `project_pocket_tts_voice_clone_eval.md`):
|
||||||
|
|
||||||
|
- **Nu suportă română oficial** — doar english, french, german, portuguese, italian, spanish. Testele confirmă: text românesc sintetizat cu modelul englez sună cu accent englezesc puternic.
|
||||||
|
- **Voice cloning funcționează bine** din sample-uri scurte (~4-35s testate), via `get_state_for_audio_prompt(path_sau_nume_voce_predefinita)`.
|
||||||
|
- **Viteză reală măsurată pe acest CPU:** ~2.7-2.9x faster-than-real-time (Kyutai raportează 6x pe Apple M4 — CPU-ul ăsta e mai lent per-core, dar tot confortabil peste real-time).
|
||||||
|
- **Modelul cu voice cloning e gated pe HuggingFace** — cere account + accept termeni + token (`HF_TOKEN`). Tokenul lui Marius e deja în keyring (`hf_token`).
|
||||||
|
- **Instalare testată:** `pip install pocket-tts` (venv izolat de test la `/tmp/pocket-tts-test/.venv`, nu producție). CLI: `pocket-tts generate --text "..." --voice <nume_sau_path.wav> --output-path out.wav`.
|
||||||
|
- **Sample-uri de voce ale lui Marius, deja înregistrate** (la `~/workspace/pocket-tts-test/`, convertite la wav mono 24kHz):
|
||||||
|
- `marius_real_voice.wav` (7.8s) → devine voce **"Marius 1"**
|
||||||
|
- `marius_real_voice_2.wav` (12.6s) → devine voce **"Marius 2"**
|
||||||
|
- `marius_real_voice_3.wav` (35s, mai expresiv) → devine voce **"Marius 3"**
|
||||||
|
- **Tool nou creat, deja în repo:** `tools/discord_send_file.py` — trimite fișier direct pe un canal Discord via REST API (necesar pentru că `__AUDIO__:` prefix nu funcționează în afara fast_commands — vezi docstring din script și `personality/TOOLS.md` § "Discord — trimite fișier direct").
|
||||||
|
|
||||||
|
## Cerințe confirmate de Marius (decizii arhitecturale — NU le renegocia, doar implementează)
|
||||||
|
|
||||||
|
1. **pocket-tts rulează ca proces/serviciu separat**, exact ca Supertonic — systemd propriu, port propriu, apelat via HTTP din restul codului. Nu integrat direct în procesul bot (evită să bagi `torch` ca dependență grea în `.venv` principal).
|
||||||
|
2. **pocket-tts = engine default. Supertonic = fallback.**
|
||||||
|
3. **pocket-tts răspunde MEREU în engleză** (fonetic), indiferent de limba textului de intrare — inclusiv pe text românesc (va suna cu accent, e acceptat/intenționat). **Nu** face rutare automată pe limbă.
|
||||||
|
4. **Fallback pe Supertonic doar la:**
|
||||||
|
- (a) eșec tehnic pocket-tts (model indisponibil, HF token expirat, server jos, etc.)
|
||||||
|
- (b) comandă manuală de schimbare engine de la Marius — **trebuie adăugată o comandă nouă** pentru switch engine (ex: `/engine supertonic` / `/engine pockettts`, sau similar — alege convenția care se potrivește cel mai bine cu comenzile existente).
|
||||||
|
5. **Scope complet:** atât **mod live** (Discord Voice, streaming pe clauze) **cât și on-demand** (`/audio`). Nu doar unul din ele.
|
||||||
|
6. **Selector de voce unificat:** vocile Supertonic existente (M1-M5, F1-F5) **plus** vocile clonate, ca opțiuni suplimentare în același selector/aceeași comandă.
|
||||||
|
7. **Voci clonate salvate ca `.safetensors`** (export via `export_model_state()`, încărcare rapidă — vezi README pocket-tts pentru API exact), cu convenție de nume **`<Nume> <N>`** (ex: `Marius 1`, `Marius 2`, `Marius 3`; extensibil la alte persoane, ex: `Paula 1`, dacă Marius mai trimite sample-uri).
|
||||||
|
8. Marius va mai trimite sample-uri de voce în timp — trebuie un flux clar (comandă + atașament) pentru a adăuga o voce clonată nouă la catalog, fără intervenție manuală de cod de fiecare dată.
|
||||||
|
|
||||||
|
## Ce trebuie să decidă/detalieze planul (edge cases de rezolvat explicit)
|
||||||
|
|
||||||
|
- Cum se comportă fallback-ul din mod **live** dacă pocket-tts pică la mijlocul unei conversații (schimbare de engine mid-sesiune, fără să rupă turul curent)?
|
||||||
|
- Ce se întâmplă cu `lang="ro"` / `--lang` parametrul existent din `tools/tts.py` odată ce pocket-tts (mereu engleză) devine default — rămâne relevant doar pentru calea Supertonic?
|
||||||
|
- Cum se integrează streaming-ul propriu al pocket-tts cu `clause_segments()` din `tts_stream.py` (format audio, sample rate, chunking) — pocket-tts produce 24kHz, pipeline-ul Discord așteaptă 48kHz stereo s16le (vezi `_ffmpeg_resample` în `tts_stream.py`).
|
||||||
|
- Unde se stochează fișierele `.safetensors` ale vocilor clonate (director nou în repo? `models/voices/`? `~/workspace/`?) — și cum se mapează nume → fișier (config nou în `config.json`, sau fișier JSON dedicat).
|
||||||
|
- Cum se gestionează HF_TOKEN pentru serviciul systemd nou (environment file, similar cu cum `discord_token` e disponibil altor procese).
|
||||||
|
- Comanda de switch engine — ephemeral (doar sesiunea curentă) sau persistă în `config.json` / `sessions/active.json`?
|
||||||
|
- Ce se întâmplă cu comanda `/audio` existentă când alegi o voce clonată — flow-ul de azi (`fast_dispatch` → `__AUDIO__:` prefix) rămâne valid pentru pocket-tts, sau outputul trebuie tratat diferit?
|
||||||
|
|
||||||
|
## Instrucțiuni pentru sesiunea care preia
|
||||||
|
|
||||||
|
1. Citește `memory/kb/` relevant + `project_pocket_tts_voice_clone_eval.md` din memory pentru context complet de research.
|
||||||
|
2. Intră în **plan mode**, scrie un plan concret care acoperă toate punctele de mai sus.
|
||||||
|
3. Rulează planul prin **`/plan-eng-review`** (arhitectură, edge cases, performance) înainte de orice modificare de cod.
|
||||||
|
4. Prezintă planul lui Marius pentru aprobare explicită înainte de implementare.
|
||||||
|
5. Respectă principiile din `CLAUDE.md`: impact minim, fără scope creep, verifică înainte de "done".
|
||||||
@@ -70,6 +70,12 @@
|
|||||||
- **Streaming TTS:** răspunsul iese pe clauze, nu cuvânt-cu-cuvânt și nu frază întreagă — primul sunet pleacă imediat ce am o propoziție scurtă.
|
- **Streaming TTS:** răspunsul iese pe clauze, nu cuvânt-cu-cuvânt și nu frază întreagă — primul sunet pleacă imediat ce am o propoziție scurtă.
|
||||||
- **Limitări:** 1-3 propoziții max (vezi AGENTS.md § Voice mode). Cuvinte rare, nume proprii sau acronime pot apărea ciudat în STT — dacă sună greșit, cer reformulare în loc să ghicesc.
|
- **Limitări:** 1-3 propoziții max (vezi AGENTS.md § Voice mode). Cuvinte rare, nume proprii sau acronime pot apărea ciudat în STT — dacă sună greșit, cer reformulare în loc să ghicesc.
|
||||||
|
|
||||||
|
### Discord — trimite fișier direct (audio, imagini)
|
||||||
|
- **Script:** `python3 tools/discord_send_file.py --channel <id> --file /path --text "mesaj opțional"`
|
||||||
|
- **De ce nu merge `__AUDIO__:<path>`:** convenția aia (din `fast_commands.py`) e interceptată de `discord_bot.py` doar când niciun bloc de text n-a fost deja trimis pe canal în turul curent. Într-un răspuns conversațional normal (situația mea), fiecare bloc de text pe care îl scriu e streamat live — deci `__AUDIO__:` ajunge trimis ca text brut, nu convertit în attachment. Nu încerca trucul ăsta pentru fișiere trimise din conversație normală.
|
||||||
|
- **Channel ID:** dacă nu-l ai din context, citește `sessions/active.json` (cheia e channel_id; de regulă o singură sesiune activă).
|
||||||
|
- **Modul Python:** `from tools.discord_send_file import send_file` → `send_file(channel_id, path, content="text opțional")`
|
||||||
|
|
||||||
### Pauze respirație
|
### Pauze respirație
|
||||||
- **Script:** `python3 tools/pauza_random.py`
|
- **Script:** `python3 tools/pauza_random.py`
|
||||||
- **Bancă:** memory/kb/tehnici-pauza.md
|
- **Bancă:** memory/kb/tehnici-pauza.md
|
||||||
|
|||||||
184
tasks/pocket-tts-integration-plan.md
Normal file
184
tasks/pocket-tts-integration-plan.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# Plan: integrare pocket-tts (Kyutai) ca engine TTS nou
|
||||||
|
|
||||||
|
Status: APROBAT de Marius — plan + review conversațional (`/plan-eng-review` manual, fără AskUserQuestion — indisponibil în mediul Echo Core) complete. Gata de implementare.
|
||||||
|
Sursă cerință: `memory/kb/projects/pocket-tts-integration-handoff-prompt.md`
|
||||||
|
|
||||||
|
## 0. Descoperire tehnică nouă (nu era în handoff/eval anterior)
|
||||||
|
|
||||||
|
Am verificat sursa pachetului `pocket-tts` (`/tmp/pocket-tts-test/.venv/.../pocket_tts/main.py` + `models/tts_model.py`):
|
||||||
|
|
||||||
|
- `pocket-tts` are deja **server HTTP built-in**: `pocket-tts serve --host --port [--language] [--quantize]`, expune `GET /health` și `POST /tts` (form-data: `text`, plus `voice_url` SAU `voice_wav` upload).
|
||||||
|
- `TTSModel.get_state_for_audio_prompt(path)` **detectează automat sufixul `.safetensors`** și îl încarcă rapid (`_import_model_state`), fără să re-proceseze audio. Endpoint-ul `/tts` primește `voice_wav` ca `UploadFile` și păstrează sufixul fișierului original când îl salvează temporar — deci dacă trimitem un fișier cu nume `Marius 1.safetensors`, serverul îl încarcă pe calea rapidă, automat, **fără nicio modificare la codul pocket-tts**.
|
||||||
|
- Concluzie: **nu trebuie scris un server HTTP custom** care încarcă modelul manual — folosim serverul built-in ca atare. Simplifică planul semnificativ față de ce anticipam inițial (evită fork/monkeypatch, evită gestiune proprie a modelului în memorie).
|
||||||
|
- Singurul lucru care tot trebuie scris manual: un script de **export** (`get_state_for_audio_prompt(wav) → export_model_state() → .safetensors`) pentru fluxul de adăugare voce nouă, pentru că endpoint-ul HTTP nu expune un pas separat de "doar exportă starea" — el mereu generează și audio.
|
||||||
|
|
||||||
|
## 1. Arhitectură
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐
|
||||||
|
/audio, voice live ──▶│ tools/tts.py │ (neschimbat ca interfață publică:
|
||||||
|
│ synthesize(text, │ synthesize(text, voice, lang))
|
||||||
|
│ voice, lang) │
|
||||||
|
└──────────┬───────────┘
|
||||||
|
│ decide engine după voice_id (catalog)
|
||||||
|
┌──────────────┴───────────────┐
|
||||||
|
▼ ▼
|
||||||
|
engine=pockettts (default) engine=supertonic (fallback / M*/F*)
|
||||||
|
POST :7789/tts (multipart) POST :7788/v1/audio/speech (json, existent)
|
||||||
|
│ │
|
||||||
|
pocket-tts.service (nou, supertonic-tts.service (existent,
|
||||||
|
venv separat .venv-pockettts, neschimbat)
|
||||||
|
built-in server, port 7789)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Servicii noi:**
|
||||||
|
- `pocket-tts.service` (systemd user unit, model pe template `supertonic-tts.service`), pornește serverul built-in pe `127.0.0.1:7789`, venv separat `~/echo-core/.venv-pockettts` (torch e greu — nu intră în `.venv` principal, per cerința #1 din handoff).
|
||||||
|
- HF_TOKEN: launcher mic (`tools/pocket_tts_env_launch.sh` sau echivalent Python) care citește `hf_token` din keyring (`src/credential_store.get_secret`) și îl exportă ca `HF_TOKEN` înainte de `exec pocket-tts serve ...`. Evită să scriem tokenul în plaintext într-un `EnvironmentFile=`. `keyring` trebuie instalat și în venv-ul nou (dependință mică, nu torch).
|
||||||
|
- **Verificare acces keyring din systemd --user** — **CONFIRMAT de Marius**: primul pas la implementare, nu presupunere. Bot-ul principal rulează deja ca `systemctl --user` și citește keyring cu succes, deci `pocket-tts.service` (tot user-level) ar trebui să aibă același acces — dar se verifică explicit înainte de a construi restul serviciului. Launcher-ul eșuează CLAR (log + exit non-zero) dacă `get_secret("hf_token")` întoarce `None`, nu silențios.
|
||||||
|
- **`hf_token` lipsă din `REQUIRED_SECRETS`** — **CONFIRMAT de Marius**: `src/credential_store.py` verifică azi doar `discord_token`. Se adaugă `hf_token` la o listă de verificare (fie `REQUIRED_SECRETS`, fie check dedicat în `/voice doctor` §7) — altfel un token lipsă/expirat se descoperă abia la runtime, cu eroare confuză.
|
||||||
|
|
||||||
|
## 2. Catalog voci (unificat, extensibil fără redeploy de cod)
|
||||||
|
|
||||||
|
Fișier nou `tts_voices.json` la rădăcina repo-ului (pattern identic cu `approved-tasks.json` — JSON plat, scris prin `src/jsonlock.py` pentru concurrent-safety, la fel ca `sessions/*.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"M1": {"engine": "supertonic"}, "M2": {"engine": "supertonic"}, ...,
|
||||||
|
"F1": {"engine": "supertonic"}, ...,
|
||||||
|
"Marius 1": {"engine": "pockettts", "state_path": "models/voices/marius-1.safetensors", "owner": "Marius"},
|
||||||
|
"Marius 2": {"engine": "pockettts", "state_path": "models/voices/marius-2.safetensors", "owner": "Marius"},
|
||||||
|
"Marius 3": {"engine": "pockettts", "state_path": "models/voices/marius-3.safetensors", "owner": "Marius"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `.safetensors` blobs în `models/voices/` — `models/` e deja în `.gitignore` (verificat), consistent cu `models/whisper-small-ro-cv11-int8/` existent. Nu intră în git (fișiere binare per-persoană, private).
|
||||||
|
- `tts_voices.json` (doar mapare nume→path, mic) **intră în git** — la fel ca `approved-tasks.json`.
|
||||||
|
- La pornire, seed cu cele 3 voci deja existente ale lui Marius (sample-urile din `~/workspace/pocket-tts-test/marius_real_voice*.wav`, deja înregistrate) — le exportăm în `.safetensors` o singură dată la implementare, nu trebuie re-trimise de Marius.
|
||||||
|
- Sample-urile **Paula** deja clonate manual în sesiunea de research (`paula_*.wav`) — **CONFIRMAT de Marius: se includ în seed-ul inițial** al catalogului, alături de cele 3 voci Marius.
|
||||||
|
|
||||||
|
## 3. `tools/tts.py` — refactor minimal, interfață publică neschimbată
|
||||||
|
|
||||||
|
- `synthesize(text, voice=DEFAULT_VOICE, lang=DEFAULT_LANG)` rămâne semnătura folosită de tot restul codului (`tts_stream.py`, `fast_commands.py`) — **zero schimbări la apelanți**.
|
||||||
|
- Intern: citește `tts_voices.json`, rezolvă `engine` din `voice` (dacă voice nu e în catalog → fallback la `default_engine` din `config.json`, voce default a acelui engine).
|
||||||
|
- **Sursă unică de adevăr pentru voci valide** — **CONFIRMAT de Marius**: `tts_voices.json` înlocuiește complet cele 4 liste hardcodate existente azi (`VOICES` din `tools/tts.py`, `_VOICES` din `fast_commands.py`, choices statice din `discord_bot.py`/`discord_voice.py`). Toate se rescriu să citească din catalog — elimină riscul de drift între liste. `voice_commands.py._VALID_VOICES` rămâne SEPARAT și neschimbat (regex in-band, vezi §6) — nu e un duplicat de eliminat, e un scop diferit (comenzi vorbite, doar M*/F*).
|
||||||
|
- **Cap de lungime text** — **CONFIRMAT de Marius**: `_MAX_TTS_CHARS = 400` rămâne DOAR pe calea `_synthesize_supertonic` (e specific limitării ONNX, documentat ca atare azi). `_synthesize_pockettts` fără cap inițial — revizuim dacă apar probleme de memorie/latență la text lung, nu preventiv.
|
||||||
|
- `_synthesize_pockettts(text, voice_entry)`: POST multipart la `:7789/tts` cu `text` + (`voice_wav=<.safetensors deschis>` dacă e voce clonată, sau `voice_url=<nume predefinit>` dacă e voce non-clonată default pocket-tts, ex. "alba"). `lang` e ignorat complet (cerința #3 — pocket-tts vorbește mereu "englezește fonetic", fără rutare pe limbă).
|
||||||
|
- **Voce predefinită pocket-tts** (non-clonată, ex. "alba") — **CONFIRMAT de Marius: se include în selector** ca opțiune suplimentară alături de M1-M5/F1-F5/vocile clonate, adăugată în `tts_voices.json` seed cu `{"engine": "pockettts", "voice_url": "alba"}` (fără `state_path`).
|
||||||
|
- `_synthesize_supertonic(...)` = codul actual, neschimbat, redenumit intern.
|
||||||
|
- **Fallback automat** (cerința #4a): dacă `_synthesize_pockettts` eșuează cu eroare tehnică (`httpx.ConnectError`, timeout, `HTTPStatusError` 5xx, HF token invalid) → log warning + retry automat pe `_synthesize_supertonic(text, voice="M2", lang="ro")` (voce default Supertonic, pentru că vocea pocket-tts cerută n-are corespondent Supertonic 1:1). Rezultatul returnat include `"engine_used"` în dict — **CONFIRMAT de Marius: se implementează**, pentru observabilitate/debug la fallback-uri silențioase.
|
||||||
|
- Fallback NU se declanșează pe erori de conținut (text gol, voce inexistentă) — doar pe eșec tehnic de conectare/serviciu, per cerința #4a explicită.
|
||||||
|
|
||||||
|
## 4. Config nou (`config.json`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
"tts": {
|
||||||
|
"default_engine": "pockettts",
|
||||||
|
"pockettts_url": "http://127.0.0.1:7789"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `default_engine` persistă în `config.json` (nu ephemeral) — răspunde la întrebarea deschisă din handoff (§ edge cases, ultimul punct "ephemeral sau persistă"): **persistă**, motivat de faptul că Marius vrea un comportament stabil între restart-uri de sesiune/bot, nu un toggle per conversație. Comanda de switch engine (§5) scrie aici via `Config().set(...)`.
|
||||||
|
- Când `voice` explicit cerut de user e o voce catalogată (ex. "Marius 1" sau "M2"), engine-ul e determinat de catalog — `default_engine` contează DOAR când nu se specifică nicio voce (default absolut, ex. `/audio` fără parametri, sau intrare în voice live fără `/voice setvoice` anterior). Asta rezolvă ambiguitatea "ce înseamnă switch engine dacă fiecare voce știe deja ce engine e" — răspunde la "care e vocea implicită" nu "forțează un engine peste o voce incompatibilă".
|
||||||
|
|
||||||
|
## 5. Comenzi Discord noi/modificate
|
||||||
|
|
||||||
|
Toate sub grupul `/voice` existent (`src/adapters/discord_voice.py`) + `/audio` (`discord_bot.py`):
|
||||||
|
|
||||||
|
1. **`/voice engine <pockettts|supertonic>`** — comandă nouă, setează `config.tts.default_engine`, persistă. Răspunde cu confirmare + vocea default curentă a engine-ului ales.
|
||||||
|
2. **`/voice addvoice <nume> <sample>`** — comandă nouă, `sample: discord.Attachment` (wav). Flow:
|
||||||
|
- `await interaction.response.defer(ephemeral=True)` (pattern obligatoriu din CLAUDE.md pentru I/O în callback).
|
||||||
|
- descarcă attachment-ul, validează extensie audio + durată minimă (~3s, sub asta cloning-ul e slab per research anterior).
|
||||||
|
- rulează `<.venv-pockettts>/bin/python tools/pocket_tts_add_voice.py --wav <path> --name "<Nume>"` (subprocess, script nou care încarcă modelul o singură dată — separat de venv principal, deci nu se poate face în-proces).
|
||||||
|
- scriptul determină automat următorul `N` liber pentru acel nume (scanând `tts_voices.json`), exportă `.safetensors`, scrie catalog-ul prin `jsonlock`.
|
||||||
|
- răspunde cu numele final atribuit (ex. "Marius 4") + un sample audio generat pe loc cu vocea nouă, ca preview.
|
||||||
|
- **Notă cost**: încărcarea modelului pentru export durează ~zeci de secunde și consumă memorie suplimentară CÂT TIME rulează. **CONFIRMAT de Marius: `/voice addvoice` oprește temporar `pocket-tts.service` cât durează export-ul** (evită două instanțe TTSModel simultan pe memorie strânsă), cu mesaj către user: "Adaug voce, TTS indisponibil ~30s".
|
||||||
|
3. **`/audio`** (`discord_bot.py`) — parametrul `voce` trece de la `@app_commands.choices` (listă statică, max 25 hardcodate) la **`@app_commands.autocomplete`** care citește `tts_voices.json` live — necesar ca vocile clonate noi să apară fără redeploy (cerința #8: "flux clar... fără intervenție manuală de cod"). Restul comenzii (`fast_dispatch`, `__AUDIO__:` convenție) neschimbat.
|
||||||
|
4. **`/voice setvoice`** (deja există, `discord_voice.py`) — același tratament: choices statice → autocomplete din catalog, pentru selectorul unificat cerut la §6 din handoff.
|
||||||
|
|
||||||
|
## 6. Integrare mod live (`src/voice/pipeline.py` + `tts_stream.py`)
|
||||||
|
|
||||||
|
- `TTSQueue.__init__(voice_id, lang)` — neschimbat structural. `_worker_loop` apelează `synthesize(item, voice=self.voice_id, lang=self.lang)` — deja engine-agnostic după refactor-ul din §3, **zero schimbări în `tts_stream.py`** dincolo de faptul că `synthesize()` intern rutează diferit.
|
||||||
|
- `_ffmpeg_resample` deja normalizează orice WAV primit la 48kHz stereo s16le — pocket-tts produce 24kHz mono (mimi codec), cade pe calea `_ffmpeg_resample` automat (nu e "target format"), **zero schimbări** (răspunde la edge case-ul din handoff despre sample rate).
|
||||||
|
- `detect_voice_change` (in-band voice switching prin STT, `voice_commands.py`) — regex-urile curente prind DOAR `M1-M5`/`F1-F5`. Vocile clonate ("Marius 1") nu vor fi comutabile din voce vorbită cu regex-urile actuale (ambiguu — "Marius 1" ar suna identic cu multe fraze normale). **Decizie propusă**: las in-band switching neschimbat (doar M*/F*), vocile clonate se schimbă doar din `/voice setvoice` (text/slash command), nu din voce. Flag pentru confirmare — nu extindem regex-ul de voce vorbită la nume libere (risc fals-pozitive).
|
||||||
|
- Barge-in mid-conversație pe eșec pocket-tts (edge case explicit din handoff): fallback-ul e în `tools/tts.py` (§3), deci e transparent pentru `tts_stream.py` — un clause care eșuează tehnic pe pocket-tts cade automat pe Supertonic ÎN ACEEAȘI clauză, fără să rupă turul. Restul clauzelor din același turn continuă pe orice engine a răspuns ultima dată cu succes (nu schimbă `self.voice_id`/engine persistent — fallback e per-apel, nu schimbă starea sesiunii).
|
||||||
|
|
||||||
|
## 7. `/voice doctor` — extindere
|
||||||
|
|
||||||
|
Comanda existentă de health-check extinde cu un ping la `GET :7789/health` pentru pocket-tts, alături de verificarea Supertonic existentă (dacă există deja — de verificat implementarea curentă la implementare).
|
||||||
|
|
||||||
|
## 8. Ce NU se schimbă (impact minim, per CLAUDE.md)
|
||||||
|
|
||||||
|
- `normalize_for_tts` (normalizare numere/timp în cuvinte românești) — rămâne neschimbată. Da, asta înseamnă că textul normalizat românește va fi rostit de un model englez (accent + posibil pronunție ciudată pe cuvinte românești vs cifre brute) — dar cerința #3 din handoff acceptă explicit accentul, iar schimbarea normalizării pe baza engine-ului activ ar fi scope creep nesolicitat. Notă pentru Marius, nu acțiune.
|
||||||
|
- Nimic din `src/router.py`, `src/claude_session.py` nu se atinge.
|
||||||
|
- `supertonic-tts.service` neschimbat.
|
||||||
|
|
||||||
|
## 9. Fișiere noi vs modificate (rezumat pentru review)
|
||||||
|
|
||||||
|
**Noi:**
|
||||||
|
- `tools/pocket_tts_add_voice.py` (script export voce, rulat în venv separat)
|
||||||
|
- `tts_voices.json` (catalog, git-tracked)
|
||||||
|
- `models/voices/*.safetensors` (blobs, gitignored)
|
||||||
|
- `~/.config/systemd/user/pocket-tts.service`
|
||||||
|
- `.venv-pockettts/` (venv separat, gitignored)
|
||||||
|
- launcher HF_TOKEN pentru systemd (script mic)
|
||||||
|
|
||||||
|
**Modificate:**
|
||||||
|
- `tools/tts.py` (rutare engine, fallback)
|
||||||
|
- `config.json` (secțiune `tts`)
|
||||||
|
- `src/adapters/discord_bot.py` (`/audio` → autocomplete voce)
|
||||||
|
- `src/adapters/discord_voice.py` (`/voice engine`, `/voice addvoice`, `/voice setvoice` → autocomplete, `/voice doctor` → check pocket-tts)
|
||||||
|
- `personality/TOOLS.md` (documentare comenzi noi, pattern din regulile proiectului)
|
||||||
|
|
||||||
|
**Neschimbate:** `src/voice/pipeline.py`, `src/voice/tts_stream.py`, `src/voice/voice_commands.py`, `src/router.py`, `src/claude_session.py`, `supertonic-tts.service`.
|
||||||
|
|
||||||
|
## 10. Puncte deschise — TOATE APROBATE de Marius (DA la toate)
|
||||||
|
|
||||||
|
1. Vocile Paula deja clonate manual în research — **incluse** în seed-ul inițial al catalogului.
|
||||||
|
2. `/voice addvoice` — **oprește temporar** `pocket-tts.service` cât exportă (UX: mesaj "Adaug voce, TTS indisponibil ~30s").
|
||||||
|
3. `engine_used` în răspunsul `synthesize()` — **implementat**, pentru debug/observabilitate.
|
||||||
|
4. Voce predefinită pocket-tts (non-clonată, ex. "alba") — **inclusă** ca opțiune suplimentară în selector.
|
||||||
|
|
||||||
|
## 11. Teste (minime, realiste — fără model real)
|
||||||
|
|
||||||
|
Coverage azi: zero teste pe `tools/tts.py`, `tts_stream.py`, `discord_voice.py` (doar `voice_commands.py` e testat, `tests/test_voice_commands.py`). **CONFIRMAT de Marius**: se adaugă teste minime, pure-function, fără dependență de model/hardware real:
|
||||||
|
|
||||||
|
- `tests/test_tts.py` (nou):
|
||||||
|
- rutare engine din `tts_voices.json`: voce catalogată → engine corect ales (mock fișier catalog).
|
||||||
|
- voce necatalogată → fallback la `default_engine` din config.
|
||||||
|
- trigger fallback tehnic: mock `httpx.ConnectError`/`HTTPStatusError` 5xx pe `_synthesize_pockettts` → verifică apel automat pe `_synthesize_supertonic` cu voce default, și `engine_used` corect în rezultat.
|
||||||
|
- fallback NU se declanșează pe eroare de conținut (voce inexistentă) — verifică `_synthesize_pockettts` nu e apelat de două ori / nu cade pe supertonic pentru input gol.
|
||||||
|
- `lang="na"` retry recursiv existent în `_synthesize_supertonic` — regression test, verifică nu s-a pierdut la redenumire (§3, punctul "notă cod").
|
||||||
|
- `tests/test_config.py` (extindere, dacă există, sau nou): `Config().set("tts.default_engine", ...)` persistă corect prin `save()`/`reload()`.
|
||||||
|
- **NU** se testează: modelul pocket-tts propriu-zis, `/voice addvoice` end-to-end (necesită model+audio real), streaming live (necesită Discord voice real) — astea rămân verificare manuală la implementare (`/qa` sau test manual ghidat).
|
||||||
|
|
||||||
|
## 12. Performanță
|
||||||
|
|
||||||
|
- **CONFIRMAT de Marius**: verificare manuală RAM disponibil pe box ÎNAINTE de a porni `pocket-tts.service` ca engine default (nu doar la `/voice addvoice`, unde deja era acoperit la §5 punctul 2). Rulare simultană pocket-tts (torch) + Supertonic (ONNX, idle) + Whisper STT (activ în timpul sesiunilor voice) + bot-ul principal, pe același box — pas manual de 30 secunde (`free -h` înainte de a activa `pocket-tts.service`), nu automatizare. Dacă memoria e strânsă, flag pentru Marius înainte de a continua.
|
||||||
|
|
||||||
|
## NOT in scope
|
||||||
|
|
||||||
|
- Rutare automată pe limbă (pocket-tts mereu engleză fonetic, indiferent de input) — respinsă explicit de Marius în handoff (cerința #3).
|
||||||
|
- Extinderea regex-ului de voce vorbită (`voice_commands.py`) la nume libere ("Marius 1") — risc fals-pozitive prea mare; vocile clonate se schimbă doar din `/voice setvoice`.
|
||||||
|
- Teste end-to-end cu model real / audio real — necesită hardware, nu se pretează unit test; verificare manuală la implementare.
|
||||||
|
- Schimbarea `normalize_for_tts` în funcție de engine activ — scope creep nesolicitat (notă în §8, nu acțiune).
|
||||||
|
- Server HTTP custom pentru pocket-tts — serverul built-in e suficient (descoperire §0), evită fork/monkeypatch.
|
||||||
|
|
||||||
|
## What already exists (reused, not rebuilt)
|
||||||
|
|
||||||
|
- `synthesize(text, voice, lang) -> dict` — interfața publică rămâne neschimbată, toți apelanții (`tts_stream.py`, `fast_commands.py`) nu se ating.
|
||||||
|
- `_ffmpeg_resample` — deja generic (auto-detectează sample rate din header WAV), gestionează 24kHz mono de la pocket-tts fără nicio schimbare.
|
||||||
|
- `src/jsonlock.py` — refolosit ca atare pentru `tts_voices.json`, pattern identic cu `approved-tasks.json`.
|
||||||
|
- `src/credential_store.py` — refolosit ca atare pentru `hf_token` (deja în keyring).
|
||||||
|
- `/voice doctor` — pattern de health-check existent, extins (nu reconstruit) cu ping pocket-tts.
|
||||||
|
- `supertonic-tts.service` — folosit ca template pentru `pocket-tts.service`, neschimbat el însuși.
|
||||||
|
|
||||||
|
## Rezumat review (`/plan-eng-review` manual, conversațional — AskUserQuestion indisponibil în Echo Core)
|
||||||
|
|
||||||
|
- Arhitectură: 3 probleme găsite, toate rezolvate (unificare liste voci, cap text Supertonic-only, verificare keyring explicită).
|
||||||
|
- Code quality: 2 probleme găsite (regresie fallback lang="na", `hf_token` lipsă din REQUIRED_SECRETS) — rezolvate.
|
||||||
|
- Teste: gap total identificat (zero coverage) → secțiune de teste minime adăugată (§11).
|
||||||
|
- Performanță: 1 problemă (memorie concurentă) → pas manual de verificare adăugat (§12).
|
||||||
|
- NOT in scope: scris.
|
||||||
|
- What already exists: scris.
|
||||||
|
- Outside voice (codex/subagent independent): skip — nu era disponibil tool-ul interactiv pentru workflow-ul complet; planul a fost deja verificat direct pe sursa pocket-tts (§0) și pe codul echo-core existent (agent Explore dedicat).
|
||||||
|
- Toate cele 7 puncte + cele 4 din §10: aprobate explicit de Marius.
|
||||||
70
tools/discord_send_file.py
Normal file
70
tools/discord_send_file.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/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)}
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
result = send_file(args.channel, args.file, content=args.text)
|
||||||
|
print(result)
|
||||||
|
sys.exit(0 if result.get("ok") else 1)
|
||||||
Reference in New Issue
Block a user