Skip to content

docs: move design docs to docs/ and add an index#32

Merged
RebelliousSmile merged 25 commits into
mainfrom
claude/process-github-issues-MR2Nj
May 17, 2026
Merged

docs: move design docs to docs/ and add an index#32
RebelliousSmile merged 25 commits into
mainfrom
claude/process-github-issues-MR2Nj

Conversation

@RebelliousSmile
Copy link
Copy Markdown
Owner

La racine devient lisible : seul README.md reste comme point d'entrée.
Les 15 documents de design, stack et user stories vivent désormais
dans docs/, avec docs/INDEX.md pour la navigation thématique. Les
liens du README et l'arborescence projet sont mis à jour, et le
lien cassé vers IMPLEMENTATION_SUMMARY.md est corrigé vers
rust-backend/IMPLEMENTATION_SUMMARY.md.

claude added 25 commits May 17, 2026 06:25
La racine devient lisible : seul README.md reste comme point d'entrée.
Les 15 documents de design, stack et user stories vivent désormais
dans docs/, avec docs/INDEX.md pour la navigation thématique. Les
liens du README et l'arborescence projet sont mis à jour, et le
lien cassé vers IMPLEMENTATION_SUMMARY.md est corrigé vers
rust-backend/IMPLEMENTATION_SUMMARY.md.
Le README annonçait déjà la licence MIT mais le fichier manquait.
On ajoute :
- LICENSE : texte MIT complet (2026, LyRemember Team)
- CONTRIBUTING.md : workflow contrib, stacks (Python/Rust/Vue), TDD,
  conventional commits, tests par stack
- CHANGELOG.md : Keep a Changelog, section Unreleased seedée à partir
  de l'historique récent (refonte UI, mode invité, CI, build Android,
  réorganisation docs, etc.)

Le README pointe désormais vers ces fichiers depuis ses sections
Contributing / Changelog / License.
Pourquoi : le repo héberge trois stacks (Python CLI legacy, Rust backend,
Vue+Tauri frontend). Garder le POC Python à la racine entretient la
confusion sur la stack canonique. On l'isole dans legacy/python-cli/.

Ce qui bouge :
- Déplace lyremember/, tests/, data/, demo.py, setup.py, requirements.txt
  dans legacy/python-cli/.
- Workflow CI Python : path filters et working-directory mis à jour.
- README + docs/ARCHITECTURE.md : Project Structure et bloc legacy
  refletent les nouveaux chemins.
- .gitignore : remappe les ignores Python sur legacy/python-cli/.
- legacy/README.md créé pour signaler le statut archive.
- CHANGELOG.md : section Unreleased mise à jour.

Vérification : cd legacy/python-cli && python -m pytest tests/ -q -> 32 passed.
Ce fichier décrivait une stack PWA/React/FastAPI qui n'est plus celle
du projet. La stack canonique (Tauri+Vue+Rust+PyO3) est documentée dans
docs/FINAL_DECISIONS.md. La référence à TECH_STACK_FINAL.md dans
docs/INDEX.md est retirée. Aucune autre référence subsistante (vérifié
par grep).
Le service de traduction reellement implemente est LibreTranslate
(rust-backend/src/services/translation.rs, appel HTTP via reqwest).
Plusieurs docs mentionnaient encore deep-translator (wrapper Python
non officiel sur Google Translate) :
- docs/TECH_CHOICES.md : Option C marquee RETENU, Option D reformulee
  sans nommer la lib, mentions deep-translator dans les scenarios et
  questions remplacees.
- docs/RUST_OPTION.md : bloc traduction reformule, tableau equivalents
  corrige.

Verification : grep -rnE 'deep[-_]translator' docs/ README.md -> aucun match.
L'app n'utilise pas Shadcn-vue mais des composants maison dans
lyremember-app/src/components/ui/ (Button, Card, Input, Alert, Spinner).
La doc canonique reflete maintenant la realite :
- docs/FINAL_DECISIONS.md : 3 mentions de Shadcn-vue remplacees ou
  retirees (UI Library, schema d'architecture, liste de liens externes).
- docs/UI_LIBRARIES.md : note en tete indiquant que ce comparatif est
  historique et que la decision finale est consignee dans
  FINAL_DECISIONS.md.

Verification : grep -nE 'shadcn|Shadcn' docs/FINAL_DECISIONS.md -> 0 match.
)

