コンテンツにスキップ

アーキテクチャ入門

アプリバージョン: v0.1.1 (fork: DarrellThomas/en-parlant) 技術スタック: Tauri v2 (Rust) + React 19 (TypeScript) + Vite


Tauri はデスクトップアプリケーションを構築するためのフレームワークです。Electron のようにフルブラウザを同梱するのではなく、Tauri は UI に OS 組み込みの webview を使用し、バックエンドには Rust プロセスを使用します。その結果、小さく高速なバイナリが生成されます。

2つの部分は 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個のコマンドを登録し、プラグイン(filesystem、dialog、HTTP、shell、logging、updater)を初期化して、アプリウィンドウを起動します。

コマンドはマクロで定義されます:

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

specta クレートがこれらの Rust 関数から TypeScript の型定義を自動生成するため、フロントエンドは手動作業ゼロで完全な型安全性を得られます。

モジュール機能
db/mod.rsDiesel ORM による SQLite データベース — ゲームクエリ、プレイヤー統計、インポート、局面検索
game.rsライブゲームエンジン — エンジン対人間、エンジン対エンジンのゲーム管理、持ち時間制御、着手バリデーション
chess.rsエンジン解析 — UCI エンジンを起動し、最善手の結果をイベント経由でフロントエンドにストリーミング
engine/UCI プロトコル実装 — プロセス起動、stdin/stdout パイプ、マルチ PV サポート
pgn.rsPGN ファイルの読み書き・トークン化
opening.rsFEN からのオープニング名検索(バイナリデータがアプリに組み込み済み)
puzzle.rsLichess パズルデータベース — メモリマップによるランダムアクセス
fs.rsレジューム対応ファイルダウンロード、実行権限の設定
sound.rsオーディオストリーミング用ローカル HTTP サーバー(Linux オーディオの回避策)
tts.rsspeech-dispatcher(Linux)/ ネイティブ OS 音声 API によるシステム TTS、および KittenTTS サーバー管理
oauth.rsLichess/Chess.com アカウント連携のための OAuth2 フロー
  • 全面非同期: Tokio ランタイム、ノンブロッキング I/O
  • 並行状態管理: エンジンプロセス、DB 接続、キャッシュに DashMap(並行 HashMap)を使用
  • コネクションプーリング: r2d2 が SQLite のコネクションプールを管理
  • メモリマップ検索: mmap されたバイナリインデックスによる局面検索で瞬時に結果を返却
  • イベントストリーミング: Rust がイベント(最善手、時計のティック、ゲーム終了)を発行し、React がリアルタイムで受信

vite.config.ts の構成:

  • React プラグイン(Babel コンパイラ使用)
  • TanStack Router プラグインroutes/ フォルダからルートツリーを自動生成
  • Vanilla Extract — ゼロランタイム CSS-in-JS
  • パスエイリアス: @./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 UI テーマを設定
  • ルーターを登録
  • アプリのアップデートを確認

Jotai atomssrc/state/atoms.ts)— 軽量なリアクティブ状態:

カテゴリ
タブtabsAtomactiveTabAtom(マルチドキュメントインターフェース)
ディレクトリstoredDocumentDirAtomstoredDatabasesDirAtom
UI 設定primaryColorAtomfontSizeAtompieceSetAtom
エンジンengineMovesFamilyengineProgressFamily(atomFamily によるタブごとの状態)
TTSttsEnabledAtomttsProviderAtomttsVoiceIdAtomttsVolumeAtomttsSpeedAtomttsLanguageAtom

atomWithStorage() を使用したアトムは自動的に localStorage に永続化されます。

Zustand ストアは複雑なドメイン状態を管理します:

  • src/state/store/tree.ts — ゲームツリーのナビゲーション、手の分岐、アノテーション、コメント。不変更新に Immer を使用。
  • src/state/store/database.ts — データベースビューのフィルタ、選択中のゲーム、ページネーション

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 を呼び出す方法”

コマンド(リクエスト/レスポンス)

Section titled “コマンド(リクエスト/レスポンス)”

Specta が src/bindings/generated.ts に TypeScript バインディングを生成します:

// 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-shellUCI エンジンの実行
@tauri-apps/plugin-updater自動アップデートチェック
@tauri-apps/plugin-log構造化ログ
@tauri-apps/plugin-osCPU/RAM 検出

テキスト読み上げ(TTS):入門

Section titled “テキスト読み上げ(TTS):入門”

En Parlant~ はゲームをステップ操作する際に、チェスの着手やコメンタリーを音声で読み上げることができます。このセクションでは TTS システムの構築方法 — 前処理パイプライン、プロバイダーアーキテクチャ、キャッシュ戦略について説明します。セットアップ手順については、TTS メニューの TTS ガイドをご覧ください。

テキスト読み上げ(Text-to-speech)は、書かれたテキストを音声に変換します。現代の TTS システムは、何千時間もの人間の音声で学習されたディープニューラルネットワークに基づいて構築されています。モデルはテキスト(文字、単語、句読点)と音声の音響特徴(ピッチ、タイミング、強調、呼吸の間)の関係を学習します。推論時にテキストを入力すると、音声波形が返されます。

