Aller au contenu

Introduction à l'architecture

Version de l’application : v0.1.1 (fork : DarrellThomas/en-parlant) Stack : Tauri v2 (Rust) + React 19 (TypeScript) + Vite


Tauri est un framework pour créer des applications de bureau. Au lieu d’embarquer un navigateur complet comme le fait Electron, Tauri utilise la webview intégrée au système d’exploitation pour l’interface et un processus Rust pour le backend. Le résultat est un binaire compact et rapide.

Les deux parties communiquent via IPC (communication inter-processus) :

+---------------------------+ IPC +---------------------------+
| Rust Backend | <--------------> | React/TS Frontend |
| | (commands + | |
| - Chess engines (UCI) | events) | - Chessboard UI |
| - SQLite database | | - Analysis panels |
| - File I/O | | - Settings |
| - PGN parsing | | - Game tree navigation |
| - Position search index | | - TTS narration |
+---------------------------+ +---------------------------+
src-tauri/src/ src/

Rust gère tout ce qui nécessite de la performance ou un accès au système.

Enregistre environ 50 commandes que le frontend peut appeler, initialise les plugins (système de fichiers, dialogues, HTTP, shell, journalisation, mises à jour) et lance la fenêtre de l’application.

Les commandes sont définies à l’aide d’une macro :

#[tauri::command]
async fn get_best_moves(id: String, engine: String, ...) -> Result<...> {
// spawn UCI engine, return analysis
}

Le crate specta génère automatiquement les définitions de types TypeScript à partir de ces fonctions Rust, offrant ainsi au frontend une sécurité de typage complète sans aucun effort manuel.

ModuleRôle
db/mod.rsBase de données SQLite via Diesel ORM — requêtes de parties, statistiques de joueurs, imports, recherche de positions
game.rsMoteur de jeu en direct — gère les parties moteur-contre-humain et moteur-contre-moteur, contrôles de temps, validation des coups
chess.rsAnalyse par moteur — lance les moteurs UCI, retransmet les meilleurs coups au frontend via des événements
engine/Implémentation du protocole UCI — lancement de processus, pipes stdin/stdout, support multi-PV
pgn.rsLecture/écriture/tokenisation de fichiers PGN
opening.rsRecherche du nom d’ouverture à partir du FEN (données binaires intégrées à l’application)
puzzle.rsBase de données de puzzles Lichess — accès aléatoire par mémoire mappée
fs.rsTéléchargement de fichiers avec reprise, configuration des permissions d’exécution
sound.rsServeur HTTP local pour le streaming audio (contournement audio sous Linux)
tts.rsTTS système via speech-dispatcher (Linux) / API vocales natives de l’OS, plus gestion du serveur KittenTTS
oauth.rsFlux OAuth2 pour la liaison des comptes Lichess/Chess.com
  • Asynchrone partout : runtime Tokio, E/S non bloquantes
  • État concurrent : DashMap (HashMap concurrent) pour les processus de moteurs, connexions à la base de données, caches
  • Pool de connexions : r2d2 gère les pools de connexions SQLite
  • Recherche par mémoire mappée : recherche de positions via un index binaire mmap’d pour des résultats instantanés
  • Flux d’événements : Rust émet des événements (meilleurs coups, tic-tac de l’horloge, fin de partie) que React écoute en temps réel

vite.config.ts configure :

  • Plugin React avec le compilateur Babel
  • Plugin TanStack Router — génère automatiquement l’arbre de routes à partir du dossier routes/
  • Vanilla Extract — CSS-in-JS sans runtime
  • Alias de chemin : @ correspond à ./src
  • Serveur de développement sur le port 1420

Flux de build :

pnpm dev → Vite on :1420 + Tauri opens webview pointing to it
pnpm build → tsc (typecheck) → vite build (bundle to dist/) → tauri build (native binary)

Le composant racine :

  • Initialise les plugins Tauri (log, process, updater)
  • Charge les préférences utilisateur depuis des atomes persistants
  • Configure le thème de l’UI Mantine
  • Enregistre le routeur
  • Vérifie les mises à jour de l’application

Atomes Jotai (src/state/atoms.ts) — état réactif léger :

