Salta ai contenuti

Introduzione all'architettura

Versione dell’app: v0.1.1 (fork: DarrellThomas/en-parlant) Stack: Tauri v2 (Rust) + React 19 (TypeScript) + Vite


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/

Rust gestisce tutto ciò che deve essere veloce o che necessita di accesso al sistema.

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.

ModuloFunzione
db/mod.rsDatabase SQLite tramite Diesel ORM — query sulle partite, statistiche dei giocatori, importazioni, ricerca per posizione
game.rsMotore 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.rsAnalisi 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.rsLettura/scrittura/tokenizzazione di file PGN
opening.rsRicerca del nome dell’apertura da FEN (dati binari incorporati nell’app)
puzzle.rsDatabase di puzzle Lichess — accesso casuale tramite memory mapping
fs.rsDownload di file con ripresa, impostazione dei permessi di esecuzione
sound.rsServer HTTP locale per lo streaming audio (soluzione alternativa per l’audio su Linux)
tts.rsTTS di sistema tramite speech-dispatcher (Linux) / API vocali native del sistema operativo, più gestione del server KittenTTS
oauth.rsFlusso OAuth2 per il collegamento degli account Lichess/Chess.com
  • 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

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 it
pnpm build → tsc (typecheck) → vite build (bundle to dist/) → tauri build (native binary)

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

Atom Jotai (src/state/atoms.ts) — stato reattivo leggero:

CategoriaEsempi
SchedetabsAtom, activeTabAtom (interfaccia multi-documento)
DirectorystoredDocumentDirAtom, storedDatabasesDirAtom
Preferenze UIprimaryColorAtom, fontSizeAtom, pieceSetAtom
MotoreengineMovesFamily, engineProgressFamily (per scheda tramite atomFamily)
TTSttsEnabledAtom, 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 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 management
GruppoScopo
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

Specta genera i binding TypeScript in 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...
}

I componenti React li chiamano come normali funzioni asincrone:

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

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))

L’app utilizza diversi plugin ufficiali per l’accesso al sistema:

PluginScopo
@tauri-apps/plugin-fsLettura/scrittura di file
@tauri-apps/plugin-dialogSelezione file, finestre di dialogo
@tauri-apps/plugin-httpClient HTTP (download dei motori, TTS cloud)
@tauri-apps/plugin-shellEsecuzione dei motori UCI
@tauri-apps/plugin-updaterVerifica automatica degli aggiornamenti
@tauri-apps/plugin-logLogging strutturato
@tauri-apps/plugin-osRilevamento CPU/RAM

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.

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’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:

ProviderTipoBackend
ElevenLabsCloudVoci neurali tramite API REST. Restituisce MP3.
Google Cloud TTSCloudVoci WaveNet tramite API REST. Restituisce MP3 codificato in base64.
KittenTTSLocaleServer TTS integrato, avviato automaticamente dal backend Rust. Comunica via HTTP su localhost.
OpenTTSLocaleServer TTS self-hosted. Supporta molti motori (espeak, MaryTTS, Piper, ecc.).
System TTSLocaleMotore 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.

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.

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():

  1. Rimuove i tag PGN: [%eval ...], [%csl ...], [%cal ...], [%clk ...]
  2. Elimina le parole di annotazione duplicate (quando ”??” ha già detto “Blunder”)
  3. Espande la notazione SAN inline nel testo: "7.Nf3 controls e5""7, Knight f3 controls e5"
  4. Corregge i termini scacchistici che i motori TTS pronunciano male (es. “en prise” → “on preez”)
  5. Espande le abbreviazioni dei pezzi nel testo: "R vs R""Rook versus Rook"

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.

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.

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 — discard

Ogni 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.

I punti di integrazione sono minimi:

FileCosa succede
src/state/store/tree.tsOgni funzione di navigazione (goToNext, goToPrevious, ecc.) chiama stopSpeaking(). Quando la narrazione automatica è attiva, goToNext chiama anche speakMoveNarration().
src/components/common/Comment.tsxUn’icona altoparlante accanto a ogni commento permette di attivare manualmente il TTS per quel commento.
src/components/settings/TTSSettings.tsxInterfaccia 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.


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/ # 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 deps

  1. Rust fa il lavoro pesante — motori, database, I/O su file, parsing PGN. React non tocca mai il filesystem e non avvia processi direttamente.

  2. 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.

  3. 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).

  4. 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() e cleanCommentForTTS() sono dove avviene il vero lavoro.

  5. 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.

  6. La build produce un singolo binario in src-tauri/target/release/en-parlant che include il backend Rust e gli asset del frontend compilati con Vite.