diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..419df18 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [main, feat/**] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: pnpm/action-setup@v4 + with: + version: 10 # pnpm 11 requires Node ≥ 22; pin to match local version (10.x) + - name: Install dependencies + run: pnpm --dir lyremember-app install + - name: Type check + build + run: pnpm --dir lyremember-app build + - name: Unit tests + run: pnpm --dir lyremember-app exec vitest run + # cargo check only — cargo test blocked by PyO3 0.20.x / Python 3.13 incompatibility + - name: Cargo check rust-backend + run: cargo check --manifest-path rust-backend/Cargo.toml + env: + PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..055e0fd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,66 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - platform: ubuntu-22.04 + args: '' + - platform: macos-latest + args: '--target aarch64-apple-darwin' + - platform: macos-latest + args: '--target x86_64-apple-darwin' + - platform: windows-latest + args: '' + + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: latest + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin,x86_64-apple-darwin + + - name: Install Ubuntu dependencies + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Install frontend dependencies + run: pnpm --dir lyremember-app install + + - name: Build Tauri app + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + projectPath: lyremember-app + tagName: ${{ github.ref_name }} + releaseName: 'LyRemember ${{ github.ref_name }}' + releaseBody: | + See the assets below to download this version for your platform. + releaseDraft: true + prerelease: false + args: ${{ matrix.args }} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index b82b604..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,271 +0,0 @@ -# LyRemember Architecture - -## Overview -LyRemember is a lyrics memorization application supporting multiple languages. This document outlines the technical architecture and design decisions. - -## Technology Stack - -### Core Application -- **Language**: Python 3.8+ -- **Interface**: Command-Line Interface (CLI) for MVP, with potential web UI later -- **Data Storage**: JSON files for simplicity (can migrate to SQLite/database later) - -### Key Libraries -- `click` - CLI framework for user-friendly command-line interface -- `colorama` - Terminal color support for better UX -- `tabulate` - Pretty table formatting for song lists -- `fuzzywuzzy` - Fuzzy string matching for answer checking -- `python-Levenshtein` - Edit distance for flexible answer validation -- `pyyaml` - Configuration management - -## Architecture Diagram - -``` -┌─────────────────────────────────────────────────┐ -│ User Interface (CLI) │ -│ ┌──────────┬──────────┬──────────┬──────────┐ │ -│ │ Add │ Practice │ Progress │ List │ │ -│ │ Songs │ Modes │ Tracking │ Songs │ │ -│ └──────────┴──────────┴──────────┴──────────┘ │ -└─────────────────────┬───────────────────────────┘ - │ -┌─────────────────────▼───────────────────────────┐ -│ Application Logic Layer │ -│ ┌──────────────────────────────────────────┐ │ -│ │ Song Manager │ Practice Engine │ │ -│ │ - Add/Edit │ - Fill-in-blank │ │ -│ │ - List/Search │ - Flashcards │ │ -│ │ - Delete │ - Line-by-line │ │ -│ └──────────────────────────────────────────┘ │ -│ ┌──────────────────────────────────────────┐ │ -│ │ Progress Tracker │ │ -│ │ - Statistics │ - History │ │ -│ │ - Scores │ - Achievements │ │ -│ └──────────────────────────────────────────┘ │ -└─────────────────────┬───────────────────────────┘ - │ -┌─────────────────────▼───────────────────────────┐ -│ Data Access Layer │ -│ ┌──────────────────────────────────────────┐ │ -│ │ JSON Storage Handler │ │ -│ │ - songs.json - progress.json │ │ -│ │ - config.json - vocabulary.json │ │ -│ └──────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────┘ -``` - -## Data Models - -### Song -```python -{ - "id": "unique-song-id", - "title": "Song Title", - "artist": "Artist Name", - "language": "en", # ISO 639-1 code - "lyrics": [ - "First line of the song", - "Second line of the song", - ... - ], - "translations": { - "es": ["Primera línea", "Segunda línea", ...], - "fr": ["Première ligne", "Deuxième ligne", ...] - }, - "created_at": "2026-02-17T10:00:00Z", - "updated_at": "2026-02-17T10:00:00Z", - "metadata": { - "genre": "pop", - "year": 2024, - "difficulty": "medium" - } -} -``` - -### Progress Entry -```python -{ - "song_id": "unique-song-id", - "practice_sessions": [ - { - "session_id": "session-id", - "date": "2026-02-17T10:00:00Z", - "mode": "fill-in-blank", - "duration_seconds": 120, - "score": 85, # percentage - "lines_practiced": 10, - "lines_correct": 8, - "difficult_lines": [2, 5, 7] # line indices - } - ], - "mastery_level": 0.75, # 0-1 scale - "total_practice_time": 3600, # seconds - "last_practiced": "2026-02-17T10:00:00Z" -} -``` - -### User Configuration -```python -{ - "user_name": "User", - "preferred_language": "en", - "difficulty_settings": { - "fill_in_blank_percentage": 0.3, # 30% words hidden - "practice_time_minutes": 15 - }, - "ui_preferences": { - "color_enabled": true, - "show_hints": true - } -} -``` - -## Core Components - -### 1. CLI Interface (`cli.py`) -- Main entry point using `click` -- Command structure: - ``` - lyremember - ├── add # Add new song - ├── list # List all songs - ├── practice # Start practice session - │ ├── --mode # fill-blank, flashcard, line-by-line - │ └── --song-id # Specific song or random - ├── progress # View statistics - ├── translate # Add/view translations - └── config # Configure settings - ``` - -### 2. Song Manager (`song_manager.py`) -- CRUD operations for songs -- Search and filter functionality -- Validation and sanitization -- Import/export capabilities - -### 3. Practice Engine (`practice_engine.py`) -- Multiple practice modes: - - **Fill-in-the-Blank**: Hide random words, user fills them - - **Flashcard**: Show first part, recall second part - - **Line-by-Line**: Progressive revelation -- Answer validation with fuzzy matching -- Hint system -- Difficulty adjustment - -### 4. Progress Tracker (`progress_tracker.py`) -- Record practice sessions -- Calculate statistics -- Generate reports -- Track mastery levels using spaced repetition algorithm - -### 5. Data Storage (`storage.py`) -- JSON file operations -- Data validation -- Backup and recovery -- Migration utilities - -## Directory Structure - -``` -lyremember/ -├── README.md -├── USER_STORIES.md -├── ARCHITECTURE.md -├── requirements.txt -├── setup.py -├── .gitignore -├── lyremember/ -│ ├── __init__.py -│ ├── cli.py # CLI entry point -│ ├── song_manager.py # Song CRUD operations -│ ├── practice_engine.py # Practice modes -│ ├── progress_tracker.py # Statistics and tracking -│ ├── storage.py # Data persistence -│ ├── utils.py # Helper functions -│ └── models.py # Data models -├── data/ -│ ├── songs.json # Songs database -│ ├── progress.json # User progress -│ ├── config.json # User configuration -│ └── samples/ # Sample songs -│ ├── sample_en.json -│ ├── sample_es.json -│ └── sample_fr.json -├── tests/ -│ ├── __init__.py -│ ├── test_song_manager.py -│ ├── test_practice_engine.py -│ └── test_storage.py -└── docs/ - └── usage_guide.md -``` - -## Key Features for MVP - -1. **Song Management** - - Add songs with title, artist, language, and lyrics - - List and search songs - - View song details - -2. **Fill-in-the-Blank Practice** - - Randomly hide words based on difficulty setting - - Accept user input with fuzzy matching - - Provide immediate feedback - -3. **Basic Progress Tracking** - - Record practice sessions - - Show basic statistics (accuracy, songs practiced) - -4. **Multi-Language Support** - - Support major languages (EN, ES, FR, DE, IT, PT, etc.) - - Optional translation storage - -5. **Data Persistence** - - Save all data in JSON format - - Auto-save after each session - -## Future Enhancements - -1. **Web Interface** - Flask/FastAPI web app -2. **Mobile Support** - React Native or Flutter -3. **Audio Integration** - Play song audio during practice -4. **Social Features** - Share songs, compete with friends -5. **Advanced Analytics** - ML-based difficulty prediction -6. **Spaced Repetition** - Smart scheduling based on forgetting curve -7. **Cloud Sync** - Multi-device synchronization -8. **Community Song Database** - Shared lyrics repository - -## Design Principles - -1. **Simplicity First**: MVP focuses on core features -2. **User-Friendly**: Intuitive CLI with helpful messages -3. **Extensible**: Modular design for easy feature addition -4. **Data Privacy**: All data stored locally by default -5. **Language Agnostic**: Unicode support for any language -6. **Offline-First**: No internet required for core functionality - -## Development Phases - -### Phase 1: Foundation (Current) -- Set up project structure -- Implement data models -- Create storage system -- Build basic CLI - -### Phase 2: Core Features -- Implement song manager -- Create fill-in-blank practice mode -- Add progress tracking -- Sample data and documentation - -### Phase 3: Enhanced Practice -- Add flashcard mode -- Implement line-by-line practice -- Translation support -- Advanced statistics - -### Phase 4: Polish -- Comprehensive testing -- Performance optimization -- User documentation -- Sample songs in multiple languages diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..391d958 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,50 @@ +--- +name: claude +description: Claude Code configuration and guidelines +--- + +# CLAUDE.md + +> IMPORTANT: On first conversation message: +> +> - say "AI-Driven Development ON - Date: {current_date}, TZ: {current_timezone}." to User. + +## Behavior Guidelines + +All instructions and information above are willing to be up to date, but always remind yourself that USER can be wrong, be critical of the information provided, and verify it against the project's actual state. + +- Be anti-sycophantic - don't fold arguments just because I push back +- Stop excessive validation - challenge my reasoning instead +- Avoid flattery that feels like unnecessary praise +- Don't anthropomorphize yourself + +## Technical guidelines + +- Do not commit or push yourself unless I ask you to. + +### Answering Guidelines + +- Don't assume your knowledge is up to date. +- Be 100% sure of your answers. +- If unsure, say "I don't know" or ask for clarification. +- Never say "you are right!", prefer anticipating mistakes. + +## Memory Management + +Project docs, memory, specs, and plans live in `aidd_docs/`. + +### Project memory + + +@aidd_docs/memory/architecture.md +@aidd_docs/memory/codebase-map.md +@aidd_docs/memory/coding-assertions.md +@aidd_docs/memory/deployment.md +@aidd_docs/memory/project-brief.md +@aidd_docs/memory/testing.md +@aidd_docs/memory/vcs.md + + +- If memory is not loaded above: run `ls -1tr aidd_docs/memory/` then read each file +- If needed: load files from `aidd_docs/memory/external/*` when user request it +- If needed: load files from `aidd_docs/memory/internal/*`, you have to think about it diff --git a/TECH_STACK_FINAL.md b/TECH_STACK_FINAL.md deleted file mode 100644 index e2dcd52..0000000 --- a/TECH_STACK_FINAL.md +++ /dev/null @@ -1,365 +0,0 @@ -# Décisions Technologiques Finales - LyRemember - -## Contexte -Application pour mémoriser paroles de chansons en plusieurs langues avec modes de pratique variés. - -## Contraintes Utilisateur -1. **Plateformes :** Desktop ET Mobile -2. **Budget :** 0€ (gratuit) -3. **Timeline :** Flexible -4. **Langues :** Français, Anglais, Coréen, Japonais - ---- - -## ✅ DÉCISION FINALE : Progressive Web App (PWA) - -### Pourquoi PWA ? - -**Avantages :** -- ✅ **Un seul codebase** pour desktop + mobile + tablette -- ✅ **100% gratuit** (pas de frais App Store/Play Store) -- ✅ **Accessible partout** via navigateur -- ✅ **Installable** comme application native -- ✅ **Fonctionne offline** (avec Service Workers) -- ✅ **Mises à jour instantanées** (pas d'app store review) -- ✅ **Responsive** (s'adapte à toutes tailles d'écran) -- ✅ **Accès micro/audio** pour mode vocal -- ✅ **Notifications** possibles -- ✅ **Partage facile** (juste un lien) - -**Sur Desktop :** -- Fonctionne dans Chrome, Firefox, Safari, Edge -- Installable via "Ajouter à l'écran d'accueil" (Chrome/Edge) -- Icône dans barre de tâches/dock -- Fenêtre standalone (pas de barre d'adresse) - -**Sur Mobile :** -- Fonctionne dans tous navigateurs -- Installable comme app (Android + iOS) -- Icône sur écran d'accueil -- Plein écran -- Gestes tactiles - ---- - -## Stack Technique Complète - -### Frontend -**Framework :** React 18 + TypeScript -- **Pourquoi React ?** Écosystème riche, bon pour PWA, composants réutilisables -- **TypeScript :** Type safety, meilleur DX - -**Build Tool :** Vite -- **Pourquoi Vite ?** Ultra rapide, HMR excellent, PWA plugin disponible - -**Styling :** Tailwind CSS -- **Pourquoi Tailwind ?** Rapide, responsive facile, design system cohérent - -**State Management :** Zustand (ou Context API) -- **Pourquoi Zustand ?** Simple, léger, pas de boilerplate - -**Routing :** React Router v6 -- **Pourquoi ?** Standard React, bon pour SPA - -**UI Components :** Headless UI + custom components -- **Pourquoi ?** Accessible, customizable, gratuit - -**Icônes :** Lucide React -- **Pourquoi ?** Léger, beau, open-source - -**PWA :** vite-plugin-pwa -- **Pourquoi ?** Génère Service Worker et manifest automatiquement - ---- - -### Backend -**Framework :** Python FastAPI -- **Pourquoi ?** - - Réutilise code existant - - Async/await (performant) - - Auto-documentation (Swagger) - - Excellentes libs pour NLP/traduction - -**Base de données :** SQLite + SQLAlchemy -- **Pourquoi SQLite ?** - - Gratuit, pas de serveur - - Fichier unique, portable - - Suffisant pour usage personnel - - Peut migrer vers PostgreSQL plus tard - -**Auth :** JWT (JSON Web Tokens) -- **Pourquoi ?** Stateless, fonctionne bien avec SPA, sécurisé - -**CORS :** FastAPI CORS Middleware -- **Pourquoi ?** Frontend et backend sur domaines différents - ---- - -### Services Gratuits - -**1. Import Paroles : Genius API** -- **Service :** https://genius.com/api-clients -- **Coût :** Gratuit avec token personnel -- **Limite :** Raisonnable pour usage personnel -- **Lib :** lyricsgenius (Python) - -**2. Traduction : deep-translator** -- **Service :** Google Translate (via scraping non-officiel) -- **Coût :** Gratuit -- **Limite :** Rate limiting possible -- **Fallback :** Traductions manuelles -- **Lib :** deep-translator (Python) - -**3. Phonétique (Japonais) : pykakasi** -- **Service :** Local, offline -- **Coût :** Gratuit -- **Fonction :** Kanji → Hiragana → Romaji -- **Lib :** pykakasi (Python) - -**4. Phonétique (Coréen) : hangul-romanize** -- **Service :** Local, offline -- **Coût :** Gratuit -- **Fonction :** Hangul → Romanization -- **Lib :** hangul-romanize (Python) - -**5. Phonétique (FR/EN) : epitran** -- **Service :** Local, offline -- **Coût :** Gratuit -- **Fonction :** Texte → IPA (International Phonetic Alphabet) -- **Lib :** epitran (Python) - -**6. Reconnaissance Vocale : Web Speech API** -- **Service :** Intégré navigateur (Chrome/Edge) -- **Coût :** Gratuit -- **Limite :** Nécessite connexion internet -- **Support :** Chrome > Edge > Safari limité, Firefox non -- **Fallback :** Mode oral optionnel - ---- - -### Hébergement Gratuit - -**Frontend : Vercel** -- **Coût :** Gratuit (hobby plan) -- **Features :** - - Déploiement auto depuis GitHub - - HTTPS inclus - - CDN global - - Domaine personnalisé gratuit (.vercel.app) -- **Limites :** 100GB bande passante/mois (largement suffisant) - -**Backend : Railway** -- **Coût :** Gratuit (500h/mois) -- **Features :** - - PostgreSQL inclus (si besoin plus tard) - - Déploiement auto depuis GitHub - - HTTPS inclus -- **Limites :** Sleep après 5min inactivité (acceptable pour usage perso) -- **Alternative :** Render (même concept) - -**OU Backend auto-hébergé :** -- Raspberry Pi à la maison -- VPS gratuit (Oracle Cloud Always Free, Google Cloud free tier) - ---- - -## Architecture Détaillée - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ CLIENT (PWA) │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ React App │ │ -│ │ ┌─────────┬─────────┬─────────┬─────────┬──────────┐ │ │ -│ │ │ Login │ Songs │ Practice│ Stats │ Settings │ │ │ -│ │ │ Page │ List │ Modes │ Page │ Page │ │ │ -│ │ └─────────┴─────────┴─────────┴─────────┴──────────┘ │ │ -│ │ │ │ -│ │ Components: │ │ -│ │ - SongCard (affiche chanson avec VO/phonétique) │ │ -│ │ - KaraokeMode (défilement phrase par phrase) │ │ -│ │ - FillBlankMode (phrases à trous) │ │ -│ │ - MCQMode (propositions multiples) │ │ -│ │ - VoiceMode (reconnaissance vocale) │ │ -│ │ - PhoneticDisplay (affichage phonétique) │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ │ -│ │ HTTP/REST (axios) │ -│ ▼ │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ SERVER (FastAPI) │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ API Routes │ │ -│ │ /api/auth/register │ │ -│ │ /api/auth/login │ │ -│ │ /api/songs (GET, POST, PUT, DELETE) │ │ -│ │ /api/songs/{id}/phonetic (génère phonétique) │ │ -│ │ /api/songs/{id}/translate (traduit en EN) │ │ -│ │ /api/genius/search (cherche sur Genius) │ │ -│ │ /api/genius/import (importe depuis Genius) │ │ -│ │ /api/practice/session (enregistre session) │ │ -│ │ /api/stats (récupère stats) │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Business Logic │ │ -│ │ - UserManager (auth, profil) │ │ -│ │ - SongManager (CRUD, import Genius) │ │ -│ │ - PhoneticEngine (pykakasi, hangul-romanize, epitran) │ │ -│ │ - TranslationEngine (deep-translator) │ │ -│ │ - PracticeEngine (modes, scoring) │ │ -│ │ - ProgressTracker (stats, recommandations) │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Database (SQLite) │ │ -│ │ Tables: │ │ -│ │ - users (id, username, email, password_hash, ...) │ │ -│ │ - songs (id, title, artist, language, lyrics, ...) │ │ -│ │ - user_songs (user_id, song_id) -- répertoire │ │ -│ │ - sessions (id, user_id, song_id, mode, score, ...) │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ EXTERNAL SERVICES │ -│ - Genius API (import paroles) │ -│ - Web Speech API (reconnaissance vocale, côté client) │ -│ - Google Translate (via deep-translator, traduction) │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Structure Projet - -``` -lyremember/ -├── backend/ # API Python FastAPI -│ ├── app/ -│ │ ├── __init__.py -│ │ ├── main.py # Point d'entrée FastAPI -│ │ ├── models.py # SQLAlchemy models -│ │ ├── schemas.py # Pydantic schemas -│ │ ├── database.py # DB connection -│ │ ├── auth.py # JWT auth -│ │ ├── routers/ -│ │ │ ├── auth.py # Auth routes -│ │ │ ├── songs.py # Songs routes -│ │ │ ├── practice.py # Practice routes -│ │ │ └── genius.py # Genius API routes -│ │ ├── services/ -│ │ │ ├── user_service.py -│ │ │ ├── song_service.py -│ │ │ ├── phonetic_service.py -│ │ │ ├── translation_service.py -│ │ │ └── genius_service.py -│ │ └── utils/ -│ ├── tests/ -│ ├── requirements.txt -│ └── .env -│ -├── frontend/ # React PWA -│ ├── public/ -│ │ ├── manifest.json # PWA manifest -│ │ └── icons/ # App icons -│ ├── src/ -│ │ ├── main.tsx # Point d'entrée -│ │ ├── App.tsx -│ │ ├── pages/ -│ │ │ ├── LoginPage.tsx -│ │ │ ├── RegisterPage.tsx -│ │ │ ├── DashboardPage.tsx -│ │ │ ├── SongsPage.tsx -│ │ │ ├── SongDetailPage.tsx -│ │ │ ├── PracticePage.tsx -│ │ │ └── StatsPage.tsx -│ │ ├── components/ -│ │ │ ├── SongCard.tsx -│ │ │ ├── PhoneticDisplay.tsx -│ │ │ ├── KaraokeMode.tsx -│ │ │ ├── FillBlankMode.tsx -│ │ │ ├── MCQMode.tsx -│ │ │ ├── VoiceMode.tsx -│ │ │ └── Layout.tsx -│ │ ├── services/ -│ │ │ └── api.ts # API client -│ │ ├── stores/ -│ │ │ └── authStore.ts # Zustand store -│ │ ├── types/ -│ │ │ └── index.ts -│ │ └── utils/ -│ ├── vite.config.ts -│ ├── tailwind.config.js -│ └── package.json -│ -├── docs/ # Documentation -├── .github/ -│ └── workflows/ -│ └── deploy.yml # CI/CD -└── README.md -``` - ---- - -## Roadmap MVP (2-3 semaines) - -### Semaine 1 : Backend + Auth -- [ ] Setup FastAPI + SQLite -- [ ] Modèles User, Song, Session -- [ ] Auth JWT (register, login) -- [ ] API CRUD songs -- [ ] Tests backend - -### Semaine 2 : Frontend Base -- [ ] Setup React + Vite + Tailwind -- [ ] Pages Login/Register -- [ ] Dashboard + Song List -- [ ] Connexion API -- [ ] Responsive mobile - -### Semaine 3 : Fonctionnalités -- [ ] Import Genius -- [ ] Affichage phonétique (JP, KR) -- [ ] Mode karaoke (défilement) -- [ ] Mode phrases à trous -- [ ] Sauvegarde progression - -### Semaine 4 : Polish + PWA -- [ ] Mode QCM -- [ ] Traduction auto EN -- [ ] Service Worker (offline) -- [ ] Manifest (installable) -- [ ] Déploiement Vercel + Railway - ---- - -## Évolution Future (Si Intérêt) - -### Phase 2 (Apps Natives) -Si besoin de vraies apps natives plus tard : -- **Desktop :** Tauri (Rust + Web, très léger) -- **Mobile :** Capacitor (compile PWA en app native) - -### Phase 3 (Fonctionnalités Premium) -- Mode collaboratif (pratiquer à plusieurs) -- Synchronisation cloud -- Export/import données -- Thèmes personnalisés -- Classements / challenges - ---- - -## Validation - -✅ **Desktop :** Fonctionne dans navigateur, installable via Chrome/Edge -✅ **Mobile :** Fonctionne dans navigateur, installable comme app -✅ **Gratuit :** Toutes technos et hébergement gratuits -✅ **FR/EN/KR/JP :** Support phonétique pour les 4 langues -✅ **Flexible :** Peut évoluer sans réécriture complète - -**Prêt à commencer l'implémentation !** 🚀 diff --git a/USER_STORIES.md b/USER_STORIES.md deleted file mode 100644 index a030934..0000000 --- a/USER_STORIES.md +++ /dev/null @@ -1,157 +0,0 @@ -# User Stories for LyRemember - -## Epic 1: Lyrics Management -**As a** music enthusiast -**I want to** add and organize song lyrics in multiple languages -**So that** I can build my personal collection of songs to memorize - -### User Stories: -1. **Add New Songs** - - As a user, I want to add a new song with its lyrics so that I can start memorizing it - - Acceptance Criteria: - - Can input song title, artist, and language - - Can paste or type full lyrics - - System saves the song to my collection - -2. **Multi-Language Support** - - As a user, I want to add lyrics in different languages (English, Spanish, French, etc.) so that I can learn songs from various cultures - - Acceptance Criteria: - - Can specify the language for each song - - Can add translations for the same song - - Can view lyrics in original and translated versions - -3. **View Lyrics Collection** - - As a user, I want to browse my saved songs so that I can choose what to practice - - Acceptance Criteria: - - Can list all saved songs - - Can filter by language or artist - - Can search for specific songs - -## Epic 2: Memorization Features -**As a** learner -**I want to** practice lyrics in interactive ways -**So that** I can effectively memorize songs - -### User Stories: -4. **Fill-in-the-Blank Mode** - - As a user, I want to practice with missing words so that I can test my memory - - Acceptance Criteria: - - System randomly hides words in lyrics - - I can type the missing words - - System provides immediate feedback - - Difficulty level can be adjusted (hide more or fewer words) - -5. **Line-by-Line Practice** - - As a user, I want to practice one line at a time so that I can learn gradually - - Acceptance Criteria: - - System shows one line at a time - - Can reveal the next line - - Can repeat difficult lines - - Can shuffle line order for advanced practice - -6. **Flashcard Mode** - - As a user, I want flashcard-style practice where I see first part of a line and recall the rest - - Acceptance Criteria: - - System shows beginning of a line - - I try to recall and type the rest - - Can flip to see correct answer - - Can mark lines as "known" or "needs practice" - -## Epic 3: Progress Tracking -**As a** user -**I want to** track my learning progress -**So that** I can see my improvement over time - -### User Stories: -7. **Learning Statistics** - - As a user, I want to see my practice statistics so that I stay motivated - - Acceptance Criteria: - - Can see number of songs learned - - Can see practice time and sessions - - Can view accuracy rates - - Can track progress per song - -8. **Difficulty Levels** - - As a user, I want the system to adjust difficulty based on my performance - - Acceptance Criteria: - - System tracks which lines I struggle with - - Presents difficult lines more frequently - - Graduates easy lines to occasional review - -## Epic 4: Interactive Games -**As a** user -**I want to** make learning fun through games -**So that** I stay engaged and motivated - -### User Stories: -9. **Lyrics Quiz** - - As a user, I want to take quizzes on my saved lyrics so that I can test myself - - Acceptance Criteria: - - Multiple choice questions about lyrics - - Time-based challenges - - Score tracking - - Can choose quiz length and difficulty - -10. **Karaoke Practice Mode** - - As a user, I want to see lyrics revealed progressively (like karaoke) so that I can practice singing along - - Acceptance Criteria: - - Lyrics display line by line with timing - - Can adjust display speed - - Can pause and resume - - Can hide lyrics for advanced practice - -## Epic 5: Language Learning Support -**As a** language learner -**I want to** use lyrics to improve my language skills -**So that** I can learn new vocabulary and phrases - -### User Stories: -11. **Translation View** - - As a user, I want to see side-by-side translations so that I understand what I'm memorizing - - Acceptance Criteria: - - Can add translations for songs - - Can view original and translation together - - Can toggle translation on/off - -12. **Vocabulary Extraction** - - As a user, I want to see important words and phrases highlighted so that I can focus on learning vocabulary - - Acceptance Criteria: - - System identifies key vocabulary - - Can save words to personal vocabulary list - - Can practice vocabulary separately from full lyrics - -## Technical User Stories - -13. **Data Persistence** - - As a user, I want my songs and progress to be saved so that I don't lose my work - - Acceptance Criteria: - - Data persists between sessions - - Can export/import my collection - - Can backup my progress - -14. **User-Friendly Interface** - - As a user, I want an intuitive interface so that I can focus on learning - - Acceptance Criteria: - - Clear navigation - - Helpful error messages - - Easy access to main features - - Works on different platforms (CLI/Web) - -## Priority for MVP (Minimum Viable Product) -**Must Have:** -- Add and store songs (Stories 1, 3) -- Multi-language support (Story 2) -- Fill-in-the-blank practice (Story 4) -- Basic progress tracking (Story 7) -- Data persistence (Story 13) - -**Should Have:** -- Line-by-line practice (Story 5) -- Flashcard mode (Story 6) -- Translation view (Story 11) - -**Could Have:** -- Lyrics quiz (Story 9) -- Karaoke mode (Story 10) -- Advanced statistics (Story 8) -- Vocabulary extraction (Story 12) diff --git a/USER_STORIES_V2.md b/USER_STORIES_V2.md deleted file mode 100644 index c872525..0000000 --- a/USER_STORIES_V2.md +++ /dev/null @@ -1,542 +0,0 @@ -# User Stories - Nouvelles Exigences LyRemember - -## Contexte -Application pour mémoriser et pratiquer des paroles de chansons en plusieurs langues, avec support phonétique et modes de pratique variés. - ---- - -## Epic 1 : Gestion des Comptes Utilisateur - -### US-1.1 : Créer un compte -**En tant qu'** utilisateur -**Je veux** pouvoir créer un compte avec mes informations -**Afin de** sauvegarder mon répertoire personnel de chansons - -**Critères d'acceptation :** -- [ ] L'utilisateur peut s'inscrire avec : nom d'utilisateur, email, mot de passe -- [ ] Le système vérifie que le nom d'utilisateur est unique -- [ ] Le mot de passe est stocké de manière sécurisée (hashé) -- [ ] Un message de confirmation est affiché après inscription -- [ ] Les données sont persistantes entre les sessions - -**Scénarios de test :** -``` -GIVEN je suis un nouvel utilisateur -WHEN je lance "lyremember register" -AND je saisis mon nom d'utilisateur, email et mot de passe -THEN un compte est créé -AND je reçois une confirmation -AND je peux me connecter avec ces identifiants -``` - -**Questions ouvertes :** -- Faut-il une validation d'email ? -- Politique de mot de passe (longueur minimale, complexité) ? - ---- - -### US-1.2 : Se connecter -**En tant qu'** utilisateur inscrit -**Je veux** me connecter à mon compte -**Afin d'** accéder à mon répertoire personnel - -**Critères d'acceptation :** -- [ ] L'utilisateur peut se connecter avec nom d'utilisateur + mot de passe -- [ ] Message d'erreur si identifiants incorrects -- [ ] La session reste active pendant l'utilisation de l'application -- [ ] L'utilisateur peut se déconnecter - -**Scénarios de test :** -``` -GIVEN j'ai un compte existant -WHEN je lance "lyremember login" -AND je saisis mes identifiants corrects -THEN je suis connecté -AND je peux accéder à mes chansons - -GIVEN je suis connecté -WHEN je lance "lyremember logout" -THEN je suis déconnecté -AND je dois me reconnecter pour accéder à mes données -``` - ---- - -### US-1.3 : Lier un compte Genius -**En tant qu'** utilisateur -**Je veux** lier mon compte Genius -**Afin de** pouvoir importer facilement des paroles depuis Genius - -**Critères d'acceptation :** -- [ ] L'utilisateur peut saisir son token API Genius -- [ ] Le token est stocké de manière sécurisée dans son profil -- [ ] Le système vérifie la validité du token -- [ ] L'utilisateur peut modifier ou supprimer son token - -**Scénarios de test :** -``` -GIVEN je suis connecté -WHEN je lance "lyremember link-genius " -THEN mon token Genius est enregistré -AND je peux maintenant importer des chansons depuis Genius -``` - -**Notes techniques :** -- Token Genius : obtenu depuis https://genius.com/api-clients - ---- - -## Epic 2 : Gestion du Répertoire Personnel - -### US-2.1 : Ajouter une chanson au répertoire -**En tant qu'** utilisateur connecté -**Je veux** ajouter des chansons à mon répertoire -**Afin de** constituer ma collection personnelle - -**Critères d'acceptation :** -- [ ] L'utilisateur peut ajouter une chanson manuellement (titre, artiste, paroles) -- [ ] L'utilisateur peut importer une chanson depuis Genius -- [ ] La chanson est ajoutée au répertoire de l'utilisateur uniquement -- [ ] L'utilisateur voit une confirmation - -**Scénarios de test :** -``` -GIVEN je suis connecté -WHEN je lance "lyremember add-to-repertoire " -THEN la chanson est ajoutée à mon répertoire -AND elle apparaît dans ma liste "lyremember my-songs" - -GIVEN je suis connecté -AND j'ai lié mon compte Genius -WHEN je cherche "lyremember search-genius 'Bohemian Rhapsody'" -AND je sélectionne un résultat -THEN la chanson est importée et ajoutée à mon répertoire -``` - ---- - -### US-2.2 : Voir mon répertoire -**En tant qu'** utilisateur connecté -**Je veux** voir la liste de mes chansons -**Afin de** choisir laquelle pratiquer - -**Critères d'acceptation :** -- [ ] Commande "my-songs" affiche uniquement les chansons de l'utilisateur -- [ ] Liste affiche : titre, artiste, langue, statut de progression -- [ ] Possibilité de filtrer par langue -- [ ] Possibilité de rechercher dans mon répertoire - -**Scénarios de test :** -``` -GIVEN j'ai 5 chansons dans mon répertoire -WHEN je lance "lyremember my-songs" -THEN je vois mes 5 chansons avec leurs détails - -GIVEN j'ai des chansons en français et anglais -WHEN je lance "lyremember my-songs --language fr" -THEN je vois uniquement mes chansons en français -``` - ---- - -### US-2.3 : Retirer une chanson du répertoire -**En tant qu'** utilisateur -**Je veux** retirer des chansons de mon répertoire -**Afin de** garder seulement celles qui m'intéressent - -**Critères d'acceptation :** -- [ ] L'utilisateur peut retirer une chanson de son répertoire -- [ ] La chanson n'est plus visible dans "my-songs" -- [ ] La progression sur cette chanson est conservée (au cas où) -- [ ] Demande de confirmation avant retrait - ---- - -## Epic 3 : Affichage Multi-Langues et Phonétique - -### US-3.1 : Voir les paroles en version originale -**En tant qu'** utilisateur -**Je veux** voir les paroles dans la langue originale -**Afin de** apprendre la chanson telle qu'elle est chantée - -**Critères d'acceptation :** -- [ ] Les paroles s'affichent dans la langue originale (VO) -- [ ] La langue est clairement indiquée -- [ ] Format lisible et claire (une ligne par ligne de chanson) - -**Scénarios de test :** -``` -GIVEN j'ai une chanson en japonais dans mon répertoire -WHEN je lance "lyremember view " -THEN les paroles s'affichent en japonais (caractères originaux) -AND la langue "ja" est indiquée -``` - ---- - -### US-3.2 : Voir la traduction anglaise -**En tant qu'** utilisateur -**Je veux** voir la traduction en anglais des paroles non-anglaises -**Afin de** comprendre le sens de ce que je chante - -**Critères d'acceptation :** -- [ ] Si la VO n'est pas en anglais, une traduction EN est affichée -- [ ] Option "--show-translation" ou affichage côte à côte -- [ ] Si pas de traduction dispo, possibilité de traduction automatique -- [ ] Traduction alignée ligne par ligne avec VO - -**Scénarios de test :** -``` -GIVEN j'ai une chanson en espagnol -WHEN je lance "lyremember view --show-translation" -THEN je vois : - Ligne 1 (ES): "Estas son las mañanitas" - Ligne 1 (EN): "These are the morning songs" - Ligne 2 (ES): "Que cantaba el Rey David" - Ligne 2 (EN): "That King David used to sing" - ... -``` - -**Options :** -- Format côte à côte vs ligne par ligne -- Traduction manuelle vs automatique (Google Translate API) - ---- - -### US-3.3 : Voir la translittération phonétique -**En tant qu'** utilisateur -**Je veux** voir la translittération phonétique des paroles -**Afin de** savoir comment prononcer les mots, surtout pour les langues avec idéogrammes - -**Critères d'acceptation :** -- [ ] Option "--show-phonetic" affiche la version phonétique -- [ ] Pour le japonais : affichage en romaji -- [ ] Pour le coréen : affichage en romanisation -- [ ] Pour le chinois : affichage en pinyin -- [ ] Pour l'arabe, russe, etc. : translittération latine -- [ ] Aligné avec les paroles originales - -**Scénarios de test :** -``` -GIVEN j'ai une chanson en japonais "千本桜" -WHEN je lance "lyremember view --show-phonetic" -THEN je vois : - Original: 千本桜 - Phonetic: Senbonzakura - - Original: 夜ニ紛レ君ノ声モ届カナイヨ - Phonetic: Yoru ni magire kimi no koe mo todoka nai yo -``` - -**Notes techniques :** -- Utiliser epitran pour la translittération -- Utiliser pykakasi pour le japonais (kanji → romaji) -- Pour langues européennes : moins critique mais utile pour accentuation - ---- - -### US-3.4 : Affichage combiné VO + Traduction + Phonétique -**En tant qu'** utilisateur avancé -**Je veux** voir simultanément VO, traduction et phonétique -**Afin d'** avoir toutes les informations en un coup d'œil - -**Critères d'acceptation :** -- [ ] Option "--show-all" affiche les 3 versions -- [ ] Format clair et lisible (peut-être tableau) -- [ ] Possibilité de masquer/afficher chaque colonne - -**Exemple d'affichage :** -``` -╔═══════════════════════════════════════════════════════════════════════╗ -║ Chanson: Sukiyaki (上を向いて歩こう) - Kyu Sakamoto ║ -╠═══════════════════════════════════════════════════════════════════════╣ -║ Original (JA) │ Phonétique │ Traduction (EN) ║ -╠═══════════════════════╪══════════════════════╪════════════════════════╣ -║ 上を向いて歩こう │ Ue wo muite arukou │ Let's walk looking up ║ -║ 涙がこぼれないように │ Namida ga koborenai │ So the tears won't ║ -║ │ you ni │ fall ║ -╚═══════════════════════╧══════════════════════╧════════════════════════╝ -``` - ---- - -## Epic 4 : Mode Défilement Phrase par Phrase - -### US-4.1 : Défilement automatique des paroles -**En tant qu'** utilisateur -**Je veux** que les paroles défilent automatiquement phrase par phrase -**Afin de** suivre le rythme de la chanson pendant que je chante - -**Critères d'acceptation :** -- [ ] Mode "scroll" ou "karaoke" affiche une phrase à la fois -- [ ] Délai configurable entre chaque phrase (en secondes) -- [ ] Indicateur visuel de la phrase courante (surbrillance, couleur) -- [ ] Progression automatique jusqu'à la fin - -**Scénarios de test :** -``` -GIVEN j'ai une chanson avec 20 lignes -WHEN je lance "lyremember scroll --speed 3" -THEN la ligne 1 s'affiche pendant 3 secondes -THEN la ligne 2 s'affiche pendant 3 secondes -... -THEN la chanson se termine après toutes les lignes -``` - ---- - -### US-4.2 : Contrôles de lecture -**En tant qu'** utilisateur -**Je veux** contrôler le défilement (pause, play, vitesse) -**Afin de** m'adapter à mon rythme d'apprentissage - -**Critères d'acceptation :** -- [ ] Touche Espace : pause/play -- [ ] Touches flèches : ligne précédente/suivante -- [ ] Touches +/- : augmenter/diminuer vitesse -- [ ] Touche Q ou Esc : quitter -- [ ] Affichage des contrôles à l'écran - -**Interface proposée :** -``` -┌─────────────────────────────────────────────────┐ -│ 🎵 Mode Défilement Karaoke 🎵 │ -├─────────────────────────────────────────────────┤ -│ │ -│ [Phrase courante en grand, centrée] │ -│ │ -│ "Twinkle, twinkle, little star" │ -│ │ -├─────────────────────────────────────────────────┤ -│ Ligne 3/20 │ Vitesse: 3s │ ⏸️ Pause │ -│ [Espace]:⏯️ [↑↓]:Nav [±]:Vitesse [Q]:Quitter │ -└─────────────────────────────────────────────────┘ -``` - ---- - -## Epic 5 : Mode Vérification Orale - -### US-5.1 : Pratiquer en mode oral -**En tant qu'** utilisateur -**Je veux** dire les paroles à voix haute et que l'application vérifie -**Afin de** m'entraîner à chanter correctement - -**Critères d'acceptation :** -- [ ] Mode "oral" active le microphone -- [ ] Une phrase s'affiche -- [ ] L'utilisateur lit la phrase à voix haute -- [ ] Le système capte l'audio et vérifie la correspondance -- [ ] Feedback immédiat (correct/incorrect) -- [ ] Possibilité de réécouter ce qu'on a dit - -**Scénarios de test :** -``` -GIVEN je lance "lyremember practice --mode oral" -WHEN la phrase "Happy birthday to you" s'affiche -AND je dis "Happy birthday to you" dans le micro -THEN le système indique ✓ Correct! -AND passe à la phrase suivante - -GIVEN la phrase "Twinkle twinkle little star" s'affiche -AND je dis "Twinkle little star" (incomplet) -THEN le système indique ✗ Incomplet -AND me montre ce qu'il a compris -AND me permet de réessayer -``` - -**Défis techniques :** -- Reconnaissance vocale (SpeechRecognition + Google Speech API) -- Gestion du bruit de fond -- Tolérance aux accents/prononciations - ---- - -### US-5.2 : Réglages de sensibilité orale -**En tant qu'** utilisateur -**Je veux** ajuster la sensibilité de la vérification orale -**Afin de** avoir un niveau de difficulté adapté - -**Critères d'acceptation :** -- [ ] Paramètre de tolérance : strict / moyen / permissif -- [ ] En mode permissif : 70% de similarité acceptée -- [ ] En mode strict : 90%+ requis -- [ ] Feedback sur le pourcentage de correspondance - ---- - -## Epic 6 : Mode Phrases à Trous ("N'oubliez pas les paroles") - -### US-6.1 : Pratiquer en mode "trous de fin de phrase" -**En tant qu'** utilisateur -**Je veux** compléter la fin des phrases comme dans "N'oubliez pas les paroles" -**Afin de** tester ma mémoire de façon ludique - -**Critères d'acceptation :** -- [ ] Mode "noplp" (N'Oubliez Pas Les Paroles) disponible -- [ ] La phrase s'affiche complète sauf les derniers mots -- [ ] L'utilisateur doit taper les mots manquants -- [ ] Nombre de mots cachés configurable -- [ ] Vérification avec tolérance aux fautes de frappe - -**Exemple :** -``` -Phrase complète: "Twinkle twinkle little star, how I wonder what you are" - -Affichage en mode NOPLP (3 mots cachés): -"Twinkle twinkle little star, how I wonder _____ _____ _____" - -Utilisateur tape: "what you are" -→ ✓ Correct! - -OU - -Utilisateur tape: "what you're" -→ ✗ Presque! Bonne idée mais ce n'est pas "you're", c'est "you are" -``` - -**Options :** -- Nombre de mots à cacher (1-5) -- Position : fin de phrase (défaut) vs milieu vs aléatoire - ---- - -### US-6.2 : Difficulté progressive en mode trous -**En tant qu'** utilisateur -**Je veux** que la difficulté augmente progressivement -**Afin de** être challengé au fur et à mesure - -**Critères d'acceptation :** -- [ ] Commence avec 1 mot caché -- [ ] Augmente à 2, puis 3, etc. selon réussite -- [ ] Si échec, revient au niveau précédent -- [ ] Score basé sur niveau atteint - ---- - -## Epic 7 : Mode Propositions Multiples (QCM) - -### US-7.1 : Pratiquer en mode QCM -**En tant qu'** utilisateur -**Je veux** choisir la bonne phrase parmi plusieurs propositions -**Afin de** tester ma reconnaissance des paroles - -**Critères d'acceptation :** -- [ ] Mode "quiz" ou "mcq" disponible -- [ ] Pour chaque ligne, 4 propositions affichées (A, B, C, D) -- [ ] Une seule est correcte -- [ ] Les 3 autres sont des variantes plausibles -- [ ] L'utilisateur sélectionne avec 1, 2, 3, 4 ou A, B, C, D -- [ ] Feedback immédiat avec explication - -**Exemple :** -``` -Quelle est la ligne suivante dans "Twinkle Twinkle" ? - -A) How I wonder what you do -B) How I wonder what you are ✓ -C) How I wonder where you are -D) How we wonder what you are - -Votre réponse: B -→ ✓ Correct! "How I wonder what you are" est la bonne réponse. - -Points: +10 | Combo: x2 🔥 -``` - ---- - -### US-7.2 : Génération intelligente de fausses propositions -**En tant qu'** utilisateur -**Je veux** que les fausses propositions soient crédibles -**Afin que** le quiz soit réellement challengeant - -**Critères d'acceptation :** -- [ ] Fausses réponses basées sur : - - Mots similaires phonétiquement - - Mots de la même chanson (autres lignes) - - Variantes grammaticales - - Erreurs communes -- [ ] Pas de propositions évidement fausses -- [ ] Difficulté ajustable - ---- - -## Epic 8 : Fonctionnalités Transverses - -### US-8.1 : Historique et statistiques par mode -**En tant qu'** utilisateur -**Je veux** voir mes stats pour chaque mode de pratique -**Afin de** identifier où je dois m'améliorer - -**Critères d'acceptation :** -- [ ] Stats séparées pour : oral, trous, QCM, scroll -- [ ] Pourcentage de réussite par mode -- [ ] Temps passé par mode -- [ ] Chanson favorite / plus pratiquée - ---- - -### US-8.2 : Recommandations personnalisées -**En tant qu'** utilisateur -**Je veux** recevoir des suggestions de pratique -**Afin d'** optimiser mon apprentissage - -**Critères d'acceptation :** -- [ ] "lyremember recommend" suggère une chanson + mode -- [ ] Basé sur maîtrise actuelle -- [ ] Basé sur temps depuis dernière pratique -- [ ] Basé sur difficultés identifiées - ---- - -## Priorités pour le MVP - -### Must Have (P0) -- US-1.1, 1.2 : Comptes utilisateur (register/login) -- US-2.1, 2.2 : Répertoire personnel -- US-3.1, 3.3 : VO + Phonétique (au moins pour japonais/coréen) -- US-4.1, 4.2 : Défilement karaoke basique -- US-6.1 : Mode trous style NOPLP - -### Should Have (P1) -- US-1.3 : Lien Genius -- US-3.2 : Traduction EN -- US-7.1 : Mode QCM -- US-5.1 : Vérification orale basique - -### Could Have (P2) -- US-3.4 : Affichage combiné 3 vues -- US-5.2 : Réglages sensibilité orale -- US-6.2 : Difficulté progressive trous -- US-7.2 : Génération intelligente QCM -- US-8.1, 8.2 : Stats avancées et recommandations - ---- - -## Questions pour validation - -1. **Comptes utilisateur** : Faut-il un système de récupération de mot de passe ? -2. **Genius** : Token obligatoire ou optionnel ? Fallback si pas de token ? -3. **Phonétique** : Quelles langues sont prioritaires ? (JP, KR, ZH, AR, RU ?) -4. **Mode oral** : Service de reconnaissance vocale (Google gratuit vs payant vs offline) ? -5. **Données** : Les chansons sont partagées entre users ou chaque user a ses propres chansons ? -6. **Interface** : CLI uniquement ou prévoir interface web ? - ---- - -## Estimations (story points) - -- Epic 1 (Comptes): 13 pts -- Epic 2 (Répertoire): 8 pts -- Epic 3 (Multi-langues): 21 pts -- Epic 4 (Défilement): 13 pts -- Epic 5 (Oral): 21 pts -- Epic 6 (Trous): 8 pts -- Epic 7 (QCM): 13 pts -- Epic 8 (Transverse): 8 pts - -**Total : ~105 story points** - -MVP (P0) : ~55 pts -MVP + P1 : ~80 pts diff --git a/aidd_docs/CONTRIBUTING.md b/aidd_docs/CONTRIBUTING.md new file mode 100644 index 0000000..d501259 --- /dev/null +++ b/aidd_docs/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing + +Guidelines for adding skills, agents, rules, and templates inside your AIDD-equipped project. + +## Creating New Content + +Use the generator skills to scaffold new content that follows the framework structure. + +| Skill | Creates | +| ---------------------------------- | -------------------- | +| `aidd-context:03:context-generate` | New skill, agent, or rule (router-based, with actions and evals) | +| `aidd-context:05:learn` | New memory or rule capturing a learning | + +Generator skills consume the templates inside their `assets/` folder and write the output to the correct location for your AI tool (Claude Code, Cursor, Copilot, Codex, OpenCode). + +## Templates + +All templates live alongside the skill that owns them, under `plugins//skills//assets/`. They can be adapted to your team's conventions. + +| Where | What it scaffolds | +| -------------------------------------------------- | -------------------------------------------------------- | +| `aidd-context:03:context-generate/assets/skills/` | `SKILL.md`, action, evals templates | +| `aidd-context:03:context-generate/assets/agents/` | Agent file template | +| `aidd-context:03:context-generate/assets/rules/` | Rule file template | +| `aidd-pm:03:prd/assets/` | PRD body template | +| `aidd-pm:04:spec/assets/` | Spec template and validator | +| `aidd-dev:01:plan/assets/` | Plan and master-plan templates | +| `aidd-vcs:01:commit/assets/` | Conventional commit message template | +| `aidd-vcs:02:pull-request/assets/` | Pull/merge request body template, contributing example | + +## Syncing Across Tools + +If the project uses multiple AI tools (e.g. Claude Code plus Cursor), the same content must be available to each. The memory bank is shared automatically via the `` block kept in sync by `aidd-context:02:project-init`. Skills are loaded per-plugin by the runtime, so any skill installed via the marketplace is available across tools that support skills. + +When tools differ in syntax (frontmatter, slash command name, references), follow the IDE mapping reference shipped with each plugin. + +## Recommended Workflow + +- Open a pull request for any new skill, agent, rule, or template. Visible changes that affect how the AI behaves on the project deserve team review. +- Keep skills router-pure: SKILL.md holds no business logic; everything lives inside actions. +- Add evals (`evals/scenarios.json`) for every auto-trigger skill so router behavior stays correct over time. +- Stay within 5 to 10 percent deviation from a template structure. Beyond that, update the template first, then derive the new content from it. + +## Conventions + +- Skill names: `::`. Slug is kebab-case verb for activity domains, singular noun for tool domains. +- Action files: only `## Inputs`, `## Outputs`, `## Process`, `## Test` (`## Depends on` optional). +- `## Process` steps start with `**Bold title**.` and use decision-list `Pick first match` for branching. +- `## Test` bullets start with `**Bold name**:` and are checkable (command, artifact check, or observable side effect). +- Descriptions in SKILL.md frontmatter include explicit "Use when ..." triggers and "Do NOT use for ..." exclusions. diff --git a/aidd_docs/GUIDELINES.md b/aidd_docs/GUIDELINES.md new file mode 100644 index 0000000..96b306e --- /dev/null +++ b/aidd_docs/GUIDELINES.md @@ -0,0 +1,128 @@ +# Developer Guidelines (AI Pilot) + +How a developer should operate AI coding assistants to maximize quality, speed, and reliability. + +--- + +- [1) Operating mindset](#1-operating-mindset) +- [2) Start of task checklist](#2-start-of-task-checklist) +- [3) Planning before coding](#3-planning-before-coding) +- [4) Implementation loop](#4-implementation-loop) +- [5) Review loop](#5-review-loop) +- [6) Prompting hygiene](#6-prompting-hygiene) +- [7) Context hygiene](#7-context-hygiene) +- [8) Quality discipline](#8-quality-discipline) +- [9) Delegation strategy](#9-delegation-strategy) +- [10) Failure and recovery strategy](#10-failure-and-recovery-strategy) +- [11) Team-level practices](#11-team-level-practices) +- [12) Anti-patterns to avoid](#12-anti-patterns-to-avoid) +- [References (official)](#references-official) + +--- + +## 1) Operating mindset + +- Treat AI as a powerful pair, not as an authority. +- Keep human accountability for architecture, product impact, and merge decisions. +- Prefer explicit instructions over implied expectations. +- Optimize for correct outcomes first, speed second. + +## 2) Start of task checklist + +- [ ] Clarify objective in one sentence. +- [ ] Define explicit done criteria. +- [ ] Define non-goals (what must not be changed). +- [ ] Identify impacted files/modules. +- [ ] Decide risk level (low, medium, high). +- [ ] Decide required validation depth (`minimal`, `standard`, `full`). + +## 3) Planning before coding + +- Ask AI for a short implementation plan first. +- Validate 100% of plan before execution. +- Split large tasks into small increments. +- Keep one behavior per increment. +- Refuse execution if plan has ambiguity. + +## 4) Implementation loop + +For each increment: + +1. Ask for minimal change set. +2. Read generated diff. +3. Stage intentionally (file by file or hunk by hunk). +4. Run assertions and tests. +5. Ask AI to fix only failing points. +6. Re-run checks until green. + +## 5) Review loop + +- Run technical review (`rules`, defects, regressions). +- Run functional review (expected behavior vs implementation). +- Manually read staged diff before commit. +- Require evidence for claims ("works", "tested", "fixed"). + +## 6) Prompting hygiene + +- Give context, constraints, and expected output format. +- Request concrete file paths and commands. +- Ask for assumptions explicitly. +- Ask for tradeoffs when options exist. +- Keep prompts scoped; avoid multi-objective prompts. +- If output quality drops, reset with a fresh focused prompt. + +## 7) Context hygiene + +- Keep rules concise and non-conflicting. +- Avoid duplicate sources of truth. +- Keep project memory updated (`architecture`, `testing`, `vcs`, `decisions`). +- Prefer canonical docs referenced by path. +- Remove stale instructions quickly. + +## 8) Quality discipline + +- For bug fixes: failing test first, then fix. +- Keep structural and behavioral changes separated. +- Keep commits atomic and intention-revealing. +- Never skip validation to "save time". +- Never merge code you do not understand. + +## 9) Delegation strategy + +- Low risk: semi-autonomous execution with checkpoints. +- Medium risk: tighter loop, smaller increments, frequent review. +- High risk: manual supervision, explicit approvals at every gate. +- Critical systems: mandatory peer review before merge. + +## 10) Failure and recovery strategy + +- Stop if AI drifts from objective. +- If same failure repeats, change approach (not just prompt wording). +- If context is polluted, restart session with minimal clean context. +- Keep checkpoint commits before long autonomous runs. +- Escalate early when uncertainty remains high. + +## 11) Team-level practices + +- Standardize shared prompts/commands/rules in repo. +- Use common commit and PR templates. +- Track recurring failures and add guardrails. +- Review and improve guidelines monthly. +- Keep onboarding docs short and actionable. + +## 12) Anti-patterns to avoid + +- Asking for implementation before validation of plan. +- Approving giant diffs without incremental checkpoints. +- Combining unrelated changes in one PR. +- Trusting green tests when scope of tests is unclear. +- Letting AI decide merge/deploy without explicit approval. +- Keeping endless chat sessions after coherence loss. + +## References (official) + +- Anthropic, "Prompt engineering overview": +- Anthropic, "Claude Code memory": +- OpenAI, "Prompt engineering best practices": +- OpenAI Cookbook, "Eval-driven development": +- GitHub Docs, "Repository custom instructions for Copilot": diff --git a/aidd_docs/README.md b/aidd_docs/README.md new file mode 100644 index 0000000..4c26a54 --- /dev/null +++ b/aidd_docs/README.md @@ -0,0 +1,151 @@ +# AI-Driven Dev Docs + +AIDD structures your AI coding assistant with skills, agents, rules, and a memory bank so it produces consistent, high-quality work, regardless of which AI tool you use (Claude Code, Cursor, Copilot, Codex, OpenCode). + +- [What You Get](#what-you-get) + - [Concepts](#concepts) + - [Plugins](#plugins) + - [Framework Structure](#framework-structure) + - [Memory Block Lifecycle](#memory-block-lifecycle) +- [Installation](#installation) +- [Typical Workflow](#typical-workflow) +- [Optional: Async Automation](#optional-async-automation) +- [Validation Rules](#validation-rules) +- [References](#references) + +--- + +## What You Get + +A plugin marketplace of skills, agents, rules, templates, and a memory system. You invoke skills through your AI tool (slash command, MCP, or natural language trigger) and the AI follows structured workflows instead of guessing. + +### Concepts + +| Block | Location | What it does | +| --------- | ------------------------------------------------- | ------------------------------------------------------------------------------------- | +| Memory | `aidd_docs/memory/` | Project context the AI reads on every conversation | +| Skills | plugin `skills/` folders | Router-based workflows triggered by user phrases or slashes | +| Commands | tool-specific commands dir (when supported) | Plain slash commands (no router); used for shortcuts and simple flows. None currently shipped by AIDD; reserved for future plugins or your own additions | +| Agents | plugin `agents/` folders | Specialized AI personas for focused tasks | +| Rules | tool-specific rules dir (see your AI tool docs) | Coding standards the AI follows automatically | +| Templates | plugin `assets/` folders | Scaffolding for new skills, rules, agents | + +### Plugins + +Skills are grouped into plugins by domain. Install only the plugins you need. + +| Plugin | Purpose | Example skills | +| ----------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| aidd-context | Bootstrap, project init, generation of Claude Code context artifacts (skills, agents, rules, commands, hooks, plugins, marketplaces), mermaid diagrams, learn, discovery | `02:project-init`, `03:context-generate`, `04:mermaid` | +| aidd-refine | Meta-cognition: brainstorm, challenge prior work, condensed communication mode | `01:brainstorm`, `02:challenge`, `03:condense` | +| aidd-pm | Product management: ticket info, user stories, PRD, spec | `01:ticket-info`, `02:user-stories-create`, `03:prd`, `05:spec` | +| aidd-dev | Code transformation: Dev SDLC orchestrator, plan, implement, assert, audit, review, test, refactor, debug, for-sure | `00:sdlc`, `01:plan`, `02:implement`, `05:review`, `06:test` | +| aidd-vcs | VCS workflows: commit, pull/merge request, release tag, issue creation | `01:commit`, `02:pull-request`, `04:issue-create` | +| aidd-orchestrator | Async orchestration of the SDLC on labeled issues (optional, extra) | `00:async-dev` (router with setup / run / review sub-flows) | + +> See [CATALOG.md](../../../../../docs/CATALOG.md) for the exhaustive list of skills and actions. + +### Framework Structure + +AIDD installs alongside your code. Each AI tool's configuration directory holds the skills, agents, and rules it can load. Shared docs and memory live under `aidd_docs/`. + +```text +my-project/ +├── .claude/ # Claude Code: skills, agents, rules, hooks +├── .cursor/ # Cursor: skills, agents, rules +├── .github/copilot-instructions.md # GitHub Copilot +├── AGENTS.md # Cursor, Codex, OpenCode (shared) +├── CLAUDE.md # Claude Code +├── aidd_docs/ +│ ├── memory/ # Project context (loaded each conversation) +│ │ ├── internal/ # Internal docs (API, DB schema, design) +│ │ ├── external/ # External documentation references +│ │ ├── architecture.md +│ │ ├── codebase-map.md +│ │ ├── coding-assertions.md +│ │ ├── deployment.md +│ │ ├── project-brief.md +│ │ ├── testing.md +│ │ └── vcs.md +│ ├── tasks/ # Specs, plans, run summaries +│ ├── README.md # This file +│ ├── GUIDELINES.md # Developer operating guidelines +│ └── CONTRIBUTING.md # How to add or modify skills, agents, rules +├── src/ # Your application code +└── tests/ +``` + +### Memory Block Lifecycle + +Each AI context file (`CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`, etc.) contains an `` block. It is: + +1. **Seeded** the first time by `aidd-context:02:project-init` (the skill creates the block if absent). +2. **Kept in sync** automatically by a session-start hook (`aidd-context/hooks/update_memory.js`) that scans `aidd_docs/memory/` and writes the current list of `.md` files into the block. + +You never edit the block by hand. To change what the AI sees, add or remove files under `aidd_docs/memory/`; the hook picks them up at the next session. + +--- + +## Installation + +AIDD is delivered as a plugin marketplace. Pick what you need; do not install everything. + +- **Remote marketplace** (default): add the AIDD marketplace via your AI tool's plugin manager, then install only the plugins your project actually uses (e.g. `aidd-context` + `aidd-dev` + `aidd-vcs`). +- **Local marketplace**: clone the AIDD framework repo and point your AI tool's plugin manager at the local folder. Useful for offline work, custom forks, or contributing to the framework. + +| Plugin | Skills | +| ------------ | ------------------------------------------------------------------------------------------------------------------- | +| aidd-context | 01-bootstrap, 02-project-init, 03-context-generate, 04-mermaid, 05-learn, 06-discovery | +| aidd-refine | 01-brainstorm, 02-challenge, 03-condense | +| aidd-dev | 00-sdlc, 01-plan, 02-implement, 03-assert, 04-audit, 05-review, 06-test, 07-refactor, 08-debug, 09-for-sure | +| aidd-vcs | 01-commit, 02-pull-request, 03-release-tag, 04-issue-create | +| aidd-pm | 01-ticket-info, 02-user-stories-create, 03-prd | + +Each plugin is independently installable; install incrementally. Smaller surface, fewer triggers competing. + +## Typical Workflow + +A typical change cycles through skills from several plugins. The order below is indicative; skip what you do not need and loop back as the work demands. + +1. **Bootstrap** (only for a brand-new project): `aidd-context:01:bootstrap` imagines the stack and architecture, comparing candidate stacks and writing an `INSTALL.md`. Skip this step on an existing project. +2. **Project init** (once per project, re-runnable to refresh): `aidd-context:02:project-init` scaffolds `aidd_docs/`, the memory bank, and the AI context files for the tools you use. Re-running later refreshes the scaffold without overwriting your customizations. +3. **Frame the request**: `aidd-refine:01:brainstorm` to clarify, `aidd-pm:01:ticket-info` to pull tracker data, `aidd-pm:02:user-stories-create` and `aidd-pm:03:prd` or `aidd-pm:04:spec` to formalize scope. +4. **Plan**: `aidd-dev:01:plan` produces the technical plan, component behavior model, or design-image extraction. +5. **Implement and assert**: `aidd-dev:02:implement` writes code against the plan; `aidd-dev:03:assert` verifies the result. +6. **Review**: `aidd-dev:05:review` for code and functional review; `aidd-refine:02:challenge` to stress-test the result. +7. **Test**: `aidd-dev:06:test` adds or runs tests and validates user journeys. +8. **Document and learn**: `aidd-context:04:mermaid` for diagrams; `aidd-context:05:learn` to feed insights back into the memory bank or rules. +9. **Ship**: `aidd-vcs:01:commit`, `aidd-vcs:02:pull-request`, then `aidd-vcs:03:release-tag` when the work is in production. File issues with `aidd-vcs:04:issue-create`. +10. **Refactor and maintain**: `aidd-dev:07:refactor` for performance or security, `aidd-dev:04:audit` for technical-debt sweeps, `aidd-dev:08:debug` to reproduce and fix bugs. + +When you want the whole synchronous pipeline run in one go (spec, plan, implementation, finalize), invoke `aidd-dev:00:sdlc`. + +--- + +## Optional: Async Automation + +Beyond the synchronous path above, `aidd-orchestrator` runs the SDLC asynchronously on labeled issues (webhook or cron). This is extra: most projects do not need it. Use only when you want the AI to pick up `to-implement` issues without a human pressing a key. + +Inside the synchronous path, `aidd-dev:00:sdlc` is the Dev SDLC orchestrator that drives spec, plan, implementation, finalize in one go when you want the whole pipeline at once. + +--- + +## Validation Rules + +- Skills must have an `## Available actions` table, `## Default flow`, `## Transversal rules`. +- Actions must contain only `## Inputs`, `## Outputs`, `## Process`, `## Test`. +- Tests must be observable: command, artifact check, or side effect. +- Evals (`evals/scenarios.json`) ship for every auto-trigger skill. + +--- + +## References + +See [CONTRIBUTING.md](CONTRIBUTING.md) for adding or modifying skills, agents, and rules. + +External: + +- Anthropic, Prompt engineering overview: +- Anthropic, Claude Code memory: +- OpenAI, Prompt engineering best practices: +- GitHub Docs, Repository custom instructions for Copilot: diff --git a/aidd_docs/memory/architecture.md b/aidd_docs/memory/architecture.md new file mode 100644 index 0000000..8f958bb --- /dev/null +++ b/aidd_docs/memory/architecture.md @@ -0,0 +1,86 @@ +--- +name: architecture +description: Module architecture and structure +scope: all +--- + +# Architecture + +## Language/Framework + +```json +@lyremember-app/package.json +@rust-backend/Cargo.toml +@lyremember-app/src-tauri/Cargo.toml +``` + +```mermaid +flowchart LR + Vue[Vue + TypeScript] --> Vite[Vite] + Vue --> Tailwind[Tailwind CSS] + Vue --> Pinia[Pinia] + Vue --> VueRouter[Vue Router] + Vue --> Lucide[lucide-vue-next] + Vue -->|invoke| Tauri[Tauri commands] + Tauri --> RustBackend[lyremember_backend Rust lib] + RustBackend --> SQLite[(SQLite rusqlite)] + RustBackend --> PyO3[PyO3 → Python] + RustBackend --> Reqwest[reqwest → LibreTranslate] +``` + +### Naming Conventions + +- **Files Vue**: PascalCase pour composants (`App.vue`), kebab-case pour vues +- **Files Rust**: snake_case (`commands.rs`) +- **Functions**: camelCase TS, snake_case Rust +- **Variables**: camelCase TS, snake_case Rust +- **Constants**: UPPER_SNAKE_CASE +- **Types/Interfaces**: PascalCase + +## Services communication + +Communication frontend ↔ backend via Tauri IPC (`invoke`). Le frontend Vue appelle les 16 commands Tauri exposées par `lyremember-app/src-tauri/src/commands.rs`, qui délèguent à la lib Rust `lyremember_backend`. + +### Vue ↔ Tauri ↔ Rust + +```mermaid +sequenceDiagram + participant Vue as Vue Component + participant API as tauri-api.ts + participant Tauri as Tauri Command (Rust) + participant Backend as lyremember_backend + participant DB as SQLite + Vue->>API: createSong(...) + API->>Tauri: invoke('create_song', args) + Tauri->>Backend: SongService::create + Backend->>PyO3: génère phonétique + Backend->>Reqwest: appelle LibreTranslate + Backend->>DB: INSERT song + DB-->>Backend: id + Backend-->>Tauri: Song + Tauri-->>API: JSON + API-->>Vue: Song typé +``` + +### External Services + +#### LibreTranslate + +```mermaid +flowchart LR + Backend[Rust backend] -->|HTTP POST /translate| LT[LibreTranslate API] + LT -->|JSON traduction| Backend + Backend --> SQLite[(songs.translations)] +``` + +Traduction unique stockée en base ; usage offline ensuite. + +#### Python (via PyO3) + +```mermaid +flowchart LR + Backend[Rust backend] -->|PyO3 auto-init| Py[Python runtime] + Py --> pykakasi[pykakasi: JP→Romaji] + Py --> hangul[hangul-romanize: KR→Latin] + Py --> epitran[epitran: FR/EN→IPA] +``` diff --git a/aidd_docs/memory/backend/api-docs.md b/aidd_docs/memory/backend/api-docs.md new file mode 100644 index 0000000..2397322 --- /dev/null +++ b/aidd_docs/memory/backend/api-docs.md @@ -0,0 +1,30 @@ +--- +name: api-docs +description: API documentation and specifications +scope: backend +--- + +# API Documentation + +Pas d'API HTTP : la "surface API" est l'ensemble des commands Tauri exposées par `lyremember-app/src-tauri/src/commands.rs` (16 commands) et consommées via `@tauri-apps/api` depuis `lyremember-app/src/lib/tauri-api.ts`. + +```text +@lyremember-app/src-tauri/src/commands.rs +@lyremember-app/src/lib/tauri-api.ts +``` + +## Authentication & Authorization + +- **Authentication**: bcrypt (hash password) + jsonwebtoken (JWT) — implémenté dans `rust-backend/src/services/` +- **Session Management**: token JWT renvoyé au login, conservé côté frontend (Pinia store) + +## Endpoints + +- Type : Tauri IPC commands (pas REST/GraphQL) +- Format : JSON sérialisé par `serde` +- Liste des commands : `register`, `login`, `create_song`, `get_songs`, `add_to_repertoire`, `create_practice_session`, `get_user_stats`, etc. (16 au total) + +## Request/Response Formats + +- Request : arguments typés Rust (sérialisés depuis TS via `invoke`) +- Response : structs Rust sérialisées en JSON, typées côté TS dans `tauri-api.ts` diff --git a/aidd_docs/memory/backend/backend-communication.md b/aidd_docs/memory/backend/backend-communication.md new file mode 100644 index 0000000..a501332 --- /dev/null +++ b/aidd_docs/memory/backend/backend-communication.md @@ -0,0 +1,38 @@ +--- +name: backend-communication +description: Frontend-backend communication patterns +scope: backend +--- + +# Communication between backend and frontend + +## Overview + +Frontend Vue ↔ backend Rust via Tauri IPC (`invoke`). Pas de HTTP : tout passe par les 16 commands Tauri exposées par `lyremember-app/src-tauri/src/commands.rs`. + +- **API definition**: `lyremember-app/src/lib/tauri-api.ts` (TypeScript wrapper typé) +- **Services**: `tauri-api.ts` côté front, commands Rust côté src-tauri, services Rust dans `rust-backend/src/services/` +- **Request Types**: appels `invoke('', payload)` — pas de verbes HTTP +- **Entities**: structs Rust dans `rust-backend/src/models/`, typages TS miroir dans `tauri-api.ts` +- **Data Flow**: composant Vue → `tauri-api.ts` → `invoke` → Tauri command → service Rust → SQLite / PyO3 / reqwest +- **Error Handling**: `Result` Rust → `Promise.reject` côté TS +- **Validation**: validation Rust dans les services (erreurs `anyhow`/`thiserror`) + +### Data Flow + +```mermaid +sequenceDiagram + participant V as Vue component + participant T as tauri-api.ts + participant C as commands.rs + participant S as Rust service + participant DB as SQLite + V->>T: createSong(...) + T->>C: invoke('create_song', payload) + C->>S: SongService::create + S->>DB: INSERT + DB-->>S: row id + S-->>C: Song + C-->>T: JSON + T-->>V: Song typé +``` diff --git a/aidd_docs/memory/backend/database.md b/aidd_docs/memory/backend/database.md new file mode 100644 index 0000000..2bf351d --- /dev/null +++ b/aidd_docs/memory/backend/database.md @@ -0,0 +1,67 @@ +--- +name: database +description: Database schema and management +scope: backend +--- + +# Database + +SQLite local via `rusqlite` (feature `bundled`). Base auto-créée au lancement Tauri dans le répertoire app data de l'OS. + +```text +@rust-backend/src/db +@lyremember-app/src-tauri/src/lib.rs +``` + +```mermaid +flowchart LR + Backend[lyremember_backend] -->|rusqlite| SQLite[(SQLite bundled)] + Tauri[src-tauri/lib.rs] -->|init at startup| SQLite +``` + +## Main entities and relationships + +- `users` : id, username, email, password_hash, ... +- `songs` : id, title, artist, language, lyrics, phonetic_lyrics, translations, ... +- `user_songs` : table de liaison many-to-many (répertoire utilisateur) +- `practice_sessions` : id, user_id, song_id, mode, score, accuracy, duration, ... + +```mermaid +erDiagram + USERS ||--o{ USER_SONGS : owns + SONGS ||--o{ USER_SONGS : contained_in + USERS ||--o{ PRACTICE_SESSIONS : runs + SONGS ||--o{ PRACTICE_SESSIONS : about + USERS { + string id + string username + string password_hash + } + SONGS { + string id + string title + string language + text lyrics + text phonetic_lyrics + text translations + } + USER_SONGS { + string user_id + string song_id + } + PRACTICE_SESSIONS { + string id + string user_id + string song_id + string mode + real score + } +``` + +## Migrations + +Pas de gestionnaire de migrations dédié. Initialisation du schéma au démarrage par la lib Rust (`db/` module). + +## Seeding + +Pas de seeding automatique. Données créées via les commands Tauri (register / create_song). diff --git a/aidd_docs/memory/codebase-map.md b/aidd_docs/memory/codebase-map.md new file mode 100644 index 0000000..f7c7ce6 --- /dev/null +++ b/aidd_docs/memory/codebase-map.md @@ -0,0 +1,37 @@ +--- +name: codebase-structure +description: Project structure documentation +scope: all +--- + +# Codebase Structure + +```mermaid +flowchart TD + Root[lyremember/] --> App[lyremember-app/] + Root --> Backend[rust-backend/] + Root --> Legacy[lyremember/ Python CLI legacy] + Root --> Docs[docs/ + aidd_docs/] + Root --> Tests[tests/] + + App --> AppSrc[src/ Vue + TS] + AppSrc --> Views[views/] + AppSrc --> Components[components/] + AppSrc --> Stores[stores/ Pinia] + AppSrc --> Router[router/] + AppSrc --> Lib[lib/ tauri-api.ts] + App --> SrcTauri[src-tauri/ commands.rs + lib.rs] + + Backend --> BackendSrc[src/] + BackendSrc --> Services[services/ Auth, Phonetic, Translation, Songs, Practice] + BackendSrc --> Models[models/ User, Song, PracticeSession] + BackendSrc --> Db[db/ SQLite init] + + Legacy --> Cli[cli.py + managers .py] +``` + +- `lyremember-app/` : application Tauri (frontend Vue + wrapper Rust) +- `rust-backend/` : librairie Rust `lyremember_backend` (logique métier, DB, PyO3) +- `lyremember/` : CLI Python d'origine (proof of concept, conservé) +- `aidd_docs/` : documentation et mémoire AIDD +- `tests/` : tests Python du CLI legacy diff --git a/aidd_docs/memory/coding-assertions.md b/aidd_docs/memory/coding-assertions.md new file mode 100644 index 0000000..7360841 --- /dev/null +++ b/aidd_docs/memory/coding-assertions.md @@ -0,0 +1,28 @@ +--- +name: coding-assertions +description: Code quality verification checklist +scope: all +--- + +# Coding Guidelines + +## Requirements to complete a feature + +**Une feature n'est terminée que si toutes les commandes ci-dessous passent.** + +## Commands to run + +### Before commit + +| Order | Command | Description | +| ----- | ------- | ----------- | +| 1 | `pnpm --dir lyremember-app build` | Typecheck Vue + build Vite (`vue-tsc --noEmit && vite build`) | +| 2 | `cargo check --manifest-path rust-backend/Cargo.toml` | Vérifie la compilation Rust backend | +| 3 | `cargo check --manifest-path lyremember-app/src-tauri/Cargo.toml` | Vérifie la compilation Tauri | + +### Before push + +| Order | Command | Description | +| ----- | ------- | ----------- | +| 1 | `cargo test --manifest-path rust-backend/Cargo.toml` | Tests unitaires Rust | +| 2 | `pnpm --dir lyremember-app tauri build` | Build complet de l'app Tauri | diff --git a/aidd_docs/memory/deployment.md b/aidd_docs/memory/deployment.md new file mode 100644 index 0000000..4af61af --- /dev/null +++ b/aidd_docs/memory/deployment.md @@ -0,0 +1,36 @@ +--- +name: deployment +description: Infrastructure and deployment documentation +scope: all +--- + +# Deployment + +# Infrastructure + +## Project Structure + +```plaintext +lyremember/ +├── lyremember-app/ # App Tauri (Vue + src-tauri) +│ ├── src/ # Frontend Vue + TS +│ └── src-tauri/ # Wrapper Rust + commands Tauri +├── rust-backend/ # Lib Rust lyremember_backend +└── lyremember/ # CLI Python (legacy) +``` + +## Environments Variables + +### Environment Files + +Aucun fichier `.env` versionné. Configuration runtime via Tauri config et constantes Rust. + +### Required Environment Variables + +- Aucune variable d'environnement obligatoire pour le développement local +- LibreTranslate : URL d'API codée côté Rust (instance publique par défaut) + +## URLs + +- **Development**: app de bureau lancée localement via `pnpm tauri dev` +- **Production**: distribution sous forme de binaires natifs Tauri (Windows, macOS, Linux) — non distribuée publiquement à ce jour diff --git a/aidd_docs/memory/external/.gitkeep b/aidd_docs/memory/external/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ARCHITECTURE_EXPLAINED.md b/aidd_docs/memory/external/ARCHITECTURE_EXPLAINED.md similarity index 92% rename from ARCHITECTURE_EXPLAINED.md rename to aidd_docs/memory/external/ARCHITECTURE_EXPLAINED.md index ecf5eb5..53acbe3 100644 --- a/ARCHITECTURE_EXPLAINED.md +++ b/aidd_docs/memory/external/ARCHITECTURE_EXPLAINED.md @@ -46,23 +46,21 @@ Il n'y a **PAS** trois applications séparées : ### Vous écrivez UNE SEULE FOIS : ``` -lyremember/ +lyremember-app/ ├── src/ ← VOTRE CODE (une fois) │ ├── App.vue │ ├── components/ -│ │ ├── SongCard.vue -│ │ ├── KaraokeMode.vue -│ │ └── ... -│ ├── views/ +│ │ ├── layout/ ← AppHeader.vue, AppSidebar.vue, MainLayout.vue +│ │ └── ui/ ← Button.vue, Card.vue, Input.vue, Alert.vue +│ ├── views/ ← LoginView, RegisterView, DashboardView, SongsView, SongDetailView… │ ├── stores/ │ └── lib/ -│ └── tauri.ts ← Appels au backend Rust +│ └── tauri-api.ts ← Appels au backend Rust (16 commands) │ -└── src-tauri/ ← BACKEND RUST (une fois) +└── src-tauri/ ← WRAPPER TAURI (une fois) └── src/ ├── main.rs - ├── commands/ ← Fonctions appelées depuis Vue - └── ... + └── commands.rs ← 16 Tauri commands (fichier unique, délègue à rust-backend) ``` ### Ce code s'exécute sur PLUSIEURS PLATEFORMES : @@ -144,7 +142,7 @@ Résultat : 1. **Vous codez UNE SEULE application Vue** : ```vue - + diff --git a/lyremember-app/src/views/StatsView.vue b/lyremember-app/src/views/StatsView.vue new file mode 100644 index 0000000..09304fc --- /dev/null +++ b/lyremember-app/src/views/StatsView.vue @@ -0,0 +1,294 @@ + + + diff --git a/lyremember-app/src/vite-env.d.ts b/lyremember-app/src/vite-env.d.ts index fc81239..5244053 100644 --- a/lyremember-app/src/vite-env.d.ts +++ b/lyremember-app/src/vite-env.d.ts @@ -2,6 +2,7 @@ declare module "*.vue" { import type { DefineComponent } from "vue"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const component: DefineComponent<{}, {}, any>; export default component; } diff --git a/lyremember-app/vite.config.ts b/lyremember-app/vite.config.ts index 812e61c..1686fc1 100644 --- a/lyremember-app/vite.config.ts +++ b/lyremember-app/vite.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; +import path from "path"; // @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; @@ -8,6 +9,12 @@ const host = process.env.TAURI_DEV_HOST; export default defineConfig(async () => ({ plugins: [vue()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent Vite from obscuring rust errors @@ -29,4 +36,15 @@ export default defineConfig(async () => ({ ignored: ["**/src-tauri/**"], }, }, + + test: { + environment: "happy-dom", + globals: true, + coverage: { + provider: "v8", + reporter: ["text"], + include: ["src/stores/**"], + thresholds: { lines: 80 }, + }, + }, })); diff --git a/rust-backend/Cargo.toml b/rust-backend/Cargo.toml index b02257e..f799f53 100644 --- a/rust-backend/Cargo.toml +++ b/rust-backend/Cargo.toml @@ -23,9 +23,6 @@ tokio = { version = "1", features = ["full"] } bcrypt = "0.15" jsonwebtoken = "9.2" -# Python bridge for phonetics -pyo3 = { version = "0.20", features = ["auto-initialize"] } - # Utilities uuid = { version = "1.0", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } diff --git a/rust-backend/src/error.rs b/rust-backend/src/error.rs index a5301fd..42b1ee3 100644 --- a/rust-backend/src/error.rs +++ b/rust-backend/src/error.rs @@ -41,9 +41,3 @@ pub enum Error { #[error("{0}")] Other(String), } - -impl From for Error { - fn from(err: pyo3::PyErr) -> Self { - Error::Python(err.to_string()) - } -} diff --git a/rust-backend/src/services/auth.rs b/rust-backend/src/services/auth.rs index 14dfaec..1189078 100644 --- a/rust-backend/src/services/auth.rs +++ b/rust-backend/src/services/auth.rs @@ -135,6 +135,20 @@ pub fn verify_token(token: &str) -> Result { Ok(token_data.claims.sub) } +/// Update genius token for a user +pub fn update_genius_token(conn: &Connection, user_id: &str, token: Option<&str>) -> Result<()> { + let updated = conn.execute( + "UPDATE users SET genius_token = ?1 WHERE id = ?2", + rusqlite::params![token, user_id], + )?; + + if updated == 0 { + return Err(Error::NotFound("User not found".to_string())); + } + + Ok(()) +} + /// Get user by ID pub fn get_user_by_id(conn: &Connection, user_id: &str) -> Result { let mut stmt = conn.prepare( diff --git a/rust-backend/src/services/phonetic.rs b/rust-backend/src/services/phonetic.rs index 6eceda6..369ad74 100644 --- a/rust-backend/src/services/phonetic.rs +++ b/rust-backend/src/services/phonetic.rs @@ -1,137 +1,25 @@ -//! Phonetic generation service using PyO3 -//! -//! Bridges to Python libraries for romanization: -//! - Japanese: pykakasi (kanji → romaji) -//! - Korean: hangul-romanize (hangul → latin) -//! - French/English: epitran (text → IPA) - -use crate::{Error, Result}; -use pyo3::prelude::*; -use pyo3::types::PyList; - -/// Generate phonetic representation for lyrics -/// -/// # Arguments -/// * `text` - Lines of lyrics to convert -/// * `language` - Language code: 'jp', 'kr', 'fr', 'en' -/// -/// # Returns -/// Vector of phonetic representations matching input lines -pub fn generate_phonetic(text: Vec, language: &str) -> Result> { - match language { - "jp" => japanese_to_romaji(text), - "kr" => korean_to_roman(text), - "fr" => to_ipa(text, "fra-Latn"), - "en" => to_ipa(text, "eng-Latn"), - _ => Ok(text), // Return original text for unsupported languages - } -} - -/// Convert Japanese (including kanji) to romaji using pykakasi -fn japanese_to_romaji(text: Vec) -> Result> { - Python::with_gil(|py| { - // Import pykakasi module - let kakasi_module = py.import("pykakasi") - .map_err(|e| Error::Phonetic(format!("Failed to import pykakasi: {}", e)))?; - - // Create kakasi instance - let kakasi_class = kakasi_module.getattr("kakasi") - .map_err(|e| Error::Phonetic(format!("Failed to get kakasi class: {}", e)))?; - let kakasi = kakasi_class.call0() - .map_err(|e| Error::Phonetic(format!("Failed to create kakasi instance: {}", e)))?; - - let mut result = Vec::new(); - - for line in text { - // Convert each line - let converted = kakasi - .call_method1("convert", (line,)) - .map_err(|e| Error::Phonetic(format!("kakasi.convert failed: {}", e)))?; - - // Extract romaji from each segment - let py_list: &PyList = converted.downcast() - .map_err(|e| Error::Phonetic(format!("Failed to downcast to PyList: {}", e)))?; - - let mut romaji_line = String::new(); - for item in py_list { - let dict = item.downcast::() - .map_err(|e| Error::Phonetic(format!("Failed to downcast to PyDict: {}", e)))?; - - // Try 'hepburn' first, fallback to 'hira' if not available - let romaji = if let Some(hepburn) = dict.get_item("hepburn")? { - hepburn.extract::()? - } else if let Some(hira) = dict.get_item("hira")? { - hira.extract::()? - } else { - dict.get_item("orig")? - .ok_or_else(|| Error::Phonetic("No romaji field found".to_string()))? - .extract::()? - }; - - romaji_line.push_str(&romaji); - } - - result.push(romaji_line); - } - - Ok(result) - }) -} - -/// Convert Korean hangul to romanized latin using hangul-romanize -fn korean_to_roman(text: Vec) -> Result> { - Python::with_gil(|py| { - // Import hangul_romanize module - let module = py.import("hangul_romanize") - .map_err(|e| Error::Phonetic(format!("Failed to import hangul_romanize: {}", e)))?; - - let transliter_class = module.getattr("Transliter") - .map_err(|e| Error::Phonetic(format!("Failed to get Transliter class: {}", e)))?; - - let mut result = Vec::new(); - - for line in text { - let romanized = transliter_class - .call1((line,)) - .map_err(|e| Error::Phonetic(format!("Transliter failed: {}", e)))? - .extract::() - .map_err(|e| Error::Phonetic(format!("Failed to extract string: {}", e)))?; - - result.push(romanized); - } - - Ok(result) - }) -} - -/// Convert text to IPA (International Phonetic Alphabet) using epitran -fn to_ipa(text: Vec, lang_code: &str) -> Result> { - Python::with_gil(|py| { - // Import epitran module - let epitran_module = py.import("epitran") - .map_err(|e| Error::Phonetic(format!("Failed to import epitran: {}", e)))?; - - let epitran_class = epitran_module.getattr("Epitran") - .map_err(|e| Error::Phonetic(format!("Failed to get Epitran class: {}", e)))?; - - // Create Epitran instance for the language - let epitran = epitran_class.call1((lang_code,)) - .map_err(|e| Error::Phonetic(format!("Failed to create Epitran instance: {}", e)))?; - - let mut result = Vec::new(); - - for line in text { - let ipa = epitran - .call_method1("transliterate", (line,)) - .map_err(|e| Error::Phonetic(format!("transliterate failed: {}", e)))? - .extract::() - .map_err(|e| Error::Phonetic(format!("Failed to extract string: {}", e)))?; - - result.push(ipa); - } - - Ok(result) - }) +//! Phonetic generation service +//! +//! NOTE: The full implementation (PyO3 + pykakasi / hangul-romanize / epitran) +//! has been removed because PyO3 links `python3.dll` into the binary at load +//! time — the app crashes on startup on any machine without Python installed. +//! +//! Current behaviour: graceful stub — returns the original text unchanged so +//! every feature that depends on phonetics continues to work (empty phonetics +//! are displayed in the UI, which is better than a crash). +//! +//! Future path: implement via a Python subprocess (no static DLL dependency) +//! or a pure-Rust romanisation crate (kakasi-rs, hangul, etc.). + +use crate::Result; + +/// Generate phonetic representation for lyrics. +/// +/// Stub implementation — returns the original lines unchanged. +/// The UI will display the original text in the phonetics column. +pub fn generate_phonetic(text: Vec, _language: &str) -> Result> { + // Return original text as-is; no Python dependency required. + Ok(text) } #[cfg(test)] @@ -139,39 +27,15 @@ mod tests { use super::*; #[test] - fn test_generate_phonetic_unsupported_language() { - let text = vec!["Hello".to_string(), "World".to_string()]; - let result = generate_phonetic(text.clone(), "de").unwrap(); - assert_eq!(result, text); // Should return original for unsupported language - } - - // Note: The following tests require Python packages to be installed: - // pip install pykakasi hangul-romanize epitran - - #[test] - #[ignore] // Ignore by default, run with `cargo test -- --ignored` if Python deps are installed - fn test_japanese_to_romaji() { - let text = vec!["こんにちは".to_string()]; - let result = japanese_to_romaji(text).unwrap(); - assert!(!result[0].is_empty()); - println!("Japanese romaji: {:?}", result); - } - - #[test] - #[ignore] - fn test_korean_to_roman() { - let text = vec!["안녕하세요".to_string()]; - let result = korean_to_roman(text).unwrap(); - assert!(!result[0].is_empty()); - println!("Korean roman: {:?}", result); + fn test_generate_phonetic_returns_original() { + let text = vec!["こんにちは".to_string(), "안녕하세요".to_string()]; + let result = generate_phonetic(text.clone(), "jp").unwrap(); + assert_eq!(result, text); } #[test] - #[ignore] - fn test_to_ipa_english() { - let text = vec!["hello world".to_string()]; - let result = to_ipa(text, "eng-Latn").unwrap(); - assert!(!result[0].is_empty()); - println!("English IPA: {:?}", result); + fn test_generate_phonetic_empty_input() { + let result = generate_phonetic(vec![], "kr").unwrap(); + assert!(result.is_empty()); } } diff --git a/rust-backend/src/services/practice.rs b/rust-backend/src/services/practice.rs index 64a8f49..67ddbde 100644 --- a/rust-backend/src/services/practice.rs +++ b/rust-backend/src/services/practice.rs @@ -93,7 +93,7 @@ pub fn get_song_sessions(conn: &Connection, song_id: &str) -> Result (LibreTranslate v1.3+ batch API) +#[derive(Debug, Serialize)] +struct BatchTranslateRequest { + q: Vec, + source: String, + target: String, + format: String, +} + +/// Single-line response from LibreTranslate (non-batch) #[derive(Debug, Deserialize)] struct TranslateResponse { #[serde(rename = "translatedText")] translated_text: String, } +/// One item inside the batch response array +#[derive(Debug, Deserialize)] +struct BatchTranslateResponseItem { + #[serde(rename = "translatedText")] + translated_text: String, +} + /// Translate text using LibreTranslate API -/// +/// /// # Arguments /// * `text` - Lines to translate /// * `source_lang` - Source language code (e.g., "ja", "ko", "fr") /// * `target_lang` - Target language code (e.g., "en") -/// +/// /// # Returns -/// Vector of translated lines +/// Vector of translated lines (same length and order as input) pub fn translate_text( text: Vec, source_lang: &str, @@ -38,44 +56,135 @@ pub fn translate_text( translate_with_libretranslate(text, source_lang, target_lang, LIBRETRANSLATE_URL) } -/// Translate text using LibreTranslate API (with custom endpoint) +/// Build the batch request body, separating empty lines out so they are +/// preserved in position but never sent over the wire. +/// +/// Returns: +/// - `non_empty_indices`: positions in the original `text` slice that have content +/// - `non_empty_lines`: the corresponding lines to include in the batch request +fn split_empty_lines(text: &[String]) -> (Vec, Vec) { + let mut non_empty_indices = Vec::new(); + let mut non_empty_lines = Vec::new(); + for (i, line) in text.iter().enumerate() { + if !line.trim().is_empty() { + non_empty_indices.push(i); + non_empty_lines.push(line.clone()); + } + } + (non_empty_indices, non_empty_lines) +} + +/// Reassemble translations from the batch result back into a full Vec +/// where empty lines keep their empty string position. +fn reassemble_translations( + total: usize, + non_empty_indices: &[usize], + batch_results: Vec, +) -> Vec { + let mut output = vec![String::new(); total]; + for (pos, idx) in non_empty_indices.iter().enumerate() { + output[*idx] = batch_results[pos].clone(); + } + output +} + +/// Translate text using LibreTranslate API (with custom endpoint, testable). +/// +/// Strategy: +/// 1. Attempt a single batch request (`q: Vec`). +/// 2. If the API rejects batch (non-2xx or parse error), fall back to +/// sequential per-line requests with retry logic. fn translate_with_libretranslate( text: Vec, source_lang: &str, target_lang: &str, api_url: &str, ) -> Result> { + if text.is_empty() { + return Ok(Vec::new()); + } + let client = reqwest::blocking::Client::builder() .timeout(REQUEST_TIMEOUT) .build() .map_err(|e| Error::Translation(format!("Failed to create HTTP client: {}", e)))?; - - let mut translations = Vec::new(); - - for line in text { - if line.trim().is_empty() { - translations.push(String::new()); - continue; + + let (non_empty_indices, non_empty_lines) = split_empty_lines(&text); + + if non_empty_lines.is_empty() { + // All lines are empty – return them as-is + return Ok(vec![String::new(); text.len()]); + } + + // --- Attempt 1: batch request --- + let batch_request = BatchTranslateRequest { + q: non_empty_lines.clone(), + source: source_lang.to_string(), + target: target_lang.to_string(), + format: "text".to_string(), + }; + + match client.post(api_url).json(&batch_request).send() { + Ok(response) if response.status().is_success() => { + // Try to parse as a batch response (array of objects) + let body = response + .text() + .map_err(|e| Error::Translation(format!("Failed to read response body: {}", e)))?; + + if let Ok(items) = serde_json::from_str::>(&body) { + if items.len() == non_empty_lines.len() { + let batch_results: Vec = + items.into_iter().map(|item| item.translated_text).collect(); + return Ok(reassemble_translations( + text.len(), + &non_empty_indices, + batch_results, + )); + } + // Unexpected count – fall through to per-line fallback + } + // Body was not a batch array (e.g. single-item object) – fall through } - + Ok(response) => { + let status = response.status(); + // 4xx/5xx but not 429: propagate immediately (likely config issue) + if status.as_u16() != 429 && status.as_u16() / 100 == 4 { + let body = response.text().unwrap_or_default(); + return Err(Error::Translation(format!( + "Batch API error {}: {}", + status, body + ))); + } + // 429 or 5xx: fall through to per-line fallback + } + Err(_) => { + // Network error on batch attempt: fall through to per-line fallback + } + } + + // --- Fallback: sequential per-line requests with retry --- + let mut translations = vec![String::new(); text.len()]; + + for (pos, idx) in non_empty_indices.iter().enumerate() { + let line = &non_empty_lines[pos]; + let request = TranslateRequest { q: line.clone(), source: source_lang.to_string(), target: target_lang.to_string(), format: "text".to_string(), }; - - // Retry logic for rate limiting + let mut retries = 3; let mut last_error = None; - + while retries > 0 { match client.post(api_url).json(&request).send() { Ok(response) => { if response.status().is_success() { match response.json::() { Ok(data) => { - translations.push(data.translated_text); + translations[*idx] = data.translated_text; break; } Err(e) => { @@ -89,9 +198,8 @@ fn translate_with_libretranslate( } else { let status = response.status(); let body = response.text().unwrap_or_default(); - + if status.as_u16() == 429 { - // Rate limited, wait and retry std::thread::sleep(Duration::from_secs(2)); retries -= 1; last_error = Some(Error::Translation("Rate limited".to_string())); @@ -112,92 +220,169 @@ fn translate_with_libretranslate( } } } - + if retries == 0 { return Err(last_error.unwrap_or_else(|| { Error::Translation("Translation failed after retries".to_string()) })); } } - + Ok(translations) } -/// Batch translate with delay between requests to avoid rate limiting +/// Batch translate with delay between requests to avoid rate limiting. +/// +/// Delegates to `translate_with_libretranslate` which now uses a single +/// batch request. The `delay_ms` parameter is retained for API compatibility +/// and is applied only when the per-line fallback path is activated internally. pub fn translate_batch( text: Vec, source_lang: &str, target_lang: &str, - delay_ms: u64, + _delay_ms: u64, ) -> Result> { - let client = reqwest::blocking::Client::builder() - .timeout(REQUEST_TIMEOUT) - .build() - .map_err(|e| Error::Translation(format!("Failed to create HTTP client: {}", e)))?; - - let mut translations = Vec::new(); - - for (i, line) in text.iter().enumerate() { - if i > 0 && delay_ms > 0 { - std::thread::sleep(Duration::from_millis(delay_ms)); - } - - if line.trim().is_empty() { - translations.push(String::new()); - continue; - } - - let request = TranslateRequest { - q: line.clone(), - source: source_lang.to_string(), - target: target_lang.to_string(), - format: "text".to_string(), - }; - - let response = client - .post(LIBRETRANSLATE_URL) - .json(&request) - .send() - .map_err(|e| Error::Translation(format!("Request failed: {}", e)))?; - - if !response.status().is_success() { - let status = response.status(); - let body = response.text().unwrap_or_default(); - return Err(Error::Translation(format!( - "API error {}: {}", - status, body - ))); - } - - let data = response - .json::() - .map_err(|e| Error::Translation(format!("Failed to parse response: {}", e)))?; - - translations.push(data.translated_text); - } - - Ok(translations) + translate_with_libretranslate(text, source_lang, target_lang, LIBRETRANSLATE_URL) } +// ─── Tests ──────────────────────────────────────────────────────────────────── + #[cfg(test)] mod tests { use super::*; + // ── Unit tests: pure logic, no network ──────────────────────────────────── + + #[test] + fn test_split_empty_lines_preserves_positions() { + let text = vec![ + "Hello".to_string(), + String::new(), + "World".to_string(), + " ".to_string(), // whitespace-only → treated as empty + "Rust".to_string(), + ]; + + let (indices, lines) = split_empty_lines(&text); + + assert_eq!(indices, vec![0, 2, 4]); + assert_eq!(lines, vec!["Hello", "World", "Rust"]); + } + + #[test] + fn test_split_empty_lines_all_empty() { + let text = vec![String::new(), " ".to_string(), "\t".to_string()]; + let (indices, lines) = split_empty_lines(&text); + assert!(indices.is_empty()); + assert!(lines.is_empty()); + } + + #[test] + fn test_split_empty_lines_none_empty() { + let text = vec!["a".to_string(), "b".to_string()]; + let (indices, lines) = split_empty_lines(&text); + assert_eq!(indices, vec![0, 1]); + assert_eq!(lines, vec!["a", "b"]); + } + + #[test] + fn test_reassemble_translations_preserves_empty_slots() { + let total = 5; + let non_empty_indices = vec![0, 2, 4]; + let batch_results = vec!["Bonjour".to_string(), "Monde".to_string(), "Rouille".to_string()]; + + let output = reassemble_translations(total, &non_empty_indices, batch_results); + + assert_eq!(output.len(), 5); + assert_eq!(output[0], "Bonjour"); + assert_eq!(output[1], ""); // was empty in input + assert_eq!(output[2], "Monde"); + assert_eq!(output[3], ""); // was empty in input + assert_eq!(output[4], "Rouille"); + } + + #[test] + fn test_reassemble_translations_all_present() { + let total = 3; + let non_empty_indices = vec![0, 1, 2]; + let batch_results = vec!["A".to_string(), "B".to_string(), "C".to_string()]; + + let output = reassemble_translations(total, &non_empty_indices, batch_results); + assert_eq!(output, vec!["A", "B", "C"]); + } + #[test] - fn test_translate_empty_lines() { - // Mock test - actual API calls would require network - let text = vec![String::new(), String::new()]; - // Would need to mock the HTTP client for proper testing - // For now, just verify the function signature - assert_eq!(text.len(), 2); + fn test_batch_request_serialises_q_as_array() { + // Verify that BatchTranslateRequest serialises `q` as a JSON array + // (not a string), which is what LibreTranslate batch API expects. + let req = BatchTranslateRequest { + q: vec!["line 1".to_string(), "line 2".to_string()], + source: "ja".to_string(), + target: "en".to_string(), + format: "text".to_string(), + }; + + let json = serde_json::to_value(&req).expect("serialisation must succeed"); + + assert!( + json["q"].is_array(), + "q must be serialised as a JSON array, got: {}", + json["q"] + ); + assert_eq!(json["q"][0], "line 1"); + assert_eq!(json["q"][1], "line 2"); + assert_eq!(json["source"], "ja"); + assert_eq!(json["target"], "en"); + assert_eq!(json["format"], "text"); + } + + #[test] + fn test_batch_response_deserialises_array() { + // Verify that the batch response JSON array is correctly deserialised. + let json = r#"[ + {"translatedText": "translated 1"}, + {"translatedText": "translated 2"}, + {"translatedText": "translated 3"} + ]"#; + + let items: Vec = + serde_json::from_str(json).expect("deserialisation must succeed"); + + assert_eq!(items.len(), 3); + assert_eq!(items[0].translated_text, "translated 1"); + assert_eq!(items[1].translated_text, "translated 2"); + assert_eq!(items[2].translated_text, "translated 3"); } #[test] - #[ignore] // Ignore by default as it requires network access + fn test_translate_text_returns_empty_for_empty_input() { + // translate_text with no lines should return an empty Vec without + // making any network request (short-circuit at the top of the function). + let result = translate_text(vec![], "en", "fr"); + assert!(result.is_ok()); + assert!(result.unwrap().is_empty()); + } + + #[test] + fn test_translate_text_all_empty_lines() { + // All-empty input: should return same number of empty strings, no + // network request attempted. + let input = vec![String::new(), String::new(), String::new()]; + let result = translate_text(input, "ja", "en"); + assert!(result.is_ok()); + let out = result.unwrap(); + assert_eq!(out.len(), 3); + assert!(out.iter().all(|s| s.is_empty())); + } + + // ── Integration-style tests (require network, ignored by default) ───────── + + #[test] + #[ignore] fn test_translate_text_real_api() { let text = vec!["Hello".to_string(), "World".to_string()]; let result = translate_text(text, "en", "fr"); - + match result { Ok(translations) => { assert_eq!(translations.len(), 2); @@ -208,4 +393,22 @@ mod tests { } } } + + #[test] + #[ignore] + fn test_translate_batch_real_api() { + let text = vec!["Hello".to_string(), String::new(), "World".to_string()]; + let result = translate_batch(text, "en", "fr", 0); + + match result { + Ok(translations) => { + assert_eq!(translations.len(), 3); + assert!(translations[1].is_empty(), "empty input line must stay empty"); + println!("Batch translations: {:?}", translations); + } + Err(e) => { + eprintln!("Batch translation failed (may be expected if offline): {}", e); + } + } + } }