CatégorieExemples
OngletstabsAtom, activeTabAtom (interface multi-documents)
RépertoiresstoredDocumentDirAtom, storedDatabasesDirAtom
Préférences d’interfaceprimaryColorAtom, fontSizeAtom, pieceSetAtom
MoteurengineMovesFamily, engineProgressFamily (par onglet via atomFamily)
TTSttsEnabledAtom, ttsProviderAtom, ttsVoiceIdAtom, ttsVolumeAtom, ttsSpeedAtom, ttsLanguageAtom

Les atomes utilisant atomWithStorage() sont automatiquement persistés dans le localStorage.

Stores Zustand pour l’état complexe du domaine :

  • src/state/store/tree.ts — navigation dans l’arbre de partie, ramifications de coups, annotations, commentaires. Utilise Immer pour les mises à jour immuables.
  • src/state/store/database.ts — filtres de la vue base de données, partie sélectionnée, pagination

Routage basé sur les fichiers dans src/routes/ :

routes/
__root.tsx # Root layout (AppShell, menu bar)
index.tsx # Home/dashboard
databases/ # Database browsing
accounts.tsx # Lichess/Chess.com accounts
settings.tsx # App preferences
engines.tsx # Engine management
GroupeFonction
boards/Échiquier (chessground), saisie de coups, barre d’évaluation, affichage de l’analyse, modale de promotion, dessin de flèches
panels/Panneaux latéraux : analyse du moteur (BestMoves), recherche de position dans la base de données, édition d’annotations, informations sur la partie, mode d’entraînement
databases/Interface base de données : tableau de parties, tableau de joueurs, fiches détaillées, filtrage
settings/Formulaires de préférences, chemins des moteurs, paramètres TTS
home/Cartes de comptes, interface d’import
common/Éléments partagés : TreeStateContext, affichage du matériel, icône de haut-parleur pour les commentaires
tabs/Barre multi-onglets

Specta génère les bindings TypeScript dans src/bindings/generated.ts :

// Auto-generated from Rust #[tauri::command] functions
export const commands = {
async getBestMoves(id, engine, tab, goMode, options) {
return await TAURI_INVOKE("get_best_moves", { id, engine, tab, goMode, options });
},
// ~50 more commands...
}

Les composants React les appellent comme des fonctions asynchrones classiques :

import { commands } from "@/bindings";
const result = await commands.getBestMoves(id, engine, tab, goMode, options);

Pour les données en temps réel (analyse du moteur, tic-tac de l’horloge, coups de jeu) :

Rust: app.emit("best_moves_payload", BestMovesPayload { depth: 24, ... })
React: listen("best_moves_payload", (event) => updateBestMoves(event.payload))

L’application utilise plusieurs plugins officiels pour l’accès au système :

PluginFonction
@tauri-apps/plugin-fsLecture/écriture de fichiers
@tauri-apps/plugin-dialogSélecteurs de fichiers, boîtes de dialogue
@tauri-apps/plugin-httpClient HTTP (téléchargement de moteurs, TTS cloud)
@tauri-apps/plugin-shellExécution de moteurs UCI
@tauri-apps/plugin-updaterVérification automatique des mises à jour
@tauri-apps/plugin-logJournalisation structurée
@tauri-apps/plugin-osDétection CPU/RAM

En Parlant~ peut lire à voix haute les coups d’échecs et les commentaires lorsque vous parcourez une partie. Cette section explique comment le système TTS est construit — le pipeline de prétraitement, l’architecture des fournisseurs et la stratégie de mise en cache. Pour les instructions de configuration, consultez les guides TTS dans le menu TTS.

La synthèse vocale convertit du texte écrit en audio parlé. Les systèmes TTS modernes reposent sur des réseaux de neurones profonds entraînés sur des milliers d’heures de parole humaine. Le modèle apprend la relation entre le texte (lettres, mots, ponctuation) et les caractéristiques acoustiques de la parole (hauteur, rythme, accentuation, pauses respiratoires). Au moment de l’inférence, vous envoyez du texte et récupérez une forme d’onde audio.

