Gå til innholdet

Arkitekturguide

App-versjon: v0.1.1 (fork: DarrellThomas/en-parlant) Teknologistabel: Tauri v2 (Rust) + React 19 (TypeScript) + Vite


Tauri er et rammeverk for å bygge skrivebordsapplikasjoner. I stedet for å sende med en fullstendig nettleser slik Electron gjør, bruker Tauri operativsystemets innebygde webview for brukergrensesnittet og en Rust-prosess for backend. Resultatet er en liten, rask binærfil.

De to halvdelene kommuniserer over IPC (inter-prosesskommunikasjon):

+---------------------------+ 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 håndterer alt som må være raskt eller som trenger systemtilgang.

Registrerer ~50 kommandoer som frontend kan kalle, initialiserer plugins (filsystem, dialog, HTTP, shell, logging, oppdatering), og starter appvinduet.

Kommandoer defineres med en makro:

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

specta-craten auto-genererer TypeScript-typedefinisjoner fra disse Rust-funksjonene, slik at frontend får full typesikkerhet uten manuelt arbeid.

ModulHva den gjør
db/mod.rsSQLite-database via Diesel ORM — spillforespørsler, spillerstatistikk, import, posisjonssøk
game.rsLevende spillmotor — håndterer motor-mot-menneske og motor-mot-motor-spill, tidskontroller, trekkvalidering
chess.rsMotoranalyse — starter UCI-motorer, strømmer beste-trekk-resultater tilbake til frontend via hendelser
engine/UCI-protokollimplementasjon — prosessstart, stdin/stdout-rør, multi-PV-støtte
pgn.rsPGN-fillesing/-skriving/-tokenisering
opening.rsÅpningsnavnoppslag fra FEN (binærdata bakt inn i appen)
puzzle.rsLichess-oppgavedatabase — minnetilordnet tilfeldig tilgang
fs.rsFilnedlastinger med gjenopptakelse, innstilling av kjøretillatelser
sound.rsLokal HTTP-server for lydstrømming (Linux-lydløsning)
tts.rsSystem-TTS via speech-dispatcher (Linux) / native OS-tale-APIer, pluss KittenTTS-serveradministrasjon
oauth.rsOAuth2-flyt for Lichess/Chess.com-kontokobling
  • Asynkront overalt: Tokio-kjøretid, ikke-blokkerende I/O
  • Samtidig tilstand: DashMap (samtidig HashMap) for motorprosesser, databasetilkoblinger, hurtiglagre
  • Tilkoblingspooling: r2d2 håndterer SQLite-tilkoblingspooler
  • Minnetilordnet søk: Posisjonsoppslag via mmap’d binærindeks for umiddelbare resultater
  • Hendelsesstrømming: Rust sender hendelser (beste trekk, klokkeslett, spill over) som React lytter til i sanntid

vite.config.ts konfigurerer:

  • React-plugin med Babel-kompilator
  • TanStack Router-plugin — auto-genererer rutetre fra routes/-mappen
  • Vanilla Extract — nullkjøretids-CSS-in-JS
  • Stialias: @ peker til ./src
  • Utviklingsserver på port 1420

Byggeflyt:

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

Rotkomponenten:

  • Initialiserer Tauri-plugins (log, prosess, oppdatering)
  • Laster brukerpreferanser fra persistente atomer
  • Setter opp Mantine UI-tema
  • Registrerer ruteren
  • Sjekker etter appoppdateringer

Jotai-atomer (src/state/atoms.ts) — lettvekts reaktiv tilstand:

KategoriEksempler
FanertabsAtom, activeTabAtom (flerdokumentgrensesnitt)
MapperstoredDocumentDirAtom, storedDatabasesDirAtom
Brukergrensesnitt-preferanserprimaryColorAtom, fontSizeAtom, pieceSetAtom
MotorengineMovesFamily, engineProgressFamily (per fane via atomFamily)
TTSttsEnabledAtom, ttsProviderAtom, ttsVoiceIdAtom, ttsVolumeAtom, ttsSpeedAtom, ttsLanguageAtom

Atomer med atomWithStorage() lagres automatisk til localStorage.

Zustand-butikker for kompleks domenetilstand:

  • src/state/store/tree.ts — spilltrenavigasjon, trekkforgreining, annotasjoner, kommentarer. Bruker Immer for uforanderlige oppdateringer.
  • src/state/store/database.ts — databasevisningsfiltre, valgt spill, paginering

Filbasert ruting i 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
GruppeFormål
boards/Sjakkbrett (chessground), trekkinntasting, evalueringsbar, analysevisning, promoveringsmodal, piltegning
panels/Sidepaneler: motoranalyse (BestMoves), databaseposisjonssøk, annotasjonsredigering, spillinfo, øvingsmodus
databases/Database-brukergrensesnitt: spilltabell, spillertabell, detaljkort, filtrering
settings/Preferanseskjemaer, motorstier, TTS-innstillinger
home/Kontokort, import-brukergrensesnitt
common/Delt: TreeStateContext, materialvisning, kommentar-høyttalerikon
tabs/Flerganebar

