Przejdź do głównej zawartości

Wprowadzenie do architektury

Wersja aplikacji: v0.1.1 (fork: DarrellThomas/en-parlant) Stos technologiczny: Tauri v2 (Rust) + React 19 (TypeScript) + Vite


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/

Rust obsługuje wszystko, co wymaga szybkości lub dostępu do systemu.

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.

ModułOpis
db/mod.rsBaza danych SQLite przez Diesel ORM — zapytania o partie, statystyki graczy, importy, wyszukiwanie pozycji
game.rsSilnik gry na żywo — zarządza grami silnik-kontra-człowiek i silnik-kontra-silnik, kontrolą czasu, walidacją ruchów
chess.rsAnaliza 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.rsOdczyt/zapis/tokenizacja plików PGN
opening.rsWyszukiwanie nazw otwarć na podstawie FEN (dane binarne wbudowane w aplikację)
puzzle.rsBaza zadań Lichess — losowy dostęp przez mapowanie pamięci
fs.rsPobieranie plików z wznawianiem, ustawianie uprawnień do wykonywania
sound.rsLokalny serwer HTTP do strumieniowania audio (obejście problemu z dźwiękiem na Linuksie)
tts.rsSystemowe TTS przez speech-dispatcher (Linux) / natywne API mowy systemu operacyjnego, plus zarządzanie serwerem KittenTTS
oauth.rsPrzepływ OAuth2 do łączenia kont Lichess/Chess.com
  • 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

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

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

Atomy Jotai (src/state/atoms.ts) — lekki, reaktywny stan:

KategoriaPrzykłady
ZakładkitabsAtom, activeTabAtom (interfejs wielodokumentowy)
KatalogistoredDocumentDirAtom, storedDatabasesDirAtom
Preferencje interfejsuprimaryColorAtom, fontSizeAtom, pieceSetAtom
SilnikengineMovesFamily, engineProgressFamily (per zakładka przez atomFamily)
TTSttsEnabledAtom, 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 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 management
GrupaPrzeznaczenie
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

Specta generuje wiązania TypeScript w 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...
}

Komponenty React wywołują je jak zwykłe funkcje asynchroniczne:

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

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

Aplikacja korzysta z kilku oficjalnych wtyczek zapewniających dostęp do systemu:

WtyczkaPrzeznaczenie
@tauri-apps/plugin-fsOdczyt/zapis plików
@tauri-apps/plugin-dialogOkna wyboru plików, okna komunikatów
@tauri-apps/plugin-httpKlient HTTP (pobieranie silników, chmurowe TTS)
@tauri-apps/plugin-shellUruchamianie silników UCI
@tauri-apps/plugin-updaterAutomatyczne sprawdzanie aktualizacji
@tauri-apps/plugin-logStrukturalne logowanie
@tauri-apps/plugin-osWykrywanie CPU/RAM

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.

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.

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:

DostawcaTypBackend
ElevenLabsChmurowyNeuronowe głosy przez REST API. Zwraca MP3.
Google Cloud TTSChmurowyGłosy WaveNet przez REST API. Zwraca MP3 zakodowany w base64.
KittenTTSLokalnyDołączony serwer TTS, automatycznie uruchamiany przez backend Rust. Komunikuje się przez HTTP na localhost.
OpenTTSLokalnySamodzielnie hostowany serwer TTS. Obsługuje wiele silników (espeak, MaryTTS, Piper itp.).
System TTSLokalnyNatywny 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.

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.

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.

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

  1. Usuwa tagi PGN: [%eval ...], [%csl ...], [%cal ...], [%clk ...]
  2. Usuwa zduplikowane słowa adnotacji (gdy „??” już odczytało „Blunder”)
  3. Rozwija inline SAN w tekście: "7.Nf3 controls e5""7, Knight f3 controls e5"
  4. Poprawia terminy szachowe, które silniki TTS źle wymawiają (np. „en prise” → „on preez”)
  5. Rozwija skróty figur w tekście: "R vs R""Rook versus Rook"

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

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

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

Każ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.

Punktów integracji jest niewiele:

PlikCo się dzieje
src/state/store/tree.tsKaż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.tsxIkona głośnika obok każdego komentarza umożliwia ręczne uruchomienie TTS dla tego komentarza.
src/components/settings/TTSSettings.tsxInterfejs 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.


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 # 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 frontendu

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

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

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

  4. 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() i cleanCommentForTTS() to miejsce, gdzie odbywa się prawdziwa praca.

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

  6. Budowanie daje pojedynczy plik binarny w src-tauri/target/release/en-parlant, który zawiera backend Rust + zasoby frontendu zbudowane przez Vite.