Il existe deux grandes approches :

  • TTS cloud — le texte est envoyé à un serveur distant (Google, ElevenLabs, etc.), qui exécute un grand réseau de neurones sur du matériel GPU et renvoie l’audio. Qualité excellente, mais nécessite une connexion internet et engendre des coûts par requête (bien que la plupart des fournisseurs proposent des paliers gratuits).

  • TTS local — un modèle s’exécute directement sur votre machine. Pas besoin d’internet, pas de coût par requête, et votre texte ne quitte jamais votre ordinateur. Les modèles open source récents (comme Kokoro et Piper) ont considérablement réduit l’écart de qualité.

Si vous êtes curieux de savoir comment les modèles TTS fonctionnent en détail, HuggingFace (huggingface.co) héberge des centaines de modèles open source de synthèse vocale que vous pouvez explorer, télécharger et exécuter localement. Recherchez « text-to-speech » pour trouver des modèles allant d’options légères adaptées au CPU à des modèles de recherche à la pointe de l’état de l’art.

L’implémentation principale du TTS se trouve dans src/utils/tts.ts. Elle est conçue autour d’une interface publique unique (speakText()) avec des backends interchangeables. Le reste de l’application ne sait ni ne se soucie du fournisseur actif — il appelle simplement speakText() et l’audio est produit.

Cinq fournisseurs sont pris en charge :

FournisseurTypeBackend
ElevenLabsCloudVoix neuronales via API REST. Retourne du MP3.
Google Cloud TTSCloudVoix WaveNet via API REST. Retourne du MP3 encodé en base64.
KittenTTSLocalServeur TTS intégré, démarré automatiquement par le backend Rust. Communique via HTTP sur localhost.
OpenTTSLocalServeur TTS auto-hébergé. Supporte de nombreux moteurs (espeak, MaryTTS, Piper, etc.).
System TTSLocalMoteur vocal natif de l’OS via commandes Rust/Tauri (speech-dispatcher sous Linux, SAPI sous Windows, AVSpeechSynthesizer sous macOS).

La sélection du fournisseur est stockée dans un seul atome Jotai (ttsProviderAtom). Changer de fournisseur est instantané — modifiez l’atome, et le prochain appel à speakText() sera dirigé vers le nouveau backend.

Le défi : la notation échiquéenne n’est pas de l’anglais

Section intitulée « Le défi : la notation échiquéenne n’est pas de l’anglais »

Les coups d’échecs sont écrits en Notation Algébrique Standard (SAN) : Nf3, Bxe5+, O-O-O, e8=Q#. Si vous envoyez cela directement à un moteur TTS, vous obtenez du charabia — il pourrait essayer de prononcer « Nf3 » comme un mot, ou lire « O-O-O » comme « oh oh oh ».

La solution est un pipeline de prétraitement qui traduit la notation échiquéenne en langage naturel avant qu’elle n’atteigne le moteur TTS :

SAN Input → Preprocessing → Spoken Output
─────────────────────────────────────────────────────
"Nf3" → sanToSpoken() → "Knight f3"
"Bxe5+" → sanToSpoken() → "Bishop takes e5, check"
"O-O-O" → sanToSpoken() → "castles queenside"
"e8=Q#" → sanToSpoken() → "e8 promotes to Queen, checkmate"

La fonction sanToSpoken() utilise la correspondance par expressions régulières pour décomposer toute chaîne SAN en ses composants (pièce, désambiguïsation, capture, destination, promotion, échec/mat) et les réassemble en langage naturel à partir d’un tableau de vocabulaire.

Le vocabulaire échiquéen est traduit dans de nombreuses langues (anglais, français, espagnol, allemand, japonais, russe, chinois, coréen, et bien d’autres). La table CHESS_VOCAB associe chaque terme :

English: "Knight takes e5, check"
French: "Cavalier prend e5, échec"
German: "Springer schlägt e5, Schach"
Japanese: "ナイト テイクス e5, チェック"
Russian: "Конь берёт e5, шах"

Le paramètre de langue détermine quel tableau de vocabulaire est utilisé pour le prétraitement et quelle voix/accent le moteur TTS utilise pour la synthèse.

Les annotations de parties contiennent souvent des balises spécifiques au PGN qui sonneraient affreusement si elles étaient lues à voix haute :

