Pular para o conteúdo

Introdução à Arquitetura

Versão da Aplicação: v0.1.1 (fork: DarrellThomas/en-parlant) Stack: Tauri v2 (Rust) + React 19 (TypeScript) + Vite


O Tauri é uma framework para criar aplicações de ambiente de trabalho. Em vez de incluir um navegador completo como o Electron, o Tauri utiliza a webview nativa do sistema operativo para a interface e um processo Rust para o backend. O resultado é um binário pequeno e rápido.

As duas partes comunicam através de IPC (comunicação entre processos):

+---------------------------+ 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/

O Rust trata de tudo o que precisa de ser rápido ou de aceder ao sistema.

Regista ~50 comandos que o frontend pode invocar, inicializa plugins (sistema de ficheiros, diálogo, HTTP, shell, logging, atualizador) e inicia a janela da aplicação.

Os comandos são definidos com uma macro:

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

O crate specta gera automaticamente definições de tipos TypeScript a partir destas funções Rust, de modo que o frontend obtém segurança de tipos completa sem qualquer esforço manual.

MóduloFunção
db/mod.rsBase de dados SQLite via Diesel ORM — consultas de jogos, estatísticas de jogadores, importações, pesquisa de posições
game.rsMotor de jogo ao vivo — gere jogos motor-vs-humano e motor-vs-motor, controlos de tempo, validação de lances
chess.rsAnálise de motor — inicia motores UCI, transmite resultados de melhores lances para o frontend via eventos
engine/Implementação do protocolo UCI — criação de processos, pipes stdin/stdout, suporte multi-PV
pgn.rsLeitura/escrita/tokenização de ficheiros PGN
opening.rsPesquisa de nomes de aberturas a partir de FEN (dados binários integrados na aplicação)
puzzle.rsBase de dados de puzzles do Lichess — acesso aleatório com mapeamento em memória
fs.rsTransferência de ficheiros com retoma, definição de permissões de execução
sound.rsServidor HTTP local para streaming de áudio (solução alternativa para áudio no Linux)
tts.rsTTS de sistema via speech-dispatcher (Linux) / APIs nativas de fala do SO, mais gestão do servidor KittenTTS
oauth.rsFluxo OAuth2 para vinculação de contas Lichess/Chess.com
  • Assíncrono em toda a parte: Runtime Tokio, I/O não bloqueante
  • Estado concorrente: DashMap (HashMap concorrente) para processos de motores, ligações à base de dados, caches
  • Pool de ligações: r2d2 gere pools de ligações SQLite
  • Pesquisa com mapeamento em memória: Pesquisa de posições via índice binário mapeado em memória para resultados instantâneos
  • Streaming de eventos: O Rust emite eventos (melhores lances, tiques de relógio, fim de jogo) que o React escuta em tempo real

vite.config.ts configura:

  • Plugin React com compilador Babel
  • Plugin TanStack Router — gera automaticamente a árvore de rotas a partir da pasta routes/
  • Vanilla Extract — CSS-in-JS sem runtime
  • Alias de caminho: @ mapeia para ./src
  • Servidor de desenvolvimento na porta 1420

Fluxo de build:

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

O componente raiz:

  • Inicializa os plugins Tauri (log, processo, atualizador)
  • Carrega as preferências do utilizador a partir de átomos persistentes
  • Configura o tema da interface Mantine
  • Regista o router
  • Verifica se existem atualizações da aplicação

Átomos Jotai (src/state/atoms.ts) — estado reativo leve:

CategoriaExemplos
SeparadorestabsAtom, activeTabAtom (interface multi-documento)
DiretóriosstoredDocumentDirAtom, storedDatabasesDirAtom
Preferências de interfaceprimaryColorAtom, fontSizeAtom, pieceSetAtom
MotorengineMovesFamily, engineProgressFamily (por separador via atomFamily)
TTSttsEnabledAtom, ttsProviderAtom, ttsVoiceIdAtom, ttsVolumeAtom, ttsSpeedAtom, ttsLanguageAtom

Os átomos com atomWithStorage() persistem automaticamente no localStorage.

Stores Zustand para estado de domínio complexo:

  • src/state/store/tree.ts — navegação na árvore de jogo, ramificação de lances, anotações, comentários. Utiliza Immer para atualizações imutáveis.
  • src/state/store/database.ts — filtros da vista de base de dados, jogo selecionado, paginação