Specta genererer TypeScript-bindinger i 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...
}

React-komponenter kaller dem som vanlige asynkrone funksjoner:

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

For sanntidsdata (motoranalyse, klokkeslett, spilltrekk):

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

Appen bruker flere offisielle plugins for systemtilgang:

PluginFormål
@tauri-apps/plugin-fsLese/skrive filer
@tauri-apps/plugin-dialogFilvelgere, meldingsbokser
@tauri-apps/plugin-httpHTTP-klient (motornedlastinger, sky-TTS)
@tauri-apps/plugin-shellKjøre UCI-motorer
@tauri-apps/plugin-updaterAutomatiske oppdateringssjekker
@tauri-apps/plugin-logStrukturert logging
@tauri-apps/plugin-osCPU/RAM-gjenkjenning

En Parlant~ kan lese sjakktrekk og kommentarer høyt mens du stegvis går gjennom et parti. Denne seksjonen forklarer hvordan TTS-systemet er bygget — forbehandlingspipelinen, leverandørarkitekturen og hurtiglagringsstrategien. For oppsettsinstruksjoner, se TTS-guidene i TTS-menyen.

Tekst-til-tale konverterer skrevet tekst til talt lyd. Moderne TTS-systemer er bygget på dype nevrale nettverk trent på tusenvis av timer med menneskelig tale. Modellen lærer sammenhengen mellom tekst (bokstaver, ord, tegnsetting) og de akustiske egenskapene til tale (tonehøyde, timing, betoning, pustpauser). Ved inferens sender du inn tekst og får tilbake en lydbølgeform.

Det finnes to hovedtilnærminger:

  • Sky-TTS — tekst sendes til en ekstern server (Google, ElevenLabs osv.), som kjører et stort nevralt nettverk på GPU-maskinvare og returnerer lyd. Utmerket kvalitet, men krever internett og har kostnad per forespørsel (selv om de fleste leverandører tilbyr gratis nivåer).

  • Lokal TTS — en modell kjører direkte på din maskin. Ingen internett nødvendig, ingen kostnad per forespørsel, og teksten din forlater aldri datamaskinen din. Nyere åpen kildekode-modeller (som Kokoro og Piper) har redusert kvalitetsgapet betraktelig.

Hvis du er nysgjerrig på hvordan TTS-modeller fungerer under panseret, hoster HuggingFace (huggingface.co) hundrevis av åpen kildekode-talesyntese-modeller du kan utforske, laste ned og kjøre lokalt. Søk etter «text-to-speech» for å finne modeller som spenner fra lette CPU-vennlige alternativer til toppmoderne forskningsmodeller.

Kjerne-TTS-implementasjonen ligger i src/utils/tts.ts. Den er designet rundt et enkelt offentlig grensesnitt (speakText()) med utbyttbare backends. Resten av appen vet aldri og bryr seg aldri om hvilken leverandør som er aktiv — den kaller bare speakText() og lyd kommer ut.

Fem leverandører støttes:

LeverandørTypeBackend
ElevenLabsSkyNevrale stemmer via REST API. Returnerer MP3.
Google Cloud TTSSkyWaveNet-stemmer via REST API. Returnerer base64-kodet MP3.
KittenTTSLokalMedfølgende TTS-server, auto-startet av Rust-backend. Kommuniserer over HTTP på localhost.
OpenTTSLokalSelvhostet TTS-server. Støtter mange motorer (espeak, MaryTTS, Piper osv.).
System-TTSLokalOS-native talemotor via Rust/Tauri-kommandoer (speech-dispatcher på Linux, SAPI på Windows, AVSpeechSynthesizer på macOS).

Leverandørvalg lagres i et enkelt Jotai-atom (ttsProviderAtom). Å bytte leverandør skjer umiddelbart — endre atomet, og neste speakText()-kall rutes til den nye backend-en.

Utfordringen: Sjakknotasjon er ikke engelsk

Section titled “Utfordringen: Sjakknotasjon er ikke engelsk”

Sjakktrekk skrives i standard algebraisk notasjon (SAN): Nf3, Bxe5+, O-O-O, e8=Q#. Hvis du mater dette direkte til en TTS-motor, får du tull — den kan prøve å uttale «Nf3» som et ord, eller lese «O-O-O» som «oh oh oh.»

Løsningen er en forbehandlingspipeline som oversetter sjakknotasjon til naturlig språk før det når TTS-motoren:

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"