Raw comment: "BLUNDER. 7.Nf3 was better [%eval -2.3] [%cal Gg1f3]"
After cleaning: "7, Knight f3 was better"

La fonction cleanCommentForTTS() :

  1. Supprime les balises PGN : [%eval ...], [%csl ...], [%cal ...], [%clk ...]
  2. Retire les mots d’annotation en double (quand « ?? » a déjà dit « Blunder »)
  3. Développe le SAN intégré dans la prose : "7.Nf3 controls e5""7, Knight f3 controls e5"
  4. Corrige les termes échiquéens que les moteurs TTS prononcent mal (par ex. « en prise » → « on preez »)
  5. Développe les abréviations de pièces dans la prose : "R vs R""Rook versus Rook"

Lorsque vous avancez vers un nouveau coup, buildNarration() assemble le texte parlé complet à partir de trois sources :

Move: "12, Knight f3, check." ← from sanToSpoken()
Annotation: "Good move." ← from annotation symbol (!)
Comment: "Developing with tempo." ← from cleanCommentForTTS()
Full narration: "12, Knight f3, check. Good move. Developing with tempo."

Le double espace entre les parties donne aux moteurs TTS une pause respiratoire naturelle.

Les appels TTS cloud coûtent de l’argent et prennent du temps (~200-500 ms aller-retour). Pour éviter de re-télécharger le même audio, chaque clip généré est mis en cache en mémoire sous forme de blob URL :

Cache key: "elevenlabs:pNInz6obpgDQGcFmaJgB:en:12, Knight f3, check."
Cache value: blob:http://localhost/abc123 (the MP3 audio in browser memory)

En cas de cache hit, la lecture est instantanée. Le cache est indexé par provider:voice:language:text, de sorte que changer de voix ou de langue crée des entrées séparées.

Pour les parties avec de nombreuses annotations, vous pouvez pré-mettre en cache l’ensemble de l’arbre de jeu en arrière-plan. L’application parcourt chaque nœud, construit le texte de narration et lance des appels API séquentiels pour remplir le cache avant que vous ne commenciez à naviguer.

La navigation rapide avec les touches fléchées crée un problème : si l’utilisateur avance de 5 coups rapidement, on ne veut pas que 5 clips audio se chevauchent. La solution est un compteur de génération :

const thisGeneration = ++requestGeneration;
// ... fetch audio ...
if (thisGeneration !== requestGeneration) return; // stale — discard

Chaque nouvel appel à speakText() incrémente le compteur et annule toute requête HTTP en cours via AbortController. Lorsque l’audio arrive, il vérifie si sa génération est toujours la plus récente. Si l’utilisateur est déjà passé à autre chose, la réponse est silencieusement ignorée. Cela produit un audio propre, sans accroc, même en cliquant rapidement à travers les coups.

Points d’intégration du TTS dans l’application

Section intitulée « Points d’intégration du TTS dans l’application »

Les points d’intégration sont minimaux :

FichierCe qui se passe
src/state/store/tree.tsChaque fonction de navigation (goToNext, goToPrevious, etc.) appelle stopSpeaking(). Lorsque la narration automatique est activée, goToNext appelle également speakMoveNarration().
src/components/common/Comment.tsxUne icône de haut-parleur à côté de chaque commentaire permet de déclencher manuellement le TTS pour ce commentaire.
src/components/settings/TTSSettings.tsxInterface de paramètres pour choisir le fournisseur, la voix, la langue, le volume, la vitesse et saisir les clés API.

Lorsque le TTS est désactivé, aucun de ce code ne s’exécute. L’application se comporte de manière identique à En Croissant en amont.