Roteamento baseado em ficheiros em 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
GrupoFinalidade
boards/Tabuleiro de xadrez (chessground), entrada de lances, barra de avaliação, exibição de análise, modal de promoção, desenho de setas
panels/Painéis laterais: análise do motor (BestMoves), pesquisa de posições na base de dados, edição de anotações, informação do jogo, modo de prática
databases/Interface da base de dados: tabela de jogos, tabela de jogadores, cartões de detalhe, filtragem
settings/Formulários de preferências, caminhos de motores, definições de TTS
home/Cartões de conta, interface de importação
common/Partilhados: TreeStateContext, exibição de material, ícone de altifalante nos comentários
tabs/Barra multi-separadores

O Specta gera bindings TypeScript em 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...
}

Os componentes React chamam-nos como funções assíncronas normais:

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

Para dados em tempo real (análise do motor, tiques de relógio, lances do jogo):

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

A aplicação utiliza vários plugins oficiais para acesso ao sistema:

PluginFinalidade
@tauri-apps/plugin-fsLeitura/escrita de ficheiros
@tauri-apps/plugin-dialogSeletores de ficheiros, caixas de mensagem
@tauri-apps/plugin-httpCliente HTTP (transferência de motores, TTS na nuvem)
@tauri-apps/plugin-shellExecução de motores UCI
@tauri-apps/plugin-updaterVerificação automática de atualizações
@tauri-apps/plugin-logLogging estruturado
@tauri-apps/plugin-osDeteção de CPU/RAM

O En Parlant~ pode ler lances de xadrez e comentários em voz alta à medida que avança num jogo. Esta secção explica como o sistema de TTS está construído — o pipeline de pré-processamento, a arquitetura de fornecedores e a estratégia de cache. Para instruções de configuração, consulte os guias de TTS no menu TTS.

A síntese de fala converte texto escrito em áudio falado. Os sistemas de TTS modernos são construídos sobre redes neuronais profundas treinadas com milhares de horas de fala humana. O modelo aprende a relação entre texto (letras, palavras, pontuação) e as características acústicas da fala (tom, ritmo, ênfase, pausas de respiração). No momento da inferência, envia-se texto e recebe-se de volta uma forma de onda de áudio.

Existem duas abordagens gerais:

  • TTS na Nuvem — o texto é enviado para um servidor remoto (Google, ElevenLabs, etc.), que executa uma rede neuronal de grande dimensão em hardware GPU e devolve áudio. Excelente qualidade, mas requer internet e tem custos por pedido (embora a maioria dos fornecedores ofereça tiers gratuitos).

  • TTS Local — um modelo é executado diretamente na sua máquina. Não precisa de internet, sem custo por pedido, e o seu texto nunca sai do seu computador. Modelos recentes de código aberto (como Kokoro e Piper) reduziram significativamente a diferença de qualidade.

Se tem curiosidade sobre como os modelos de TTS funcionam internamente, o HuggingFace (huggingface.co) aloja centenas de modelos de síntese de fala de código aberto que pode explorar, descarregar e executar localmente. Pesquise por “text-to-speech” para encontrar modelos que vão desde opções leves otimizadas para CPU até modelos de investigação de última geração.

A implementação central do TTS encontra-se em src/utils/tts.ts. Está concebida em torno de uma interface pública única (speakText()) com backends permutáveis. O resto da aplicação nunca sabe nem se preocupa com qual o fornecedor ativo — simplesmente chama speakText() e o áudio é reproduzido.

São suportados cinco fornecedores:

FornecedorTipoBackend
ElevenLabsNuvemVozes neuronais via API REST. Devolve MP3.
Google Cloud TTSNuvemVozes WaveNet via API REST. Devolve MP3 codificado em base64.
KittenTTSLocalServidor TTS incluído, iniciado automaticamente pelo backend Rust. Comunica via HTTP em localhost.
OpenTTSLocalServidor TTS auto-alojado. Suporta muitos motores (espeak, MaryTTS, Piper, etc.).
System TTSLocalMotor de fala nativo do SO via comandos Rust/Tauri (speech-dispatcher no Linux, SAPI no Windows, AVSpeechSynthesizer no macOS).

A seleção do fornecedor é armazenada num único átomo Jotai (ttsProviderAtom). Mudar de fornecedor é instantâneo — altera-se o átomo e a próxima chamada a speakText() é encaminhada para o novo backend.

O Desafio: A Notação de Xadrez Não É Linguagem Natural

Seção intitulada “O Desafio: A Notação de Xadrez Não É Linguagem Natural”

Os lances de xadrez são escritos em Notação Algébrica Standard (SAN): Nf3, Bxe5+, O-O-O, e8=Q#. Se alimentar isto diretamente a um motor TTS, obtém-se algo sem sentido — pode tentar pronunciar “Nf3” como uma palavra, ou ler “O-O-O” como “ó ó ó”.

A solução é um pipeline de pré-processamento que traduz a notação de xadrez para linguagem natural antes de chegar ao motor 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"