Funksjonen sanToSpoken() bruker regex-mønstermatching for å dekomponere enhver SAN-streng til sine komponenter (brikke, disambiguering, slag, destinasjon, promotering, sjakk/sjakkmatt) og sette dem sammen igjen ved hjelp av naturlig språk fra en ordforråds-tabell.

Sjakkvokabular er oversatt til mange språk (engelsk, fransk, spansk, tysk, japansk, russisk, kinesisk, koreansk og flere). CHESS_VOCAB-tabellen kartlegger hvert begrep:

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

Språkinnstillingen bestemmer hvilken ordforråds-tabell som brukes til forbehandling og hvilken stemme/aksent TTS-motoren bruker for syntese.

Spillannotasjoner inneholder ofte PGN-spesifikk markup som ville hørt forferdelig ut hvis det ble lest høyt:

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

Funksjonen cleanCommentForTTS():

  1. Fjerner PGN-tagger: [%eval ...], [%csl ...], [%cal ...], [%clk ...]
  2. Fjerner dupliserte annotasjonsord (når «??» allerede har sagt «Blunder»)
  3. Ekspanderer innebygd SAN i prosa: "7.Nf3 controls e5""7, Knight f3 controls e5"
  4. Fikser sjakktermer TTS-motorer uttaler feil (f.eks. «en prise» → «on preez»)
  5. Ekspanderer brikkeforkortelser i prosa: "R vs R""Rook versus Rook"

Når du stegger til et nytt trekk, setter buildNarration() sammen den komplette talte teksten fra tre kilder:

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

Dobbeltmellomrommet mellom delene gir TTS-motorer en naturlig pustpause.

Sky-TTS-kall koster penger og tar tid (~200–500 ms tur-retur). For å unngå å hente samme lyd på nytt, hurtiglagres hvert genererte klipp i minnet som en blob-URL:

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

Ved hurtiglagringstreff er avspillingen umiddelbar. Hurtiglageret nøkles etter provider:voice:language:text, slik at bytte av stemme eller språk oppretter separate oppføringer.

For spill med mange annotasjoner kan du forhåndslaste hele spilltreet i bakgrunnen. Appen traverserer hver node, bygger narrasjonsteksten, og sender sekvensielle API-kall for å fylle hurtiglageret før du begynner å navigere.

Rask piltastnavigasjon skaper et problem: hvis brukeren stegger fremover 5 ganger raskt, vil du ikke ha 5 overlappende lydklipp som kjemper mot hverandre. Løsningen er en generasjonsteller:

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

Hvert nye speakText()-kall inkrementerer telleren og avbryter eventuelle pågående HTTP-forespørsler via AbortController. Når lyden ankommer, sjekker den om generasjonen fremdeles er gjeldende. Hvis brukeren allerede har gått videre, forkastes svaret stille. Dette gir ren, feilfri lyd selv ved rask klikking gjennom trekk.

Integrasjonspunktene er minimale:

FilHva som skjer
src/state/store/tree.tsHver navigasjonsfunksjon (goToNext, goToPrevious osv.) kaller stopSpeaking(). Når auto-narrasjon er på, kaller goToNext også speakMoveNarration().
src/components/common/Comment.tsxEt høyttalerikon ved siden av hver kommentar lar deg manuelt utløse TTS for den kommentaren.
src/components/settings/TTSSettings.tsxInnstillings-UI for valg av leverandør, stemme, språk, volum, hastighet og inntasting av API-nøkler.

Når TTS er slått av, kjører ingen av denne koden. Appen oppfører seg identisk med oppstrøms 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 gjør det tunge løftet — motorer, database, fil-I/O, PGN-parsing. React berører aldri filsystemet eller starter prosesser direkte.

  2. Typesikkerhet over grensen — Specta genererer TypeScript-typer fra Rust-strukturer, så hvis en Rust-kommando endrer signaturen sin, bryter TypeScript-bygget umiddelbart.

  3. To tilstandssystemer — Jotai for enkel reaktiv tilstand (innstillinger, UI-preferanser, per-fane-motortilstand), Zustand for kompleks domenetilstand (spilltre med forgreining og uforanderlige oppdateringer).

  4. TTS er et forbehandlingsproblem — den vanskelige delen er ikke å kalle et tale-API, det er å oversette sjakknotasjon og PGN-markup til ren, naturlig-klingende tekst på tvers av mange språk. sanToSpoken()- og cleanCommentForTTS()-pipelinene er der det virkelige arbeidet skjer.

  5. Fem leverandører, ett grensesnitt — enten lyden kommer fra ElevenLabs, Google Cloud, KittenTTS, OpenTTS eller operativsystemets talemotor, kaller resten av appen bare speakText(). Leverandørvalg er en enkel atombryter.

  6. Bygget produserer én enkelt binærfilsrc-tauri/target/release/en-parlant som pakker sammen Rust-backend + de Vite-bygde frontend-ressursene.