Wprowadzenie do architektury
Wersja aplikacji: v0.1.1 (fork: DarrellThomas/en-parlant) Stos technologiczny: Tauri v2 (Rust) + React 19 (TypeScript) + Vite
Czym jest Tauri?
Dział zatytułowany „Czym jest Tauri?”Tauri to framework do budowania aplikacji desktopowych. Zamiast dostarczać pełną przeglądarkę, jak robi to Electron, Tauri korzysta z wbudowanego w system operacyjny webview do wyświetlania interfejsu użytkownika oraz z procesu Rust jako backendu. Rezultatem jest mały, szybki plik binarny.
Obie połowy komunikują się poprzez IPC (komunikacja międzyprocesowa):
+---------------------------+ 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/Strona Rust: src-tauri/src/
Dział zatytułowany „Strona Rust: src-tauri/src/”Rust obsługuje wszystko, co wymaga szybkości lub dostępu do systemu.
Punkt wejścia: main.rs
Dział zatytułowany „Punkt wejścia: main.rs”Rejestruje ~50 komend, które może wywoływać frontend, inicjalizuje wtyczki (system plików, okna dialogowe, HTTP, powłoka, logowanie, aktualizator) i uruchamia okno aplikacji.
Komendy definiowane są za pomocą makra:
#[tauri::command]async fn get_best_moves(id: String, engine: String, ...) -> Result<...> { // spawn UCI engine, return analysis}Crate specta automatycznie generuje definicje typów TypeScript z tych funkcji Rust, dzięki czemu frontend otrzymuje pełne bezpieczeństwo typów bez żadnego ręcznego wysiłku.
Kluczowe moduły
Dział zatytułowany „Kluczowe moduły”| Moduł | Opis |
|---|---|
db/mod.rs | Baza danych SQLite przez Diesel ORM — zapytania o partie, statystyki graczy, importy, wyszukiwanie pozycji |
game.rs | Silnik gry na żywo — zarządza grami silnik-kontra-człowiek i silnik-kontra-silnik, kontrolą czasu, walidacją ruchów |
chess.rs | Analiza silnikowa — uruchamia silniki UCI, przesyła strumieniowo wyniki najlepszych ruchów do frontendu poprzez zdarzenia |
engine/ | Implementacja protokołu UCI — uruchamianie procesów, potoki stdin/stdout, obsługa multi-PV |
pgn.rs | Odczyt/zapis/tokenizacja plików PGN |
opening.rs | Wyszukiwanie nazw otwarć na podstawie FEN (dane binarne wbudowane w aplikację) |
puzzle.rs | Baza zadań Lichess — losowy dostęp przez mapowanie pamięci |
fs.rs | Pobieranie plików z wznawianiem, ustawianie uprawnień do wykonywania |
sound.rs | Lokalny serwer HTTP do strumieniowania audio (obejście problemu z dźwiękiem na Linuksie) |
tts.rs | Systemowe TTS przez speech-dispatcher (Linux) / natywne API mowy systemu operacyjnego, plus zarządzanie serwerem KittenTTS |
oauth.rs | Przepływ OAuth2 do łączenia kont Lichess/Chess.com |
Wzorce projektowe
Dział zatytułowany „Wzorce projektowe”- Asynchroniczność wszędzie: runtime Tokio, nieblokujące I/O
- Współbieżny stan:
DashMap(współbieżna HashMap) dla procesów silników, połączeń z bazą danych, pamięci podręcznych - Pule połączeń: r2d2 zarządza pulami połączeń SQLite
- Wyszukiwanie przez mapowanie pamięci: wyszukiwanie pozycji przez binarny indeks mapowany w pamięci dla natychmiastowych wyników
- Strumieniowanie zdarzeń: Rust emituje zdarzenia (najlepsze ruchy, taktowanie zegara, koniec gry), których React nasłuchuje w czasie rzeczywistym
Strona React/TypeScript: src/
Dział zatytułowany „Strona React/TypeScript: src/”Pipeline budowania: Vite
Dział zatytułowany „Pipeline budowania: Vite”vite.config.ts konfiguruje:
- Wtyczkę React z kompilatorem Babel
- Wtyczkę TanStack Router — automatycznie generuje drzewo tras z folderu
routes/ - Vanilla Extract — CSS-in-JS bez kosztu uruchomieniowego
- Alias ścieżki:
@wskazuje na./src - Serwer deweloperski na porcie 1420
Przebieg budowania:
pnpm dev → Vite on :1420 + Tauri opens webview pointing to itpnpm build → tsc (typecheck) → vite build (bundle to dist/) → tauri build (native binary)Punkt wejścia: App.tsx
Dział zatytułowany „Punkt wejścia: App.tsx”Komponent główny:
- Inicjalizuje wtyczki Tauri (log, process, updater)
- Ładuje preferencje użytkownika z trwałych atomów
- Konfiguruje motyw Mantine UI
- Rejestruje router
- Sprawdza dostępność aktualizacji aplikacji
Zarządzanie stanem
Dział zatytułowany „Zarządzanie stanem”Atomy Jotai (src/state/atoms.ts) — lekki, reaktywny stan:
| Kategoria | Przykłady |
|---|---|
| Zakładki | tabsAtom, activeTabAtom (interfejs wielodokumentowy) |
| Katalogi | storedDocumentDirAtom, storedDatabasesDirAtom |
| Preferencje interfejsu | primaryColorAtom, fontSizeAtom, pieceSetAtom |
| Silnik | engineMovesFamily, engineProgressFamily (per zakładka przez atomFamily) |
| TTS | ttsEnabledAtom, ttsProviderAtom, ttsVoiceIdAtom, ttsVolumeAtom, ttsSpeedAtom, ttsLanguageAtom |
Atomy z atomWithStorage() automatycznie utrwalają się w localStorage.
Magazyny Zustand dla złożonego stanu domenowego:
src/state/store/tree.ts— nawigacja po drzewie gry, rozgałęzianie ruchów, adnotacje, komentarze. Używa Immer do niemutowalnych aktualizacji.src/state/store/database.ts— filtry widoku bazy danych, wybrana partia, paginacja
Routing: TanStack Router
Dział zatytułowany „Routing: TanStack Router”Routing oparty na plikach w 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 managementKomponenty: src/components/
Dział zatytułowany „Komponenty: src/components/”| Grupa | Przeznaczenie |
|---|---|
boards/ | Szachownica (chessground), wprowadzanie ruchów, pasek oceny, wyświetlanie analizy, modal promocji, rysowanie strzałek |
panels/ | Panele boczne: analiza silnikowa (BestMoves), wyszukiwanie pozycji w bazie, edycja adnotacji, informacje o partii, tryb ćwiczeniowy |
databases/ | Interfejs bazy danych: tabela partii, tabela graczy, karty szczegółów, filtrowanie |
settings/ | Formularze preferencji, ścieżki silników, ustawienia TTS |
home/ | Karty kont, interfejs importu |
common/ | Współdzielone: TreeStateContext, wyświetlanie materiału, ikona głośnika przy komentarzu |
tabs/ | Pasek wielu zakładek |
Jak frontend wywołuje Rust
Dział zatytułowany „Jak frontend wywołuje Rust”Komendy (żądanie/odpowiedź)
Dział zatytułowany „Komendy (żądanie/odpowiedź)”Specta generuje wiązania TypeScript w 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...}Komponenty React wywołują je jak zwykłe funkcje asynchroniczne:
import { commands } from "@/bindings";const result = await commands.getBestMoves(id, engine, tab, goMode, options);Zdarzenia (strumieniowanie, Rust do React)
Dział zatytułowany „Zdarzenia (strumieniowanie, Rust do React)”Dla danych w czasie rzeczywistym (analiza silnikowa, taktowanie zegara, ruchy w grze):
Rust: app.emit("best_moves_payload", BestMovesPayload { depth: 24, ... }) ↓React: listen("best_moves_payload", (event) => updateBestMoves(event.payload))Wtyczki Tauri
Dział zatytułowany „Wtyczki Tauri”Aplikacja korzysta z kilku oficjalnych wtyczek zapewniających dostęp do systemu:
| Wtyczka | Przeznaczenie |
|---|---|
@tauri-apps/plugin-fs | Odczyt/zapis plików |
@tauri-apps/plugin-dialog | Okna wyboru plików, okna komunikatów |
@tauri-apps/plugin-http | Klient HTTP (pobieranie silników, chmurowe TTS) |
@tauri-apps/plugin-shell | Uruchamianie silników UCI |
@tauri-apps/plugin-updater | Automatyczne sprawdzanie aktualizacji |
@tauri-apps/plugin-log | Strukturalne logowanie |
@tauri-apps/plugin-os | Wykrywanie CPU/RAM |
Synteza mowy (TTS): wprowadzenie
Dział zatytułowany „Synteza mowy (TTS): wprowadzenie”En Parlant~ potrafi odczytywać na głos ruchy szachowe i komentarze podczas przeglądania partii. Ta sekcja wyjaśnia, jak zbudowany jest system TTS — pipeline przetwarzania wstępnego, architekturę dostawców oraz strategię cache’owania. Instrukcje konfiguracji znajdziesz w przewodnikach TTS w menu TTS.
Jak działa TTS (krótka wersja)
Dział zatytułowany „Jak działa TTS (krótka wersja)”Synteza mowy przekształca tekst pisany w mówione audio. Nowoczesne systemy TTS oparte są na głębokich sieciach neuronowych trenowanych na tysiącach godzin ludzkiej mowy. Model uczy się relacji między tekstem (litery, słowa, interpunkcja) a akustycznymi cechami mowy (wysokość tonu, tempo, emfaza, pauzy oddechowe). W momencie inferencji wysyłasz tekst i otrzymujesz z powrotem przebieg audio.
Istnieją dwa główne podejścia:
-
Chmurowe TTS — tekst jest wysyłany do zdalnego serwera (Google, ElevenLabs itp.), który uruchamia dużą sieć neuronową na sprzęcie GPU i zwraca audio. Doskonała jakość, ale wymaga internetu i wiąże się z kosztami za zapytanie (choć większość dostawców oferuje darmowe limity).
-
Lokalne TTS — model działa bezpośrednio na Twoim komputerze. Nie wymaga internetu, nie generuje kosztów za zapytanie, a Twój tekst nigdy nie opuszcza komputera. Najnowsze modele open-source (takie jak Kokoro i Piper) znacząco zmniejszyły różnicę w jakości.
Jeśli ciekawi Cię, jak modele TTS działają od wewnątrz, HuggingFace (huggingface.co) hostuje setki open-source’owych modeli syntezy mowy, które możesz przeglądać, pobierać i uruchamiać lokalnie. Wyszukaj „text-to-speech”, aby znaleźć modele — od lekkich opcji przyjaznych CPU po najnowocześniejsze modele badawcze.
Architektura dostawców
Dział zatytułowany „Architektura dostawców”Główna implementacja TTS znajduje się w src/utils/tts.ts. Jest zaprojektowana wokół jednego publicznego interfejsu (speakText()) z wymiennymi backendami. Reszta aplikacji nigdy nie wie ani nie dba o to, który dostawca jest aktywny — po prostu wywołuje speakText() i audio jest odtwarzane.
Obsługiwanych jest pięciu dostawców:
| Dostawca | Typ | Backend |
|---|---|---|
| ElevenLabs | Chmurowy | Neuronowe głosy przez REST API. Zwraca MP3. |
| Google Cloud TTS | Chmurowy | Głosy WaveNet przez REST API. Zwraca MP3 zakodowany w base64. |
| KittenTTS | Lokalny | Dołączony serwer TTS, automatycznie uruchamiany przez backend Rust. Komunikuje się przez HTTP na localhost. |
| OpenTTS | Lokalny | Samodzielnie hostowany serwer TTS. Obsługuje wiele silników (espeak, MaryTTS, Piper itp.). |
| System TTS | Lokalny | Natywny silnik mowy systemu operacyjnego przez komendy Rust/Tauri (speech-dispatcher na Linuksie, SAPI na Windows, AVSpeechSynthesizer na macOS). |
Wybór dostawcy jest przechowywany w pojedynczym atomie Jotai (ttsProviderAtom). Przełączanie dostawców jest natychmiastowe — zmień atom, a następne wywołanie speakText() skieruje żądanie do nowego backendu.
Wyzwanie: notacja szachowa to nie język naturalny
Dział zatytułowany „Wyzwanie: notacja szachowa to nie język naturalny”Ruchy szachowe zapisywane są w Standardowej Notacji Algebraicznej (SAN): Nf3, Bxe5+, O-O-O, e8=Q#. Jeśli podasz to bezpośrednio silnikowi TTS, otrzymasz bełkot — może próbować wymówić „Nf3” jako słowo lub przeczytać „O-O-O” jako „o o o”.
Rozwiązaniem jest pipeline przetwarzania wstępnego, który tłumaczy notację szachową na język naturalny, zanim trafi ona do silnika 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"Funkcja sanToSpoken() wykorzystuje dopasowywanie wzorców regex do rozłożenia dowolnego ciągu SAN na składniki (figura, ujednoznacznienie, bicie, pole docelowe, promocja, szach/mat) i ponownie składa je przy użyciu języka naturalnego z tabeli słownictwa.
Obsługa wielu języków
Dział zatytułowany „Obsługa wielu języków”Słownictwo szachowe jest przetłumaczone na wiele języków (angielski, francuski, hiszpański, niemiecki, japoński, rosyjski, chiński, koreański i inne). Tabela CHESS_VOCAB mapuje każdy termin:
English: "Knight takes e5, check"French: "Cavalier prend e5, échec"German: "Springer schlägt e5, Schach"Japanese: "ナイト テイクス e5, チェック"Russian: "Конь берёт e5, шах"Ustawienie języka określa, która tabela słownictwa jest używana do przetwarzania wstępnego oraz jaki głos/akcent silnika TTS jest używany do syntezy.
Czyszczenie komentarzy
Dział zatytułowany „Czyszczenie komentarzy”Adnotacje do partii często zawierają znaczniki specyficzne dla PGN, które brzmiałyby fatalnie odczytane na głos:
Raw comment: "BLUNDER. 7.Nf3 was better [%eval -2.3] [%cal Gg1f3]"After cleaning: "7, Knight f3 was better"Funkcja cleanCommentForTTS():
- Usuwa tagi PGN:
[%eval ...],[%csl ...],[%cal ...],[%clk ...] - Usuwa zduplikowane słowa adnotacji (gdy „??” już odczytało „Blunder”)
- Rozwija inline SAN w tekście:
"7.Nf3 controls e5"→"7, Knight f3 controls e5" - Poprawia terminy szachowe, które silniki TTS źle wymawiają (np. „en prise” → „on preez”)
- Rozwija skróty figur w tekście:
"R vs R"→"Rook versus Rook"
Budowanie pełnej narracji
Dział zatytułowany „Budowanie pełnej narracji”Gdy przechodzisz do nowego ruchu, buildNarration() składa kompletny tekst mówiony z trzech źródeł:
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."Podwójna spacja między częściami zapewnia silnikom TTS naturalną pauzę oddechową.
Cache’owanie i odtwarzanie
Dział zatytułowany „Cache’owanie i odtwarzanie”Wywołania chmurowego TTS kosztują pieniądze i zabierają czas (~200-500ms w obie strony). Aby uniknąć ponownego pobierania tego samego audio, każdy wygenerowany klip jest cache’owany w pamięci jako blob URL:
Cache key: "elevenlabs:pNInz6obpgDQGcFmaJgB:en:12, Knight f3, check."Cache value: blob:http://localhost/abc123 (the MP3 audio in browser memory)Przy trafieniu w cache odtwarzanie jest natychmiastowe. Klucz cache zawiera provider:voice:language:text, więc zmiana głosu lub języka tworzy osobne wpisy.
Dla partii z dużą liczbą adnotacji można wstępnie załadować cache dla całego drzewa gry w tle. Aplikacja przechodzi przez każdy węzeł, buduje tekst narracji i wysyła sekwencyjne wywołania API, aby wypełnić cache, zanim zaczniesz nawigację.
Współbieżność i anulowanie
Dział zatytułowany „Współbieżność i anulowanie”Szybka nawigacja klawiszami strzałek stwarza problem: jeśli użytkownik przeskoczy 5 ruchów do przodu w szybkim tempie, nie chcesz, żeby 5 nakładających się klipów audio walczyło ze sobą. Rozwiązaniem jest licznik generacji:
const thisGeneration = ++requestGeneration;// ... fetch audio ...if (thisGeneration !== requestGeneration) return; // stale — discardKażde nowe wywołanie speakText() inkrementuje licznik i przerywa wszelkie trwające żądania HTTP przez AbortController. Gdy audio nadchodzi, sprawdza, czy jego generacja jest nadal aktualna. Jeśli użytkownik już przeszedł dalej, odpowiedź jest po cichu odrzucana. Zapewnia to czyste, pozbawione zakłóceń audio nawet przy szybkim przeskakiwaniu między ruchami.
Gdzie TTS integruje się z aplikacją
Dział zatytułowany „Gdzie TTS integruje się z aplikacją”Punktów integracji jest niewiele:
| Plik | Co się dzieje |
|---|---|
src/state/store/tree.ts | Każda funkcja nawigacyjna (goToNext, goToPrevious itp.) wywołuje stopSpeaking(). Gdy automatyczna narracja jest włączona, goToNext wywołuje również speakMoveNarration(). |
src/components/common/Comment.tsx | Ikona głośnika obok każdego komentarza umożliwia ręczne uruchomienie TTS dla tego komentarza. |
src/components/settings/TTSSettings.tsx | Interfejs ustawień do wyboru dostawcy, głosu, języka, głośności, szybkości i wprowadzania kluczy API. |
Gdy TTS jest wyłączony, żaden z tych fragmentów kodu nie jest wykonywany. Aplikacja zachowuje się identycznie jak upstream En Croissant.
Przykłady przepływu danych
Dział zatytułowany „Przykłady przepływu danych”Analiza silnikowa
Dział zatytułowany „Analiza silnikowa”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 flagWyszukiwanie pozycji w bazie danych
Dział zatytułowany „Wyszukiwanie pozycji w bazie danych”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 tableNarracja TTS
Dział zatytułowany „Narracja 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 atomsMapa katalogów
Dział zatytułowany „Mapa katalogów”en-parlant/├── src-tauri/ # BACKEND RUST│ ├── src/│ │ ├── main.rs # Punkt wejścia, rejestracja komend, wtyczki│ │ ├── chess.rs # Analiza silnikowa│ │ ├── game.rs # Zarządzanie grą na żywo│ │ ├── db/ # Baza danych SQLite (największy moduł)│ │ ├── engine/ # Protokół UCI│ │ ├── pgn.rs # Parsowanie PGN│ │ ├── puzzle.rs # Baza zadań│ │ ├── opening.rs # Wyszukiwanie otwarć│ │ └── tts.rs # Systemowe TTS + zarządzanie KittenTTS│ ├── Cargo.toml # Zależności Rust│ ├── tauri.conf.json # Konfiguracja Tauri│ └── capabilities/main.json # Uprawnienia bezpieczeństwa│├── src/ # FRONTEND REACT/TS│ ├── App.tsx # Komponent główny│ ├── state/│ │ ├── atoms.ts # Atomy Jotai (cały stan aplikacji)│ │ └── store/tree.ts # Drzewo gry (Zustand + hooki TTS)│ ├── routes/ # TanStack Router (oparty na plikach)│ ├── components/│ │ ├── boards/ # Szachownica + analiza│ │ ├── panels/ # Panele boczne│ │ ├── databases/ # Interfejs przeglądania bazy danych│ │ ├── common/ # Wyświetlanie komentarzy (z ikoną głośnika TTS)│ │ └── settings/ # Preferencje, ustawienia TTS│ ├── utils/│ │ ├── chess.ts # Logika gry│ │ ├── tts.ts # Silnik TTS (SAN-to-spoken, cache, 5 dostawców)│ │ └── treeReducer.ts # Struktura danych drzewa│ ├── bindings/ # Automatycznie generowane TS z Rust│ └── translation/ # i18n (13 języków)│├── docs/ # Dołączona dokumentacja (wyświetlana w menu Pomoc)├── vite.config.ts # Konfiguracja budowania└── package.json # Zależności frontenduKluczowe wnioski
Dział zatytułowany „Kluczowe wnioski”-
Rust wykonuje ciężką pracę — silniki, baza danych, I/O plików, parsowanie PGN. React nigdy nie dotyka systemu plików ani nie uruchamia procesów bezpośrednio.
-
Bezpieczeństwo typów na granicy — Specta generuje typy TypeScript ze struktur Rust, więc jeśli komenda Rust zmieni swoją sygnaturę, build TypeScript natychmiast się nie powiedzie.
-
Dwa systemy zarządzania stanem — Jotai dla prostego stanu reaktywnego (ustawienia, preferencje interfejsu, stan silnika per zakładka), Zustand dla złożonego stanu domenowego (drzewo gry z rozgałęzieniami i niemutowalnymi aktualizacjami).
-
TTS to problem przetwarzania wstępnego — trudna część to nie wywołanie API mowy, lecz tłumaczenie notacji szachowej i znaczników PGN na czysty, naturalnie brzmiący tekst w wielu językach. Pipeline’y
sanToSpoken()icleanCommentForTTS()to miejsce, gdzie odbywa się prawdziwa praca. -
Pięciu dostawców, jeden interfejs — niezależnie od tego, czy audio pochodzi z ElevenLabs, Google Cloud, KittenTTS, OpenTTS, czy natywnego silnika mowy systemu operacyjnego, reszta aplikacji zawsze wywołuje tylko
speakText(). Wybór dostawcy to przełączenie jednego atomu. -
Budowanie daje pojedynczy plik binarny w
src-tauri/target/release/en-parlant, który zawiera backend Rust + zasoby frontendu zbudowane przez Vite.