User clicks "Analyze"
→ React calls commands.getBestMoves(position, engine, settings)
→ Rust spawns UCI engine process, sends position via stdin
→ Engine writes "info depth 18 score cp 45 pv e2e4 ..." to stdout
→ Rust parses UCI output, emits BestMovesPayload event
→ React's EvalListener receives event, updates atoms
→ UI re-renders: eval bar moves, best move arrows appear
→ User clicks "Stop" → commands.stopEngine() → Rust sets AtomicBool flag
User reaches a position on the board
→ React calls commands.searchPosition(fen, gameQuery)
→ Rust queries memory-mapped binary search index
→ Returns: PositionStats (wins/losses/draws) + matching games
→ React renders DatabasePanel with results table
User steps forward with arrow key
→ tree.ts calls stopSpeaking(), then checks isAutoNarrateEnabled()
→ Calls speakMoveNarration(san, comment, annotations, halfMoves)
→ buildNarration() assembles text:
sanToSpoken("Nf3+") → "Knight f3, check"
annotationsToSpoken(["!"]) → "Good move."
cleanCommentForTTS(comment) → strips [%eval], expands inline SAN
→ speakText() checks audioCache
HIT → play blob URL instantly
MISS → fetch from provider API → cache as blob URL → play
→ HTMLAudioElement.play() with volume and playbackRate from atoms

en-parlant/
├── src-tauri/ # BACKEND RUST
│ ├── src/
│ │ ├── main.rs # Entrée, enregistrement des commandes, plugins
│ │ ├── chess.rs # Analyse par moteur
│ │ ├── game.rs # Gestion de partie en direct
│ │ ├── db/ # Base de données SQLite (plus gros module)
│ │ ├── engine/ # Protocole UCI
│ │ ├── pgn.rs # Parsing PGN
│ │ ├── puzzle.rs # Base de données de puzzles
│ │ ├── opening.rs # Recherche d'ouvertures
│ │ └── tts.rs # TTS système + gestion de KittenTTS
│ ├── Cargo.toml # Dépendances Rust
│ ├── tauri.conf.json # Configuration Tauri
│ └── capabilities/main.json # Permissions de sécurité
├── src/ # FRONTEND REACT/TS
│ ├── App.tsx # Composant racine
│ ├── state/
│ │ ├── atoms.ts # Atomes Jotai (tout l'état de l'application)
│ │ └── store/tree.ts # Arbre de jeu (Zustand + hooks TTS)
│ ├── routes/ # TanStack Router (basé sur les fichiers)
│ ├── components/
│ │ ├── boards/ # Échiquier + analyse
│ │ ├── panels/ # Panneaux latéraux
│ │ ├── databases/ # Interface de navigation de la base de données
│ │ ├── common/ # Affichage des commentaires (avec icône haut-parleur TTS)
│ │ └── settings/ # Préférences, paramètres TTS
│ ├── utils/
│ │ ├── chess.ts # Logique de jeu
│ │ ├── tts.ts # Moteur TTS (SAN-to-spoken, cache, 5 fournisseurs)
│ │ └── treeReducer.ts # Structure de données de l'arbre
│ ├── bindings/ # TS auto-généré depuis Rust
│ └── translation/ # i18n (13 langues)
├── docs/ # Documentation intégrée (affichée dans le menu Aide)
├── vite.config.ts # Configuration du build
└── package.json # Dépendances frontend

  1. Rust fait le gros du travail — moteurs, base de données, E/S fichiers, parsing PGN. React ne touche jamais au système de fichiers et ne lance jamais de processus directement.

  2. Sécurité de typage à travers la frontière — Specta génère les types TypeScript à partir des structures Rust, de sorte que si une commande Rust change de signature, le build TypeScript échoue immédiatement.

  3. Deux systèmes d’état — Jotai pour l’état réactif simple (paramètres, préférences d’interface, état du moteur par onglet), Zustand pour l’état complexe du domaine (arbre de jeu avec ramifications et mises à jour immuables).

  4. Le TTS est un problème de prétraitement — la partie difficile n’est pas d’appeler une API de synthèse vocale, mais de traduire la notation échiquéenne et les balises PGN en un texte propre et naturel dans de nombreuses langues. Les pipelines sanToSpoken() et cleanCommentForTTS() sont là où le vrai travail se fait.

  5. Cinq fournisseurs, une seule interface — que l’audio provienne d’ElevenLabs, Google Cloud, KittenTTS, OpenTTS ou du moteur vocal de votre OS, le reste de l’application ne fait qu’appeler speakText(). La sélection du fournisseur se résume à un simple basculement d’atome.

  6. Le build produit un binaire unique situé à src-tauri/target/release/en-parlant qui embarque le backend Rust + les assets frontend construits par Vite.