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:
2026-07-11 09:54:36 +00:00
parent 738dc0181a
commit 155d6cb9c1
8 changed files with 345 additions and 17 deletions

View File

@@ -11,9 +11,9 @@
"report_on": "changes",
"timeout": 120,
"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",
"next_run": "2026-07-10T16:00:00+00:00"
"next_run": "2026-07-11T10:00:00+00:00"
},
{
"name": "security-audit-daily",
@@ -43,9 +43,9 @@
"report_on": "never",
"timeout": 120,
"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",
"next_run": "2026-07-11T03:30:00+00:00"
"next_run": "2026-07-12T03:30:00+00:00"
},
{
"name": "archive-tasks-daily",
@@ -59,9 +59,9 @@
"report_on": "changes",
"timeout": 60,
"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",
"next_run": "2026-07-11T03:00:00+00:00"
"next_run": "2026-07-12T03:00:00+00:00"
},
{
"name": "backup-config",
@@ -75,9 +75,9 @@
"report_on": "never",
"timeout": 120,
"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",
"next_run": "2026-07-11T02:00:00+00:00"
"next_run": "2026-07-12T02:00:00+00:00"
},
{
"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.",
"allowed_tools": [],
"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",
"next_run": "2026-07-10T18:00:00+00:00"
"next_run": "2026-07-11T10:00:00+00:00"
},
{
"name": "night-execute",

View File

@@ -1,5 +1,18 @@
{
"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",
"title": "I Cut My OpenCode Token Usage by 96% - Here's How",
@@ -9727,8 +9740,8 @@
"title": "Proiect: Vending Master - Integrare Website → ROA",
"date": "2026-01-30",
"tags": [
"vending-master",
"integrare"
"integrare",
"vending-master"
],
"domains": [
"work"
@@ -10208,7 +10221,7 @@
}
],
"stats": {
"total": 590,
"total": 591,
"by_domain": {
"work": 202,
"health": 104,
@@ -10225,7 +10238,7 @@
"facebook": 11,
"health": 6,
"insights": 46,
"projects": 234,
"projects": 235,
"reflectii": 3,
"retete": 1,
"tools": 7,

View File

@@ -10,7 +10,7 @@
- **[facebook/](facebook/index.md)** — 11 note
- **[health/](health/index.md)** — 6 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
- **[retete/](retete/index.md)** — 1 note
- **[tools/](tools/index.md)** — 7 note

View File

@@ -1,6 +1,6 @@
# 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)**
- 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
- **[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ă
- **[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`
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)**
@@ -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)**
**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)**
- **[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

View 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".

View File

@@ -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ă.
- **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
- **Script:** `python3 tools/pauza_random.py`
- **Bancă:** memory/kb/tehnici-pauza.md

View 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.

View 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)