A função sanToSpoken() utiliza correspondência de padrões com expressões regulares para decompor qualquer cadeia SAN nos seus componentes (peça, desambiguação, captura, destino, promoção, xeque/xeque-mate) e reassembla-os usando linguagem natural a partir de uma tabela de vocabulário.

O vocabulário de xadrez está traduzido em muitas línguas (inglês, francês, espanhol, alemão, japonês, russo, chinês, coreano, entre outras). A tabela CHESS_VOCAB mapeia cada termo:

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

A definição de língua determina qual a tabela de vocabulário utilizada para o pré-processamento e qual a voz/sotaque que o motor TTS usa para a síntese.

As anotações de jogos contêm frequentemente marcações específicas de PGN que soariam terrivelmente se lidas em voz alta:

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

A função cleanCommentForTTS():

  1. Remove etiquetas PGN: [%eval ...], [%csl ...], [%cal ...], [%clk ...]
  2. Remove palavras de anotação duplicadas (quando ”??” já disse “Blunder”)
  3. Expande SAN em linha no texto: "7.Nf3 controls e5""7, Knight f3 controls e5"
  4. Corrige termos de xadrez que os motores TTS pronunciam mal (por ex., “en prise” → “on preez”)
  5. Expande abreviaturas de peças no texto: "R vs R""Rook versus Rook"

Quando avança para um novo lance, buildNarration() monta o texto falado completo a partir de três fontes:

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

O espaço duplo entre as partes proporciona aos motores TTS uma pausa natural de respiração.

As chamadas de TTS na nuvem custam dinheiro e levam tempo (~200-500ms de ida e volta). Para evitar buscar novamente o mesmo áudio, cada clip gerado é armazenado em cache na memória como um blob URL:

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

Quando há acerto no cache, a reprodução é instantânea. A chave de cache é composta por provider:voice:language:text, de modo que mudar de voz ou de língua cria entradas separadas.

Para jogos com muitas anotações, pode pré-carregar o cache de toda a árvore de jogo em segundo plano. A aplicação percorre cada nó, constrói o texto de narração e dispara chamadas sequenciais à API para preencher o cache antes de começar a navegar.

A navegação rápida com teclas de seta cria um problema: se o utilizador avança 5 vezes rapidamente, não se pretende que 5 clips de áudio sobrepostos disputem entre si. A solução é um contador de geração:

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

Cada nova chamada a speakText() incrementa o contador e aborta qualquer pedido HTTP em curso via AbortController. Quando o áudio chega, verifica se a sua geração ainda é a atual. Se o utilizador já avançou, a resposta é silenciosamente descartada. Isto proporciona áudio limpo e sem falhas, mesmo quando se clica rapidamente através dos lances.

Os pontos de integração são mínimos:

FicheiroO Que Acontece
src/state/store/tree.tsCada função de navegação (goToNext, goToPrevious, etc.) chama stopSpeaking(). Quando a narração automática está ativada, goToNext também chama speakMoveNarration().
src/components/common/Comment.tsxUm ícone de altifalante junto a cada comentário permite acionar manualmente o TTS para esse comentário.
src/components/settings/TTSSettings.tsxInterface de definições para escolher fornecedor, voz, língua, volume, velocidade e introduzir chaves de API.

Quando o TTS está desativado, nenhum deste código é executado. A aplicação comporta-se de forma idêntica ao En Croissant original.


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. O Rust faz o trabalho pesado — motores, base de dados, I/O de ficheiros, análise de PGN. O React nunca acede diretamente ao sistema de ficheiros nem cria processos.

  2. Segurança de tipos além da fronteira — o Specta gera tipos TypeScript a partir das structs Rust, de modo que se um comando Rust alterar a sua assinatura, o build TypeScript falha imediatamente.

  3. Dois sistemas de estado — Jotai para estado reativo simples (definições, preferências de interface, estado do motor por separador), Zustand para estado de domínio complexo (árvore de jogo com ramificação e atualizações imutáveis).

  4. O TTS é um problema de pré-processamento — a parte difícil não é chamar uma API de fala, é traduzir notação de xadrez e marcações PGN em texto limpo e de sonoridade natural em muitas línguas. Os pipelines sanToSpoken() e cleanCommentForTTS() são onde o verdadeiro trabalho acontece.

  5. Cinco fornecedores, uma interface — quer o áudio venha do ElevenLabs, Google Cloud, KittenTTS, OpenTTS ou do motor de fala do seu SO, o resto da aplicação apenas chama speakText(). A seleção do fornecedor é um simples toggle de átomo.

  6. O build produz um único binário em src-tauri/target/release/en-parlant que inclui o backend Rust + os recursos do frontend construídos pelo Vite.