Introduzione all'architettura
Versione dell’app: v0.1.1 (fork: DarrellThomas/en-parlant) Stack: Tauri v2 (Rust) + React 19 (TypeScript) + Vite
Cos’è Tauri?
Sezione intitolata “Cos’è Tauri?”Tauri è un framework per la creazione di applicazioni desktop. Invece di includere un browser completo come fa Electron, Tauri utilizza la webview nativa del sistema operativo per l’interfaccia utente e un processo Rust per il backend. Il risultato è un binario piccolo e veloce.
Le due parti comunicano tramite IPC (comunicazione tra processi):
+---------------------------+ 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/Il lato Rust: src-tauri/src/
Sezione intitolata “Il lato Rust: src-tauri/src/”Rust gestisce tutto ciò che deve essere veloce o che necessita di accesso al sistema.
Punto di ingresso: main.rs
Sezione intitolata “Punto di ingresso: main.rs”Registra circa 50 comandi che il frontend può invocare, inizializza i plugin (filesystem, dialog, HTTP, shell, logging, updater) e avvia la finestra dell’applicazione.
I comandi sono definiti tramite una macro:
#[tauri::command]async fn get_best_moves(id: String, engine: String, ...) -> Result<...> { // spawn UCI engine, return analysis}Il crate specta genera automaticamente le definizioni dei tipi TypeScript a partire dalle funzioni Rust, garantendo al frontend piena sicurezza dei tipi senza alcuno sforzo manuale.
Moduli principali
Sezione intitolata “Moduli principali”| Modulo | Funzione |
|---|---|
db/mod.rs | Database SQLite tramite Diesel ORM — query sulle partite, statistiche dei giocatori, importazioni, ricerca per posizione |
game.rs | Motore per le partite in tempo reale — gestisce le partite motore-contro-umano e motore-contro-motore, i controlli del tempo, la validazione delle mosse |
chess.rs | Analisi del motore — avvia motori UCI, trasmette i risultati delle mosse migliori al frontend tramite eventi |
engine/ | Implementazione del protocollo UCI — avvio dei processi, pipe stdin/stdout, supporto multi-PV |
pgn.rs | Lettura/scrittura/tokenizzazione di file PGN |
opening.rs | Ricerca del nome dell’apertura da FEN (dati binari incorporati nell’app) |
puzzle.rs | Database di puzzle Lichess — accesso casuale tramite memory mapping |
fs.rs | Download di file con ripresa, impostazione dei permessi di esecuzione |
sound.rs | Server HTTP locale per lo streaming audio (soluzione alternativa per l’audio su Linux) |
tts.rs | TTS di sistema tramite speech-dispatcher (Linux) / API vocali native del sistema operativo, più gestione del server KittenTTS |
oauth.rs | Flusso OAuth2 per il collegamento degli account Lichess/Chess.com |
Pattern di progettazione
Sezione intitolata “Pattern di progettazione”- Async ovunque: runtime Tokio, I/O non bloccante
- Stato concorrente:
DashMap(HashMap concorrente) per i processi dei motori, le connessioni al database, le cache - Connection pooling: r2d2 gestisce i pool di connessioni SQLite
- Ricerca con memory mapping: ricerca delle posizioni tramite indice binario mappato in memoria per risultati istantanei
- Streaming di eventi: Rust emette eventi (mosse migliori, ticchettio dell’orologio, fine partita) che React ascolta in tempo reale
Il lato React/TypeScript: src/
Sezione intitolata “Il lato React/TypeScript: src/”Pipeline di build: Vite
Sezione intitolata “Pipeline di build: Vite”vite.config.ts configura:
- Plugin React con compilatore Babel
- Plugin TanStack Router — genera automaticamente l’albero delle rotte dalla cartella
routes/ - Vanilla Extract — CSS-in-JS a runtime zero
- Alias dei percorsi:
@corrisponde a./src - Server di sviluppo sulla porta 1420
Flusso di build:
pnpm dev → Vite on :1420 + Tauri opens webview pointing to itpnpm build → tsc (typecheck) → vite build (bundle to dist/) → tauri build (native binary)Punto di ingresso: App.tsx
Sezione intitolata “Punto di ingresso: App.tsx”Il componente radice:
- Inizializza i plugin Tauri (log, process, updater)
- Carica le preferenze utente da atom persistenti
- Configura il tema dell’interfaccia Mantine
- Registra il router
- Verifica la disponibilità di aggiornamenti dell’app
Gestione dello stato
Sezione intitolata “Gestione dello stato”Atom Jotai (src/state/atoms.ts) — stato reattivo leggero:
| Categoria | Esempi |
|---|---|
| Schede | tabsAtom, activeTabAtom (interfaccia multi-documento) |
| Directory | storedDocumentDirAtom, storedDatabasesDirAtom |
| Preferenze UI | primaryColorAtom, fontSizeAtom, pieceSetAtom |
| Motore | engineMovesFamily, engineProgressFamily (per scheda tramite atomFamily) |
| TTS | ttsEnabledAtom, ttsProviderAtom, ttsVoiceIdAtom, ttsVolumeAtom, ttsSpeedAtom, ttsLanguageAtom |
Gli atom con atomWithStorage() persistono automaticamente nel localStorage.
Store Zustand per lo stato di dominio complesso:
src/state/store/tree.ts— navigazione dell’albero di gioco, ramificazione delle mosse, annotazioni, commenti. Utilizza Immer per aggiornamenti immutabili.src/state/store/database.ts— filtri della vista database, partita selezionata, paginazione
Routing: TanStack Router
Sezione intitolata “Routing: TanStack Router”Routing basato su file in 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 managementComponenti: src/components/
Sezione intitolata “Componenti: src/components/”| Gruppo | Scopo |
|---|---|
boards/ | Scacchiera (chessground), inserimento mosse, barra di valutazione, visualizzazione dell’analisi, modale di promozione, disegno delle frecce |
panels/ | Pannelli laterali: analisi del motore (BestMoves), ricerca posizioni nel database, modifica annotazioni, informazioni sulla partita, modalità pratica |
databases/ | Interfaccia database: tabella partite, tabella giocatori, schede dettaglio, filtri |
settings/ | Form delle preferenze, percorsi dei motori, impostazioni TTS |
home/ | Schede account, interfaccia di importazione |
common/ | Condivisi: TreeStateContext, visualizzazione del materiale, icona altoparlante per i commenti |
tabs/ | Barra multi-scheda |
Come il frontend chiama Rust
Sezione intitolata “Come il frontend chiama Rust”Comandi (richiesta/risposta)
Sezione intitolata “Comandi (richiesta/risposta)”Specta genera i binding TypeScript in src/bindings/generated.ts:
// Auto-generated from Rust #[tauri::command] functionsexport const commands = { async getBestMoves(id, engine, tab, goMode, options) { return await TAURI_INVOKE("get_best_moves", { id, engine, tab, goMode, options }); }, // ~50 more commands...}I componenti React li chiamano come normali funzioni asincrone:
import { commands } from "@/bindings";const result = await commands.getBestMoves(id, engine, tab, goMode, options);Eventi (streaming, da Rust a React)
Sezione intitolata “Eventi (streaming, da Rust a React)”Per dati in tempo reale (analisi del motore, ticchettio dell’orologio, mosse della partita):
Rust: app.emit("best_moves_payload", BestMovesPayload { depth: 24, ... }) ↓React: listen("best_moves_payload", (event) => updateBestMoves(event.payload))Plugin Tauri
Sezione intitolata “Plugin Tauri”L’app utilizza diversi plugin ufficiali per l’accesso al sistema:
| Plugin | Scopo |
|---|---|
@tauri-apps/plugin-fs | Lettura/scrittura di file |
@tauri-apps/plugin-dialog | Selezione file, finestre di dialogo |
@tauri-apps/plugin-http | Client HTTP (download dei motori, TTS cloud) |
@tauri-apps/plugin-shell | Esecuzione dei motori UCI |
@tauri-apps/plugin-updater | Verifica automatica degli aggiornamenti |
@tauri-apps/plugin-log | Logging strutturato |
@tauri-apps/plugin-os | Rilevamento CPU/RAM |
Sintesi vocale (TTS): un’introduzione
Sezione intitolata “Sintesi vocale (TTS): un’introduzione”En Parlant~ può leggere ad alta voce le mosse e i commenti degli scacchi mentre si naviga attraverso una partita. Questa sezione spiega come è costruito il sistema TTS — la pipeline di preprocessing, l’architettura dei provider e la strategia di caching. Per le istruzioni di configurazione, consultare le guide TTS nel menu TTS.
Come funziona il TTS (la versione breve)
Sezione intitolata “Come funziona il TTS (la versione breve)”La sintesi vocale converte il testo scritto in audio parlato. I moderni sistemi TTS si basano su reti neurali profonde addestrate su migliaia di ore di parlato umano. Il modello apprende la relazione tra il testo (lettere, parole, punteggiatura) e le caratteristiche acustiche del parlato (intonazione, ritmo, enfasi, pause per il respiro). In fase di inferenza, si invia il testo e si ottiene in risposta una forma d’onda audio.
Esistono due approcci principali:
-
TTS cloud — il testo viene inviato a un server remoto (Google, ElevenLabs, ecc.), che esegue una grande rete neurale su hardware GPU e restituisce l’audio. Qualità eccellente, ma richiede una connessione internet e comporta costi per richiesta (anche se la maggior parte dei provider offre livelli gratuiti).
-
TTS locale — un modello viene eseguito direttamente sul proprio computer. Non serve internet, nessun costo per richiesta, e il testo non lascia mai il computer. I recenti modelli open-source (come Kokoro e Piper) hanno ridotto significativamente il divario qualitativo.
Per chi è curioso di sapere come funzionano i modelli TTS nel dettaglio, HuggingFace (huggingface.co) ospita centinaia di modelli open-source per la sintesi vocale che si possono esplorare, scaricare ed eseguire localmente. Cercando “text-to-speech” si trovano modelli che vanno da opzioni leggere adatte alla CPU fino a modelli di ricerca all’avanguardia.
L’architettura dei provider
Sezione intitolata “L’architettura dei provider”L’implementazione principale del TTS si trova in src/utils/tts.ts. È progettata attorno a una singola interfaccia pubblica (speakText()) con backend intercambiabili. Il resto dell’app non sa e non si preoccupa di quale provider è attivo — chiama semplicemente speakText() e l’audio viene prodotto.
Sono supportati cinque provider:
| Provider | Tipo | Backend |
|---|---|---|
| ElevenLabs | Cloud | Voci neurali tramite API REST. Restituisce MP3. |
| Google Cloud TTS | Cloud | Voci WaveNet tramite API REST. Restituisce MP3 codificato in base64. |
| KittenTTS | Locale | Server TTS integrato, avviato automaticamente dal backend Rust. Comunica via HTTP su localhost. |
| OpenTTS | Locale | Server TTS self-hosted. Supporta molti motori (espeak, MaryTTS, Piper, ecc.). |
| System TTS | Locale | Motore vocale nativo del sistema operativo tramite comandi Rust/Tauri (speech-dispatcher su Linux, SAPI su Windows, AVSpeechSynthesizer su macOS). |
La selezione del provider è memorizzata in un singolo atom Jotai (ttsProviderAtom). Il cambio di provider è istantaneo — si modifica l’atom, e la successiva chiamata a speakText() viene instradata verso il nuovo backend.
La sfida: la notazione scacchistica non è linguaggio naturale
Sezione intitolata “La sfida: la notazione scacchistica non è linguaggio naturale”Le mosse degli scacchi sono scritte in Notazione Algebrica Standard (SAN): Nf3, Bxe5+, O-O-O, e8=Q#. Se si passa questo testo direttamente a un motore TTS, si ottengono risultati insensati — potrebbe tentare di pronunciare “Nf3” come una parola, o leggere “O-O-O” come “o o o.”
La soluzione è una pipeline di preprocessing che traduce la notazione scacchistica in linguaggio naturale prima che raggiunga il motore 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 funzione sanToSpoken() utilizza il pattern matching con espressioni regolari per scomporre qualsiasi stringa SAN nei suoi componenti (pezzo, disambiguazione, cattura, destinazione, promozione, scacco/scacco matto) e li riassembla utilizzando il linguaggio naturale da una tabella di vocaboli.
Supporto multilingue
Sezione intitolata “Supporto multilingue”Il vocabolario scacchistico è tradotto in molte lingue (inglese, francese, spagnolo, tedesco, giapponese, russo, cinese, coreano e altre). La tabella CHESS_VOCAB mappa ogni termine:
English: "Knight takes e5, check"French: "Cavalier prend e5, échec"German: "Springer schlägt e5, Schach"Japanese: "ナイト テイクス e5, チェック"Russian: "Конь берёт e5, шах"L’impostazione della lingua determina quale tabella di vocaboli viene utilizzata per il preprocessing e quale voce/accento il motore TTS utilizza per la sintesi.
Pulizia dei commenti
Sezione intitolata “Pulizia dei commenti”Le annotazioni delle partite contengono spesso markup specifico del PGN che suonerebbe pessimo se letto ad alta voce:
Raw comment: "BLUNDER. 7.Nf3 was better [%eval -2.3] [%cal Gg1f3]"After cleaning: "7, Knight f3 was better"La funzione cleanCommentForTTS():
- Rimuove i tag PGN:
[%eval ...],[%csl ...],[%cal ...],[%clk ...] - Elimina le parole di annotazione duplicate (quando ”??” ha già detto “Blunder”)
- Espande la notazione SAN inline nel testo:
"7.Nf3 controls e5"→"7, Knight f3 controls e5" - Corregge i termini scacchistici che i motori TTS pronunciano male (es. “en prise” → “on preez”)
- Espande le abbreviazioni dei pezzi nel testo:
"R vs R"→"Rook versus Rook"
Costruzione della narrazione completa
Sezione intitolata “Costruzione della narrazione completa”Quando si avanza a una nuova mossa, buildNarration() assembla il testo parlato completo da tre fonti:
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."Il doppio spazio tra le parti offre ai motori TTS una pausa naturale per il respiro.
Caching e riproduzione
Sezione intitolata “Caching e riproduzione”Le chiamate al TTS cloud costano denaro e richiedono tempo (~200-500ms di andata e ritorno). Per evitare di recuperare lo stesso audio più volte, ogni clip generata viene memorizzata nella cache in memoria come blob URL:
Cache key: "elevenlabs:pNInz6obpgDQGcFmaJgB:en:12, Knight f3, check."Cache value: blob:http://localhost/abc123 (the MP3 audio in browser memory)In caso di cache hit, la riproduzione è istantanea. La cache è indicizzata per provider:voice:language:text, quindi il cambio di voce o lingua crea voci separate.
Per le partite con molte annotazioni, è possibile precaricare la cache dell’intero albero di gioco in background. L’app percorre ogni nodo, costruisce il testo della narrazione e lancia chiamate API sequenziali per riempire la cache prima di iniziare la navigazione.
Concorrenza e cancellazione
Sezione intitolata “Concorrenza e cancellazione”La navigazione rapida con i tasti freccia crea un problema: se l’utente avanza di 5 mosse rapidamente, non si vuole che 5 clip audio sovrapposte si contendano la riproduzione. La soluzione è un contatore di generazione:
const thisGeneration = ++requestGeneration;// ... fetch audio ...if (thisGeneration !== requestGeneration) return; // stale — discardOgni nuova chiamata a speakText() incrementa il contatore e annulla qualsiasi richiesta HTTP in corso tramite AbortController. Quando l’audio arriva, verifica se la sua generazione è ancora corrente. Se l’utente ha già proseguito, la risposta viene scartata silenziosamente. Questo garantisce un audio pulito e senza glitch anche quando si clicca rapidamente attraverso le mosse.
Dove il TTS si integra nell’app
Sezione intitolata “Dove il TTS si integra nell’app”I punti di integrazione sono minimi:
| File | Cosa succede |
|---|---|
src/state/store/tree.ts | Ogni funzione di navigazione (goToNext, goToPrevious, ecc.) chiama stopSpeaking(). Quando la narrazione automatica è attiva, goToNext chiama anche speakMoveNarration(). |
src/components/common/Comment.tsx | Un’icona altoparlante accanto a ogni commento permette di attivare manualmente il TTS per quel commento. |
src/components/settings/TTSSettings.tsx | Interfaccia delle impostazioni per scegliere provider, voce, lingua, volume, velocità e inserire le chiavi API. |
Quando il TTS è disattivato, nessuno di questo codice viene eseguito. L’app si comporta in modo identico alla versione originale di En Croissant.
Esempi di flusso dei dati
Sezione intitolata “Esempi di flusso dei dati”Analisi del motore
Sezione intitolata “Analisi del motore”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 flagRicerca posizioni nel database
Sezione intitolata “Ricerca posizioni nel database”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 tableNarrazione TTS
Sezione intitolata “Narrazione TTS”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 atomsMappa delle directory
Sezione intitolata “Mappa delle directory”en-parlant/├── src-tauri/ # RUST BACKEND│ ├── src/│ │ ├── main.rs # Entry, command registration, plugins│ │ ├── chess.rs # Engine analysis│ │ ├── game.rs # Live game management│ │ ├── db/ # SQLite database (largest module)│ │ ├── engine/ # UCI protocol│ │ ├── pgn.rs # PGN parsing│ │ ├── puzzle.rs # Puzzle database│ │ ├── opening.rs # Opening lookup│ │ └── tts.rs # System TTS + KittenTTS management│ ├── Cargo.toml # Rust dependencies│ ├── tauri.conf.json # Tauri config│ └── capabilities/main.json # Security permissions│├── src/ # REACT/TS FRONTEND│ ├── App.tsx # Root component│ ├── state/│ │ ├── atoms.ts # Jotai atoms (all app state)│ │ └── store/tree.ts # Game tree (Zustand + TTS hooks)│ ├── routes/ # TanStack Router (file-based)│ ├── components/│ │ ├── boards/ # Chessboard + analysis│ │ ├── panels/ # Side panels│ │ ├── databases/ # DB browsing UI│ │ ├── common/ # Comment display (with TTS speaker icon)│ │ └── settings/ # Preferences, TTS settings│ ├── utils/│ │ ├── chess.ts # Game logic│ │ ├── tts.ts # TTS engine (SAN-to-spoken, caching, 5 providers)│ │ └── treeReducer.ts # Tree data structure│ ├── bindings/ # Auto-generated TS from Rust│ └── translation/ # i18n (13 languages)│├── docs/ # Bundled documentation (shown in Help menu)├── vite.config.ts # Build config└── package.json # Frontend depsPunti chiave
Sezione intitolata “Punti chiave”-
Rust fa il lavoro pesante — motori, database, I/O su file, parsing PGN. React non tocca mai il filesystem e non avvia processi direttamente.
-
Sicurezza dei tipi attraverso il confine — Specta genera i tipi TypeScript dalle struct Rust, quindi se un comando Rust cambia la sua firma, la build TypeScript fallisce immediatamente.
-
Due sistemi di stato — Jotai per lo stato reattivo semplice (impostazioni, preferenze UI, stato del motore per scheda), Zustand per lo stato di dominio complesso (albero di gioco con ramificazioni e aggiornamenti immutabili).
-
Il TTS è un problema di preprocessing — la parte difficile non è chiamare un’API vocale, ma tradurre la notazione scacchistica e il markup PGN in testo pulito e dal suono naturale in molte lingue. Le pipeline
sanToSpoken()ecleanCommentForTTS()sono dove avviene il vero lavoro. -
Cinque provider, una sola interfaccia — che l’audio provenga da ElevenLabs, Google Cloud, KittenTTS, OpenTTS o dal motore vocale del sistema operativo, il resto dell’app chiama sempre e solo
speakText(). La selezione del provider è un semplice toggle su un singolo atom. -
La build produce un singolo binario in
src-tauri/target/release/en-parlantche include il backend Rust e gli asset del frontend compilati con Vite.