大きく2つのアプローチがあります:

  • クラウド TTS — テキストがリモートサーバー(Google、ElevenLabs など)に送信され、GPU ハードウェア上で大規模なニューラルネットワークが実行されて音声が返されます。優れた品質ですが、インターネット接続が必要でリクエストごとにコストがかかります(ただし、ほとんどのプロバイダーは無料枠を提供しています)。

  • ローカル TTS — モデルがマシン上で直接実行されます。インターネット不要、リクエストごとのコストなし、テキストがコンピュータの外に出ることもありません。最近のオープンソースモデル(Kokoro や Piper など)は品質の差を大幅に縮めています。

TTS モデルの内部動作に興味がある方は、HuggingFace(huggingface.co)で何百ものオープンソース音声合成モデルを探索、ダウンロード、ローカル実行できます。「text-to-speech」で検索すると、軽量な CPU 向けオプションから最先端の研究モデルまで見つかります。

TTS のコア実装は src/utils/tts.ts にあります。単一の公開インターフェースspeakText())に交換可能なバックエンドを持つ設計になっています。アプリの他の部分はどのプロバイダーがアクティブかを知る必要も気にする必要もなく、ただ speakText() を呼び出すだけで音声が出力されます。

5つのプロバイダーがサポートされています:

プロバイダータイプバックエンド
ElevenLabsクラウドREST API によるニューラルボイス。MP3 を返却。
Google Cloud TTSクラウドREST API による WaveNet ボイス。base64 エンコードされた MP3 を返却。
KittenTTSローカルバンドルされた TTS サーバー、Rust バックエンドにより自動起動。localhost 上の HTTP で通信。
OpenTTSローカルセルフホスト TTS サーバー。多数のエンジン(espeak、MaryTTS、Piper など)をサポート。
System TTSローカルRust/Tauri コマンド経由の OS ネイティブ音声エンジン(Linux では speech-dispatcher、Windows では SAPI、macOS では AVSpeechSynthesizer)。

プロバイダーの選択は単一の Jotai アトム(ttsProviderAtom)に保存されます。プロバイダーの切り替えは即座に行われます — アトムを変更するだけで、次の speakText() 呼び出しが新しいバックエンドにルーティングされます。

課題:チェス記譜は英語ではない

Section titled “課題:チェス記譜は英語ではない”

チェスの着手は標準代数記法(SAN)で記述されます:Nf3Bxe5+O-O-Oe8=Q#。これを直接 TTS エンジンに渡すと意味不明な音声になります — 「Nf3」を単語として発音しようとしたり、「O-O-O」を「oh oh oh」と読んだりする可能性があります。

解決策は、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() 関数は以下を行います:

  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"

新しい手に進むと、buildNarration() は3つのソースから完全な読み上げテキストを組み立てます:

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-500ms)。同じ音声の再取得を避けるため、生成されたすべてのクリップはメモリ内に 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() 呼び出しのたびにカウンターがインクリメントされ、AbortController を介して実行中の HTTP リクエストが中止されます。音声が到着すると、その世代がまだ最新かどうかを確認します。ユーザーが既に先に進んでいる場合、レスポンスは静かに破棄されます。これにより、手を素早くクリックしていても、クリーンでグリッチのない音声が得られます。

TTS がアプリに統合されるポイント

Section titled “TTS がアプリに統合されるポイント”

統合ポイントは最小限です:

ファイル処理内容
src/state/store/tree.tsすべてのナビゲーション関数(goToNextgoToPrevious など)が stopSpeaking() を呼び出します。自動ナレーションが有効な場合、goToNextspeakMoveNarration() も呼び出します。
src/components/common/Comment.tsx各コメントの横にあるスピーカーアイコンで、そのコメントの TTS を手動でトリガーできます。
src/components/settings/TTSSettings.tsxプロバイダー、ボイス、言語、音量、速度の選択、API キーの入力のための設定 UI です。

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
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 が重い処理を担当 — エンジン、データベース、ファイル I/O、PGN パース。React はファイルシステムに直接触れたり、プロセスを直接起動したりしません。

  2. 境界を越えた型安全性 — Specta が Rust の構造体から TypeScript の型を生成するため、Rust コマンドのシグネチャが変更されると、TypeScript のビルドが即座に壊れます。

  3. 2つの状態管理システム — シンプルなリアクティブ状態(設定、UI プリファレンス、タブごとのエンジン状態)には Jotai、複雑なドメイン状態(分岐と不変更新を持つゲームツリー)には Zustand を使用。

  4. TTS は前処理の問題 — 難しいのは音声 API を呼び出すことではなく、チェス記譜と PGN マークアップを多言語にわたってクリーンで自然に聞こえるテキストに変換することです。sanToSpoken()cleanCommentForTTS() パイプラインが実際の作業が行われる場所です。

  5. 5つのプロバイダー、1つのインターフェース — 音声が ElevenLabs、Google Cloud、KittenTTS、OpenTTS、OS の音声エンジンのいずれから来ても、アプリの他の部分は speakText() を呼び出すだけです。プロバイダーの選択は単一のアトムのトグルです。

  6. ビルドは単一のバイナリを生成しますsrc-tauri/target/release/en-parlant に、Rust バックエンドと Vite でビルドされたフロントエンドアセットがバンドルされます。