From 18c05ede39a92ce69ecf8d96482a33bc747241bc Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Sat, 11 Jul 2026 10:05:05 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20US-002=20-=20Adaug=C4=83=20hf=5Ftoken?= =?UTF-8?q?=20la=20verificarea=20de=20secrete=20=C3=AEn=20credential=5Fsto?= =?UTF-8?q?re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - REQUIRED_SECRETS include acum hf_token alături de discord_token - teste actualizate pentru noul set de secrete obligatorii (+ test_partial_required) - gates rulate: tests PASS (22 eșecuri preexistente neschimbate), /review (backend) PASS --- scripts/ralph/prd.json | 333 +++++++++++++++++++++++++++++++++++++ scripts/ralph/progress.txt | 46 +++++ src/credential_store.py | 2 +- tests/test_secrets.py | 11 +- 4 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 scripts/ralph/prd.json create mode 100644 scripts/ralph/progress.txt diff --git a/scripts/ralph/prd.json b/scripts/ralph/prd.json new file mode 100644 index 0000000..9394cea --- /dev/null +++ b/scripts/ralph/prd.json @@ -0,0 +1,333 @@ +{ + "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": false, + "failed": false, + "blocked": false, + "retries": 0, + "failureReason": "", + "notes": "" + }, + { + "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": false, + "failed": false, + "blocked": false, + "retries": 0, + "failureReason": "", + "notes": "" + }, + { + "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": false, + "failed": false, + "blocked": false, + "retries": 0, + "failureReason": "", + "notes": "" + }, + { + "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": false, + "failed": false, + "blocked": false, + "retries": 0, + "failureReason": "", + "notes": "" + }, + { + "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": false, + "failed": false, + "blocked": false, + "retries": 0, + "failureReason": "", + "notes": "" + }, + { + "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": false, + "failed": false, + "blocked": false, + "retries": 0, + "failureReason": "", + "notes": "" + }, + { + "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": false, + "failed": false, + "blocked": false, + "retries": 0, + "failureReason": "", + "notes": "" + }, + { + "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": false, + "failed": false, + "blocked": false, + "retries": 0, + "failureReason": "", + "notes": "" + }, + { + "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": false, + "failed": false, + "blocked": false, + "retries": 0, + "failureReason": "", + "notes": "" + }, + { + "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": false, + "failed": false, + "blocked": false, + "retries": 0, + "failureReason": "", + "notes": "" + } + ] +} \ No newline at end of file diff --git a/scripts/ralph/progress.txt b/scripts/ralph/progress.txt new file mode 100644 index 0000000..aa059e2 --- /dev/null +++ b/scripts/ralph/progress.txt @@ -0,0 +1,46 @@ +# Ralph Progress Log +Started: 2026-07-11 09:56 +Project: echo-core +--- + +## Iterație: 2026-07-11 10:03 +### Story implementat: US-001 - Setup venv-pockettts și pocket-tts.service systemd unit (tags: infra) +### Status: Complete + +### Gates rulate: +- Typecheck: SKIP (mypy neinstalat în .venv; niciun fișier Python schimbat de acest story) +- Lint: SKIP (ruff neinstalat în .venv; niciun fișier Python schimbat) +- Tests: PASS parțial — pytest tests/ rulează cu 22 eșecuri preexistente, nelegate de acest story (nu am schimbat cod Python); 1042 passed +- /review (infra): PASS — diff minimal (.gitignore +1 linie, launcher shell script nou); fără shell injection, fără secret leakage (hf_token citit din keyring, niciodată logat/hardcodat) +- Smoke test manual: curl -s http://127.0.0.1:7789/health -> 200; systemctl --user restart pocket-tts.service -> health revine 200 în ~3-6s; systemctl --user is-enabled -> enabled + +### Learnings: +- Implementarea de bază (venv, launcher, systemd unit) fusese deja făcută într-o iterație anterioară care nu apucase să facă commit/mark passes — la începutul iterației verifică mereu starea reală (ls, systemctl status, curl) înainte să presupui că trebuie construit de la zero. +- Pattern existent: doar dashboard/echo-taskboard.service e trackuit în git ca template; supertonic-tts.service (alt serviciu existent) NU e trackuit — deci pocket-tts.service urmează același pattern, rămâne doar în ~/.config/systemd/user/, nu în repo. +- .venv al proiectului nu are ruff/mypy instalate — gate-urile de typecheck/lint din PRD sunt aspiraționale, nu configurate încă; pentru story-uri Python viitoare (US-002+) verifică disponibilitatea înainte să te bazezi pe fail/pass real. + +### Next: +- US-002 (backend, hf_token în REQUIRED_SECRETS) — eligibil, dependsOn gol +- US-003 (db+backend, catalog tts_voices.json) — eligibil, dependsOn gol +- US-005 (backend, config.json tts section) — eligibil, dependsOn gol +--- + +## Iterație: 2026-07-11 (US-002) +### Story implementat: US-002 - Adaugă hf_token la verificarea de secrete în credential_store (tags: backend) +### Status: Complete + +### Gates rulate: +- Typecheck: SKIP (mypy neinstalat în .venv) +- Lint: SKIP (ruff neinstalat în .venv) +- Tests: PASS — pytest tests/ rulează cu aceleași 22 eșecuri preexistente (neschimbate, confirmat prin git stash diff), 1043 passed (vs 1042 înainte, +1 test nou) +- /review (backend): PASS — diff trivial (o linie de date + teste actualizate), fără risc de corectitudine/securitate + +### Learnings: +- REQUIRED_SECRETS e folosit doar de check_secrets() (CLI `eco secrets test` / `eco doctor`), nu blochează startup-ul aplicației — sigur de extins fără risc de regressie funcțională. +- Testele existente pentru check_secrets asertau egalitate exactă de dict, deci orice extindere a REQUIRED_SECRETS necesită update sincron al testelor (test_missing_required, test_present_required, test_all_present din TestCLISecretsTest). +- Cei 22 de eșecuri preexistenți (test_cli.py::TestDoctor::test_all_pass, test_discord.py, test_heartbeat.py, test_voice_normalize.py etc.) sunt independenți de acest story — verificat cu `git stash` înainte/după. + +### Next: +- US-003 (db+backend, catalog tts_voices.json) — eligibil, dependsOn gol +- US-005 (backend, config.json tts section) — eligibil, dependsOn gol +--- diff --git a/src/credential_store.py b/src/credential_store.py index 7e36c79..c16bb9a 100644 --- a/src/credential_store.py +++ b/src/credential_store.py @@ -6,7 +6,7 @@ import keyring SERVICE = "echo-core" # Required secrets that should exist for full functionality -REQUIRED_SECRETS = ["discord_token"] +REQUIRED_SECRETS = ["discord_token", "hf_token"] def set_secret(name: str, value: str) -> None: diff --git a/tests/test_secrets.py b/tests/test_secrets.py index e7dc0b5..67d8570 100644 --- a/tests/test_secrets.py +++ b/tests/test_secrets.py @@ -165,12 +165,18 @@ class TestListSecrets: class TestCheckSecrets: def test_missing_required(self, mock_keyring): result = check_secrets() - assert result == {"discord_token": False} + assert result == {"discord_token": False, "hf_token": False} def test_present_required(self, mock_keyring): set_secret("discord_token", "tok-123") + set_secret("hf_token", "hf-456") result = check_secrets() - assert result == {"discord_token": True} + assert result == {"discord_token": True, "hf_token": True} + + def test_partial_required(self, mock_keyring): + set_secret("discord_token", "tok-123") + result = check_secrets() + assert result == {"discord_token": True, "hf_token": False} # =========================================================================== @@ -250,6 +256,7 @@ class TestCLISecretsTest: def test_all_present(self, mock_keyring, capsys): set_secret("discord_token", "tok") + set_secret("hf_token", "hf-tok") with patch("sys.argv", ["echo", "secrets", "test"]): cli_main() out = capsys.readouterr().out