Skip to content

Configuration

echo-words is configured in two places: a TOML table of source languages, and environment variables for everything else.

Source languages

languages.toml is the source of truth for the languages you look words up in. It lives in ECHOWORDS_DATA_DIR and moves with it; ECHOWORDS_LANGUAGES_CONFIG overrides that path. Adding a language is a configuration change, not a code change.

[languages.en]
name       = "English"
deck       = "EchoWords: English"
recordings = "En-us"           # Wikimedia Commons recording prefix, accent included;
                               # omit where Commons has none for the language
tts        = "piper"           # piper | edge
tts_voice  = "en_US-lessac-medium"
script     = "latin"           # latin | cyrillic | latin+cyrillic

[languages.sr]
name           = "Српски"
deck           = "EchoWords: Serbian"
tts            = "edge"
edge_tts_voice = "sr-RS-SophieNeural"
script         = "latin+cyrillic"
prompt_hints   = "for nouns give gender and plural, for verbs give aspect"

Each entry names its display name, its own Anki deck, the script accepted from the input field, the Wikimedia Commons prefix its human recordings are filed under where the language has them, and its pronunciation engine and voice. languages.example.toml in the repository carries complete English, German, and Serbian entries.

Note

Serbian is spoken in Cyrillic on purpose: the sr-RS voices mispronounce the Latin spelling.

Editing it from the app

The pencil beside the language row on the words screen opens an editor that adds a language, removes one, and changes its deck, voice engine, voice and recordings prefix. It rewrites this file and takes effect without a restart; a new Piper voice downloads in the background. Removing a language never touches its Anki deck, and the last remaining language cannot be removed — the app does not start without one. A save rewrites the whole file, so comments in it do not survive one.

A language is added by searching a built-in directory of the languages the app can reach and pressing the one wanted — by its own name, its English name, its Russian name or its code. Reaching one is not vouching for it: each row says whether its answers were read and vouched for, read and refused, or never looked at, both in the search row and in the editor afterwards. The directory gives it its code, its name and its script; the editor cannot set any of them, and refuses a request that tries. The code addresses Wikipedia, Wiktionary and the audio cache and cannot be changed once the language exists, the name is what the prompt calls the source language, and the script is the alphabet the input field and the card sentences are tested against. A language written into this file by hand under a code the directory does not carry keeps the name and the script given here, and stays editable. Under a code the directory does carry, the name and the script are the directory's whatever this file says: a disagreement is logged at startup and the directory's answer is what the input field, the card filter, the editor and the prompt all use.

A tts_voice is picked from the Piper voices this build can install, and the editor refuses any other: a voice it cannot download would save and then stay silent. Editing this file by hand is the way to point Piper at a voice installed into models/ some other way; the editor leaves such a value alone.

api_model and prompt_hints stay a file edit and a restart. They are the two fields whose value reaches machinery the editor can neither show nor check: prompt_hints is interpolated into the prompt, so a bad hint would degrade every later answer for that language silently, and api_model builds the paid-model map when the process starts. The editor preserves whatever this file already holds for both, so saving a voice never drops a hint.

Environment variables

Every variable is prefixed ECHOWORDS_. The committed .deploy.example/.env documents them one by one; these are the ones you are likely to change.

Variable Meaning Default
ECHOWORDS_DATA_DIR Anki collection, voice models, broker state, cached audio — no database of its own ~/.echo-words
ECHOWORDS_TARGET_LANG the language every explanation and translation is written in ru
ECHOWORDS_LANGUAGES_CONFIG path to the languages table <data dir>/languages.toml
ECHOWORDS_LLMBROKER_HOME where llmbroker keeps its curated model list and call journal <data dir>/llmbroker
ECHOWORDS_API_MODEL paid-catalog alias the cascade steps up to; empty disables paid calls entirely gpt-fast
ECHOWORDS_API_DAILY_CAP paid calls per day, 0 for unlimited 100
ECHOWORDS_DETAIL_MODEL paid-catalog alias the deeper article asks; empty disables it gpt
ECHOWORDS_DETAIL_PARAMS request parameters sent with the deeper article, as one JSON object in the provider's own vocabulary {"reasoning_effort": "none", "service_tier": "priority"}
ECHOWORDS_ANKI_SYNC sync the collection to AnkiWeb after additions true
ECHOWORDS_ANKIWEB_USER / _PASSWORD AnkiWeb credentials required when sync is on
ECHOWORDS_SYNC_ENDPOINT self-hosted Anki sync server instead of AnkiWeb empty
ECHOWORDS_ACCENT us or uk for English audio us
ECHOWORDS_AUDIO_TIMEOUT shared post-generation seconds to wait for pronunciation before sending without it 10
ECHOWORDS_HOST / ECHOWORDS_PORT bind address; keep it on loopback and let Tailscale be the front door 127.0.0.1:8080

Pronunciation roles share this one post-generation wait. Values below ten seconds shorten it; values above ten seconds cannot extend the hard cap. The wait ends as soon as the audio arrives, so it is only ever paid in full when there will be no audio at all.

The interface language of the PWA is not an environment variable. It is a per-device choice in the header (EN/RU), remembered in the browser, and it does not change ECHOWORDS_TARGET_LANG — the language your cards are written in.

LLM provider keys

The free pool needs at least one of GROQ_API_KEY, OPENROUTER_API_KEY, GEMINI_API_KEY, or ZAI_API_KEY; filling all four gives the pool its full failover set. python -m llmbroker env freetier prints the authoritative list for the installed llmbroker release, with signup links.

The default paid fallback, ECHOWORDS_API_MODEL=gpt-fast, and the default deeper article, ECHOWORDS_DETAIL_MODEL=gpt, additionally need OPENAI_API_KEY. Set ECHOWORDS_API_MODEL= to run on the unmetered pool only.

The deeper article's model and its parameters are one choice. The defaults ask OpenAI for no reasoning and priority processing, which is what makes the article start within a second; pointing ECHOWORDS_DETAIL_MODEL at another provider means writing that provider's parameters into ECHOWORDS_DETAIL_PARAMS, or {} for its defaults.

The Status screen shows which keys are missing, how many providers are usable, and how many paid calls the day has cost.

AnkiWeb

Set ECHOWORDS_ANKIWEB_USER and ECHOWORDS_ANKIWEB_PASSWORD and leave ECHOWORDS_ANKI_SYNC=true. On the first run the headless collection performs a full download of the existing account collection before any local card is added. The password is used once to obtain a sync key; the key is then kept under ECHOWORDS_DATA_DIR and reused.

Keep outbound HTTPS unrestricted for *.ankiweb.net: AnkiWeb redirects sync to numbered shards, so allowing only sync.ankiweb.net fails intermittently.

A self-hosted Anki sync server can be selected with ECHOWORDS_SYNC_ENDPOINT instead.