Skip to content

Агляд архітэктуры

Версія праграмы: v0.1.1 (форк: DarrellThomas/en-parlant) Стэк: Tauri v2 (Rust) + React 19 (TypeScript) + Vite


Tauri — гэта фрэймворк для стварэння настольных праграм. Замест таго каб пастаўляць поўны браўзер, як гэта робіць Electron, Tauri выкарыстоўвае ўбудаваны ў аперацыйную сістэму webview для інтэрфейсу і працэс на Rust для бэкенда. У выніку атрымліваецца невялікі і хуткі бінарны файл.

Дзве паловы ўзаемадзейнічаюць праз IPC (міжпрацэсная камунікацыя):

+---------------------------+ 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 апрацоўвае ўсё, што патрабуе хуткадзейнасці або доступу да сістэмы.

Рэгіструе ~50 каманд, якія можа выклікаць фронтэнд, ініцыялізуе плагіны (файлавая сістэма, дыялогі, HTTP, shell, лагіраванне, абнаўленне) і запускае акно праграмы.

Каманды вызначаюцца з дапамогай макраса:

#[tauri::command]
async fn get_best_moves(id: String, engine: String, ...) -> Result<...> {
// spawn UCI engine, return analysis
}

Крэйт specta аўтаматычна генеруе вызначэнні тыпаў TypeScript з гэтых функцый Rust, так што фронтэнд атрымлівае поўную тыпабяспеку без якіх-небудзь ручных намаганняў.

МодульШто ён робіць
db/mod.rsБаза даных SQLite праз Diesel ORM — запыты гульняў, статыстыка гульцоў, імпарт, пошук пазіцый
game.rsРухавік жывой гульні — кіруе гульнямі рухавік-супраць-чалавека і рухавік-супраць-рухавіка, кантроль часу, валідацыя хадоў
chess.rsАналіз рухавіком — запускае рухавікі UCI, перадае вынікі лепшых хадоў на фронтэнд праз падзеі
engine/Рэалізацыя пратаколу UCI — запуск працэсаў, каналы stdin/stdout, падтрымка multi-PV
pgn.rsЧытанне/запіс/такенізацыя файлаў PGN
opening.rsПошук назвы дэбюту па FEN (бінарныя даныя ўбудаваны ў праграму)
puzzle.rsБаза задач Lichess — выпадковы доступ з адлюстраваннем у памяць
fs.rsСпампоўка файлаў з аднаўленнем, устаноўка правоў на выкананне
sound.rsЛакальны HTTP-сервер для патокавага аўдыё (абыходны шлях для аўдыё ў Linux)
tts.rsСістэмны TTS праз speech-dispatcher (Linux) / натыўныя API маўлення АС, плюс кіраванне серверам KittenTTS
oauth.rsПаток OAuth2 для прывязкі ўліковых запісаў Lichess/Chess.com
  • Асінхроннасць паўсюль: асяроддзе выканання Tokio, неблакіруючы ўвод/вывад
  • Канкурэнтны стан: DashMap (канкурэнтны HashMap) для працэсаў рухавікоў, злучэнняў з БД, кэшаў
  • Пул злучэнняў: r2d2 кіруе пуламі злучэнняў SQLite
  • Пошук з адлюстраваннем у памяць: пошук пазіцый праз mmap-індэкс для імгненных вынікаў
  • Патокавая перадача падзей: Rust выпускае падзеі (лепшыя хады, такты гадзінніка, канец гульні), якія React слухае ў рэальным часе

vite.config.ts наладжвае:

  • Плагін React з кампілятарам Babel
  • Плагін TanStack Router — аўтаматычна генеруе дрэва маршрутаў з тэчкі routes/
  • Vanilla Extract — CSS-in-JS без выканання ў runtime
  • Псеўданім шляху: @ адлюстроўваецца на ./src
  • Сервер распрацоўкі на порце 1420

Працэс зборкі:

pnpm dev → Vite on :1420 + Tauri opens webview pointing to it
pnpm build → tsc (typecheck) → vite build (bundle to dist/) → tauri build (native binary)

Каранёвы кампанент:

  • Ініцыялізуе плагіны Tauri (log, process, updater)
  • Загружае карыстальніцкія налады з пастаянных атамаў
  • Наладжвае тэму інтэрфейсу Mantine
  • Рэгіструе маршрутызатар
  • Правярае наяўнасць абнаўленняў праграмы

Атамы Jotai (src/state/atoms.ts) — лёгкі рэактыўны стан:

КатэгорыяПрыклады
УкладкіtabsAtom, activeTabAtom (шматдакументны інтэрфейс)
КаталогіstoredDocumentDirAtom, storedDatabasesDirAtom
Налады UIprimaryColorAtom, fontSizeAtom, pieceSetAtom
РухавікengineMovesFamily, engineProgressFamily (для кожнай укладкі праз atomFamily)
TTSttsEnabledAtom, ttsProviderAtom, ttsVoiceIdAtom, ttsVolumeAtom, ttsSpeedAtom, ttsLanguageAtom

