Skip to content

[i18n] Polish-language support: voice dictation, deterministic NLU, and a UI language switchΒ #63

Description

@grzanka

Feature request

  • A language switch in the UI β€” a flag icon/button (πŸ‡¬πŸ‡§ ⇄ πŸ‡΅πŸ‡±) β€” that puts the app into Polish mode: typed
    queries and mic dictation are interpreted as Polish, and the answer comes back in Polish.
  • Several Polish-speaking colleagues are willing to record voice samples for a Polish eval/training set,
    the same way the existing English speakers did (eval/RECORDING.md, eval/audio/{km,lg,mn}/).
  • An LLM could plausibly help draft the Polish sentences needed for such a set, rather than hand-writing
    all of them.
  • Underlying question: what accuracy can we actually expect from the models on Polish, and what has
    to change in the app
    to support it end-to-end?

This issue is a feasibility investigation answering that question, not an implementation β€” it audits every
pipeline stage this repo already has (ASR worker, deterministic NLU matcher, alias tables, NLG templates,
eval harness, model hosting) with file:line citations, adds external research on Whisper/LLM/TTS Polish
performance, and proposes a scoped path forward. Per this repo's own convention for research/spike issues,
whoever picks this up should land measured findings in a new docs/polish-language-feasibility.md before
closing it β€” this issue is the proposal, not the final report.

Why this is a natural next step, not a stretch

  • docs/model-hosting-cyfronet.md:42-43 already justifies the Cyfronet S3 mirror partly on "Cyfronet is a
    Polish/EU host... for the project's actual user base" β€” the intended users already skew Polish.
  • The whisper-small checkpoint already mirrored in production (onnx-community/whisper-small, per
    docs/model-hosting-cyfronet.md:9-13) is OpenAI's standard multilingual checkpoint (not the
    .en-suffixed English-only variant) β€” it already covers ~99 languages, including Polish. No new model
    needs downloading or mirroring just to attempt Polish ASR.
  • Piper β€” already the planned TTS engine per docs/design.md ("NLG + TTS: templated sentence β†’
    SpeechSynthesis / Piper") β€” has real, working Polish voice models today.

None of this makes Polish support free β€” the NLU and NLG layers are the real cost, detailed below β€” but
the ASR and TTS legs are cheaper than they might look at first glance.

What performance can we actually expect?

Short answer: generic Polish speech recognition is in a good tier; domain accuracy (the number that
actually matters here) is presently unmeasured and cannot be estimated from generic numbers alone.

  • ASR, generic speech. Third-party aggregated reporting on Whisper large-v3 (Common Voice 15) puts
    Polish in the "high accuracy" tier (WER < 10%), in the same band as German/Portuguese/Dutch, well above
    genuinely low-resource languages (Arabic/Hindi/Russian/Turkish β‰ˆ 10–20%, and 25%+ for the long tail). I
    could not independently verify this against OpenAI's own primary per-language table β€” web fetches to
    primary sources (arxiv.org, the Whisper GitHub repo, Wikipedia) all returned 403 in this research
    session β€” so treat the <10% figure as directionally indicative, not authoritative, until someone confirms
    it against the actual paper. Separately, Mozilla Common Voice's Polish corpus (cv-corpus-25.0,
    2026-03) has 188.84h recorded / 176.61h validated from 3,465 speakers β€” a mid/well-resourced language for
    ASR purposes, not a low-resource one.
  • ASR, domain jargon β€” the actual open question. This repo's own numbers show generic WER is a poor
    proxy for domain accuracy: English whisper-small needed domain-prompt biasing (+7.6pp raw slot-token
    accuracy, docs/voice-pipeline-feasibility.md Β§2.4) and an extended corrector (+9pp more) to go from a
    mediocre raw transcript to 89% E2E audio→intent on physics vocabulary (keV/MeV/GeV, per-nucleon, dE/dx,
    ASTAR/PSTAR, isotope names, material trade names like Lucite/PMMA). There's no reason to expect Polish
    physics vocabulary ("zdolnoΕ›Δ‡/moc hamowania" for stopping power, "zasiΔ™g" for range, element names,
    material names) to be any different β€” expect a similarly large gap between generic Polish WER and domain
    E2E accuracy until a Polish domain-prompt string, a Polish corrector, and a Polish eval set exist. This
    is exactly the gap the offered recordings would let us measure and close
    β€” nobody can quote a
    trustworthy domain-accuracy number today because it doesn't exist yet; the honest answer is "unknown but
    cheaply measurable," not "X%."
  • On-device LLM fallback (if this project keeps one at all β€” see docs/voice-pipeline-feasibility.md
    Β§5.6, currently scoped as a rare last resort, English-only, unshipped).
    The models already benchmarked
    here (Qwen2.5-0.5B/1.5B, Β§2.5) claim "multilingual support across 29+ languages," but I found no verified
    Polish-specific benchmark for them. A better-fitting candidate: Bielik (SpeakLeash + ACK Cyfronet
    AGH β€” trained on the same Cyfronet HPC infrastructure this repo already has model-mirroring access to),
    a Polish-native-tuned open LLM family at 1.5B/4.5B/7B/11B. Bielik-11B-v2 scores 58.14 avg on the Open PL
    LLM Leaderboard (best under 20B params, +8.75pp over 2nd place); Bielik-11B-v3 scores 65.93 on the same
    leaderboard and 71.83% on the Polish Linguistic & Cultural Competency Benchmark. The 1.5B/4.5B variants
    are reported competitive with models 2–3Γ— their size, but β€” same caveat as Qwen β€” nobody has run this
    repo's actual constrained-single-token-classification task against them. Needs its own small bench, not
    an assumption; low priority given the fallback itself is unshipped for English.
  • TTS. Piper has real Polish voice models today (e.g. pl_PL-darkman-medium,
    community-trained pl_PL-jarvis_wg_glos-medium / pl_PL-justyna_wg_glos-medium /
    pl_PL-meski_wg_glos-medium / pl_PL-zenski_wg_glos-medium β€” HF WitoldG/polish_piper_models, also
    mirrored e.g. as csukuangfj/vits-piper-pl_PL-zenski_wg_glos-medium). Browser SpeechSynthesis also
    works across Chrome/Edge on Windows/macOS, but whether a Polish voice is actually installed depends on
    OS/browser state β€” unverified with a specific voice name here, needs a real-device check before relying
    on it. Piper is the lower-risk choice since its Polish weights are known and shippable exactly like the
    Whisper weights already are.
  • LLM-generated Polish sentences (for building the eval set). High confidence this works well for raw
    fluency β€” Polish is a mid/high-resource language for frontier LLMs, not a low-resource one where
    synthetic generation quality degrades. The real risk isn't fluency, it's domain naturalness (would a
    Polish medical physicist actually phrase a query that way?) β€” that needs native-speaker review, i.e.
    exactly the friends who offered to help. Treat LLM output as a first draft, not ground truth.

