Dashboard-ul face `git add -A` + commit + push fără intervenție umană (dashboard/handlers/git.py:110). Regulile pe nume exact acopereau doar fișierele observate, nu clasele lor: `bridge/whatsapp/auth/` era ignorat, dar `auth.bak-*/` nu — așa a ajuns creds.json cu chei de sesiune WhatsApp pe Gitea. Audit: din 21 de nume plauzibile testate (.env.local, credentials.json, token.json, id_rsa, auth-old/, server.pem, cookies.txt, dump.sql, config.json.bak, nohup.out), toate 21 treceau nestingherite. Acum toate sunt prinse. - bridge/whatsapp/auth*/ acoperă orice variantă de director de stare Baileys - .env* (cu excepție pentru .env.example), *.pem, *.key, id_rsa*, id_ed25519* - *token*.json, *credential*.json, client_secret*.json, creds.json, cookies*.txt - backup-uri/dump-uri: *.bak, *.backup, *.orig, *.dump, *.sql, *.tar.gz, *.zip, *.bundle — cu !memory/kb/**/*.bak pentru notițele legitime din KB - nohup.out, core.[0-9]* Verificat în ambele direcții: `git ls-files | git check-ignore --stdin` nu întoarce nimic (niciun fișier tracked nu devine ignorat), iar cele 21 de nume ipotetice sunt acum toate ignorate. Restul auditului e curat: zero secrete în fișierele tracked, config.json fără credențiale (keyring folosit corect), credentials/, dashboard/.env, memory/echo.sqlite și bridge/whatsapp/auth/ deja acoperite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188YmDTUAzVvZDVh8JL8xSa
114 lines
18 KiB
Markdown
114 lines
18 KiB
Markdown
# Lessons Learned
|
||
|
||
Lecții capturate din corectările lui Marius. Citește acest fișier la începutul oricărei sesiuni de cod (înainte de plan mode) și aplică lecțiile relevante. Iterează neobosit pentru a evita rate drop-uri pe greșeli repetate.
|
||
|
||
**Format per lecție:**
|
||
|
||
```
|
||
## <titlu scurt>
|
||
**Data:** YYYY-MM-DD
|
||
**Context:** ce făceam când a apărut corectarea
|
||
**Greșeala:** ce am făcut greșit
|
||
**Regula:** ce să fac în schimb, în viitor
|
||
**Când se aplică:** trigger-uri concrete (fișiere, task-uri, situații)
|
||
```
|
||
|
||
---
|
||
|
||
<!-- Lecțiile se adaugă mai jos, cele mai noi sus. -->
|
||
|
||
## La debugging cron, verifică ȘI crontab-ul de sistem, nu doar `cron/jobs.json`
|
||
**Data:** 2026-07-23
|
||
**Context:** Marius a întrebat de ce newsletter-ul cercetași a venit pe Discord dar nu pe WhatsApp. Am căutat doar în `cron/jobs.json` (scheduler-ul APScheduler intern), n-am găsit niciun job relevant, și am conchis greșit că nu există job automat — Marius m-a corectat ferm ("Nu este adevărat. Exista un job care rulează prima data joia").
|
||
**Greșeala:** Există o a doua sursă de cron în acest repo: crontab-ul real de sistem (`crontab -l`), separat de `cron/jobs.json`. Job-ul real era `tools/check_newsletter_cercetasi.py`, programat direct în crontab de sistem (`0 17 * * 4,5,1`), nu în scheduler-ul intern. N-am verificat crontab-ul de sistem înainte să declar "nu există job".
|
||
**Regula:** La orice întrebare despre "de ce n-a rulat / de ce n-a trimis un job automat", verifică AMBELE surse înainte să tragi concluzii: (1) `cron/jobs.json` (APScheduler intern, gestionat de `src/scheduler.py`), (2) `crontab -l` (cron de sistem, scripturi standalone în `tools/*.py` cu shebang + subprocess). Grep după numele funcțональ ("newsletter", "cercetasi") în `tools/*.py` și `crontab -l`, nu doar în jobs.json.
|
||
**Când se aplică:** Orice debugging de tip "de ce nu am primit X automat" / "unde e programat job-ul Y" în echo-core.
|
||
|
||
## Transliterează și numele proprii pe turnuri `[tts-lang:en]` — un singur diacritic declanșează fallback pe Supertonic
|
||
**Data:** 2026-07-11
|
||
**Context:** Marius a întrebat vremea la Constanța (turn de voce, vocea activă = pocket-tts, marker `[tts-lang:en]`). Am scris răspunsul în engleză dar am păstrat "Constanța" cu ț original. Rezultatul audio a venit cu o voce Supertonic, nu cu Marius 4 (pocket-tts) cum se aștepta Marius.
|
||
**Greșeala:** Am presupus greșit că "engleză" înseamnă doar propoziții englezești — am lăsat un nume propriu (Constanța) netranslit. `tools/tts.py::looks_romanian()` (folosit în `src/voice/tts_stream.py:213`) verifică prezența oricărui caracter din `_RO_DIACRITICS` (ă/â/î/ș/ț), indiferent de context — UN SINGUR caracter e suficient. Codul a funcționat corect conform design-ului: a detectat diacriticul, a presupus că tot blocul e română netradusă, și a căzut intenționat pe Supertonic (mai bine o voce RO decât tăcere totală) — vezi comentariul din `tts_stream.py:213-222`. Deci nu a fost bug de cod, ci scriere incompletă din partea mea.
|
||
**Regula:** Pe orice turn cu marker `[tts-lang:en]`, transliterează ȘI numele proprii/toponimele (Constanța → Constanta, Brașov → Brasov, etc.) — nu doar propozițiile. Verifică explicit că răspunsul nu conține NICIUN caracter din `ăâîșțĂÂÎȘȚşţŞŢ` înainte de a trimite, altfel guard-ul din `tts_stream.py` face fallback silențios pe Supertonic și vocea aleasă de Marius (pockettts) nu se aude deloc.
|
||
**Când se aplică:** Orice răspuns pe un turn de voce cu vocea activă pockettts (Marius 1-4, Paula 1-3, alba) și marker `[tts-lang:en]` în prefix. Verifică inclusiv toponime, nume, abrevieri românești din text.
|
||
|
||
## Intră în plan mode ÎNAINTE de a executa orice modificare de cod
|
||
**Data:** 2026-05-28
|
||
**Context:** Marius a descris o cerință de îmbunătățire a comenzii `/audio` cu URL (chunk by chunk). Am implementat direct fără plan mode.
|
||
**Greșeala:** Am sărit peste pasul de planificare și am modificat fișierele fără aprobarea lui Marius.
|
||
**Regula:** Pentru orice modificare de cod (nu doar task-uri cu 3+ pași), intră în plan mode, prezintă planul, și AȘTEAPTĂ aprobarea înainte de a atinge vreun fișier.
|
||
**Când se aplică:** Orice cerere de cod/implementare, indiferent de simplitate aparentă. Dacă e tentant să implementezi direct pentru că pare simplu — e exact momentul să te oprești și să planifici.
|
||
|
||
## Supertonic rejectează ghilimelele curly (Unicode) cu HTTP 500
|
||
**Data:** 2026-05-27
|
||
**Context:** Marius a dat o comandă audio pe Discord cu un URL, iar răspunsul lui Claude conținea `„foo"` (ghilimele românești curly). Supertonic a returnat `HTTP 500: synthesis failed: Found 1 unsupported character(s): ['„']` și răspunsul nu s-a mai auzit. Fără retry logic vizibil în UX — pur și simplu tace.
|
||
**Greșeala:** Am presupus că `normalize_for_tts` produce text deja "TTS-safe" pentru Supertonic. În realitate `strip_markdown` păstrează ghilimelele Unicode (`„` U+201E, `"` U+201D, `—` U+2014, `…` U+2026, etc.) pe care Supertonic le refuză.
|
||
**Regula:** Înainte de orice apel HTTP la Supertonic, **sanitizează punctuația Unicode** la echivalentele ASCII (`„` `"` `"` → `"`, `'` `'` `‚` → `'`, `–` `—` → `-`, `…` → `...`, `«` `»` → `"`). Funcția `sanitize_punctuation` în `src/voice/normalize.py` face asta și e apelată chiar după `strip_markdown` în pipeline. Dacă apar caractere noi care crapă Supertonic (ex: simboluri matematice, săgeți), adaugă-le în `_TTS_PUNCT_MAP`.
|
||
**Când se aplică:** Orice cod care trimite text la Supertonic (`tools/tts.py`, `src/voice/tts_stream.py`). Inclusiv testare manuală cu `curl` — folosește text românesc realistic (include `„foo"`, em-dash `—`, ellipsis `…`).
|
||
|
||
## Mai multe threads ≠ mai rapid — fitează `cpu_threads` pe physical cores, nu logical
|
||
**Data:** 2026-05-27
|
||
**Context:** Benchmark `tools/voice_bench.py` pentru faster-whisper `small` int8 pe i7-6700T (4 physical / 8 logical cores). Marius a urcat VM-ul de la 2 → 4 → 6 cores online, așteptând că mai multe = mai rapid.
|
||
**Greșeala:** Presupoziție implicită că `cpu_threads=N` scalează liniar cu N. La 6 threads `small.p50` a regresat la 2.79s vs 2.25s la 4 threads (+24% MAI LENT). Era ușor de ratat dacă rulam doar un singur pass.
|
||
**Regula:** Pentru workload-uri compute-bound (int8/fp16 ML inference, video encode, criptografie) setează `cpu_threads = numărul de PHYSICAL cores`, NU logical. Hyperthreads adaugă synchronization overhead și memory bandwidth contention fără paralelism real. Sweet spot tipic: `min(num_physical_cores, $optimal_threads)`. Verifică cu `lscpu` (Core(s) per socket × Socket(s) = physical; CPU(s) = logical). Dacă faci benchmark, rulează SWEEP nu single point — 2/4/6/8 threads să vezi unde e curba reală.
|
||
**Când se aplică:** Configurare `cpu_threads`, `OMP_NUM_THREADS`, `MKL_NUM_THREADS`, `torch.set_num_threads()`, ffmpeg `-threads`, sau orice runtime ML/inference. Mai ales pe Proxmox VM-uri unde "more cores online" sună ca îmbunătățire. Întreabă-te: e workload compute-bound (yes → physical only) sau IO-bound (yes → logical OK)?
|
||
|
||
## Nu șterge crontab-uri din sistem fără confirmare explicită
|
||
**Data:** 2026-05-20
|
||
**Context:** Marius a cerut să șteargă "newsletter test din cron jobs". Am interpretat că `check_newsletter_cercetasi.py` din crontab de sistem face parte din "newsletter test".
|
||
**Greșeala:** Am inclus în scop un crontab de sistem care nu fusese menționat explicit. "newsletter test" se referea doar la job-ul `newsletter-test` din `cron/jobs.json`.
|
||
**Regula:** Crontab-ul de sistem (`crontab -l`) este separat de `cron/jobs.json`. Nu îl modifica fără instrucțiuni explicite. Dacă scope-ul nu e clar, întreabă înainte de a acționa pe crontab sistem.
|
||
**Când se aplică:** Orice task care implică ștergerea sau modificarea cron jobs — distinge întotdeauna între `cron/jobs.json` (APScheduler) și crontab-ul de sistem.
|
||
|
||
## Nu scrie manual în index.json — rulează update_notes_index.py
|
||
**Data:** 2026-04-29
|
||
**Context:** Salvam o notiță din Facebook reel în memory/kb/. Am adăugat manual o intrare în index.json cu schema greșită (`id` + `path` în loc de `file`), ceea ce a blocat notes.html pe "Se încarcă..." cu un TypeError în renderNoteCard.
|
||
**Greșeala:** Am editat index.json direct, cu o schemă diferită față de ce produce update_notes_index.py.
|
||
**Regula:** Niciodată nu scriei manual în `memory/kb/index.json`. Fluxul corect: (1) creezi fișierul `.md` în `memory/kb/<categorie>/`, (2) rulezi `python3 tools/update_notes_index.py`. Dacă ai nevoie să salvezi o notiță din Facebook/video, folosești `scripts/transcribe_video.sh <URL> auto --save-kb --bg --notify <channel_id>` care face totul corect (`--bg` e obligatoriu dintr-un turn de chat — vezi lecția despre timeout-ul de 300s).
|
||
**Când se aplică:** Orice salvare de notiță în KB (Facebook, YouTube, coaching, insights, orice). Dacă ești tentat să `json.dump` în index.json — stop, rulează scriptul.
|
||
|
||
## Verifică că modelul/tool-ul numit chiar are capabilitatea ÎNAINTE de a planifica în jurul lui
|
||
**Data:** 2026-06-27
|
||
**Context:** Marius a cerut să folosesc `gemma4:31b-cloud` (Ollama) pentru decodare audio ca alternativă la Whisper. Am verificat pe pagina oficială Ollama: variantele cloud (31b) suportă doar Text+Image — audio există DOAR pe E2B/E4B (edge, local), iar acela e stricat de o regresie upstream deschisă (issue #16584). Premisa cererii era infezabilă.
|
||
**Greșeala (evitată):** Dacă planificam direct integrarea fără să verific pagina modelului, scriam cod de cablare Ollama audio care n-ar fi funcționat niciodată. Search-ul generic spunea „Gemma 4 are audio" — adevărat la nivel de familie, fals pentru modelul cloud specific cerut.
|
||
**Regula:** Când userul numește un model/serviciu specific pentru o capabilitate (audio, vision, tool-use, context lung), verifică pagina/docs ACELUI model exact înainte de a planifica. Capabilitățile diferă per variantă (cloud vs edge, sizes). Fetch pagina oficială, nu te baza pe search agregat la nivel de familie.
|
||
**Când se aplică:** Orice task care pornește de la „folosește modelul X pentru Y". Confirmă X→Y pe sursa primară înainte de plan mode.
|
||
|
||
## Corecția post-STT de text e cosmetică dacă consumatorul e un LLM — fixează la sursă (model), nu cu dicționar
|
||
**Data:** 2026-06-27
|
||
**Context:** Plan inițial pentru curățarea transcrierii Whisper avea 4 piese, inclusiv dicționar de restaurare diacritice + canonicalizare wake-word. Două review-uri independente (/autoplan CEO+Eng) au arătat: textul transcris merge la Claude, care citește română fără diacritice perfect; NU există wake-word gate în cod (`on_segment_done` dispatch necondiționat); singurul consumator precis (`detect_voice_change`) e deja fuzz-hardenat. Un spike a confirmat că modelul RO-finetuned (`mikr/whisper-small-ro-cv11`) înjumătățește WER (24%→10%) și fixează numerele la SURSĂ, +0.33s latență.
|
||
**Greșeala (evitată):** Construirea unui strat de corecție hand-curat (întreținere perpetuă, risc de regresie pe cuvinte ambigue) când fix-ul real era un model finetuned cu același cost de inferență.
|
||
**Regula:** Înainte de a peticit output-ul unui model ML cu post-procesare rule-based, întreabă: (1) cine e CONSUMATORUL textului? (un LLM tolerează erori; un parser regex nu); (2) există un model finetuned care fixează la sursă cu același cost? Spike-uiește modelul ÎNAINTE de a scrie straturi de corecție. Verifică unde merge output-ul prin cod, nu presupune un gate care „pare" că există.
|
||
**Când se aplică:** Orice îmbunătățire de calitate STT/OCR/ML output. Tool de spike: `tools/voice_stt_spike.py`.
|
||
|
||
## Fallback-urile nu trebuie să schimbe identitatea percepută (vocea) — degradează în interiorul aceleiași identități
|
||
**Data:** 2026-07-11
|
||
**Context:** Fix pentru voice mode: când modelul scăpa diacritice RO pe o voce pocket-tts (English-only), am ales fallback pe Supertonic M2 (altă voce, română) ca să evit tăcerea. Un singur nume propriu („Constanța") într-un răspuns altfel englezesc a comutat vocea întregului bloc — Marius a perceput asta ca bug mai grav decât problema inițială.
|
||
**Greșeala:** Am optimizat pentru „să se audă ceva" fără să întreb ce dimensiune e prioritară pentru user. Vocea clonată E produsul; schimbarea ei e regresia maximă percepută.
|
||
**Regula:** Când un input parțial invalid ajunge la un sistem cu identitate configurată de user (voce, persona, stil), remediază conținutul ca să treacă (transliterare, sanitizare), nu comuta pe altă identitate. Fallback pe alt engine/voce doar la eșec TEHNIC (server picat), nu la eșec de conținut.
|
||
**Când se aplică:** Orice pipeline TTS/persona cu fallback. Implementare: `fold_ro_diacritics` în tools/tts.py + push_text în src/voice/tts_stream.py.
|
||
|
||
## Munca mai lungă de 300s nu are ce căuta într-un turn de chat — rulează detașat și raportează pe canal
|
||
**Data:** 2026-08-31
|
||
**Context:** Marius a trimis două linkuri de video Facebook pe Discord. Niciunul n-a produs un răspuns. În jurnal: `Claude CLI timed out after 300s`, de cinci ori, pe patru încercări diferite. `scripts/transcribe_video.sh` rula în interiorul turnului, iar yt-dlp + ffmpeg + openai-whisper pe 9 minute de audio depășeau constant `DEFAULT_TIMEOUT` (`src/claude_session.py:37`). Procesul era omorât la mijlocul Whisper-ului.
|
||
**Greșeala:** Am tratat o sarcină cu durată nemărginită (descărcare rețea + inferență ML) ca pe un apel sincron într-un turn conversațional. Efectele în cascadă au fost mai rele decât timeout-ul în sine:
|
||
- **Retry-uri oarbe pe linkul greșit** — la reîncercare am reprocesat linkul 1 de două ori (două notițe duplicate) și n-am procesat niciodată linkul 2. Când mai multe linkuri eșuează, verifică *care* a eșuat înainte să reiei.
|
||
- **Documente pe jumătate** — scriptul scrie template-ul cu TL;DR gol, iar rezumatul îl completez eu *după* ce scriptul întoarce. Procesul omorât ⇒ notițe cu TL;DR gol, care arată „gata" în index dar nu sunt.
|
||
- **Orfani pe disc** — `trap ... EXIT` nu rulează la SIGKILL; rămâneau 33MB în `/tmp/transcribe_$$`.
|
||
- **Eșec tăcut la nivel de user** — Marius n-a primit nici rezultat, nici eroare acționabilă.
|
||
**Regula:** Dacă un pas poate depăși ~2-3 minute, nu-l rula în turn. Pornește-l detașat (`setsid nohup`), întoarce imediat un confirm cu job id + cale de log, și raportează finalizarea pe canal (`tools/discord_send_file.py --text`). Turnul confirmă *pornirea*, nu *terminarea*.
|
||
**Corolare descoperite la fix:**
|
||
- **Nu forța limba.** Rularea cu `ro` pe audio englezesc a produs transcriere halucinată (română inventată amestecată cu CJK). Implicit e acum `auto`; forțează doar dacă ești sigur.
|
||
- **Alege motorul înainte de a ridica timeout-ul.** faster-whisper int8 face 554s de audio în 113s (~5x realtime); openai-whisper depășea 300s pe același fișier. `faster-whisper` era deja în `requirements.txt`, openai-whisper nici măcar nu era declarat. Un motor de 5x a fost fix mai bun decât un timeout mai mare.
|
||
- **Folosește `.venv/bin/python3` explicit în scripturi shell.** Scriptul chema `python3` bare, iar `whisper` nu există în python-ul de sistem — mergea doar din noroc, când era chemat cu venv-ul activat.
|
||
- **Nu trece JSON mare printr-o variabilă de shell.** Descrierile Facebook conțin caractere de control care corup variabila (`character not in range`); scrie-l pe disc și parsează fișierul o singură dată. `|| echo Unknown` masca eșecul.
|
||
- **Titlurile Facebook încep cu statistici,** nu cu titlul: `830K views · 15K reactions | Titlul real | Pagina`. `split('|')[0]` producea notițe numite `2026-08-31_807k-views-15k-reactions.md`. Sari segmentele de statistici și pe cel egal cu numele paginii.
|
||
**Când se aplică:** Orice transcriere video/audio, OCR pe fișiere mari, scraping multi-pagină, build sau inferență ML pornită dintr-un mesaj de chat. Implementare: `scripts/transcribe_video.sh` (`--bg`, `--notify`) + `send_text()` în `tools/discord_send_file.py`.
|
||
|
||
## Unde există `git add -A`, scrie `.gitignore` cu tipare, nu cu nume exacte
|
||
**Data:** 2026-08-31
|
||
**Context:** `bridge/whatsapp/auth.bak-*/` cu `creds.json` (chei de sesiune WhatsApp) a ajuns pe Gitea prin `chore: auto-commit from dashboard`. `.gitignore` avea `bridge/whatsapp/auth/` — regula acoperea directorul viu, nu backup-ul lui. Auditul de după a arătat că **toate** cele 21 de nume plauzibile testate (`.env.local`, `credentials.json`, `id_rsa`, `token.json`, `auth-old/`, `config.json.bak`, `dump.sql`) treceau nestingherite.
|
||
**Greșeala:** `.gitignore` era o listă de nume exacte observate, nu de clase de fișiere. Într-un repo unde `dashboard/handlers/git.py` face `git add -A` + `commit` + `push` fără intervenție umană, orice fișier neignorat e publicat automat — nu există pasul în care cineva se uită la ce se stagează.
|
||
**Regula:** Dacă ceva din sistem face `git add -A` neasistat, `.gitignore` e o barieră de securitate, nu o conveniență. Scrie-l pe tipare (`auth*/`, `.env*`, `*token*.json`, `*.bak`, `*.pem`), nu pe nume exacte, și adaugă excepții (`!memory/kb/**/*.bak`) pentru fals pozitivele legitime. Verifică ambele direcții: (1) `git ls-files | git check-ignore --stdin` să nu întoarcă nimic — niciun fișier tracked nu devine ignorat; (2) o listă de nume ipotetice de secrete să fie toate prinse.
|
||
**Când se aplică:** Orice repo cu auto-commit, dar și înainte de a adăuga un backup/dump lângă un fișier deja ignorat. Întrebarea de reflex: „regula acoperă și variantele de nume ale acestui fișier?"
|
||
|