Атамы з atomWithStorage() аўтаматычна захоўваюцца ў localStorage.

Сховішчы Zustand для складанага даменнага стану:

  • src/state/store/tree.ts — навігацыя па дрэве гульні, разгалінаванне хадоў, анатацыі, каментарыі. Выкарыстоўвае Immer для нязменных абнаўленняў.
  • src/state/store/database.ts — фільтры прагляду базы даных, абраная гульня, пагінацыя

Маршрутызацыя: TanStack Router

Section titled “Маршрутызацыя: TanStack Router”

Файлавая маршрутызацыя ў 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
ГрупаПрызначэнне
boards/Шахматная дошка (chessground), увод хадоў, шкала ацэнкі, адлюстраванне аналізу, мадальнае акно ператварэння, маляванне стрэлак
panels/Бакавыя панэлі: аналіз рухавіком (BestMoves), пошук пазіцый у базе, рэдагаванне анатацый, інфармацыя пра гульню, рэжым практыкі
databases/UI базы даных: табліца гульняў, табліца гульцоў, карткі дэталяў, фільтраванне
settings/Формы налад, шляхі да рухавікоў, налады TTS
home/Карткі ўліковых запісаў, UI імпарту
common/Агульныя: TreeStateContext, адлюстраванне матэрыялу, іконка дынаміка каментарыя
tabs/Панэль з мноствам ўкладак

Як фронтэнд выклікае Rust

Section titled “Як фронтэнд выклікае Rust”

Specta генеруе прывязкі TypeScript у 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...
}

Кампаненты React выклікаюць іх як звычайныя асінхронныя функцыі:

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

Падзеі (патокавая перадача, з Rust у React)

Section titled “Падзеі (патокавая перадача, з Rust у React)”

Для даных у рэальным часе (аналіз рухавіком, такты гадзінніка, хады гульні):

Rust: app.emit("best_moves_payload", BestMovesPayload { depth: 24, ... })
React: listen("best_moves_payload", (event) => updateBestMoves(event.payload))

Праграма выкарыстоўвае некалькі афіцыйных плагінаў для сістэмнага доступу:

ПлагінПрызначэнне
@tauri-apps/plugin-fsЧытанне/запіс файлаў
@tauri-apps/plugin-dialogВыбар файлаў, акны паведамленняў
@tauri-apps/plugin-httpHTTP-кліент (спампоўка рухавікоў, хмарны TTS)
@tauri-apps/plugin-shellЗапуск рухавікоў UCI
@tauri-apps/plugin-updaterАўтаматычная праверка абнаўленняў
@tauri-apps/plugin-logСтруктураванае лагіраванне
@tauri-apps/plugin-osВызначэнне CPU/RAM

Сінтэз маўлення (TTS): уводзіны

Section titled “Сінтэз маўлення (TTS): уводзіны”

En Parlant~ можа чытаць уголас шахматныя хады і каментарыі, калі вы прагортваеце гульню. Гэты раздзел тлумачыць, як пабудавана сістэма TTS — канвеер папярэдняй апрацоўкі, архітэктура правайдараў і стратэгія кэшавання. Інструкцыі па наладжванні глядзіце ў кіраўніцтвах TTS у меню TTS.

Як працуе TTS (кароткая версія)

Section titled “Як працуе TTS (кароткая версія)”

Сінтэз маўлення (text-to-speech) пераўтварае пісьмовы тэкст у гукавое аўдыё. Сучасныя сістэмы TTS пабудаваны на глыбокіх нейронных сетках, навучаных на тысячах гадзін чалавечага маўлення. Мадэль вывучае сувязь паміж тэкстам (літары, словы, пунктуацыя) і акустычнымі характарыстыкамі маўлення (вышыня тону, тэмп, акцэнты, паўзы на дыханне). Падчас інферэнсу вы адпраўляеце тэкст і атрымліваеце назад гукавую хвалевую форму.

Ёсць два асноўныя падыходы:

  • Хмарны TTS — тэкст адпраўляецца на аддалены сервер (Google, ElevenLabs і г.д.), які запускае вялікую нейронную сетку на GPU і вяртае аўдыё. Выдатная якасць, але патрабуе інтэрнэту і мае кошт за запыт (хоць большасць правайдараў прапаноўваюць бясплатныя ўзроўні).

  • Лакальны TTS — мадэль працуе непасрэдна на вашай машыне. Інтэрнэт не патрэбны, няма кошту за запыт, і ваш тэкст ніколі не пакідае ваш камп’ютар. Нядаўнія адкрытыя мадэлі (як Kokoro і Piper) значна скарацілі разрыў у якасці.