What exists today, and exactly where English is hardcoded

Stage File(s) Finding
ASR language token src/lib/asr/transcribe.ts:229 `const languageEnglish = Number(langToId["<
ASR domain prompt src/lib/asr/transcribe.ts:75-78 DOMAIN_PROMPT β€” the English biasing string ("MeV, keV, GeV, ..., stopping power, Lucite, adipose tissue"). Needs a Polish counterpart; units (MeV/keV) are language-invariant, words like "stopping power" aren't.
ASR model src/lib/models/manifest.ts:34-41 One whisper entry, onnx-community/whisper-small q8 β€” already the standard multilingual checkpoint. No per-language variant concept in ModelManifestEntry, and none needed for a first pass.
NLU matcher src/lib/intent/matcher.ts (705 lines, all of it) Entirely inline English regex/Set literals, not data-driven β€” e.g. INDIRECT_IDIOMS (73-102, /\bhow far\b/ β†’ csdaRange), DIRECT_STOPPING/DIRECT_RANGE (104-106), detectInverse() (109-124, `/\b(?:what
QueryIntent schema src/lib/intent/query-intent.ts, src/lib/intent/coverage.ts No lang/locale field anywhere in QueryIntent, EvalExample, EVAL_TAGS, or the coverage types. Language would need to be threaded in as new state, not slotted into an existing field.
Alias tables src/lib/aliases/materials.ts, particles.ts, elements.ts Flat alias β†’ id maps (MATERIAL_ALIAS_INDEX, PARTICLE_ALIAS_INDEX). Adding Polish aliases (["wΔ™giel", 6], ["woda", 276], a Polish protony particle entry, …) fits the existing shape with no structural change to the lookup mechanism itself. Fun accident: elements.ts:163 already has ["wolfram", 74] as an English variant name for tungsten β€” that's also the standard Polish word for tungsten, a free hit. Caveat: normalize.ts's NFKD stripping (61-70) folds Polish Δ‡/Ε„/Γ³/Ε›/Δ…/Δ™ to their base letters fine, but "Ε‚" (U+0142) does not NFKD-decompose and survives as a distinct letter β€” an unaccented "l" typed by a user won't match a stored "Ε‚" alias without an explicit extra variant. Recommend a locale-scoped index (reusing the same lookup.ts fuzzy-match machinery) rather than merging Polish aliases into the same flat map as English/trade names, to avoid cross-language collisions.
NLG templates src/lib/nlg/render.ts (216 lines, all of it) Hardcoded English template literals throughout, e.g. `The ${QUANTITY_PHRASE[quantity]} of ${energyLabel(...)} ${particle} in ${material} is ${value} (...)`. β€” English word order and prepositions baked in; no lang parameter on renderAnswer(). The harder problem isn't translation, it's Polish grammatical case: "in water" is "w wodzie" (locative), not "w woda" β€” a naive string-table swap doesn't handle noun declension. Recommend a v1 that sidesteps this with a labeled/colon-style Polish output ("ZdolnoΕ›Δ‡ hamowania: 12.3 MeVΒ·cmΒ²/g (materiaΕ‚: woda, czΔ…stka: proton, energia: 100 MeV)") instead of attempting fully natural declined sentences, deferring a real declension table to a stretch goal.
UI strings / i18n scaffolding src/routes/*.svelte (3 files), src/lib/components/**/*.svelte (10 files), plus answer-status.svelte.ts, asr-status.svelte.ts, model-status.svelte.ts No i18n library anywhere (package.json has exactly one runtime dependency, @huggingface/transformers; no svelte-i18n/paraglide/i18next/etc.). No centralized strings module β€” every .svelte file inlines its own English text. Roughly 45–55 distinct user-facing strings across all of it (button labels, status messages, the "Sorry, I couldn't understand that..." fallback, mic states "Warming up…"/"Listening…", download/cache dialogs). src/app.html:2 hardcodes <html lang="en"> β€” a real fix needed (a11y-relevant), not a false positive. Two earlier locale/translate grep hits were confirmed false positives: DarkModeToggle.svelte:35-36 (translate-x-* Tailwind transform classes) and coverage.ts:220 (localeCompare for sorting tag strings).
Eval set eval/intents.jsonl, scripts/validate-intents.ts No lang field in the EvalExample schema, so the format itself is language-neutral β€” but validate-intents.ts:17 hardcodes the file path (../eval/intents.jsonl) and line 21 hardcodes MIN_EXAMPLES = 100. A Polish set needs a genuinely separate frozen file (eval/intents.pl.jsonl, matching CLAUDE.md's rule that the existing intents.jsonl is a frozen regression suite not to be touched) plus a parameterized validator or a second script with its own (lower, initially) minimum-count threshold. EVAL_TAGS categories (phrasing/quantity/comparison/unit/ambiguity/special) are language-agnostic and should be reusable as-is; per CLAUDE.md, LLM-drafted/hard Polish examples must be tagged "adversarial", never "stress-test" (reserved for the two existing Β§7 sentences).
Model hosting docs/model-hosting-cyfronet.md:9-13,25-29, src/lib/models/remote.ts:12 Only whisper (the multilingual checkpoint already discussed) is mirrored; qwen/llama aren't yet. Because the mirrored Whisper weights already cover Polish, basic Polish ASR needs zero new model mirroring β€” the change is code-level (parametrize the `"<

The one strategic fork worth deciding early

Option A β€” native Polish pipeline. Polish ASR (task: "transcribe", <|pl|> token) β†’ a Polish
variant of the matcher β†’ Polish-aware alias lookups β†’ Polish NLG. Full fidelity: the user sees their own
Polish question echoed back for confirmation (this repo's whole "trust UX" design depends on that echo,
per docs/voice-pipeline-feasibility.md Β§4/Β§5.4). Highest cost: forking/parametrizing all 705 lines of
matcher.ts, plus the NLG declension problem above.

Option B β€” Whisper's built-in translate task as a shortcut. Whisper's decoder supports a task
token alongside the language token β€” "transcribe" (current: Polish audio β†’ Polish text) vs.
"translate" (Polish audio β†’ English text, directly, in one decode pass). If Polish queries are run
through task: "translate" instead, the output could flow through the existing, unmodified English
matcher/aliases/NLG entirely β€” sidestepping the expensive matcher fork completely. Real tradeoffs, all
unmeasured: (1) translation is a strictly harder task than transcription, so domain-jargon accuracy on
this path is unknown and needs its own benchmark pass, same as the transcribe-task English work needed;
(2) the "echoed" query shown to the user would be in English even though they spoke Polish β€” plausibly
fine for bilingual physicist users (this project's actual audience), but a real trust/UX regression worth
a deliberate decision, not an accident; (3) it only solves dictation, not the flag-toggle's promise of a
fully Polish-language app β€” NLG/UI still need Polish localization regardless.

Suggested pragmatic path: a hybrid β€” use Option B to shortcut the hard NLU parsing step for a v1 (Polish
speech in β†’ Whisper translate β†’ existing English matcher/compute pipeline), while still localizing NLG
output, TTS voice, and UI chrome to Polish (Polish in, Polish out; English only in the invisible middle).
Defer the full Option A matcher fork unless the translate-task's domain accuracy or the trust-UX regression
turns out to be unacceptable once measured. This isn't free either β€” it needs its own accuracy benchmark β€”
but it avoids committing to the single largest cost item (the matcher fork) before knowing if it's
necessary.

Data collection plan (ties the two feature-request ideas together)

  1. Draft ~30 Polish sentences covering the same coverage matrix eval/RECORDING.md used for English
    (direct stopping-power/range queries, indirect phrasing, conversational filler, multi-material/particle/
    energy comparisons, program-name comparison, tricky units, isotope names, an inverse query, a material
    alias) β€” an LLM can produce this first draft.
  2. Native-speaker review pass (the colleagues who offered to help): correct anything that reads as
    unnatural or a literal translation rather than how a Polish physicist would actually ask. This is the
    step that actually de-risks the "LLM generates sentences" idea β€” treat model output as a draft, not
    ground truth.
  3. Record sessions mirroring scripts/record-session.sh (same WAV/sample-rate conventions), 2–3+ speakers,
    into a new eval/audio/<speaker>/ set kept separate from the English speakers.
  4. Hand-label gold QueryIntents for each sentence into a new eval/intents.pl.jsonl (same schema, new
    file β€” see "Eval set" row above).
  5. Run the existing pipeline scripts (asr-transcribe.mjs, asr-score-slots.mjs, e2e-audio-intents.ts)
    against the Polish set once the ASR language plumbing exists, to get the first real Polish domain
    E2E number
    β€” the thing nobody can quote yet.

Smallest useful next step, doable this week: record even 10–15 of these sentences from 1–2 friends,
run them through whisper-small forced to <|pl|> with no Polish domain prompt yet, and get a first
honest raw-Polish-domain-WER baseline β€” exactly how the English work started, before any tuning existed.

Effort/risk at a glance

Component Relative effort Why
ASR language plumbing (thread a language param through worker β†’ transcribe) Low One hardcoded lookup key to parametrize; no new model.
Polish domain prompt + corrector Medium Needs real Polish recordings to tune against β€” data-bound, not design-bound.
NLU matcher High 705 lines of inline English regex/grammar; no data-driven shortcut (Option B above is the way to defer this).
Alias tables (materials/particles) Low Same alias β†’ id shape already supports this; mainly data entry + native-speaker verification.
NLG templates Medium–High Polish case declension is a real linguistic problem, not just translation; a labeled/non-declined v1 output sidesteps it.
UI strings / i18n scaffolding Medium No library exists yet; ~45–55 short strings to extract; recommend a minimal messages.ts + a runes-based locale.svelte.ts store (matching the existing *.svelte.ts store convention, e.g. model-status.svelte.ts) over pulling in a full i18n framework for 2 languages.
TTS (Piper Polish voice) Low Voices already exist; mirrors the same model-hosting pattern already used for Whisper.
Eval tooling Low–Medium New file + a parameterized/duplicated validator script.
On-device Polish LLM fallback Medium, low priority Needs its own Bielik-vs-Qwen bench; the fallback itself is unshipped for English too.

Open questions (need a human decision, not more research)

  1. Option A vs. B vs. the hybrid above β€” is showing an English-translated echo of a Polish spoken
    query acceptable for v1, or does trust UX require the literal Polish transcript even if it costs the
    full matcher fork?
  2. NLG v1 style β€” is a labeled/colon-style Polish answer ("materiaΕ‚: woda") acceptable initially, or
    is naturally-declined Polish a hard requirement from day one?
  3. UI toggle design β€” a flag-emoji button (πŸ‡¬πŸ‡§/πŸ‡΅πŸ‡±, as originally suggested) or a switch control matching
    the existing DarkModeToggle.svelte pattern? Does switching language also need to persist across
    sessions (localStorage, matching how ASR progress calibration already persists)?
  4. Auto-detect vs. explicit toggle β€” should the app ever attempt to detect Polish vs. English from
    speech automatically, or should the toggle always be an explicit, user-driven choice? (Recommend the
    latter for v1 β€” simpler, and matches how the ASR domain-prompt mechanism already assumes a known,
    fixed language per request.)
  5. How many speakers/sentences is "enough" for a first Polish eval set? The English set used 3
    speakers Γ— ~30 sentences as its first iteration (eval/RECORDING.md) and grew from there β€” is that the
    right starting scale here too, given however many friends actually volunteer?
  6. Physicist/native-speaker sign-off, same as docs/design.md already flags for English isotope/
    energy conventions β€” Polish terminology choices (e.g. "moc hamowania" vs. "zdolnoΕ›Δ‡ hamowania" for
    stopping power) need a real decision from a Polish-speaking physicist, not a model guess.

References

Gathered during this research pass β€” general/background reading, not load-bearing citations for any
specific number quoted above (see caveats inline where a figure is unverified):


Filed from an investigation session; no code changes made. Scope intentionally left as an open proposal β€”
see "Open questions" before picking an implementation path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions