Introdução à Arquitetura
Versão da Aplicação: v0.1.1 (fork: DarrellThomas/en-parlant) Stack: Tauri v2 (Rust) + React 19 (TypeScript) + Vite
O Que É o Tauri?
Seção intitulada “O Que É o Tauri?”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 Lado Rust: src-tauri/src/
Seção intitulada “O Lado Rust: src-tauri/src/”O Rust trata de tudo o que precisa de ser rápido ou de aceder ao sistema.
Ponto de Entrada: main.rs
Seção intitulada “Ponto de Entrada: main.rs”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ódulos Principais
Seção intitulada “Módulos Principais”| Módulo | Função |
|---|---|
db/mod.rs | Base de dados SQLite via Diesel ORM — consultas de jogos, estatísticas de jogadores, importações, pesquisa de posições |
game.rs | Motor de jogo ao vivo — gere jogos motor-vs-humano e motor-vs-motor, controlos de tempo, validação de lances |
chess.rs | Aná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.rs | Leitura/escrita/tokenização de ficheiros PGN |
opening.rs | Pesquisa de nomes de aberturas a partir de FEN (dados binários integrados na aplicação) |
puzzle.rs | Base de dados de puzzles do Lichess — acesso aleatório com mapeamento em memória |
fs.rs | Transferência de ficheiros com retoma, definição de permissões de execução |
sound.rs | Servidor HTTP local para streaming de áudio (solução alternativa para áudio no Linux) |
tts.rs | TTS de sistema via speech-dispatcher (Linux) / APIs nativas de fala do SO, mais gestão do servidor KittenTTS |
oauth.rs | Fluxo OAuth2 para vinculação de contas Lichess/Chess.com |
Padrões de Conceção
Seção intitulada “Padrões de Conceção”- 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
O Lado React/TypeScript: src/
Seção intitulada “O Lado React/TypeScript: src/”Pipeline de Build: Vite
Seção intitulada “Pipeline de Build: Vite”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 itpnpm build → tsc (typecheck) → vite build (bundle to dist/) → tauri build (native binary)Entrada: App.tsx
Seção intitulada “Entrada: App.tsx”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
Gestão de Estado
Seção intitulada “Gestão de Estado”Átomos Jotai (src/state/atoms.ts) — estado reativo leve:
| Categoria | Exemplos |
|---|---|
| Separadores | tabsAtom, activeTabAtom (interface multi-documento) |
| Diretórios | storedDocumentDirAtom, storedDatabasesDirAtom |
| Preferências de interface | primaryColorAtom, fontSizeAtom, pieceSetAtom |
| Motor | engineMovesFamily, engineProgressFamily (por separador via atomFamily) |
| TTS | ttsEnabledAtom, 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: TanStack Router
Seção intitulada “Roteamento: TanStack Router”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 managementComponentes: src/components/
Seção intitulada “Componentes: src/components/”| Grupo | Finalidade |
|---|---|
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 |
Como o Frontend Chama o Rust
Seção intitulada “Como o Frontend Chama o Rust”Comandos (pedido/resposta)
Seção intitulada “Comandos (pedido/resposta)”O Specta gera bindings TypeScript em 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...}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);Eventos (streaming, Rust para React)
Seção intitulada “Eventos (streaming, Rust para React)”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))Plugins Tauri
Seção intitulada “Plugins Tauri”A aplicação utiliza vários plugins oficiais para acesso ao sistema:
| Plugin | Finalidade |
|---|---|
@tauri-apps/plugin-fs | Leitura/escrita de ficheiros |
@tauri-apps/plugin-dialog | Seletores de ficheiros, caixas de mensagem |
@tauri-apps/plugin-http | Cliente HTTP (transferência de motores, TTS na nuvem) |
@tauri-apps/plugin-shell | Execução de motores UCI |
@tauri-apps/plugin-updater | Verificação automática de atualizações |
@tauri-apps/plugin-log | Logging estruturado |
@tauri-apps/plugin-os | Deteção de CPU/RAM |
Síntese de Fala (TTS): Uma Introdução
Seção intitulada “Síntese de Fala (TTS): Uma Introdução”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.
Como Funciona o TTS (A Versão Resumida)
Seção intitulada “Como Funciona o TTS (A Versão Resumida)”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 Arquitetura de Fornecedores
Seção intitulada “A Arquitetura de Fornecedores”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:
| Fornecedor | Tipo | Backend |
|---|---|---|
| ElevenLabs | Nuvem | Vozes neuronais via API REST. Devolve MP3. |
| Google Cloud TTS | Nuvem | Vozes WaveNet via API REST. Devolve MP3 codificado em base64. |
| KittenTTS | Local | Servidor TTS incluído, iniciado automaticamente pelo backend Rust. Comunica via HTTP em localhost. |
| OpenTTS | Local | Servidor TTS auto-alojado. Suporta muitos motores (espeak, MaryTTS, Piper, etc.). |
| System TTS | Local | Motor 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.
Suporte Multilingue
Seção intitulada “Suporte Multilingue”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.
Limpeza de Comentários
Seção intitulada “Limpeza de Comentários”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():
- Remove etiquetas PGN:
[%eval ...],[%csl ...],[%cal ...],[%clk ...] - Remove palavras de anotação duplicadas (quando ”??” já disse “Blunder”)
- Expande SAN em linha no texto:
"7.Nf3 controls e5"→"7, Knight f3 controls e5" - Corrige termos de xadrez que os motores TTS pronunciam mal (por ex., “en prise” → “on preez”)
- Expande abreviaturas de peças no texto:
"R vs R"→"Rook versus Rook"
Construção da Narração Completa
Seção intitulada “Construção da Narração Completa”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.
Cache e Reprodução
Seção intitulada “Cache e Reproduçã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.
Concorrência e Cancelamento
Seção intitulada “Concorrência e Cancelamento”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 — discardCada 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.
Onde o TTS Se Liga à Aplicação
Seção intitulada “Onde o TTS Se Liga à Aplicação”Os pontos de integração são mínimos:
| Ficheiro | O Que Acontece |
|---|---|
src/state/store/tree.ts | Cada 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.tsx | Um ícone de altifalante junto a cada comentário permite acionar manualmente o TTS para esse comentário. |
src/components/settings/TTSSettings.tsx | Interface 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.
Exemplos de Fluxo de Dados
Seção intitulada “Exemplos de Fluxo de Dados”Análise do Motor
Seção intitulada “Análise do Motor”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 flagPesquisa de Posição na Base de Dados
Seção intitulada “Pesquisa de Posição na Base de Dados”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 tableNarração TTS
Seção intitulada “Narração 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 atomsMapa de Diretórios
Seção intitulada “Mapa de Diretórios”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 depsPrincipais Conclusões
Seção intitulada “Principais Conclusões”-
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.
-
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.
-
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).
-
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()ecleanCommentForTTS()são onde o verdadeiro trabalho acontece. -
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. -
O build produz um único binário em
src-tauri/target/release/en-parlantque inclui o backend Rust + os recursos do frontend construídos pelo Vite.