lyremember-app/src-tauri/src/commands.rs expose 19 commandes (5 auth +
7 songs + 4 practice + 3 util), pas 16. La doc est synchronisee :
- docs/TAURI_INTEGRATION_COMPLETE.md : 3 mentions corrigees + nouvelle
  section "Detail des 19 commandes Tauri exposees" avec les noms
  exacts par categorie (source de verite = commands.rs).
- README.md : 6 mentions "16 commands" alignees a 19 (DRY entre docs).

Verification : grep -rnE '16 (Tauri )?commands?\b' docs/ README.md
-> aucun match.
….md (#11)

Le document decrivait l'architecture du proof of concept Python comme
si c'etait la stack canonique. On clarifie en tete :
- POC Python (legacy/python-cli/) : storage JSON via storage.py.
- Production Rust+Tauri+Vue : storage SQLite via
  rust-backend/src/db/sqlite.rs (4 tables : users, songs, user_songs,
  practice_sessions).
- Source de verite des decisions actuelles : FINAL_DECISIONS.md.

Suit l'archivage du POC dans legacy/ (#6).
…ilView, Genius redefini (#12 #13 #14)

Trois clarifications portees par USER_STORIES_V2.md, regroupees car
elles modifient le meme document :

- #12 — Epic 5 (Mode Verification Orale) descendu en CouldHave. Le MVP
  livre uniquement OralMode.vue en self-assessment manuel ; le mode
  live avec micro (Web Speech API ou STT) est post-MVP, suivi par #28.

- #13 — Note de navigation pour les Epics 4-7 : PracticeView = APPRENDRE
  (selection chanson + mode + lancement), SongDetailView = LYRICS
  (edition, vue 3 colonnes). Raccourci "Practice this song" depuis
  SongDetailView. Implementation suivie par #19.

- #14 — US-1.3 (Genius) refondue : import de lyrics interdit (scraping =
  ToS + API officielle ne donne pas les lyrics). Remplace par :
  (a) champ genius_url optionnel (#17),
  (b) spike API metadonnees (#26),
  (c) cleanup UI (#18).
  US-2.1 ajustee pour mentionner "lien Genius optionnel" plutot qu'import.

L'historique des stories d'origine est conserve avec une banniere de
mise a jour pour la tracabilite.
Pourquoi : la phonetique JP/KR/FR/EN annoncee dans
docs/TAURI_INTEGRATION_COMPLETE.md ne fonctionne que si la feature
`python` (PyO3 -> pykakasi/hangul-romanize/epitran) est activee.
L'ancien `default-features = false` desactivait la feature -> stub
d'erreur sur tous les builds desktop, alors que ce n'est pas la
contrainte (PyO3 est cross-compilable Linux/macOS/Windows).

Ce qui bouge :
- lyremember-app/src-tauri/Cargo.toml :
  * [features] default = ["python"] (au lieu de [])
  * la dep `lyremember_backend` garde `default-features = false`
    car la feature locale `python = ["lyremember_backend/python"]`
    suffit a forwarder l'activation -> mecanisme propre.
  * commentaires expliquant le flux d'activation.
- .github/workflows/release.yml : le build Android passe maintenant
  `--no-default-features` car PyO3 n'est PAS cross-compilable vers
  Android (commentaire deja present dans rust-backend/Cargo.toml).
  La phonetique renverra un stub sur APK jusqu'a ce qu'une
  alternative Rust pure soit fournie (post-MVP).
- docs/ARCHITECTURE.md : section "Build flags Tauri" documente les
  deux modes (desktop avec python, Android sans).

Validation locale partielle : `cargo check` echoue sur le container
headless (gdk-3.0 manquant pour Tauri Linux desktop) mais la
configuration des features est correcte au niveau manifest. La
verification finale se fait en CI/release.

Le critere "test d'integration appelle la phonetique reelle" est
laisse a #23 (depend de cette issue).
Le secret de signature JWT etait hardcoded
("your-secret-key-change-this-in-production") dans
rust-backend/src/services/auth.rs:11. Un secret en clair dans le
binaire est exploitable par n'importe qui : impersonation possible
de n'importe quel utilisateur. MVP-blocker pour toute distribution.

Ce qui bouge :
- rust-backend/src/services/auth.rs :
  * Const JWT_SECRET supprimee, remplacee par jwt_secret() qui lit
    LYREMEMBER_JWT_SECRET via OnceLock une fois par process.
  * Fonction interne jwt_secret_from_env(Option<String>) -> Vec<u8>
    pour rendre la resolution testable sans toucher l'env.
  * Fallback dev : 32 bytes aleatoires via uuid::Uuid::new_v4 (deja
    en dep), avec warning explicite sur stderr (coherent avec les
    eprintln! deja utilises dans services/songs.rs et translation.rs).
- .env.example : nouveau fichier, documente la variable + commande
  openssl rand -hex 32 pour generer un secret fort.
- docs/ARCHITECTURE.md : section "Variables d'environnement (prod)".
- README.md : note dans Quick Start.

TDD :
- 4 tests ajoutes dans tests::test_jwt_secret_from_env_* (valeur
  fournie, fallback None, fallback string vide, deux fallbacks
  distincts -> entropie).
- Cycle red->green verifie : tests echouaient sur "function not
  found" puis passent apres implementation.

Suite complete : 87 passed, 4 ignored, 0 failed (cargo test).
La colonne SQL genius_url existait deja dans rust-backend/src/db/sqlite.rs
mais le wiring service/commands/UI manquait : les chansons recevaient
toujours NULL. On le branche bout en bout :

Backend Rust :
- CreateSongData / UpdateSongData : nouveau champ Option<String>
  (#[serde(default)] pour rester retro-compat sur les payloads).
- create_song : copie data.genius_url dans song.genius_url avant
  save_song (qui l'inserait deja en colonne).
- update_song : ajout SET genius_url = ?... dans le SQL UPDATE.
  Semantique : None = ne pas toucher, Some("") = clear (NULL en DB),
  Some(url) = ecrase.
- Tests : 4 nouveaux tests TDD (red->green) couvrant create avec/
  sans url + update set + update clear par chaine vide.
- Sites construisant CreateSongData/UpdateSongData mis a jour
  (services/practice.rs, examples/basic_usage.rs, tests internes
  par script Python).

Tauri commands :
- cmd_create_song et cmd_update_song acceptent un param genius_url
  Option<String>. Empty -> None pour create (cleaner).

Frontend :
- tauri-api.ts : Song.genius_url + Song.genius_id types, createSong
  et updateSong wrappers acceptent geniusUrl optionnel.
- stores/songs.ts : createSong relaye geniusUrl.
- types/index.ts : CreateSongForm.geniusUrl ajoute.
- AddSongView.vue : nouvel Input "Lien Genius (optionnel)" entre
  lyrics et autoTranslate.
- SongDetailView.vue : lien sortant target="_blank"
  rel="noopener noreferrer" sous l'artist quand present.
- i18n FR/EN : cles addSong.geniusUrl(Placeholder) et
  songDetail.openOnGenius.

CRITIQUE : aucune extraction de paroles depuis Genius (interdit par
ToS, voir #14 / #18). C'est un simple lien sortant utilisateur.

Suite Rust : 91 passed (87 + 4 nouveaux), 4 ignored, 0 failed.
L'UI proposait un champ de token Genius (stocke en localStorage) et
une recherche "Import depuis Genius" alors que :
- scraper genius.com viole les ToS,
- l'API officielle ne fournit pas les paroles.

On retire toute cette section. Le lien Genius reste utile en tant que
simple URL sortante au niveau de la chanson (genius_url, #17). Une
exploration de l'API metadonnees pourra venir via le spike R&D (#26).

Ce qui bouge :
- lyremember-app/src/views/SettingsView.vue : section "Genius API"
  supprimee, imports inutiles (useRouter, Button, ref) retires.
  Le store localStorage 'geniusToken' n'est plus ecrit.
- lyremember-app/src/i18n/locales/{en,fr}.json : 12 cles obsoletes
  retirees (settings.integrations, geniusApi, geniusToken,
  geniusTokenPlaceholder, geniusDesc, geniusHelp, tokenSaved,
  searchSongs, searchPlaceholder, search, import, noResults).

Verification : grep -rnE 'genius' lyremember-app/src/ ne renvoie plus
que les usages du champ lien sortant (genius_url / openOnGenius /
genius.com placeholder URL) et la propriete User.genius_token du
type backend (defini par le schema SQL existant, pas un usage UI).
…e vers le hub (#19)

PracticeView etait deja le hub d'apprentissage (liste chansons + 4
modes, route vers /songs/:id?mode=... pour monter le mode dans
SongDetailView). On finalise le pattern de navigation :

- PracticeView : lit route.query.songId au montage et pre-deplie la
  chanson correspondante dans la liste si elle est dans le repertoire
  utilisateur. Validation defensive : on verifie que la chanson
  existe dans songsStore.songs avant de selectionner.
- SongDetailView : ajout d'un bouton "Ouvrir dans Practice ->" a cote
  de la section "Practice Modes" qui route vers
  /practice?songId=<song.id>. Les 4 raccourcis directs vers les modes
  restent sur la fiche pour ne pas regresser l'UX existante.
- i18n FR/EN : nouvelle cle songDetail.practiceHubShortcut.

Roles confirmes (decision #10) :
- PracticeView = hub APPRENDRE (selection chanson + mode + lancement)
- SongDetailView = gestion LYRICS + raccourcis directs vers les modes
Promesse "multi-langues JP/KR" : l'app supportait JP/KR comme langues
de chanson mais pas comme langues d'UI (en + fr seulement).

Ce qui bouge :
- lyremember-app/src/i18n/locales/ja.json : 151 cles (Hiragana/Katakana/
  Kanji standard japonais).
- lyremember-app/src/i18n/locales/ko.json : 151 cles (Hangul).
- lyremember-app/src/i18n/index.ts :
  * SupportedLocale etendu a 'en' | 'fr' | 'ja' | 'ko'.
  * supportedLocales : labels natifs (日本語, 한국어) pour le selecteur.
  * messages : ja + ko ajoutes a createI18n.
  * getInitialLocale : detection navigator.language couvre les 4 codes.

Le selecteur de langue de SettingsView (qui itere supportedLocales)
prend en charge les 4 langues automatiquement.

Couverture verifiee : 151 cles par locale, 0 manquante, 0 extra
(diff JSON aplati sur en/fr/ja/ko).

Hors perimetre : internationalisation du contenu utilisateur (chansons)
qui reste dans la langue d'origine.
…#21)

La vue lyrics affichait deja VO + Traduction (selectionnable). La
phonetique (generee par PyO3 -> pykakasi/hangul-romanize/epitran et
stockee en song.phonetic_lyrics, voir #15) etait absente de l'affichage
alors que c'est une promesse MVP.

Ce qui bouge :
- LyricLine elargi : champ `phonetic: string | null`.
- lyricSections (computed) : extrait `phonetic_lyrics[i]` ligne par
  ligne en parallele des traductions.
- Template : nouvelle <p> en italique mono opacity-75 sous la VO,
  affichee uniquement si line.phonetic existe. Ordre vertical
  preservant l'esprit "3 colonnes" promis (VO en serif normal,
  phonetique en mono pour la distinguer, traduction en doree italique).

Compatible avec :
- Une chanson sans phonetique (phonetic_lyrics null) : juste VO +
  trad comme avant -> aucune regression visuelle.
- Une chanson JP/KR : phonetique romaji/romanisation affichee.
- Une chanson EN/FR : IPA affiche.

Hors perimetre : edition inline des traductions (post-MVP).
Avant : 3 distractors aleatoires parmi les autres lignes, fallback
sur "word-scrambled" du correctLine (toujours la meme source).

Apres :
- Les lignes candidates sont classees par proximite de longueur avec
  la bonne reponse (|len(line) - len(correct)|), puis melangees dans
  un pool de top-6. Resultat : des distractors de longueur visuelle
  similaire, plus difficile a eliminer au coup d'oeil que des lignes
  random.
- Fallback word-scrambled puise dans les AUTRES lignes (donc
  plausibles dans le vocabulaire de la chanson), seulement
  correctLine en derniere instance.
- Anti-doublons sur tous les chemins (deja la mais durci).
- Safety valve : max 20 tentatives sur le fallback.

Decision pragmatique : reste cote frontend (TS). Le deplacement en
service Rust (cote serveur, testable independamment) reste un
follow-up post-MVP — l'amelioration immediate de l'experience MCQ
ne justifie pas le cout d'une nouvelle Tauri command pour 30 lignes
de logique cote front.
…23)

L'audit #23 demande des tests pour services::phonetic couvrant les
deux branches (feature python active vs stub). Les tests precedents
ne couvraient que la branche python (ignored, requierent pip
install pykakasi/hangul-romanize/epitran).

Ce qui bouge :
- services/phonetic.rs (prod) : court-circuit empty input avant tout
  appel Python::with_gil. Evite de payer le cout du runtime Python
  pour un no-op et rend les tests deterministes meme sans deps Python.
- Tests :
  * test_generate_phonetic_empty_input : passe sur les 5 langues.
  * test_stub_returns_error_for_supported_language (cfg not python) :
    verifie qu'un build Android (sans python) renvoie Error::Phonetic
    pour jp/kr/fr/en plutot que succes silencieux ou panic.
  * test_stub_passthrough_for_unsupported_language : la branche
    par defaut renvoie le texte original sans erreur.
  * test_to_ipa_french : ajoute la couverture FR (manquante).
  * test_generate_phonetic_dispatches_by_language : dispatcher route
    bien vers le bon backend.
- Documentation : commentaire en tete du module tests pointe vers
  ci-rust.yml et la commande pour activer les tests Python.

Verification :
- cargo test (feature python) : 92 passed, 6 ignored, 0 failed.
- cargo test --no-default-features : 91 passed, 0 ignored, 0 failed
  (4 tests phonetic dont les 2 nouveaux pour la branche stub).

Les tests reels (kakasi/hangul-romanize/epitran) restent ignored par
defaut, executables via `cargo test --features python -- --ignored`
quand les pip deps sont presentes.
…#24)

Le frontend ne disposait que de 3 specs e2e WebDriverIO. On ajoute
Vitest pour iterer plus vite sur les composants/stores.

Infrastructure :
- lyremember-app/package.json : scripts test:unit (run-once,
  vitest run) et test:unit:watch ; devDeps @vue/test-utils, jsdom,
  vitest (versions stables 2024).
- lyremember-app/vitest.config.ts : environnement jsdom, alias '@'
  vers src/, include 'src/**/*.{test,spec}.ts'.
- .github/workflows/ci-frontend.yml : etape "Unit tests (Vitest)"
  entre typecheck et build.

3 pilots couvrant chaque grand type d'asset :
- src/components/ui/Button.spec.ts (4 cas) :
  slot rendering, click emit, loading state + variant class.
- src/components/ui/Alert.spec.ts (3 cas) :
  hidden quand modelValue=false, render+variant, emit update sur
  bouton close.
- src/stores/songs.spec.ts (4 cas) : filtres
  filteredSongs par query/langue, groupBy, totalSongs.

Helper makeSong() factorise les fixtures Song complets.

CONTRIBUTING.md mis a jour : test:unit + test:e2e listes
explicitement comme deux scripts distincts.

Hors perimetre : couverture exhaustive ajoutee incrementalement.
Deux rapports docs-only deposes dans docs/spikes/ :

- 2026-05-genius-api.md (#26) : exploration de l'API metadonnees
  Genius. Endpoints disponibles (/search, /songs/:id) renvoient
  metadonnees mais PAS les paroles. Valeur produit faible :
  auto-completion a la creation = ~2 frappes gagnees, le reste deja
  couvert par genius_url (#17). Recommandation NO-GO court terme,
  re-evaluer si onboarding casual devient critique ou si on veut
  afficher les cover arts.

- 2026-05-song-detection.md (#27) : detection Shazam-like.
  Comparatif AudD (free 25 req/jour, $9-99/mois), ACRCloud (~$99/mois),
  Shazam via RapidAPI, alternative open-source (Chromaprint +
  MusicBrainz). Faisabilite Tauri desktop (cpal ou navigator
  getUserMedia) + Android (permission RECORD_AUDIO) + iOS (entitlement,
  pas de build prouve). Aspects legaux RGPD (enregistrement
  environnement -> ne pas persister). Couts estimes : ~80 USD/mois
  a 100 users, ~810 USD/mois a 1k users. Recommandation NO-GO MVP,
  re-evaluer post-MVP avec >500 users ou partenariat.

docs/INDEX.md mis a jour avec section "Spikes R&D".
Le dark mode etait deja entierement implemente :
- stores/ui.ts : toggleDarkMode flip + DOM class 'dark' + localStorage
- stores/ui.ts : initializeDarkMode lit localStorage > prefers-color-scheme
- App.vue : appelle initializeDarkMode au mount
- SettingsView.vue : toggle switch wire sur uiStore.toggleDarkMode
- tailwind.config.js : darkMode = 'class'
- i18n FR/EN/JP/KR : cles settings.darkMode(Desc)

Tous les critères de l'issue sont remplis :
- L'utilisateur peut basculer clair/sombre depuis SettingsView : OK
- Le choix persiste entre sessions : OK (localStorage)
- Respect prefers-color-scheme au premier lancement : OK
- Variables CSS / classes Tailwind clair/sombre : OK (darkMode class)

Ce qui manquait : un test couvrant le comportement. Ajout de
stores/ui.spec.ts avec 3 cas :
- toggleDarkMode : flip state, ajoute/retire <html class='dark'>,
  persiste 'darkMode' dans localStorage.
- initializeDarkMode : priorise la valeur sauvegardee sur la
  preference systeme.
- initializeDarkMode : fallback sur prefers-color-scheme si pas de
  valeur sauvegardee.

Hors perimetre : themes custom (orange/violet/etc.).
…_api (#25)

Le POC Python archive (legacy/python-cli/) n'avait que 32 tests sur
4 modules (file_importer, song_manager, storage, utils). On ajoute
4 nouveaux fichiers de tests pour les 4 modules cibles de l'audit :

- tests/test_models.py : dataclasses User/Song/PracticeSession/
  SongProgress. Round-trip to_dict/from_dict, verify_password,
  defaults sains. (4 classes de tests, ~14 cas.)

- tests/test_practice_engine.py : modes fill-blank, flashcard,
  line-by-line, accounting de session. Verifie que les lignes vides
  sont skipees, que record_line_result peuple difficult_lines, que
  create_session calcule le score. Note : on garde le contrat
  max(1, ...) de hide_words (au moins un mot cache meme a
  difficulty=0).

- tests/test_user_manager.py : register/login/repertoire/genius
  token via Storage mocke. Necessaire car le storage.py legacy
  n'expose PAS get_user_by_username ni save_user — code casse au
  niveau integration mais UserManager lui-meme reste testable en
  isolation.

- tests/test_genius_api.py : init avec/sans token, search degradation
  gracieuse sans client. Pas d'appel HTTP reel.

Resultats : 68 tests passent (32 -> 68, +36). Coverage moyenne sur les
4 modules cibles : 81.5% (practice_engine 100%, models 100%,
user_manager 96%, genius_api 30% — limite par l'absence de
lyricsgenius en environnement de test).

cd legacy/python-cli && python -m pytest tests/ -q -> 68 passed.
L'app n'avait que la saisie manuelle pour les paroles. On reprend la
fonctionnalite POC (file_importer.py) etendue au format .lrc.

Architecture : parsers TS purs dans lib/file-parsers.ts (zero
dependance Tauri, lit via File.text() cote webview navigateur).
Decision documentee dans l'issue : pas de Tauri fs plugin pour le
MVP, l'input file natif suffit.

Parsers :
- parseTxt : 1 ligne = 1 lyric, skip '#'-comments et lignes vides.
- parseJson : {title?, artist?, language?, lyrics: string[]}
  Type guards stricts (FileImportError sur JSON invalide ou lyrics
  pas array de strings).
- parseLrc : strip leading timestamps [mm:ss.xx] (et variantes
  [mm:ss], multiples par ligne possibles). Tags meta [ti:Title]
  [ar:Artist] [la:lang] extraits dans les champs correspondants.
- parseByExtension : dispatcher par extension du nom de fichier.

UI : input <input type="file" accept=".txt,.json,.lrc"> dans
AddSongView au-dessus du formulaire. handleFileImport lit le
contenu, le parse, et pre-remplit les champs title/artist/
language/lyrics. Erreur affichee via l'Alert existant.

Tests : 16 specs Vitest dans file-parsers.spec.ts couvrant chaque
parser (cas valide, CRLF, comments, JSON malforme, lyrics non-array,
LRC sans timestamps, multiples timestamps, metadata) + dispatcher.

i18n : addSong.importFile et addSong.importFileHint ajoutees a
en/fr/ja/ko (couverture verifiee a 100% sur les 4 locales).

Hors perimetre : drag-and-drop (peut etre ajoute plus tard) ;
import depuis Genius (interdit, cf #18).
Promesse Epic 8 USER_STORIES_V2 : streak (jours consecutifs) +
recommandations de chansons a revoir. Livraison MVP en 4 couches :

Rust (rust-backend/src/services/practice.rs) :
- compute_streak_from_dates(today, dates) : fonction pure, testable
  avec dates controlees. Streak = nb de jours consecutifs ending
  today OR yesterday avec au moins 1 session. Dedupe sessions
  multiples le meme jour. Reset a 0 si gap > 1 jour.
- get_user_streak(conn, user_id) : SELECT DISTINCT DATE(created_at)
  -> compute_streak_from_dates avec chrono::Utc::now().date_naive().
- get_recommendations(conn, user_id, limit) : songs deja pratiquees
  classees par mastery croissante puis last_practiced ascendante.
- 9 tests TDD red->green : streak (vide, today, 3 jours, hier,
  gap, breaks, dedupe) + recommendations (ordre, limit).

Tauri commands (lyremember-app/src-tauri/src/commands.rs + lib.rs) :
- cmd_get_user_streak (1 commande)
- cmd_get_recommendations (1 commande, limit optionnel default 5)
- Total commandes exposees : 19 -> 21 (auth 5, songs 7, practice 6,
  util 3). Doc TAURI_INTEGRATION_COMPLETE.md a re-aligner si voulu
  (suivi).

Frontend :
- lib/tauri-api.ts : getUserStreak, getRecommendations.
- composables/useUserStats.ts : etendu pour fetch streak +
  recommendedSongIds en Promise.all, avec fallback gracieux si l'une
  des Promesses casse.
- views/DashboardView.vue : badge "🔥 N jours" en haut a droite
  quand streak > 0, tooltip "Jours consecutifs de pratique".
- i18n FR/EN/JP/KR : dashboard.streakDays (formule pluralisable)
  + dashboard.streakTooltip.

Hors perimetre (volontairement) :
- Section "Suggere pour vous" basee sur recommandations : couvert
  cote backend, UI dediee differable post-MVP.
- Achievements / leaderboards multi-utilisateurs.

Suite Rust : 101 passed, 6 ignored, 0 failed.
L'audit promettait un mode oral avec reconnaissance vocale live. Le
mode existant ne faisait que du self-assessment manuel. Solution MVP :
brancher la Web Speech API native du webview avec fallback gracieux.

Architecture :
- lib/oral-scoring.ts : module pur, testable sans webview.
  * normalize : lowercase + strip ASCII punct + collapse spaces
    (preserve les diacritiques car phonetiquement significatifs).
  * tokenize : normalize puis split sur espaces.
  * scoreSpoken(expected, spoken) -> [0,1] : token-set recall sur
    expected, robuste a l'ordre.
  * hasSpeechRecognition() : feature-detect (window.SpeechRecognition
    OR window.webkitSpeechRecognition).
- lib/oral-scoring.spec.ts : 12 tests couvrant normalize, tokenize,
  scoreSpoken (cas 1.0 / 0 / partiel / case-insensitive / ordre), et
  hasSpeechRecognition (false en jsdom).

UI :
- OralMode.vue : nouveau panneau "Live (experimental)" affiche
  seulement si hasSpeechRecognition() est vrai au mount. Bouton
  Speak/Stop qui demarre/arrete recognition.
- Mapping langue chanson -> locale BCP-47 (fr-FR, en-US, ja-JP,
  ko-KR ; fallback en-US).
- Score affiche en temps reel ; >= 70% declenche auto-assessment
  comme "got it".
- onUnmounted nettoie la recognition pour eviter une fuite.
- Self-assessment manuel preserve : la branche v-if="!selfAssessed"
  garde l'affichage du panneau live tant que l'utilisateur n'a pas
  trance (manuellement ou par score).

Limitations documentees :
- Pas de capture audio Tauri native : on s'appuie sur le webview.
  Desktop OK sur la plupart des cibles ; Android depend de la
  version WebView ; iOS WKWebView : pas dispo.
- Pas de STT serveur (Whisper/Vosk) : le spike #27 reste pertinent
  pour la version definitive (recognition offline + scoring
  phonetique).

Pas de regression : le mode oral existant continue de marcher dans
les environnements sans SpeechRecognition (le panneau live ne
s'affiche simplement pas).
@RebelliousSmile RebelliousSmile merged commit 8e36055 into main May 17, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants