Обзор архитектуры
Версия приложения: v0.1.1 (форк: DarrellThomas/en-parlant) Стек: Tauri v2 (Rust) + React 19 (TypeScript) + Vite
Что такое Tauri?
Заголовок раздела «Что такое 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/
Заголовок раздела «Сторона Rust: src-tauri/src/»Rust отвечает за всё, что требует высокой производительности или доступа к системным ресурсам.
Точка входа: main.rs
Заголовок раздела «Точка входа: 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, обеспечивая фронтенду полную типобезопасность без каких-либо ручных усилий.
Ключевые модули
Заголовок раздела «Ключевые модули»| Модуль | Назначение |
|---|---|
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 слушает в реальном времени
Сторона React/TypeScript: src/
Заголовок раздела «Сторона React/TypeScript: src/»Система сборки: Vite
Заголовок раздела «Система сборки: 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
Заголовок раздела «Точка входа: App.tsx»Корневой компонент:
- Инициализирует плагины Tauri (log, process, updater)
- Загружает пользовательские настройки из персистентных атомов
- Устанавливает тему Mantine UI
- Регистрирует маршрутизатор
- Проверяет наличие обновлений приложения
Управление состоянием
Заголовок раздела «Управление состоянием»Атомы Jotai (src/state/atoms.ts) — легковесное реактивное состояние:
| Категория | Примеры |
|---|---|
| Вкладки | tabsAtom, activeTabAtom (многодокументный интерфейс) |
| Директории | storedDocumentDirAtom, storedDatabasesDirAtom |
| Настройки интерфейса | 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
Заголовок раздела «Маршрутизация: 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/
Заголовок раздела «Компоненты: src/components/»| Группа | Назначение |
|---|---|
boards/ | Шахматная доска (chessground), ввод ходов, полоса оценки, отображение анализа, модальное окно превращения, рисование стрелок |
panels/ | Боковые панели: анализ движком (BestMoves), поиск позиций в базе данных, редактирование аннотаций, информация о партии, режим практики |
databases/ | Интерфейс базы данных: таблица партий, таблица игроков, карточки деталей, фильтрация |
settings/ | Формы настроек, пути к движкам, настройки TTS |
home/ | Карточки аккаунтов, интерфейс импорта |
common/ | Общие компоненты: TreeStateContext, отображение материала, иконка озвучивания комментариев |
tabs/ | Панель мультивкладок |
Как фронтенд вызывает Rust
Заголовок раздела «Как фронтенд вызывает Rust»Команды (запрос/ответ)
Заголовок раздела «Команды (запрос/ответ)»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)
Заголовок раздела «События (потоковая передача, Rust → React)»Для данных в реальном времени (анализ движком, тики часов, ходы партии):
Rust: app.emit("best_moves_payload", BestMovesPayload { depth: 24, ... }) ↓React: listen("best_moves_payload", (event) => updateBestMoves(event.payload))Плагины Tauri
Заголовок раздела «Плагины 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): введение
Заголовок раздела «Синтез речи (TTS): введение»En Parlant~ может озвучивать шахматные ходы и комментарии по мере вашего перемещения по партии. В этом разделе объясняется устройство системы TTS — конвейер предобработки, архитектура провайдеров и стратегия кэширования. Инструкции по настройке см. в руководствах по TTS в меню TTS.
Как работает TTS (краткая версия)
Заголовок раздела «Как работает TTS (краткая версия)»Синтез речи преобразует письменный текст в звучащую речь. Современные системы TTS построены на глубоких нейронных сетях, обученных на тысячах часов человеческой речи. Модель изучает связь между текстом (буквы, слова, пунктуация) и акустическими характеристиками речи (высота тона, ритм, ударение, дыхательные паузы). На этапе инференса вы подаёте текст и получаете звуковую волну.
Существуют два основных подхода:
-
Облачный TTS — текст отправляется на удалённый сервер (Google, ElevenLabs и т.д.), который запускает крупную нейронную сеть на GPU и возвращает аудио. Отличное качество, но требуется интернет и есть стоимость за каждый запрос (хотя большинство провайдеров предлагают бесплатные тарифы).
-
Локальный TTS — модель работает непосредственно на вашем компьютере. Не нужен интернет, нет стоимости за запрос, и ваш текст никогда не покидает ваш компьютер. Современные модели с открытым исходным кодом (такие как Kokoro и Piper) значительно сократили разрыв в качестве.
Если вас интересует, как модели TTS работают изнутри, на HuggingFace (huggingface.co) размещены сотни моделей синтеза речи с открытым исходным кодом, которые вы можете изучить, скачать и запустить локально. Ищите «text-to-speech», чтобы найти модели от лёгких вариантов для CPU до передовых исследовательских моделей.
Архитектура провайдеров
Заголовок раздела «Архитектура провайдеров»Основная реализация 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() направится к новому бэкенду.
Проблема: шахматная нотация — это не естественный язык
Заголовок раздела «Проблема: шахматная нотация — это не естественный язык»Шахматные ходы записываются в стандартной алгебраической нотации (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 на составляющие (фигура, уточнение, взятие, поле назначения, превращение, шах/мат) и собирает их заново, используя естественный язык из таблицы словаря.
Поддержка нескольких языков
Заголовок раздела «Поддержка нескольких языков»Шахматный словарь переведён на множество языков (английский, французский, испанский, немецкий, японский, русский, китайский, корейский и другие). Таблица 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():
- Удаляет PGN-теги:
[%eval ...],[%csl ...],[%cal ...],[%clk ...] - Убирает дублирующиеся слова аннотаций (когда «??» уже произнесло «Грубая ошибка»)
- Раскрывает встроенную SAN в тексте:
"7.Nf3 controls e5"→"7, Knight f3 controls e5" - Исправляет шахматные термины, которые TTS-движки неправильно произносят (например, «en prise» → «on preez»)
- Раскрывает сокращения фигур в тексте:
"R vs R"→"Rook versus Rook"
Формирование полного озвучивания
Заголовок раздела «Формирование полного озвучивания»Когда вы переходите к новому ходу, 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-движков.
Кэширование и воспроизведение
Заголовок раздела «Кэширование и воспроизведение»Вызовы облачного 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-запросы для заполнения кэша до того, как вы начнёте навигацию.
Параллельность и отмена
Заголовок раздела «Параллельность и отмена»Быстрая навигация клавишами-стрелками создаёт проблему: если пользователь быстро шагнёт вперёд 5 раз, не хочется, чтобы 5 аудиофрагментов перекрывали друг друга. Решение — счётчик поколений:
const thisGeneration = ++requestGeneration;// ... fetch audio ...if (thisGeneration !== requestGeneration) return; // stale — discardКаждый новый вызов speakText() увеличивает счётчик и отменяет все текущие HTTP-запросы через AbortController. Когда аудио приходит, проверяется, является ли его поколение всё ещё актуальным. Если пользователь уже перешёл дальше, ответ тихо отбрасывается. Это обеспечивает чистое воспроизведение без артефактов даже при быстром прокликивании ходов.
Точки интеграции TTS в приложении
Заголовок раздела «Точки интеграции TTS в приложении»Точки интеграции минимальны:
| Файл | Что происходит |
|---|---|
src/state/store/tree.ts | Каждая функция навигации (goToNext, goToPrevious и т.д.) вызывает stopSpeaking(). Когда автоозвучивание включено, goToNext также вызывает speakMoveNarration(). |
src/components/common/Comment.tsx | Иконка динамика рядом с каждым комментарием позволяет вручную запустить TTS для этого комментария. |
src/components/settings/TTSSettings.tsx | Интерфейс настроек для выбора провайдера, голоса, языка, громкости, скорости и ввода 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Поиск позиции в базе данных
Заголовок раздела «Поиск позиции в базе данных»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
Заголовок раздела «Озвучивание 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Карта директорий
Заголовок раздела «Карта директорий»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Ключевые выводы
Заголовок раздела «Ключевые выводы»-
Rust берёт на себя тяжёлую работу — движки, база данных, файловый ввод/вывод, разбор PGN. React никогда не обращается к файловой системе и не запускает процессы напрямую.
-
Типобезопасность через границу — Specta генерирует типы TypeScript из структур Rust, поэтому если Rust-команда изменит свою сигнатуру, сборка TypeScript немедленно сломается.
-
Две системы управления состоянием — Jotai для простого реактивного состояния (настройки, предпочтения интерфейса, состояние движка по вкладкам), Zustand для сложного доменного состояния (дерево партии с ветвлением и иммутабельными обновлениями).
-
TTS — это задача предобработки — сложность не в вызове API синтеза речи, а в переводе шахматной нотации и PGN-разметки в чистый, естественно звучащий текст на многих языках. Конвейеры
sanToSpoken()иcleanCommentForTTS()— это то, где выполняется настоящая работа. -
Пять провайдеров, один интерфейс — независимо от того, приходит ли аудио от ElevenLabs, Google Cloud, KittenTTS, OpenTTS или речевого движка вашей ОС, остальная часть приложения всегда вызывает только
speakText(). Выбор провайдера — это переключение одного атома. -
Сборка создаёт единый бинарный файл по пути
src-tauri/target/release/en-parlant, объединяющий бэкенд Rust и фронтенд-ресурсы, собранные Vite.