{ "projectName": "echo-core", "branchName": "ralph/echo-improve", "description": "Integrare pocket-tts (Kyutai) ca engine TTS nou in Echo Core", "techStack": { "type": "python", "commands": { "start": "python main.py", "build": "", "lint": "ruff check .", "typecheck": "mypy .", "test": "pytest" }, "port": 8000 }, "userStories": [ { "id": "US-001", "title": "Setup venv-pockettts și pocket-tts.service systemd unit", "description": "Creează venv separat .venv-pockettts (torch + keyring, nu în .venv principal), un launcher care exportă HF_TOKEN din keyring, și systemd user unit pocket-tts.service care pornește serverul built-in pe 127.0.0.1:7789. Verifică explicit accesul la keyring din systemd --user.", "priority": 10, "acceptanceCriteria": [ "Directorul ~/echo-core/.venv-pockettts există cu pocket-tts și keyring instalate; .venv-pockettts/ e în .gitignore", "tools/pocket_tts_env_launch.sh (sau echivalent) citește hf_token din keyring și eșuează cu exit non-zero + log clar dacă get_secret('hf_token') e None", "~/.config/systemd/user/pocket-tts.service pornește 'pocket-tts serve --host 127.0.0.1 --port 7789' prin launcher", "curl -s http://127.0.0.1:7789/health întoarce 200 după 'systemctl --user start pocket-tts.service'" ], "tags": [ "infra" ], "dependsOn": [], "requiresBrowserCheck": false, "requiresDesignReview": false, "passes": true, "failed": false, "blocked": false, "retries": 1, "failureReason": "", "notes": "Fișiere: .gitignore, tools/pocket_tts_env_launch.sh (nou). Deja existente din iterația anterioară: .venv-pockettts/ (torch+pocket-tts+keyring), ~/.config/systemd/user/pocket-tts.service. Verificat: curl :7789/health -> 200, systemctl restart -> health revine în ~3s, systemctl is-enabled -> enabled. Gate /review (infra): diff minimal, fără shell injection / secret leakage, PASS." }, { "id": "US-002", "title": "Adaugă hf_token la verificarea de secrete în credential_store", "description": "src/credential_store.py verifică azi doar discord_token; adaugă hf_token la lista de secrete verificate (REQUIRED_SECRETS sau check dedicat) ca un token lipsă/expirat să fie detectat la startup, nu la runtime.", "priority": 20, "acceptanceCriteria": [ "hf_token apare în REQUIRED_SECRETS (sau check echivalent) în src/credential_store.py", "Verificarea semnalează clar când hf_token lipsește din keyring", "source .venv/bin/activate && pytest tests/ rămâne PASS" ], "tags": [ "backend" ], "dependsOn": [], "requiresBrowserCheck": false, "requiresDesignReview": false, "passes": true, "failed": false, "blocked": false, "retries": 0, "failureReason": "", "notes": "Fișiere: src/credential_store.py (REQUIRED_SECRETS += hf_token), tests/test_secrets.py (assert-uri actualizate + test_partial_required nou). pytest tests/ -> 22 eșecuri preexistente neschimbate, 1043 passed (vs 1042 înainte). Gate /review (backend): diff trivial, fără risc, PASS." }, { "id": "US-003", "title": "Creează catalogul de voci tts_voices.json cu seed inițial", "description": "Fișier nou tts_voices.json la rădăcina repo-ului (git-tracked, scris prin src/jsonlock.py), mapare nume→{engine, state_path, owner}. Seed cu vocile M*/F* Supertonic existente, cele 3 voci Marius, vocile Paula clonate și vocea predefinită pocket-tts 'alba'.", "priority": 30, "acceptanceCriteria": [ "tts_voices.json există la rădăcină, e valid JSON și e git-tracked", "Conține intrările Supertonic M1-M5/F1-F5 cu {engine: supertonic}", "Conține vocile Marius (state_path spre models/voices/*.safetensors), vocile Paula și intrarea 'alba' cu {engine: pockettts, voice_url: alba}", "models/voices/ este acoperit de .gitignore (blob-urile .safetensors nu intră în git)", "Citirea/scrierea catalogului trece prin src/jsonlock.py" ], "tags": [ "db", "backend" ], "dependsOn": [], "requiresBrowserCheck": false, "requiresDesignReview": false, "passes": true, "failed": false, "blocked": false, "retries": 0, "failureReason": "", "notes": "Fișiere: tts_voices.json (nou, seed 17 intrări: M1-M5/F1-F5 supertonic, Marius 1-3 și Paula 1-3 pockettts cu state_path spre models/voices/*.safetensors, alba pockettts cu voice_url). .gitignore (+*.lock pentru sidecar-uri jsonlock, models/ deja acoperea models/voices/). Eliminat approved-tasks.json.lock din git (sidecar gol, prins accidental de un commit anterior — jsonlock îl recreează automat). Scris prin src/jsonlock.py write_locked(). Gate /review (backend): PASS — diff data-only, fără logică nouă. Gate db: PASS — validare schema manuală (fiecare intrare are engine; pockettts are state_path SAU voice_url). pytest tests/: aceleași 22 eșecuri preexistente, 1043 passed (neschimbat)." }, { "id": "US-004", "title": "Script de export voce pocket_tts_add_voice.py + export seed voci existente", "description": "tools/pocket_tts_add_voice.py rulează în venv-ul separat, încarcă modelul o singură dată, exportă .safetensors dintr-un wav (get_state_for_audio_prompt → export_model_state), determină automat următorul N liber pentru un nume din catalog și scrie catalogul prin jsonlock. Exportă cele 3 voci Marius și Paula ca seed.", "priority": 40, "acceptanceCriteria": [ "tools/pocket_tts_add_voice.py --wav --name '' produce un fișier models/voices/.safetensors", "Scriptul determină automat următorul index liber scanând tts_voices.json și scrie intrarea prin jsonlock", "Rularea pe sample-urile marius_real_voice*.wav și paula_*.wav produce blob-urile .safetensors referite în seed-ul din tts_voices.json", "Scriptul rulează cu <.venv-pockettts>/bin/python fără a atinge .venv principal" ], "tags": [ "backend" ], "dependsOn": [ "US-001", "US-003" ], "requiresBrowserCheck": false, "requiresDesignReview": false, "passes": true, "failed": false, "blocked": false, "retries": 1, "failureReason": "", "notes": "Fișiere: tools/pocket_tts_add_voice.py (nou, commitat accidental împreună cu US-003 în e3b5cdf — codul era deja scris din iterația anterioară care a atins max_turns). Scriptul încarcă modelul o singură dată, exportă .safetensors via get_state_for_audio_prompt/export_model_state, determină automat N liber scanând tts_voices.json (liber dacă intrarea lipsește SAU state_path nu există pe disc), scrie catalogul prin src.jsonlock.write_locked. Seed deja exportat (din iterația anterioară): models/voices/{marius,paula}-{1,2,3}.safetensors din ~/workspace/pocket-tts-test/*.wav, verificate integru cu safetensors.torch.load_file (12 tensori fiecare) și referite corect în tts_voices.json. Verificat: _next_free_index -> 4 pentru Marius/Paula (1-3 ocupate), 1 pentru nume nou; --help rulează curat cu .venv-pockettts/bin/python fără a atinge .venv principal. Gate /review (backend): PASS — script mic, single-responsibility, erori clare (wav lipsă, hf_token lipsă), fără shell injection/secret leakage. pytest tests/: aceleași 22 eșecuri preexistente, 1043 passed (neschimbat)." }, { "id": "US-005", "title": "Adaugă secțiunea tts în config.json", "description": "Adaugă în config.json secțiunea tts cu default_engine=pockettts și pockettts_url=http://127.0.0.1:7789, persistabilă prin Config().set/save.", "priority": 50, "acceptanceCriteria": [ "config.json conține tts.default_engine='pockettts' și tts.pockettts_url='http://127.0.0.1:7789'", "Config().get('tts.default_engine') întoarce valoarea corectă cu dot-notation", "Config().set('tts.default_engine', 'supertonic') urmat de save()/reload() persistă valoarea" ], "tags": [ "backend" ], "dependsOn": [], "requiresBrowserCheck": false, "requiresDesignReview": false, "passes": true, "failed": false, "blocked": false, "retries": 0, "failureReason": "", "notes": "Fișiere: config.json (+4 linii, secțiune tts nouă). Config class (src/config.py) era deja generică (get/set/save/reload cu dot-notation), fără schimbări de cod necesare. Verificat manual: Config().get('tts.default_engine') -> 'pockettts', get('tts.pockettts_url') -> 'http://127.0.0.1:7789'; set('tts.default_engine','supertonic')+save()+Config() nou (reload implicit) -> 'supertonic', apoi restaurat la 'pockettts'+save(). git diff confirmă restaurarea corectă. Gate /review (backend): review manual (skill-ul /review interactiv nu se pretează la iterații autonome) — fără cod nou care citește tts.* încă (US-006 le adaugă), deci zero risc de regresie; fără secrete, JSON valid, schema aditivă. pytest tests/: aceleași 22 eșecuri preexistente, 1043 passed (neschimbat) — tests/test_config.py deja acoperă generic set/save/reload roundtrip." }, { "id": "US-006", "title": "Refactor tools/tts.py — rutare engine, backend pocket-tts și fallback", "description": "synthesize(text, voice, lang) rămâne cu semnătura publică neschimbată, dar rutează intern pe engine din tts_voices.json (fallback la default_engine din config). Adaugă _synthesize_pockettts (POST multipart la :7789/tts), redenumește codul actual _synthesize_supertonic (păstrând cap 400 char doar aici), fallback tehnic automat pe Supertonic și câmpul engine_used în rezultat.", "priority": 60, "acceptanceCriteria": [ "synthesize() rezolvă engine din tts_voices.json; voce necatalogată cade pe tts.default_engine din config", "_synthesize_pockettts trimite POST multipart cu text + voice_wav (voce clonată) sau voice_url (voce predefinită), ignorând lang", "Eșec tehnic (httpx.ConnectError/timeout/5xx) în _synthesize_pockettts declanșează retry automat pe _synthesize_supertonic(voice='M2', lang='ro'); eroare de conținut (voce inexistentă/text gol) NU declanșează fallback", "Rezultatul synthesize() include cheia engine_used", "_MAX_TTS_CHARS=400 rămâne doar pe calea Supertonic; retry-ul recursiv lang='na' din Supertonic e păstrat" ], "tags": [ "backend", "refactor" ], "dependsOn": [ "US-003", "US-005" ], "requiresBrowserCheck": false, "requiresDesignReview": false, "passes": true, "failed": false, "blocked": false, "retries": 1, "failureReason": "", "notes": "Fișiere: tools/tts.py (refactor). synthesize() rezolvă engine din tts_voices.json (entry.get(\"engine\") sau fallback la config tts.default_engine); _synthesize_supertonic (fost synthesize, cap _MAX_TTS_CHARS=400 + retry recursiv lang=\"na\" păstrate neschimbate) și _synthesize_pockettts (POST multipart la {pockettts_url}/tts cu voice_wav din state_path sau voice_url, ignoră lang) nou. _PocketTTSUnavailable (ConnectError/Timeout/5xx) declanșează fallback automat pe _synthesize_supertonic(voice=M2, lang=ro) conform AC; erori de conținut (fișier voce lipsă, 4xx, text gol) NU declanșează fallback. Rezultatul include engine_used. Semnătura publică synthesize(text, voice, lang) neschimbată — verificat caller src/fast_commands.py. Gate /workflow:simplify (refactor, 4 agenți reuse/simplification/efficiency/altitude): aplicat 1 fix sigur (engine_used setat o singură dată, nu triplu literal); respinse ca false-positive 3 findings (voice=M2/lang=ro hardcodat pe fallback e cerință explicită din AC, nu bandaid; lookup catalog case-insensitive ar schimba comportament fără beneficiu clar în scope); notat pentru follow-up neaplicat: caching catalog/config/voice-file per sesiune în hot path-ul de streaming (src/voice/tts_stream.py) — necesită schimbări în afara scope-ului acestui story. Gate /review (backend, manual): PASS — fără shell injection, fără secret leakage, error handling scoped corect (ConnectError/TimeoutException/HTTPStatusError), API contract păstrat. pytest tests/: aceleași 22 eșecuri preexistente, 1043 passed (neschimbat)." }, { "id": "US-007", "title": "Comandă Discord /voice engine", "description": "Comandă nouă /voice engine în src/adapters/discord_voice.py care setează config.tts.default_engine (persistă) și răspunde cu confirmare + vocea default a engine-ului ales.", "priority": 70, "acceptanceCriteria": [ "/voice engine acceptă doar pockettts|supertonic și scrie config.tts.default_engine prin Config().set", "Valoarea persistă între restart-uri (verificat prin reload config)", "Răspunsul confirmă engine-ul ales și vocea default curentă" ], "tags": [ "ui", "backend" ], "dependsOn": [ "US-005", "US-006" ], "requiresBrowserCheck": true, "requiresDesignReview": false, "passes": true, "failed": false, "blocked": false, "retries": 0, "failureReason": "", "notes": "Fișiere: src/adapters/discord_voice.py (comandă nouă /voice engine + helper _default_voice_for_engine). Comanda folosește @app_commands.choices cu exact 2 valori (pockettts/supertonic, la fel ca pattern-ul /voice setvoice existent), defer(ephemeral=True), scrie tts.default_engine prin Config().set+save(), răspunde cu engine-ul ales + vocea default asociată (_default_voice_for_engine: 'alba' pentru pockettts, voice.default_voice curent pentru supertonic). Verificat: register() rulează curat într-un CommandTree izolat (fără a atinge bot-ul live) — comanda 'engine' apare cu choices corecte; Config().set/save/reload roundtrip verificat direct (pockettts<->supertonic, valoare restaurată la final, git diff config.json curat). Gate ui: browser/agent-browser nu se aplică — nu există UI web pentru această comandă (e slash command Discord, nu pagină pe techStack.port); am evitat intenționat restart-ul serviciului live echo-core.service (bot-ul de producție Discord/Telegram/WhatsApp folosit activ de Marius) pentru testare end-to-end, ca să nu întrerup serviciul mid-day — recomand test manual /voice engine după următorul restart/deploy. Gate /review (backend, manual): PASS — diff mic, urmează exact pattern-ul setvoice existent, fără shell injection/secret leakage, error handling scoped pe save failure. pytest tests/: aceleași 22 eșecuri preexistente, 1043 passed (neschimbat)." }, { "id": "US-008", "title": "Comandă Discord /voice addvoice", "description": "Comandă nouă /voice addvoice care descarcă+validează wav-ul (extensie + durată min ~3s), oprește temporar pocket-tts.service, rulează scriptul de export în venv separat, scrie catalogul și răspunde cu numele final atribuit + un preview audio generat cu vocea nouă.", "priority": 80, "acceptanceCriteria": [ "Callback-ul folosește await interaction.response.defer(ephemeral=True)", "Validează extensia audio și durata minimă (~3s), respinge clar sample-uri prea scurte", "Oprește pocket-tts.service pe durata export-ului cu mesaj 'Adaug voce, TTS indisponibil ~30s' și îl repornește după", "Invocă tools/pocket_tts_add_voice.py cu <.venv-pockettts>/bin/python și răspunde cu numele final (ex. 'Marius 4') + sample audio preview" ], "tags": [ "ui", "backend" ], "dependsOn": [ "US-004", "US-006" ], "requiresBrowserCheck": true, "requiresDesignReview": false, "passes": true, "failed": false, "blocked": false, "retries": 1, "failureReason": "", "notes": "Fișiere: src/adapters/discord_voice.py (comandă nouă /voice addvoice + helperi _systemctl_user, _parse_registered_voice_name, _tts_synthesize_preview). Codul era deja scris (necommitat) dintr-o iterație anterioară care a atins max_turns fără commit — verificat linie cu linie contra AC-urilor, nu a fost nevoie de rescriere. Validează extensia .wav + durata minimă 3s prin modulul wave (stdlib), trimite mesaj interimar 'Adaug voce, TTS indisponibil ~30s...', oprește pocket-tts.service prin systemctl --user (best-effort, nu ridică), rulează tools/pocket_tts_add_voice.py cu .venv-pockettts/bin/python (subprocess list-args, fără shell=True — fără risc de shell injection pe --name), repornește serviciul în finally indiferent de rezultat/timeout, parsează numele final atribuit din stdout ('Registered voice ... ->'), generează preview audio prin tools/tts.py synthesize() și îl atașează ca discord.File, apoi șterge fișierul temporar. Verificat: register() rulează curat într-un CommandTree izolat — comanda 'addvoice' apare cu parametrii nume:str și sample:Attachment. hf_token nu apare niciodată în discord_voice.py — rămâne complet în subprocesul pocket_tts_add_voice.py via keyring. Gate ui: fără server web pe techStack.port pentru această comandă (e slash command Discord, nu pagină) — verificare structurală prin CommandTree izolat, la fel ca US-007; nu am restart-uit echo-core.service (bot live folosit de Marius) pentru test end-to-end. Gate /review (backend, manual): PASS — subprocess cu argumente ca listă (fără shell injection), fără secret leakage, systemctl start garantat în finally chiar la timeout, fișier temporar curățat în finally. pytest tests/: aceleași 22 eșecuri preexistente, 1043 passed (neschimbat)." }, { "id": "US-009", "title": "Autocomplete voci din catalog pentru /audio și /voice setvoice", "description": "Înlocuiește listele statice @app_commands.choices din /audio (discord_bot.py) și /voice setvoice (discord_voice.py) cu @app_commands.autocomplete care citește tts_voices.json live, astfel încât vocile clonate noi apar fără redeploy. Elimină listele hardcodate VOICES/_VOICES în favoarea catalogului.", "priority": 90, "acceptanceCriteria": [ "/audio parametrul voce folosește autocomplete care citește tts_voices.json la runtime", "/voice setvoice folosește autocomplete din același catalog", "Listele hardcodate de voci (VOICES din tools/tts.py, _VOICES din fast_commands.py, choices statice din discord_bot.py/discord_voice.py) citesc din tts_voices.json; voice_commands.py._VALID_VOICES rămâne neschimbat", "O voce clonată nou adăugată în catalog apare în autocomplete fără modificare de cod" ], "tags": [ "ui", "backend" ], "dependsOn": [ "US-003", "US-006" ], "requiresBrowserCheck": true, "requiresDesignReview": false, "passes": true, "failed": false, "blocked": false, "retries": 1, "failureReason": "", "notes": "Fișiere: tools/tts.py (+list_voice_names(engine=None), citește tts_voices.json prin _load_voice_catalog/jsonlock; _synthesize_supertonic folosește list_voice_names(engine='supertonic') cu fallback pe _FALLBACK_SUPERTONIC_VOICES dacă catalogul lipsește), src/fast_commands.py (_VOICES înlocuit cu _supertonic_voice_names() care importă tools/tts.py lazy), src/adapters/discord_bot.py (/audio: @app_commands.choices static înlocuit cu @app_commands.autocomplete(voce=_voice_autocomplete) care filtrează pe input curent din tts.list_voice_names()), src/adapters/discord_voice.py (/voice setvoice: @app_commands.choices static înlocuit cu autocomplete similar; parametrul voice devine str simplu + validare explicită 'voce necunoscută' dacă nu e în catalog). Codul era deja scris (necommitat) dintr-o iterație anterioară care a atins max_turns — verificat linie cu linie contra AC-urilor, fără nevoie de rescriere. voice_commands.py._VALID_VOICES neatins, conform AC. Verificat: tts.list_voice_names() -> toate cele 17 intrări din tts_voices.json; list_voice_names(engine='supertonic') -> doar M1-M5/F1-F5; list_voice_names(engine='pockettts') -> Marius 1-3/Paula 1-3/alba. Gate ui: fără server web pe techStack.port pentru comenzi Discord — verificat structural prin create_bot()+discord_voice.register() izolat: parametrul 'voce' din /audio și 'voice' din /voice setvoice au autocomplete legat corect (p.autocomplete e callback-ul, nu None), celelalte parametri (text_sau_url, rezumat) rămân fără autocomplete. Gate /review (backend, manual): PASS — _load_voice_catalog citește prin src.jsonlock (read_locked), fără shell/subprocess nou, fără secret leakage, import lazy tools/tts.py identic cu pattern-ul existent din _tts_synthesize_preview. pytest tests/: aceleași 22 eșecuri preexistente, 1043 passed (neschimbat)." }, { "id": "US-010", "title": "Extinde /voice doctor cu health-check pocket-tts", "description": "Comanda /voice doctor adaugă un ping la GET :7789/health pentru pocket-tts alături de verificarea Supertonic existentă și un check pentru prezența hf_token în keyring.", "priority": 100, "acceptanceCriteria": [ "/voice doctor face ping la http://127.0.0.1:7789/health și raportează status pocket-tts", "Raportează prezența/absența hf_token în keyring", "Verificarea Supertonic existentă rămâne funcțională" ], "tags": [ "backend" ], "dependsOn": [ "US-001", "US-002" ], "requiresBrowserCheck": false, "requiresDesignReview": false, "passes": true, "failed": false, "blocked": false, "retries": 0, "failureReason": "", "notes": "Fișiere: src/adapters/discord_voice.py (comanda /voice doctor extinsă). Adăugat check hf_token în keyring (src.credential_store.get_secret, doar prezență, fără leakage) și check pocket-tts GET {tts.pockettts_url}/health (timeout 3s, httpx.AsyncClient, wrapped try/except -> FAIL pe orice excepție). Checks-urile existente (libopus, voice load error) neatinse. Verificat manual: get_secret(\"hf_token\") is not None -> True; GET http://127.0.0.1:7789/health -> 200 (pocket-tts.service live din US-001). Structural: register(tree, bot) izolat -> comanda doctor prezentă în /voice group. Gate /review (backend, manual): PASS — fără shell injection, fără secret leakage (doar bool presence), error handling scoped (try/except pe fiecare check nou, degrade la FAIL nu crash). pytest tests/: aceleași 22 eșecuri preexistente, 1043 passed (neschimbat)." }, { "id": "US-011", "title": "Teste minime pentru rutare TTS și config", "description": "tests/test_tts.py (nou) acoperă rutarea engine din catalog, fallback la default_engine, trigger fallback tehnic cu engine_used corect, non-declanșare pe erori de conținut și regression pe retry-ul lang='na'. tests/test_config.py verifică persistența tts.default_engine.", "priority": 110, "acceptanceCriteria": [ "tests/test_tts.py: voce catalogată → engine corect (mock catalog); voce necatalogată → default_engine din config", "tests/test_tts.py: mock httpx.ConnectError/5xx pe _synthesize_pockettts → apel automat pe _synthesize_supertonic + engine_used corect; input eronat (voce inexistentă) NU cade pe supertonic", "tests/test_tts.py: regression test pentru retry recursiv lang='na' în Supertonic", "tests/test_config.py verifică Config().set('tts.default_engine',...) persistă prin save()/reload()", "source .venv/bin/activate && pytest tests/test_tts.py tests/test_config.py iese PASS" ], "tags": [ "backend" ], "dependsOn": [ "US-006" ], "requiresBrowserCheck": false, "requiresDesignReview": false, "passes": true, "failed": false, "blocked": false, "retries": 0, "failureReason": "", "notes": "tests/test_tts.py (nou, 11 teste): rutare engine din catalog (pockettts/supertonic), fallback pe default_engine pentru voce necatalogată, fallback tehnic (ConnectError/5xx) pe Supertonic cu engine_used corect, eroare de conținut (fișier .safetensors lipsă) NU declanșează fallback, regression retry lang na->na (fără retry dublu). tests/test_config.py: test dedicat persistență tts.default_engine prin save()/reload() (peste acoperirea generică deja existentă). pytest tests/test_tts.py tests/test_config.py: 34 passed. Suită completă: 1055 passed, 22 preexisting failures neschimbate." }, { "id": "US-012", "title": "Documentează comenzile TTS noi în TOOLS.md", "description": "Actualizează personality/TOOLS.md cu comenzile noi (/voice engine, /voice addvoice, autocomplete voci) și cu noul engine pocket-tts + catalogul tts_voices.json.", "priority": 120, "acceptanceCriteria": [ "personality/TOOLS.md descrie /voice engine și /voice addvoice", "Documentează engine-ul pocket-tts, catalogul tts_voices.json și fluxul de adăugare voce nouă", "Nu contrazice comportamentul implementat (fallback, engine default din config)" ], "tags": [ "docs" ], "dependsOn": [ "US-007", "US-008", "US-009" ], "requiresBrowserCheck": false, "requiresDesignReview": false, "passes": true, "failed": false, "blocked": false, "retries": 0, "failureReason": "", "notes": "personality/TOOLS.md, secțiune Discord Voice extinsă cu subsecțiunea TTS: engine pocket-tts + Supertonic — motoare, rutare din tts_voices.json, /voice engine, /voice setvoice, /voice addvoice (venv separat .venv-pockettts, tools/pocket_tts_add_voice.py), /voice doctor (health-check), /audio. Comportament documentat (rutare din catalog, fallback tehnic vs eroare de conținut) verificat contra tools/tts.py și src/adapters/discord_voice.py — nu contrazice implementarea." } ] }