Архітектурний огляд
Версія додатку: v0.1.1 (форк: DarrellThomas/en-parlant) Стек: Tauri v2 (Rust) + React 19 (TypeScript) + Vite
Що таке Tauri?
Section titled “Що таке Tauri?”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: src-tauri/src/
Section titled “Сторона Rust: src-tauri/src/”Rust бере на себе все, що потребує швидкості або доступу до системи.
Точка входу: main.rs
Section titled “Точка входу: main.rs”Реєструє ~50 команд, які може викликати фронтенд, ініціалізує плагіни (файлова система, діалоги, HTTP, shell, логування, оновлення) та запускає вікно додатку.
Команди визначаються за допомогою макроса:
#[tauri::command]async fn get_best_moves(id: String, engine: String, ...) -> Result<...> { // spawn UCI engine, return analysis}Крейт specta автоматично генерує визначення типів TypeScript з цих функцій Rust, тому фронтенд отримує повну типобезпеку без жодних ручних зусиль.
Ключові модулі
Section titled “Ключові модулі”| Модуль | Призначення |
|---|---|
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 |
Архітектурні патерни
Section titled “Архітектурні патерни”- Асинхронність скрізь: середовище виконання Tokio, неблокуючий I/O
- Конкурентний стан:
DashMap(конкурентна HashMap) для процесів рушіїв, з’єднань з БД, кешів - Пулінг з’єднань: r2d2 керує пулами з’єднань SQLite
- Пошук через відображення в пам’ять: пошук позицій через mmap’d бінарний індекс для миттєвих результатів
- Потокова передача подій: Rust генерує події (найкращі ходи, тіки годинника, кінець гри), які React слухає в реальному часі
Сторона React/TypeScript: src/
Section titled “Сторона React/TypeScript: src/”Конвеєр збірки: Vite
Section titled “Конвеєр збірки: Vite”vite.config.ts налаштовує:
- Плагін React з компілятором Babel
- Плагін TanStack Router — автоматично генерує дерево маршрутів з папки
routes/ - Vanilla Extract — CSS-in-JS без рантайму
- Псевдонім шляху:
@вказує на./src - Dev-сервер на порту 1420
Процес збірки:
pnpm dev → Vite on :1420 + Tauri opens webview pointing to itpnpm build → tsc (typecheck) → vite build (bundle to dist/) → tauri build (native binary)Точка входу: App.tsx
Section titled “Точка входу: App.tsx”Кореневий компонент:
- Ініціалізує плагіни Tauri (логування, процеси, оновлення)
- Завантажує налаштування користувача з персистентних атомів
- Налаштовує тему UI Mantine
- Реєструє маршрутизатор
- Перевіряє наявність оновлень додатку
Управління станом
Section titled “Управління станом”Атоми Jotai (src/state/atoms.ts) — легковісний реактивний стан:
| Категорія | Приклади |
|---|---|
| Вкладки | tabsAtom, activeTabAtom (багатодокументний інтерфейс) |
| Директорії | storedDocumentDirAtom, storedDatabasesDirAtom |
| Налаштування UI | primaryColorAtom, fontSizeAtom, pieceSetAtom |
| Рушій | engineMovesFamily, engineProgressFamily (для кожної вкладки через atomFamily) |
| TTS | ttsEnabledAtom, 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Компоненти: src/components/
Section titled “Компоненти: src/components/”| Група | Призначення |
|---|---|
boards/ | Шахівниця (chessground), введення ходів, шкала оцінки, відображення аналізу, модальне вікно перетворення, малювання стрілок |
panels/ | Бічні панелі: аналіз рушієм (BestMoves), пошук позицій у базі даних, редагування анотацій, інформація про гру, режим практики |
databases/ | UI бази даних: таблиця ігор, таблиця гравців, картки деталей, фільтрація |
settings/ | Форми налаштувань, шляхи до рушіїв, налаштування TTS |
home/ | Картки акаунтів, UI імпорту |
common/ | Спільне: TreeStateContext, відображення матеріалу, іконка динаміка для коментарів |
tabs/ | Панель вкладок |
Як фронтенд викликає Rust
Section titled “Як фронтенд викликає Rust”Команди (запит/відповідь)
Section titled “Команди (запит/відповідь)”Specta генерує TypeScript-прив’язки в 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 викликають їх як звичайні асинхронні функції:
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
Section titled “Плагіни Tauri”Додаток використовує кілька офіційних плагінів для доступу до системи:
| Плагін | Призначення |
|---|---|
@tauri-apps/plugin-fs | Читання/запис файлів |
@tauri-apps/plugin-dialog | Вибір файлів, діалогові вікна |
@tauri-apps/plugin-http | HTTP-клієнт (завантаження рушіїв, хмарний 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 (коротка версія)”Синтез мовлення перетворює написаний текст на звукове мовлення. Сучасні системи 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-рушій використовує для синтезу.
Очищення коментарів
Section titled “Очищення коментарів”Анотації ігор часто містять PGN-специфічну розмітку, яка звучала б жахливо при озвученні:
Raw comment: "BLUNDER. 7.Nf3 was better [%eval -2.3] [%cal Gg1f3]"After cleaning: "7, Knight f3 was better"Функція cleanCommentForTTS():
- Видаляє PGN-теги:
[%eval ...],[%csl ...],[%cal ...],[%clk ...] - Видаляє дублікати слів анотацій (коли ”??” вже сказало “Blunder”)
- Розгортає вбудовану SAN у прозі:
"7.Nf3 controls e5"→"7, Knight f3 controls e5" - Виправляє шахові терміни, які TTS-рушії вимовляють неправильно (наприклад, “en prise” → “on preez”)
- Розгортає скорочення фігур у прозі:
"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.tsx | UI налаштувань для вибору провайдера, голосу, мови, гучності, швидкості та введення API-ключів. |
Коли TTS вимкнено, жоден з цього коду не виконується. Додаток працює ідентично до основного En Croissant.
Приклади потоків даних
Section titled “Приклади потоків даних”Аналіз рушієм
Section titled “Аналіз рушієм”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Нарація TTS
Section titled “Нарація 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 atomsКарта директорій
Section titled “Карта директорій”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Ключові висновки
Section titled “Ключові висновки”-
Rust виконує важку роботу — рушії, база даних, файловий I/O, парсинг PGN. React ніколи не звертається до файлової системи та не запускає процеси напряму.
-
Типобезпека через межу — Specta генерує типи TypeScript зі структур Rust, тому якщо Rust-команда змінить свою сигнатуру, збірка TypeScript одразу зламається.
-
Дві системи стану — Jotai для простого реактивного стану (налаштування, UI-преференції, стан рушія для кожної вкладки), Zustand для складного доменного стану (дерево гри з розгалуженням та незмінними оновленнями).
-
TTS — це задача попередньої обробки — складність полягає не у виклику API мовлення, а в перекладі шахової нотації та PGN-розмітки у чистий, природно звучний текст багатьма мовами. Конвеєри
sanToSpoken()таcleanCommentForTTS()— це те, де відбувається справжня робота. -
П’ять провайдерів, один інтерфейс — незалежно від того, чи аудіо надходить від ElevenLabs, Google Cloud, KittenTTS, OpenTTS або мовленнєвого рушія вашої ОС, решта додатку завжди викликає лише
speakText(). Вибір провайдера — це перемикання одного атома. -
Збірка створює один бінарний файл за шляхом
src-tauri/target/release/en-parlant, який об’єднує бекенд Rust та зібрані Vite ресурси фронтенду.