Калі вам цікава, як мадэлі TTS працуюць унутры, HuggingFace (huggingface.co) размяшчае сотні адкрытых мадэлей сінтэзу маўлення, якія можна даследаваць, спампоўваць і запускаць лакальна. Шукайце “text-to-speech”, каб знайсці мадэлі ад лёгкіх варыянтаў для CPU да найноўшых даследчых мадэлей.

Архітэктура правайдараў

Section titled “Архітэктура правайдараў”

Асноўная рэалізацыя TTS знаходзіцца ў src/utils/tts.ts. Яна пабудавана вакол адзінага публічнага інтэрфейсу (speakText()) са зменнымі бэкендамі. Астатняя частка праграмы ніколі не ведае і не клапоціцца пра тое, які правайдар актыўны — яна проста выклікае speakText(), і гучыць аўдыё.

Падтрымліваюцца пяць правайдараў:

ПравайдарТыпБэкенд
ElevenLabsХмарныНейронныя галасы праз REST API. Вяртае MP3.
Google Cloud TTSХмарныГаласы WaveNet праз REST API. Вяртае MP3 у кадзіроўцы base64.
KittenTTSЛакальныУбудаваны TTS-сервер, аўтаматычна запускаецца бэкендам Rust. Узаемадзейнічае праз HTTP на localhost.
OpenTTSЛакальныСамастойна размешчаны TTS-сервер. Падтрымлівае шмат рухавікоў (espeak, MaryTTS, Piper і інш.).
System TTSЛакальныНатыўны рухавік маўлення АС праз каманды Rust/Tauri (speech-dispatcher на Linux, SAPI на Windows, AVSpeechSynthesizer на macOS).

Выбар правайдара захоўваецца ў адзіным атаме Jotai (ttsProviderAtom). Пераключэнне правайдараў імгненнае — змяніце атам, і наступны выклік speakText() накіруецца да новага бэкенда.

Выклік: шахматная натацыя — гэта не англійская мова

Section titled “Выклік: шахматная натацыя — гэта не англійская мова”

Шахматныя хады запісваюцца ў стандартнай алгебраічнай натацыі (SAN): Nf3, Bxe5+, O-O-O, e8=Q#. Калі падаць гэта напрамую ў рухавік TTS, атрымаецца бязглуздзіца — ён можа паспрабаваць вымавіць “Nf3” як слова або прачытаць “O-O-O” як “о о о”.

Рашэннем з’яўляецца канвеер папярэдняй апрацоўкі, які перакладае шахматную натацыю ў натуральную мову перад тым, як яна трапіць да рухавіка 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"

Функцыя sanToSpoken() выкарыстоўвае супастаўленне шаблонаў з рэгулярнымі выразамі для разбору любога радка SAN на яго кампаненты (фігура, адназначнасць, узяцце, мэтавая клетка, ператварэнне, шах/мат) і збірае іх зноў з выкарыстаннем натуральнай мовы з табліцы слоўніка.

Падтрымка шматмоўнасці

Section titled “Падтрымка шматмоўнасці”

Шахматны слоўнік перакладзены на шмат моў (ангельская, французская, іспанская, нямецкая, японская, руская, кітайская, карэйская і іншыя). Табліца CHESS_VOCAB адлюстроўвае кожны тэрмін:

English: "Knight takes e5, check"
French: "Cavalier prend e5, échec"
German: "Springer schlägt e5, Schach"
Japanese: "ナイト テイクス e5, チェック"
Russian: "Конь берёт e5, шах"

Моўная налада вызначае, якая табліца слоўніка выкарыстоўваецца для папярэдняй апрацоўкі і які голас/акцэнт рухавік TTS выкарыстоўвае для сінтэзу.

Анатацыі гульняў часта ўтрымліваюць разметку, спецыфічную для PGN, якая гучала б жудасна пры чытанні ўголас:

Raw comment: "BLUNDER. 7.Nf3 was better [%eval -2.3] [%cal Gg1f3]"
After cleaning: "7, Knight f3 was better"

Функцыя cleanCommentForTTS():

  1. Выдаляе тэгі PGN: [%eval ...], [%csl ...], [%cal ...], [%clk ...]
  2. Выдаляе дублюючыя словы анатацый (калі ”??” ужо сказала “Blunder”)
  3. Разгортвае ўбудаваны SAN у тэксце: "7.Nf3 controls e5""7, Knight f3 controls e5"
  4. Выпраўляе шахматныя тэрміны, якія рухавікі TTS няправільна вымаўляюць (напр., “en prise” → “on preez”)
  5. Разгортвае скарачэнні фігур у тэксце: "R vs R""Rook versus Rook"

Стварэнне поўнай нарацыі

Section titled “Стварэнне поўнай нарацыі”

