Architektur-Überblick
App-Version: v0.1.1 (Fork: DarrellThomas/en-parlant) Stack: Tauri v2 (Rust) + React 19 (TypeScript) + Vite
Was ist Tauri?
Abschnitt betitelt „Was ist Tauri?“Tauri ist ein Framework zur Entwicklung von Desktop-Anwendungen. Anstatt einen vollständigen Browser wie Electron mitzuliefern, nutzt Tauri die systemeigene Webview des Betriebssystems für die Benutzeroberfläche und einen Rust-Prozess für das Backend. Das Ergebnis ist eine kleine, schnelle Binary.
Die beiden Hälften kommunizieren über IPC (Inter-Process Communication):
+---------------------------+ 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/Die Rust-Seite: src-tauri/src/
Abschnitt betitelt „Die Rust-Seite: src-tauri/src/“Rust übernimmt alles, was schnell sein muss oder Systemzugriff erfordert.
Einstiegspunkt: main.rs
Abschnitt betitelt „Einstiegspunkt: main.rs“Registriert ~50 Befehle, die das Frontend aufrufen kann, initialisiert Plugins (Dateisystem, Dialog, HTTP, Shell, Logging, Updater) und startet das App-Fenster.
Befehle werden mit einem Makro definiert:
#[tauri::command]async fn get_best_moves(id: String, engine: String, ...) -> Result<...> { // spawn UCI engine, return analysis}Das specta-Crate generiert automatisch TypeScript-Typdefinitionen aus diesen Rust-Funktionen, sodass das Frontend vollständige Typsicherheit ohne manuellen Aufwand erhält.
Wichtige Module
Abschnitt betitelt „Wichtige Module“| Modul | Funktion |
|---|---|
db/mod.rs | SQLite-Datenbank über Diesel ORM — Partieabfragen, Spielerstatistiken, Importe, Stellungssuche |
game.rs | Live-Spiel-Engine — verwaltet Engine-gegen-Mensch- und Engine-gegen-Engine-Partien, Zeitkontrollen, Zugvalidierung |
chess.rs | Engine-Analyse — startet UCI-Engines, streamt Bestmove-Ergebnisse über Events an das Frontend |
engine/ | UCI-Protokoll-Implementierung — Prozesserzeugung, stdin/stdout-Pipes, Multi-PV-Unterstützung |
pgn.rs | PGN-Dateien lesen/schreiben/tokenisieren |
opening.rs | Eröffnungsnamenssuche anhand von FEN (binäre Daten in die App eingebettet) |
puzzle.rs | Lichess-Aufgabendatenbank — speichergemappter Zufallszugriff |
fs.rs | Datei-Downloads mit Fortsetzung, Setzen von Ausführungsberechtigungen |
sound.rs | Lokaler HTTP-Server für Audio-Streaming (Workaround für Linux-Audio) |
tts.rs | System-TTS über speech-dispatcher (Linux) / native OS-Sprach-APIs, plus KittenTTS-Serververwaltung |
oauth.rs | OAuth2-Flow für Lichess/Chess.com-Kontoverknüpfung |
Entwurfsmuster
Abschnitt betitelt „Entwurfsmuster“- Überall asynchron: Tokio-Runtime, nicht-blockierendes I/O
- Nebenläufiger Zustand:
DashMap(nebenläufige HashMap) für Engine-Prozesse, DB-Verbindungen, Caches - Connection Pooling: r2d2 verwaltet SQLite-Verbindungspools
- Speichergemappte Suche: Stellungssuche über mmap’d-Binärindex für sofortige Ergebnisse
- Event-Streaming: Rust sendet Events (beste Züge, Uhrticks, Spielende), die React in Echtzeit empfängt
Die React/TypeScript-Seite: src/
Abschnitt betitelt „Die React/TypeScript-Seite: src/“Build-Pipeline: Vite
Abschnitt betitelt „Build-Pipeline: Vite“vite.config.ts konfiguriert:
- React-Plugin mit Babel-Compiler
- TanStack Router-Plugin — generiert automatisch den Routenbaum aus dem
routes/-Ordner - Vanilla Extract — CSS-in-JS ohne Laufzeit-Overhead
- Pfad-Alias:
@verweist auf./src - Dev-Server auf Port 1420
Build-Ablauf:
pnpm dev → Vite on :1420 + Tauri opens webview pointing to itpnpm build → tsc (typecheck) → vite build (bundle to dist/) → tauri build (native binary)Einstiegspunkt: App.tsx
Abschnitt betitelt „Einstiegspunkt: App.tsx“Die Root-Komponente:
- Initialisiert Tauri-Plugins (Log, Process, Updater)
- Lädt Benutzereinstellungen aus persistenten Atoms
- Richtet das Mantine-UI-Theme ein
- Registriert den Router
- Prüft auf App-Updates
Zustandsverwaltung
Abschnitt betitelt „Zustandsverwaltung“Jotai-Atoms (src/state/atoms.ts) — leichtgewichtiger reaktiver Zustand:
| Kategorie | Beispiele |
|---|---|
| Tabs | tabsAtom, activeTabAtom (Multi-Dokument-Oberfläche) |
| Verzeichnisse | storedDocumentDirAtom, storedDatabasesDirAtom |
| UI-Einstellungen | primaryColorAtom, fontSizeAtom, pieceSetAtom |
| Engine | engineMovesFamily, engineProgressFamily (pro Tab via atomFamily) |
| TTS | ttsEnabledAtom, ttsProviderAtom, ttsVoiceIdAtom, ttsVolumeAtom, ttsSpeedAtom, ttsLanguageAtom |
Atoms mit atomWithStorage() werden automatisch in localStorage persistiert.
Zustand-Stores für komplexen Fachzustand:
src/state/store/tree.ts— Spielbaum-Navigation, Zugverzweigung, Annotationen, Kommentare. Verwendet Immer für unveränderliche Updates.src/state/store/database.ts— Datenbankansicht-Filter, ausgewählte Partie, Paginierung
Routing: TanStack Router
Abschnitt betitelt „Routing: TanStack Router“Dateibasiertes Routing 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 managementKomponenten: src/components/
Abschnitt betitelt „Komponenten: src/components/“| Gruppe | Zweck |
|---|---|
boards/ | Schachbrett (Chessground), Zugeingabe, Bewertungsbalken, Analyseanzeige, Bauernumwandlungs-Modal, Pfeilzeichnung |
panels/ | Seitenpanels: Engine-Analyse (BestMoves), Datenbank-Stellungssuche, Annotationsbearbeitung, Partieinformationen, Übungsmodus |
databases/ | Datenbank-UI: Partietabelle, Spielertabelle, Detailkarten, Filterung |
settings/ | Einstellungsformulare, Engine-Pfade, TTS-Einstellungen |
home/ | Kontokarten, Import-UI |
common/ | Gemeinsam genutzt: TreeStateContext, Materialanzeige, Kommentar-Lautsprechersymbol |
tabs/ | Multi-Tab-Leiste |
Wie das Frontend Rust aufruft
Abschnitt betitelt „Wie das Frontend Rust aufruft“Befehle (Anfrage/Antwort)
Abschnitt betitelt „Befehle (Anfrage/Antwort)“Specta generiert TypeScript-Bindings 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...}React-Komponenten rufen sie wie normale asynchrone Funktionen auf:
import { commands } from "@/bindings";const result = await commands.getBestMoves(id, engine, tab, goMode, options);Events (Streaming, Rust an React)
Abschnitt betitelt „Events (Streaming, Rust an React)“Für Echtzeitdaten (Engine-Analyse, Uhrticks, Spielzüge):
Rust: app.emit("best_moves_payload", BestMovesPayload { depth: 24, ... }) ↓React: listen("best_moves_payload", (event) => updateBestMoves(event.payload))Tauri-Plugins
Abschnitt betitelt „Tauri-Plugins“Die App verwendet mehrere offizielle Plugins für Systemzugriffe:
| Plugin | Zweck |
|---|---|
@tauri-apps/plugin-fs | Dateien lesen/schreiben |
@tauri-apps/plugin-dialog | Dateiauswahl-Dialoge, Meldungsfenster |
@tauri-apps/plugin-http | HTTP-Client (Engine-Downloads, Cloud-TTS) |
@tauri-apps/plugin-shell | UCI-Engines ausführen |
@tauri-apps/plugin-updater | Automatische Update-Prüfung |
@tauri-apps/plugin-log | Strukturiertes Logging |
@tauri-apps/plugin-os | CPU-/RAM-Erkennung |
Text-to-Speech (TTS): Eine Einführung
Abschnitt betitelt „Text-to-Speech (TTS): Eine Einführung“En Parlant~ kann Schachzüge und Kommentare vorlesen, während Sie eine Partie durchgehen. Dieser Abschnitt erklärt, wie das TTS-System aufgebaut ist — die Vorverarbeitungs-Pipeline, die Provider-Architektur und die Caching-Strategie. Einrichtungsanleitungen finden Sie in den TTS-Guides im TTS-Menü.
Wie TTS funktioniert (Kurzversion)
Abschnitt betitelt „Wie TTS funktioniert (Kurzversion)“Text-to-Speech wandelt geschriebenen Text in gesprochenes Audio um. Moderne TTS-Systeme basieren auf tiefen neuronalen Netzen, die mit Tausenden von Stunden menschlicher Sprache trainiert wurden. Das Modell lernt den Zusammenhang zwischen Text (Buchstaben, Wörter, Satzzeichen) und den akustischen Merkmalen der Sprache (Tonhöhe, Timing, Betonung, Atempausen). Zur Inferenzzeit senden Sie Text ein und erhalten eine Audio-Wellenform zurück.
Es gibt zwei grundlegende Ansätze:
-
Cloud-TTS — der Text wird an einen Remote-Server gesendet (Google, ElevenLabs usw.), der ein großes neuronales Netz auf GPU-Hardware ausführt und Audio zurückliefert. Hervorragende Qualität, erfordert jedoch Internet und verursacht Kosten pro Anfrage (obwohl die meisten Anbieter kostenlose Kontingente anbieten).
-
Lokales TTS — ein Modell läuft direkt auf Ihrem Rechner. Kein Internet nötig, keine Kosten pro Anfrage, und Ihr Text verlässt niemals Ihren Computer. Aktuelle Open-Source-Modelle (wie Kokoro und Piper) haben den Qualitätsabstand deutlich verringert.
Wenn Sie neugierig sind, wie TTS-Modelle im Detail funktionieren, hostet HuggingFace (huggingface.co) Hunderte von Open-Source-Sprachsynthesemodellen, die Sie erkunden, herunterladen und lokal ausführen können. Suchen Sie nach „text-to-speech”, um Modelle zu finden — von leichtgewichtigen, CPU-freundlichen Optionen bis hin zu modernsten Forschungsmodellen.
Die Provider-Architektur
Abschnitt betitelt „Die Provider-Architektur“Die zentrale TTS-Implementierung befindet sich in src/utils/tts.ts. Sie ist um eine einzige öffentliche Schnittstelle (speakText()) mit austauschbaren Backends herum aufgebaut. Der Rest der App weiß nicht und muss nicht wissen, welcher Provider aktiv ist — er ruft einfach speakText() auf, und Audio wird ausgegeben.
Fünf Provider werden unterstützt:
| Provider | Typ | Backend |
|---|---|---|
| ElevenLabs | Cloud | Neuronale Stimmen über REST-API. Liefert MP3. |
| Google Cloud TTS | Cloud | WaveNet-Stimmen über REST-API. Liefert base64-kodiertes MP3. |
| KittenTTS | Lokal | Mitgelieferter TTS-Server, automatisch vom Rust-Backend gestartet. Kommuniziert über HTTP auf localhost. |
| OpenTTS | Lokal | Selbst gehosteter TTS-Server. Unterstützt viele Engines (espeak, MaryTTS, Piper usw.). |
| System-TTS | Lokal | Native OS-Sprach-Engine über Rust/Tauri-Befehle (speech-dispatcher unter Linux, SAPI unter Windows, AVSpeechSynthesizer unter macOS). |
Die Provider-Auswahl wird in einem einzelnen Jotai-Atom (ttsProviderAtom) gespeichert. Der Provider-Wechsel erfolgt sofort — ändern Sie das Atom, und der nächste speakText()-Aufruf wird an das neue Backend weitergeleitet.
Die Herausforderung: Schachnotation ist kein Englisch
Abschnitt betitelt „Die Herausforderung: Schachnotation ist kein Englisch“Schachzüge werden in Standardalgebraischer Notation (SAN) geschrieben: Nf3, Bxe5+, O-O-O, e8=Q#. Wenn man dies direkt an eine TTS-Engine übergibt, erhält man Unsinn — sie versucht möglicherweise, „Nf3” als Wort auszusprechen oder „O-O-O” als „oh oh oh” zu lesen.
Die Lösung ist eine Vorverarbeitungs-Pipeline, die Schachnotation in natürliche Sprache übersetzt, bevor sie die TTS-Engine erreicht:
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"Die Funktion sanToSpoken() verwendet Regex-Musterabgleich, um jeden SAN-String in seine Bestandteile zu zerlegen (Figur, Disambiguierung, Schlagen, Zielfeld, Bauernumwandlung, Schach/Matt) und setzt sie mithilfe natürlicher Sprache aus einer Vokabeltabelle wieder zusammen.
Mehrsprachige Unterstützung
Abschnitt betitelt „Mehrsprachige Unterstützung“Das Schachvokabular ist in viele Sprachen übersetzt (Englisch, Französisch, Spanisch, Deutsch, Japanisch, Russisch, Chinesisch, Koreanisch und weitere). Die CHESS_VOCAB-Tabelle ordnet jeden Begriff zu:
English: "Knight takes e5, check"French: "Cavalier prend e5, échec"German: "Springer schlägt e5, Schach"Japanese: "ナイト テイクス e5, チェック"Russian: "Конь берёт e5, шах"Die Spracheinstellung bestimmt, welche Vokabeltabelle für die Vorverarbeitung verwendet wird und welche Stimme/welchen Akzent die TTS-Engine für die Synthese nutzt.
Kommentarbereinigung
Abschnitt betitelt „Kommentarbereinigung“Partieannotationen enthalten oft PGN-spezifisches Markup, das vorgelesen schrecklich klingen würde:
Raw comment: "BLUNDER. 7.Nf3 was better [%eval -2.3] [%cal Gg1f3]"After cleaning: "7, Knight f3 was better"Die Funktion cleanCommentForTTS():
- Entfernt PGN-Tags:
[%eval ...],[%csl ...],[%cal ...],[%clk ...] - Entfernt doppelte Annotationswörter (wenn „??” bereits „Blunder” gesagt hat)
- Expandiert Inline-SAN in Prosa:
"7.Nf3 controls e5"→"7, Knight f3 controls e5" - Korrigiert Schachbegriffe, die TTS-Engines falsch aussprechen (z. B. „en prise” → „on preez”)
- Expandiert Figurenabkürzungen in Prosa:
"R vs R"→"Rook versus Rook"
Aufbau der vollständigen Narration
Abschnitt betitelt „Aufbau der vollständigen Narration“Wenn Sie zu einem neuen Zug wechseln, setzt buildNarration() den vollständigen gesprochenen Text aus drei Quellen zusammen:
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."Der doppelte Abstand zwischen den Teilen gibt TTS-Engines eine natürliche Atempause.
Caching und Wiedergabe
Abschnitt betitelt „Caching und Wiedergabe“Cloud-TTS-Aufrufe kosten Geld und benötigen Zeit (~200–500 ms Roundtrip). Um das erneute Abrufen desselben Audios zu vermeiden, wird jeder generierte Clip im Speicher als Blob-URL gecacht:
Cache key: "elevenlabs:pNInz6obpgDQGcFmaJgB:en:12, Knight f3, check."Cache value: blob:http://localhost/abc123 (the MP3 audio in browser memory)Bei einem Cache-Hit erfolgt die Wiedergabe sofort. Der Cache ist nach provider:voice:language:text geschlüsselt, sodass ein Wechsel der Stimme oder Sprache separate Einträge erzeugt.
Für Partien mit vielen Annotationen können Sie den gesamten Spielbaum im Hintergrund vorab cachen. Die App durchläuft jeden Knoten, erstellt den Narrationstext und sendet sequenzielle API-Aufrufe, um den Cache zu füllen, bevor Sie mit der Navigation beginnen.
Nebenläufigkeit und Abbruch
Abschnitt betitelt „Nebenläufigkeit und Abbruch“Schnelle Pfeiltasten-Navigation erzeugt ein Problem: Wenn der Benutzer fünfmal schnell vorwärts springt, sollen nicht fünf überlappende Audioclips miteinander konkurrieren. Die Lösung ist ein Generierungszähler:
const thisGeneration = ++requestGeneration;// ... fetch audio ...if (thisGeneration !== requestGeneration) return; // stale — discardJeder neue speakText()-Aufruf erhöht den Zähler und bricht jede laufende HTTP-Anfrage über AbortController ab. Wenn das Audio eintrifft, wird geprüft, ob seine Generation noch aktuell ist. Hat der Benutzer bereits weitergeklickt, wird die Antwort stillschweigend verworfen. Dies ergibt sauberes, störungsfreies Audio, selbst wenn man schnell durch Züge klickt.
Wo TTS in die App integriert ist
Abschnitt betitelt „Wo TTS in die App integriert ist“Die Integrationspunkte sind minimal:
| Datei | Was passiert |
|---|---|
src/state/store/tree.ts | Jede Navigationsfunktion (goToNext, goToPrevious usw.) ruft stopSpeaking() auf. Wenn Auto-Narration aktiviert ist, ruft goToNext zusätzlich speakMoveNarration() auf. |
src/components/common/Comment.tsx | Ein Lautsprechersymbol neben jedem Kommentar ermöglicht das manuelle Auslösen von TTS für diesen Kommentar. |
src/components/settings/TTSSettings.tsx | Einstellungs-UI zur Auswahl von Provider, Stimme, Sprache, Lautstärke, Geschwindigkeit und Eingabe von API-Schlüsseln. |
Wenn TTS deaktiviert ist, wird keiner dieser Codes ausgeführt. Die App verhält sich identisch zum ursprünglichen En Croissant.
Beispiele für den Datenfluss
Abschnitt betitelt „Beispiele für den Datenfluss“Engine-Analyse
Abschnitt betitelt „Engine-Analyse“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 flagDatenbank-Stellungssuche
Abschnitt betitelt „Datenbank-Stellungssuche“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 tableTTS-Narration
Abschnitt betitelt „TTS-Narration“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 atomsVerzeichnisübersicht
Abschnitt betitelt „Verzeichnisübersicht“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 depsWichtigste Erkenntnisse
Abschnitt betitelt „Wichtigste Erkenntnisse“-
Rust übernimmt die Schwerstarbeit — Engines, Datenbank, Datei-I/O, PGN-Parsing. React greift niemals direkt auf das Dateisystem zu und startet keine Prozesse.
-
Typsicherheit über die Grenze hinweg — Specta generiert TypeScript-Typen aus Rust-Structs. Wenn ein Rust-Befehl seine Signatur ändert, bricht der TypeScript-Build sofort ab.
-
Zwei Zustandssysteme — Jotai für einfachen reaktiven Zustand (Einstellungen, UI-Präferenzen, Engine-Zustand pro Tab), Zustand für komplexen Fachzustand (Spielbaum mit Verzweigung und unveränderlichen Updates).
-
TTS ist ein Vorverarbeitungsproblem — die Schwierigkeit liegt nicht im Aufruf einer Sprach-API, sondern in der Übersetzung von Schachnotation und PGN-Markup in sauberen, natürlich klingenden Text in vielen Sprachen. Die Pipelines
sanToSpoken()undcleanCommentForTTS()sind der Ort, an dem die eigentliche Arbeit stattfindet. -
Fünf Provider, eine Schnittstelle — ob das Audio von ElevenLabs, Google Cloud, KittenTTS, OpenTTS oder der Sprach-Engine Ihres Betriebssystems kommt, der Rest der App ruft immer nur
speakText()auf. Die Provider-Auswahl ist ein einziger Atom-Umschalter. -
Der Build erzeugt eine einzelne Binary unter
src-tauri/target/release/en-parlant, die das Rust-Backend und die mit Vite gebauten Frontend-Assets bündelt.