Калі вы пераходзіце да новага хаду, buildNarration() збірае поўны тэкст для агучвання з трох крыніц:

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

Падвойны прабел паміж часткамі дае рухавікам TTS натуральную паўзу для дыхання.

Кэшаванне і прайграванне

Section titled “Кэшаванне і прайграванне”

Выклікі хмарнага TTS каштуюць грошай і займаюць час (~200-500 мс у абодва бакі). Каб пазбегнуць паўторнага запыту таго ж аўдыё, кожны згенераваны кліп кэшуецца ў памяці як blob URL:

Cache key: "elevenlabs:pNInz6obpgDQGcFmaJgB:en:12, Knight f3, check."
Cache value: blob:http://localhost/abc123 (the MP3 audio in browser memory)

Пры трапленні ў кэш прайграванне імгненнае. Ключ кэша складаецца з provider:voice:language:text, так што пераключэнне галасоў або моў стварае асобныя запісы.

Для гульняў з вялікай колькасцю анатацый можна папярэдне закэшаваць усё дрэва гульні ў фонавым рэжыме. Праграма абыходзіць кожны вузел, стварае тэкст нарацыі і робіць паслядоўныя выклікі API, каб запоўніць кэш да таго, як вы пачнёце навігацыю.

Канкурэнтнасць і адмена

Section titled “Канкурэнтнасць і адмена”

Хуткая навігацыя клавішамі-стрэлкамі стварае праблему: калі карыстальнік хутка робіць 5 крокаў наперад, вы не хочаце, каб 5 аўдыёкліпаў накладваліся адзін на аднаго. Рашэнне — лічыльнік генерацый:

const thisGeneration = ++requestGeneration;
// ... fetch audio ...
if (thisGeneration !== requestGeneration) return; // stale — discard

Кожны новы выклік speakText() павялічвае лічыльнік і спыняе любы незавершаны HTTP-запыт праз AbortController. Калі аўдыё прыходзіць, яно правярае, ці з’яўляецца яго генерацыя ўсё яшчэ бягучай. Калі карыстальнік ужо рухаецца далей, адказ ціха адкідваецца. Гэта забяспечвае чыстае аўдыё без збояў нават пры хуткім праклікванні хадоў.

Дзе TTS убудоўваецца ў праграму

Section titled “Дзе TTS убудоўваецца ў праграму”

Кропак інтэграцыі мінімум:

ФайлШто адбываецца
src/state/store/tree.tsКожная функцыя навігацыі (goToNext, goToPrevious і інш.) выклікае stopSpeaking(). Калі аўтанарацыя ўключана, goToNext таксама выклікае speakMoveNarration().
src/components/common/Comment.tsxІконка дынаміка побач з кожным каментарыем дазваляе ўручную запусціць TTS для гэтага каментарыя.
src/components/settings/TTSSettings.tsxUI налад для выбару правайдара, голасу, мовы, гучнасці, хуткасці і ўводу API-ключоў.

Калі TTS выключаны, нічога з гэтага кода не выконваецца. Праграма паводзіць сябе ідэнтычна арыгінальнаму 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

Пошук пазіцыі ў базе даных

Section titled “Пошук пазіцыі ў базе даных”
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 бярэ на сябе цяжкую працу — рухавікі, база даных, файлавы ўвод/вывад, разбор PGN. React ніколі не звяртаецца да файлавай сістэмы і не запускае працэсы напрамую.

  2. Тыпабяспека праз мяжу — Specta генеруе тыпы TypeScript са структур Rust, так што калі каманда Rust зменіць сваю сігнатуру, зборка TypeScript адразу ж зламаецца.

  3. Дзве сістэмы стану — Jotai для простага рэактыўнага стану (налады, перавагі UI, стан рухавіка для кожнай укладкі), Zustand для складанага даменнага стану (дрэва гульні з разгалінаваннем і нязменнымі абнаўленнямі).

  4. TTS — гэта задача папярэдняй апрацоўкі — складанасць не ў выкліку API маўлення, а ў перакладзе шахматнай натацыі і разметкі PGN у чысты, натуральна гучны тэкст на шматлікіх мовах. Канвееры sanToSpoken() і cleanCommentForTTS() — вось дзе адбываецца сапраўдная праца.

  5. Пяць правайдараў, адзін інтэрфейс — незалежна ад таго, адкуль прыходзіць аўдыё: з ElevenLabs, Google Cloud, KittenTTS, OpenTTS або натыўнага рухавіка маўлення вашай АС — астатняя частка праграмы заўсёды выклікае толькі speakText(). Выбар правайдара — гэта адно пераключэнне атама.

  6. Зборка стварае адзіны бінарны файл у src-tauri/target/release/en-parlant, які аб’ядноўвае бэкенд Rust + фронтэнд-асеты, сабраныя Vite.