Finnish speech-to-text evaluations and a Whisper large-v3 fine-tune for Finnish dialects (GAIK project)
See the code
We taught OpenAI's Whisper to understand Finnish dialects. On dialect speech it gets about half as many words wrong as before (40 % → 21 %), far fewer than any other model we tried, Gemini 3.5 Flash included. Read and parliament speech stay about as good as before; on standard Finnish full of brand names it gets more of the names wrong. The model and its training data are free to use.
🤗 Model · 🗂️ Training data · 🎬 Video · 🔊 Listen and compare · 📖 The story: suomeksi · in English · 🧰 gaik-toolkit · 🌐 gaik.ai
Made by Akseli Leskinen (laguagu), Haaga-Helia University of Applied Sciences, in the GAIK project, co-funded by the European Union (ERDF).
| Our model against the model it started from | Whisper large-v3 | Our model |
|---|---|---|
| 🗣️ Dialect speech: words wrong | 40.4 % | 21.1 % |
| ⚖️ Another dialect corpus: meaning-error points per 100 words | 25.9 | 13.6 |
| 📖 Read Finnish (FLEURS): words wrong | 7.4 % | 7.6 % |
| 🏛️ Parliament speech (VoxPopuli): words wrong | 12.7 % | 10.8 % |
You give it Finnish speech, dialect or not. It writes standard Finnish, with punctuation.
🔊 Listen to this sentence and two more
Training ran on LUMI, the EuroHPC supercomputer hosted by CSC
in Kajaani. Every script is in scripts/; Run it again lists the commands.
Round by round:
Why is the best version not published?
Published (v1.0) | Internal version | |
|---|---|---|
| Training data | SKN + FLEURS + VoxPopuli, all open licences | the same + 56 h of LA-murre interviews |
| What the language model did | added punctuation | also rewrote LA-murre's dialect spelling in standard Finnish |
| Dialect test, words wrong | 21.1 % | 19.7 % |
| Can it be shared? | ✅ yes, Apache 2.0 | ❌ no: LA-murre's licence (CC BY-ND) does not allow sharing changed texts, so the model trained on them stays internal |
The audio and the words are people's work. For the published model a language model, Claude Opus 5.5, helped in two places (for the internal version it also rewrote LA-murre's dialect spelling). First, it added punctuation to the training text and nothing else:
Second, it judged the test transcripts blind. A real example from the webinar:
The test municipalities were kept out of training, so the test is speech the model had never heard:
Two scores: word error rate counts every word that differs, the blind judge only errors that change the meaning:
Across the open test sets, our model leads on dialect speech and parliament speech; on read sentences gpt-4o-transcribe and Gemini are better:
On ten dental webinars (30 minutes), the judges agree: Gemini 3.5 Flash and ElevenLabs make the fewest meaning errors.
So: use our model for dialect speech, and Gemini 3.5 Flash or plain Whisper large-v3 for standard business Finnish with many names. 🔊 Hear the differences · the single webinar clip and all 26 set-ups on it are in the full details.
The fourth judging round put six transcripts of every segment of the second dialect corpus side by side: base Whisper, two API services and our three models. Claude Opus 5.5 and OpenAI's GPT-6.1-sol (through GAIK's Azure) judged all 583 segments, gpt-oss-120b on CSC's Aitta a seeded sample of 200. Each judge is stricter or milder, but all three put our models far ahead, and Gemini 3.5 Flash and gpt-4o-transcribe behind base Whisper:
GPT-6.1-sol prefers the same transcript as Claude in 92 % of pairs, gpt-oss-120b in 86 %. The second round's open judges (Gemma 4 31B, Qwen3.6 27B) agreed with Claude in 92–93 % of pairs (below); in this round they were too slow on Aitta to finish and are left out.
import soundfile as sf
from transformers import pipeline # pip install "transformers>=5" torch soundfile
asr = pipeline("automatic-speech-recognition", model="GAIK-project/whisper-large-v3-fi-dialects")
audio, sr = sf.read("clip.wav", dtype="float32") # 16 kHz mono, up to 30 s
print(asr({"raw": audio, "sampling_rate": sr}, generate_kwargs={"language": "fi", "task": "transcribe"})["text"])
For longer audio, the model card has code that cuts it into pieces at quiet moments.
Everything below is the full report: every model, every number, how each was measured, and the caveats.
Organisations ask whether there are speech-to-text models that understand Finnish, dialects included, and can run locally. This repository runs 20 current speech-to-text set-ups, 12 of them open models that run on your own GPU, on one Finnish webinar clip and scores them two ways: word error rate, and two blind LLM judges that count only the errors that change the meaning. It then measures the open models on four public test sets, two of them dialect corpora, and fine-tunes Whisper large-v3 for Finnish dialects: that model and its training data are published on Hugging Face.
The clip is Ajokortti: a 180 s excerpt of a public QAdental dental-education webinar (QAdental,
Suomen Kotilääkäripalvelu Oy, is a GAIK project partner), two speakers, standard spoken Finnish with
many names and brand names. It and the University of Helsinki (HY) team's reference transcript are
published in gaik-toolkit's
transcription_eval;
get_clip.py downloads them from there, they are not copied here. Some
transcripts come from QAD-1, GAIK's earlier re-run of HY's evaluation of this clip. The clip is
not dialect speech: dialects are measured on held-out parts of two dialect corpora
(Public test sets,
Our dialect model).
timestamps revision; the main
model stays as it was
(Timestamps and long-form audio).all-skn branch (next steps).v1.0; variants on its timestamps and all-skn branches) and its SKN segments
GAIK-project/skn-finnish-dialects-asr
(CC BY 4.0). This repository holds the method, the code and the results behind them.| Model | Type | WER % | CER % | Claude pts | Gemini pts | Names (Claude / Gemini) |
|---|---|---|---|---|---|---|
| Gemini 3.5 Flash | API | 14.2 (14.2–17.9) | 5.0 | 0.8 | 1.2 | 14 / 14 of 14 |
| Google Chirp 3 | API | 19.6 | 6.6 | 1.2 | 3.7 | 14 / 11 of 14 |
| Whisper large-v3, chunked decoding | open | 15.7 | 6.7 | 1.7 | 5.5 | 14 / 14 of 14 |
| Whisper large-v3 | open | 14.7 | 6.0 | 2.7 | 5.2 | 14 / 13 of 14 |
| ElevenLabs Scribe v2 | API | 18.2 (17.2–18.4) | 4.9 | 1.2 | 7.0 | 14 / 12 of 14 |
| This project, round 2 internal (+ LA-murre), chunked | open | 15.9 | 4.7 | 3.0 | 6.2 | 14 / 12 of 14 |
| Whisper large-v3 turbo | open | 18.4 | 7.7 | 1.5 | 8.5 | 14 / 13 of 14 |
| This project's pilot: Whisper large-v3 + LoRA on FLEURS and VoxPopuli | open | 17.9 | 8.3 | 2.2 | 8.2 | 14 / 13 of 14 |
| OpenAI gpt-transcribe | API | 18.4 (18.4–19.1) | 8.5 | 4.2 | 8.5 | 12 / 12 of 14 |
| WhisperX + Finnish-NLP whisper-large-finnish-v3 (Haaga-Helia WhisperX server) | open | 11.9 | 3.5 | 4.5 | 8.7 | 13 / 11 of 14 |
| This project, round 1, chunked decoding | open | 14.9 | 5.3 | 3.7 | 9.7 | 14 / 13 of 14 |
| OpenAI gpt-4o-transcribe | API | 16.9 (16.2–18.4) | 6.7 | 6.0 | 7.5 | 11 / 10 of 14 |
| OpenAI whisper-1 | API | 23.1 | 11.2 | 3.0 | 10.4 | 14 / 14 of 14 |
| Finnish-NLP whisper-large-finnish-v3 (plain transformers) | open | 13.2 | 4.3 | 5.2 | 8.5 | 13 / 11 of 14 |
| This project, round 1: Whisper large-v3 + LoRA on SKN dialects, FLEURS and VoxPopuli | open | 17.9 | 5.6 | 4.5 | 9.4 | 13 / 12 of 14 |
| OpenAI gpt-4o-mini-transcribe | API | 13.9 (13.9–14.7) | 4.4 | 3.2 | 11.4 | 13 / 10 of 14 |
| Orukeet (Parakeet v3 fine-tune) | open | 26.4 | 10.2 | 6.2 | 10.7 | 13 / 11 of 14 |
| This project, round 2 (published), chunked decoding | open | 13.9 | 5.2 | 6.7 | 10.9 | 11 / 10 of 14 |
| RASMUS Finnish-ASR-Canary-v2, greedy | open | 23.9 | 8.1 | 6.5 | 13.7 | 11 / 8 of 14 |
| RASMUS Finnish-ASR-Canary-v2 + KenLM | open | 22.1 | 8.5 | 8.5 | 15.4 | 11 / 7 of 14 |
| Gemini 3.1 Pro | API | 22.4 (15.4–22.4) | 9.3 | 7.0 | 18.4 | 12 / 10 of 14 |
| NVIDIA Canary 1B v2 | open | 24.1 | 10.3 | 9.0 | 17.9 | 13 / 10 of 14 |
| NVIDIA Parakeet TDT 0.6B v3 | open | 31.1 | 13.8 | 12.7 | 17.4 | 13 / 12 of 14 |
| Finnish-NLP wav2vec2 XLS-R 1B (no LM) | open | 31.6 | 9.1 | 6.2 | 26.1 | 13 / 10 of 14 |
| Qwen3-ASR 1.7B | open | 58.0 | 23.2 | 20.6 | 33.6 | 9 / 7 of 14 |
| Qwen3-ASR 0.6B | open | 77.9 | 33.9 | 28.1 | 41.0 | 7 / 4 of 14 |
Results on the Ajokortti clip, sorted by the mean of the two judges. pts = meaning-error points
per 100 reference words (critical 10, major 5, minor 1), lower is better. Names = how many of the
clip's 14 names and terms each judge found right or recognisable. WER and CER use HY's normalisation
(lower-case, every Unicode punctuation character removed, runs of whitespace collapsed; see the
toolkit's What the metrics mean);
the range is over three runs where a model was run three times. "open" = weights can be downloaded
and run on your own GPU. All numbers per model are in results/scores.json.
File names in results/ start with where the model ran: openai_, vertex_ (Gemini),
elevenlabs_ and google_ (Chirp) are APIs, gaik_ the gaik toolkit's transcriber, hh_ the
Haaga-Helia WhisperX server, hf_, nemo_ and qwen_ open models on a GPU, ft_ our fine-tunes.
The clip's reference is HY's transcript, which looked like a corrected WhisperX draft. On 14 Oct
2026 Akseli Leskinen listened to the 42 spans where most of seven systems hear something else and
corrected 14 of them: mostly spoken forms HY had written in standard Finnish, two missing words and a
brand name. The reviewed text is published next to the original in gaik-toolkit
(reference.reviewed.txt).
Against it (results/scores.reviewed.json, score_asr.py --reviewed):
| Model | WER %, HY's reference | WER %, reviewed |
|---|---|---|
| OpenAI gpt-4o-mini-transcribe | 13.9 | 13.2 |
| Whisper large-v3 | 14.7 | 13.7 |
| Gemini 3.5 Flash | 14.2 | 13.7 |
| Our model, round 2 (published), chunked | 13.9 | 14.0 |
| ElevenLabs Scribe v2 | 18.2 | 14.2 |
| WhisperX + Finnish-NLP whisper-large-finnish-v3 (Haaga-Helia WhisperX server) | 11.9 | 15.7 |
| Finnish-NLP whisper-large-finnish-v3 (plain transformers) | 13.2 | 15.7 |
The WhisperX-based arms lose their lead and ElevenLabs, which writes what is said, gains four points: the review confirms that HY's text favoured WhisperX output. HY's own 2025 WhisperX file shows the same shift (10.4 → 14.0 %, reported in gaik-toolkit; that transcript is not part of this repository). The leading set now sits within about one point (13.2–14.2 %), which a 408-word reference cannot separate. Only the 42 disputed spans were checked, so read every WER here with the rest of the reference still unverified. The tables above and the judges' scores are against HY's text and were not re-run.
The clip above is one recording. Four open Finnish test sets:
prepare_skn.py.
The reference is SKN's normalised transcript: standard Finnish word forms, no punctuation. A model
is right when it hears the dialect and writes standard Finnish. The test segments are the test
split of the published dataset.prepare_lamurre.py. No published model was
trained on any of LA-murre; only the internal model was, on recordings from other localities,
never these 10. Its reference is a dialect transcript, so WER here charges a model for writing standard
Finnish: every model lands at 52–73 %. Read this column with the
meaning judge, not on its own. Only the numbers are published
(results/benchmarks/lamurre_fi/scores.json): the
licence does not allow sharing the modified references, the transcripts paired with them or the
judge records, and prepare_lamurre.py rebuilds the test from Kielipankki.FLEURS and VoxPopuli are prepared or read speech, the sets model cards report; SKN is spontaneous speech from mostly elderly speakers, recorded in the 1960s–70s.
| Model | SKN dialects WER % | LA-murre dialects WER % | FLEURS WER % | VoxPopuli WER % | FLEURS CER % | s / clip |
|---|---|---|---|---|---|---|
| This project's pilot: Whisper large-v3 + LoRA †‡ | – | – | 7.1 | 10.8 | 1.6 | 0.95 |
| This project, trained on all of SKN (its test included, so no SKN score) †‡§ | – | 57.8 | 7.5 | 11.9 | 1.7 | 0.96 |
| Orukeet (Parakeet v3 fine-tune) | – | – | 11.6 | 15.9 | 2.8 | 0.04 |
| NVIDIA Parakeet TDT 0.6B v3 | – | – | 13.0 | 17.5 | 3.1 | 0.18 |
| Finnish-NLP wav2vec2 XLS-R 1B (no LM) | – | – | 16.2 | 9.4 | 6.2 | 1.50 |
| This project, round 2 internal: + LA-murre †‡§¶ | 19.7 | 57.4 | 7.8 | 11.4 | 1.8 | 0.96 |
| This project, round 2: + punctuated SKN targets (published) †‡§ | 21.1 | 58.1 | 7.6 | 10.8 | 1.7 | 0.98 |
This project, round 2 + timestamp tokens (timestamps revision) †‡§ | 21.4 | 58.1 | 7.4 | 11.2 | 1.7 | 0.98 |
| This project, round 1: Whisper large-v3 + LoRA incl. SKN dialects †‡§ | 22.2 | 57.9 | 7.8 | 10.6 | 1.9 | 0.96 |
| Gemini 3.5 Flash (API, asked for standard Finnish) | 34.7 | 61.8 | – | – | – | – |
| Whisper large-v3 | 40.4 | 62.5 | 7.4 | 12.7 | 1.7 | 0.98 |
| Gemini 3.5 Flash (API, asked to write as spoken) | 40.8 | 60.2 | 5.1 | 11.5 | 1.9 | 2.24 |
| Google Chirp 3 (API) | 41.3 | – | 4.8 | 11.6 | 1.6 | 1.82 |
| Whisper large-v3 turbo | 42.5 | 62.7 | 8.4 | 13.5 | 2.2 | 0.19 |
| ElevenLabs Scribe v2 (API) | 44.9 | – | 3.3 | 9.1 | 1.7 | 3.09 |
| NVIDIA Canary 1B v2 | 45.6 | 65.6 | 8.7 | 11.4 | 2.1 | 0.30 |
| RASMUS Finnish-ASR-Canary-v2 + KenLM †‡ | 47.6 | 62.7 | 7.6 | 7.7 | 2.5 | 0.71 |
| RASMUS Finnish-ASR-Canary-v2, greedy †‡ | 50.1 | 58.7 | 8.4 | 6.8 | 2.4 | 0.10 |
| Finnish-NLP whisper-large-finnish-v3 † | 56.6 | 51.6 | 7.1 | 6.6 | 1.7 | 0.89 |
| OpenAI gpt-4o-transcribe (API, via Azure) | 62.5 | 72.6 | 4.0 | 11.4 | 1.4 | 0.88 |
| Qwen3-ASR 1.7B | 66.7 | 72.9 | 25.1 | 27.5 | 5.8 | 0.34 |
Corpus WER and CER after lower-casing and removing punctuation. Seconds per utterance
are FLEURS GPU time in batches of 16 on one AMD MI250X, model loading excluded. † trained
on FLEURS training data, ‡ on VoxPopuli training data, § on SKN training data from
other municipalities, ¶ on LA-murre training data from other localities (model cards, or this
project), so those numbers are on familiar material. A dash: not run on that set. The API services
(run_api_benchmark.py) ran with the clip runs' models and settings,
one request per segment; OpenAI's gpt-4o-transcribe through GAIK's Azure. Seconds per clip for them
are request wall time, which varies with the service's load. "As spoken" is the clip's
prompt (write exactly what is said); "standard Finnish" asks for standard word forms, which is what the
SKN reference has. Chirp 3 returned 1 262 words of a repeated "se on" for one 50-word segment
(SKN15b_Rautu_0133), which costs it 6.3 points (35.0 % without that segment), and heard no speech in 8
segments, counted as deletions. The API services' training data is unknown, so for them the test
municipalities cannot be called held out.
VoxPopuli references keep digits and abbreviations as the parliament wrote them, which costs every
model some WER. The RASMUS model card reports 8.39 % (greedy) and 7.75 % (KenLM) on FLEURS; this run
gives 8.4 % and 7.6 %.
The Ajokortti clip is one of ten. The other nine come from the same QAdental webinar material: ten 180 s clips (30 minutes of speech), each with the University of Helsinki's Finnish transcript (QAD-1 set A in HY's study). The files were checked against HY's originals: the same audio and the same text byte for byte.
Read the word error rate here with care. HY's transcripts leave out fillers ("niinku", "tota",
"no niin") and were not checked against the audio by a listener. A spot check found missing fillers,
a wrong word form ("yrittäjien" where the speaker says "yrittäjän") and a word missing at a clip's
start; the Ajokortti review above found the same. So models that also leave fillers out (Whisper,
gpt-4o-transcribe) get about 2 % of inserted words, and models that write what is said (ElevenLabs,
Chirp, Gemini, our model) 11–14 %. The blind judges of judge_dental.py
(the Ajokortti prompt, Claude Opus 5.5 and Gemini 3.1 Pro) ignore fillers and count only what
changes the meaning; on that measure Gemini 3.5 Flash and ElevenLabs Scribe v2 are best, and our
model, built for dialects, is last of the six judged.
| Model | WER % | Inserted words % | Meaning points, Claude Opus 5.5 | Meaning points, Gemini 3.1 Pro |
|---|---|---|---|---|
| Whisper large-v3 | 15.7 | 2.7 | 3.6 | 6.4 |
| Whisper large-v3, in 15–25 s pieces | 17.4 | 2.2 | – | – |
| OpenAI gpt-4o-transcribe (API, via Azure) | 19.1 | 2.4 | 5.3 | 9.1 |
| Whisper large-v3 turbo | 19.2 | 2.2 | – | – |
| OpenAI Whisper (API, via Azure) | 22.1 | 2.6 | – | – |
| Orukeet (Parakeet v3 fine-tune) | 24.4 | 2.3 | – | – |
| NVIDIA Canary 1B v2 | 24.5 | 2.2 | – | – |
| ElevenLabs Scribe v2 (API) | 25.4 | 11.4 | 1.6 | 2.8 |
| This project, all of SKN, in pieces †‡§ | 25.7 | 12.5 | 4.8 | 9.7 |
This project, timestamps revision, in pieces †‡§ | 25.8 | 12.9 | – | – |
| Finnish-NLP whisper-large-finnish-v3 † | 25.8 | 12.7 | – | – |
| This project, published, in 15–25 s pieces †‡§ | 25.9 | 12.8 | 5.1 | 10.0 |
| Google Chirp 3 (API) | 25.9 | 11.3 | 2.3 | 3.7 |
| This project, internal, in pieces †‡§¶ | 26.5 | 13.4 | – | – |
| Gemini 3.5 Flash (API, asked for standard Finnish) | 26.5 | 13.7 | – | – |
This project, timestamps revision, long audio in one go †‡§ | 27.0 | 13.0 | – | – |
| Gemini 3.1 Pro (API) | 28.5 | 11.4 | – | – |
| RASMUS Finnish-ASR-Canary-v2 + KenLM †‡ | 28.6 | 9.9 | – | – |
| Gemini 3.5 Flash (API, asked to write as spoken) | 28.6 | 14.0 | 1.2 | 2.1 |
| RASMUS Finnish-ASR-Canary-v2, greedy †‡ | 29.7 | 8.8 | – | – |
This project, round 2 + timestamp tokens (timestamps revision) †‡§, whole clip in one go | 29.8 | 15.8 | – | – |
| NVIDIA Parakeet TDT 0.6B v3 | 32.3 | 1.8 | – | – |
| Finnish-NLP wav2vec2 XLS-R 1B (no LM) | 42.1 | 5.9 | – | – |
| This project, round 2 internal: + LA-murre †‡§¶, whole clip in one go | 60.0 | 44.9 | – | – |
| This project, round 2: + punctuated SKN targets (published) †‡§, whole clip in one go | 60.2 | 44.8 | – | – |
| Qwen3-ASR 1.7B | 93.7 | 35.4 | – | – |
Meaning points: errors per 100 reference words over all ten clips (critical 10, major 5, minor 1).
Our models were run on 15–25 s pieces cut at quiet points; fed a whole 180 s clip in one go, the
published model repeats itself (45 % inserted words), and only the timestamps revision with its
fallback decode copes. HY's transcripts are private, so only the numbers are committed
(results/benchmarks/dental10_fi/scores.json,
results/judgements_dental/summary.json).
Can the open training data improve the best open model? A LoRA fine-tune of Whisper large-v3 (1 % of the weights, rank 32 on the attention query and value projections) on the FLEURS and VoxPopuli Finnish training sets: 10 208 recordings, 2 epochs, 51 minutes on one MI250X. Validation loss fell from 0.48 to 0.17.
| Test | Whisper large-v3 | After fine-tuning |
|---|---|---|
| FLEURS WER % | 7.4 | 7.1 |
| VoxPopuli WER % | 12.7 | 10.8 |
| Ajokortti clip WER % | 14.7 | 17.9 |
| Ajokortti, Claude / Gemini error points | 2.7 / 5.2 | 2.2 / 8.2 |
It got better on speech like its training data and not on the webinar: WER on the
clip rose and the two judges disagree on the direction. Finnish-NLP's and RASMUS's
models, trained on the same kind of data, show the same pattern (good on FLEURS and
VoxPopuli, behind base Whisper on the clip). Open read and parliamentary speech is
not the data that makes a model better at spontaneous Finnish; that needs spontaneous
and dialect speech (see below). The training log is in
results/finetune/;
the adapter is not published. lumi.sh run train hf --name <name> repeats it
with other settings.
Walk-through for readers new to this: the story page, in English and suomeksi (sources:
docs/story/), explains the data, the four places where Claude Opus 5.5 was used, the training, both measurements, the three open judges, the checked webinar reference and the timestamp variant, with two audio examples. The files also work offline. To hear the models side by side, open the listening page (built bybuild_listen.py);build_compare.pybuilds a fuller local comparison that embeds the clip, so it is not committed.
The pilot showed that read speech does not teach spontaneous Finnish. The second run adds dialect interviews: Suomen kielen näytteitä (SKN), Kotus / Kielipankki, CC BY 4.0 (urn:nbn:fi:lb-2020112937), about two hours of interview from each of 50 municipalities, every sentence time-aligned with a dialect transcript and a normalised standard-Finnish one.
Data. prepare_skn.py joins sentences into segments of
2–25 s and splits by municipality, so no test locality or informant is heard in
training: 38 municipalities for training (15 042 segments, 62 h), 2 for validation
(Pälkäne, Sotkamo; 200 segments) and every fifth SKN booklet, in publication order, for the
test (SKN05, 10 … 50; 600 segments). By Kielipankki's classification the test covers the
south-western, south-western transitional, Häme, Savo and south-eastern dialects; all nine
Ostrobothnian and Peräpohjola municipalities are in training. Interviewer speech is kept, and
some SKN fieldworkers recorded both test and training municipalities, so a few test segments
may contain an interviewer's voice heard in training. The target is the normalised text,
so the model learns to write standard Finnish from dialect speech.
Training. The same LoRA recipe as the pilot (rank 32, 2 epochs, learning rate 1e-4)
on SKN plus FLEURS and VoxPopuli train: 25 250 segments, 2 h 2 min on one MI250X (training with
per-epoch validation, from its train_log.json).
Validation loss (SKN and the open sets together) fell from 0.55 to 0.21.
| Test | Whisper large-v3 | Pilot (open data) | + SKN dialects |
|---|---|---|---|
| SKN dialect test WER % (10 held-out municipalities) | 40.4 | – | 22.2 |
| SKN dialect test CER % | 19.1 | – | 9.0 |
| FLEURS WER % | 7.4 | 7.1 | 7.8 |
| VoxPopuli WER % | 12.7 | 10.8 | 10.6 |
| Ajokortti clip WER % | 14.7 | 17.9 | 17.9 (14.9 chunked) |
| Ajokortti, Claude / Gemini error points | 2.7 / 5.2 | 2.2 / 8.2 | 4.5 / 9.4 (3.7 / 9.7 chunked) |
WER on the dialect test fell in every one of the 10 municipalities:
| Municipality | Alastaro | Artjärvi | Eurajoki | Heinola | Hinnerjoki | Ikaalinen | Jaala | Mikkeli | Rautu | Vihti |
|---|---|---|---|---|---|---|---|---|---|---|
| Whisper large-v3 | 35.7 | 44.4 | 43.7 | 36.1 | 51.8 | 31.3 | 37.3 | 38.5 | 50.4 | 32.8 |
| + SKN dialects | 19.7 | 23.3 | 23.4 | 20.6 | 30.6 | 16.2 | 19.6 | 19.9 | 33.4 | 15.3 |
What it means. Dialect data is what moves dialect accuracy: the error rate nearly
halves, and no ready-made model, including the Finnish fine-tunes trained on read
speech (48–57 %), comes close. FLEURS moved by 0.4 points the wrong way and VoxPopuli
improved, so general Finnish held. On the webinar clip the higher WER came from Whisper's
long-form decoding loop, not from the model: with the clip cut into the same chunks the
short-audio models get, it is 14.9 % against 15.7 % for base Whisper
(round 2). The judges still count more meaning
errors than for base Whisper (brand names, people's names and dental terms such as "oikoja" and
"etualue"); that clip is standard spoken Finnish with brand names, which SKN does not contain. Part
of the gain is learning SKN's own conventions (numbers as words, its normalised spellings), which
WER rewards; the second corpus below separates the two.
The training log is in
results/finetune/; round 1 was not
published (round 2 is). docs/DIALECT-EXAMPLES.md shows 20 test
segments side by side for the published model.
Attribution. Suomen kielen näytteitä, Kotimaisten kielten keskus (Kotus), via
Kielipankki – The Language Bank of Finland, CC BY 4.0. The segments and transcripts
quoted in results/benchmarks/skn_fi/ and
results/labels/ come from it.
Is the gain dialect skill, or only SKN's own conventions? The fine-tuned model was run on a corpus it has never seen any part of: the Finnish Dialect Syntax Archive (LA-murre), recorded and transcribed by other people, 10 localities outside SKN's 50 municipalities, one per dialect region.
WER cannot answer it. LA-murre's reference is written in dialect, so a model that writes a dialect word in its standard form is charged an error, and the model that copies dialect spellings best (Finnish-NLP, 51.6 %) has the lowest WER. The fine-tune still improves on base Whisper (62.5 → 57.9 % WER, 22.9 → 17.0 % CER) and leaves far less out (deleted words 12.9 → 2.6 %).
So a blind judge read the meaning. Claude Opus 5.5 got the dialect reference and three
transcripts per segment, labelled A, B, C in shuffled order, and counted only meaning
errors (wrong word, name or number, missing or invented content); standard against
dialect forms is not an error (judge_dialect.py).
| Model | Error points / 100 words | Critical | Major | Minor | Segments with no error |
|---|---|---|---|---|---|
| Whisper large-v3 + SKN dialects (this project) | 13.9 | 16 | 309 | 654 | 108 |
| Finnish-NLP whisper-large-finnish-v3 | 21.6 | 38 | 482 | 864 | 49 |
| Whisper large-v3 | 22.7 | 41 | 527 | 798 | 55 |
564 of the 583 segments (the judge left a verdict out for 19 long ones, three attempts each). Critical 10, major 5, minor 1 point. Segment by segment the fine-tune has fewer points than base Whisper in 350, more in 101 and the same in 113 (sign test p < 0.001), and it is ahead in every locality:
| Locality | Tammela | Posio | Muolaa | Laukaa* | Iitti* | Sodankylä* | Laihia | Temmes | Nilsiä* | Rauma |
|---|---|---|---|---|---|---|---|---|---|---|
| Whisper large-v3 | 14.0 | 21.7 | 22.0 | 20.0 | 26.2 | 32.9 | 19.0 | 16.0 | 30.9 | 33.7 |
| + SKN dialects | 10.9 | 12.7 | 16.7 | 7.4 | 14.4 | 19.2 | 13.2 | 10.7 | 16.1 | 27.8 |
| Finnish-NLP fine-tune | 15.3 | 20.0 | 21.4 | 20.8 | 25.3 | 25.1 | 15.8 | 18.0 | 28.7 | 30.8 |
* read from the corpus file code (kess_lau1, kyml_iit1, perpl_sod1, savp_nil1); the other
six place names are also said in the interviews.
The gain carries over to dialect speech from another corpus: about 40 % fewer meaning-error points than base Whisper in this judging (47 % when the same transcripts were judged again in round 2 below), about the size of the WER gain on SKN. One judge, itself a model, and one fine-tune run; Rauma stays hard for every model.
Attribution. Finnish Dialect Syntax Archive (Lauseopin arkiston murrekorpus),
University of Turku and Kotimaisten kielten keskus, via Kielipankki, CC BY-ND 4.0
(urn:nbn:fi:lb-2020112935). No LA-murre text is in
this repository: the references were modified (two annotation marks removed, as
prepare_lamurre.py describes) and the licence is no-derivatives, so only numbers made from them
are published (scores.json, the judges' summary*.json and agreement-r2.json).
Round 1 showed that dialect data is what moves dialect accuracy. Round 2 asked what else helps, with the same base model and method, and picked the model to keep by validation loss, never by test scores.
flowchart LR
SKN["SKN, 50 municipalities<br/>CC BY 4.0"] --> CUT["prepare_skn.py<br/>2-25 s segments,<br/>split by municipality"]
LA["LA-murre, 142 interviews<br/>CC BY-ND 4.0"] --> CUTL["prepare_lamurre.py<br/>test: 10 localities<br/>train: the others"]
CUT --> P["Opus 5.5 adds punctuation<br/>(words must stay identical)"]
CUTL --> N["Opus 5.5 rewrites dialect<br/>in standard Finnish"]
OPEN["FLEURS + VoxPopuli<br/>train"] --> FT
P --> FT["LoRA r64 on Whisper large-v3<br/>LUMI MI250X, 3 epochs,<br/>keep the best epoch"]
N -.internal model only.-> FT
FT --> T["Tests: SKN, LA-murre,<br/>FLEURS, VoxPopuli, clip"]
T --> J["Blind Opus 5.5 judge<br/>on LA-murre meaning"]
1. Training text with punctuation. SKN's standard-Finnish text has no punctuation and
almost no capitals, so round 1 learned to write almost none (1 of its 600 SKN test outputs has a
full stop or comma; base Whisper 97.5 %). prepare_targets.py
had Claude Opus 5.5 add punctuation and capitals to every training and validation segment,
20 per call, and threw away any answer whose words were not exactly the input's: 15 024 of
15 042 passed, the other 18 kept their plain text. The text-only LLM never hears audio and
never touches a test set. The targets are in
results/labels/.
2. More dialect speech. LA-murre's other recordings, without the 10 test localities, a second recording from one of them and the five localities that are also SKN test municipalities, give 15 532 segments (56 h). Their transcripts are in dialect spelling, so Opus rewrote each one in standard Finnish, word for word; 15 529 kept a word count within 30 % of the source. LA-murre is CC BY-ND, so the model trained on them is an internal comparison only and its rewritten texts are not published.
3. Training. LoRA rank 64 on the attention query and value projections (2 % of the
weights), learning rate 1e-4, batch 8, on one MI250X. A four-epoch run showed overfitting:
validation loss was lowest after epoch 2 and rose after it (0.206, 0.208, 0.215), so the
final runs train three epochs and merge the epoch with the lowest validation loss, which
was epoch 2 in both. Published model: 25 250 segments, 3 h 8 min. Internal model:
40 779 segments, 4 h 57 min (training with per-epoch validation, from the train_log.json files).
| Model | Training data | SKN WER | LA-murre judge pts | FLEURS | VoxPopuli | Clip WER (chunked) | Clip Claude / Gemini pts |
|---|---|---|---|---|---|---|---|
| Whisper large-v3 | none | 40.4 | 25.9 | 7.4 | 12.7 | 15.7 | 1.7 / 5.5 |
| Round 1 (r32, 2 epochs) | SKN + open | 22.2 | 13.8 | 7.8 | 10.6 | 14.9 | 3.7 / 9.7 |
| Round 2, published | SKN with punctuation + open | 21.1 | 13.6 | 7.6 | 10.8 | 13.9 | 6.7 / 10.9 |
| Round 2, internal | + LA-murre rewritten | 19.7 | 11.4 | 7.8 | 11.4 | 15.9 | 3.0 / 6.2 |
WER in %, lower is better. LA-murre judge: meaning-error points per 100 reference words,
Claude Opus 5.5, all four models judged together on each of the 583 segments in shuffled,
unlabelled order (round 2 of judge_dialect.py). Its scale
differs from the round-1 judging above, which compared other models: base Whisper gets 22.7
points in round 1 and 25.9 in round 2 (critical errors 41 and 74), the round-1 model 13.9 and
13.8. Compare models only within one round. The four-epoch variants are in
results/benchmarks/*/scores.json (-r64e4, -punct, -lamurre); their last epoch is slightly
better on SKN and worse on VoxPopuli.
What it shows.
run_open_models.py, recipe whisper_chunks)
removes it. The model card's table of decoding settings is the published model downloaded back
from the Hub and run on the clip six ways
(results/clip/decoding_check.json, scored by
score_asr.py --decoding into decoding_scores.json):
quiet-point pieces 13.7 % WER, the pipeline's chunk_length_s=20 14.7 %, chunk_length_s=30
52.5 % (it loops) and 14.4 % with no_repeat_ngram_size=6; against the reviewed reference 13.5,
13.5, 51.5 and 14.2 %.Cost. Opus 5.5 for the training text, about 100 USD (punctuation 52, LA-murre rewriting
49); the blind judging of rounds 1 and 2 about 25 USD (round 3, for the timestamp variant below, used
about as many tokens as each of them); about 40 GCD-hours of LUMI GPU time for every job in both
rounds (about 20 billed GPU-hours). The published model (tag v1.0), its timestamp variant (branch
timestamps) and its SKN segments are on Hugging Face
(model,
dataset); their cards are
in release/ and publish_hf.py uploads them. The internal
LA-murre model is not published: it is kept in the private Hugging Face repository
GAIK-project/whisper-large-v3-fi-dialects-internal for use inside the project, and its rewritten
LA-murre training texts are not published either.
Attribution. SKN as above. LA-murre: Lauseopin arkiston murrekorpus, University of Turku and Kotus, via Kielipankki, CC BY-ND 4.0, used for testing and for the internal model only.
The published model was trained on text without Whisper's timestamp tokens. Does that break its
timestamps and its long-form decoding, and does training with them help?
eval_timestamps.py measured both against SKN's own sentence times
(prepare_timestamps.py):
return_timestamps=True. For each of the
791 sentence starts inside a segment (after its first sentence), the distance to the nearest
timestamp the model wrote there.results/labels/skn_longform.jsonl, window edges in pauses
no sentence crosses), decoded three ways: Whisper's own sequential long-form mode with timestamps;
the same with OpenAI's temperature fallback (temperatures 0.0–1.0, compression-ratio threshold
1.35, log-probability threshold −1.0, no-speech threshold 0.6, condition_on_prev_tokens=False),
which decodes a 30 s window again at a higher temperature when its text compresses too well or its
log-probability is too low; and 15–25 s pieces cut at the quietest 100 ms (the model card's code).
A decode counts as a loop when one 5-word sequence occurs four times or more and more often than
in the reference.| Model | SKN sentence starts within 0.5 s / 1 s | Median distance | Long-form WER, sequential (loops) | Long-form WER, with fallback (loops) | Long-form starts within 1 s, with fallback | Long-form WER, pieces |
|---|---|---|---|---|---|---|
| Whisper large-v3 | 53.5 % / 60.8 % | 0.18 s | 37.5 % (0 of 10) | 37.5 % (0) | 62.6 % | 39.4 % |
| Published model | 0.8 % / 0.8 % | 5.33 s | 21.5 % (1 of 10) | 19.8 % (0) | 18.9 % | 17.3 % |
| Timestamp variant | 56.4 % / 57.8 % | 0.04 s | 25.2 % (3 of 10) | 18.1 % (0) | 49.0 % | 17.1 % |
The published model's timestamps are unusable: it writes them, but almost never where a sentence
starts. Its text is still good, and in pieces it is far ahead of base Whisper (17.3 % against
39.4 %). So decode it in pieces and take times from the cut points, as the model card's code does.
WER with timestamps on the short segments is 21.2 % (base 41.0 %), level with the scores above.
Results: results/timestamps/scores.json, raw decodes beside it.
Can training put the timestamps back? finetune_whisper.py --timestamps 0.5 repeats the published
model's recipe, but half of the SKN items, drawn again every epoch, carry one timestamp pair per SKN
sentence (<|0.00|> text<|1.84|>, times from
results/labels/skn_parts.jsonl); 15 040 of the 15 042 training
items had usable times. Epoch 2 was kept, with validation loss 0.2158 (not comparable with the
published model's 0.206, because about half of its SKN validation segments carry timestamp tokens), after 3 h
6 min on one MI250X (train_log.json).
| Test | Whisper large-v3 | Published model | Timestamp variant |
|---|---|---|---|
| SKN dialect test, WER % | 40.4 | 21.1 | 21.4 |
| FLEURS, WER % | 7.4 | 7.6 | 7.4 |
| VoxPopuli, WER % | 12.7 | 10.8 | 11.2 |
| LA-murre, WER % (dialect reference) | 62.5 | 58.1 | 58.1 |
| LA-murre, blind judge round 3, points per 100 words | 24.2 | 13.7 | 13.7 |
| Clip, long-form with timestamps, WER % (HY's / reviewed reference) | 14.7 / 13.7 | 62.7 / 62.2 (loops) | 16.7 / 16.2 |
| Clip, quiet-point pieces, WER % (HY's / reviewed reference) | 15.7 / 15.7 | 13.9 / 14.0 | 15.2 / 14.5 |
| SKN sentence starts within 1 s | 60.8 % | 0.8 % | 57.8 % |
| Long-form interviews with fallback, WER % | 37.5 | 19.8 | 18.1 |
The blind judge's round 3 (judge_dialect.py --round r3,
summary-r3.json) judged base Whisper, the
published model and the variant together on 577 of the 583 LA-murre segments: 24.18, 13.72 and 13.68
points. The variant has fewer points than the published model in 98 segments and more in 100 (sign
test p = 0.94): no difference.
Decision. The published model stays the main model. The rule set before the run was that the
variant replaces it only if SKN gets at most about 0.5 points worse, FLEURS and VoxPopuli get no
worse, and the timestamps are usable. VoxPopuli got 0.4 points worse, so the variant did not pass; the
clip in pieces was also 0.5–1.3 points worse. The variant is published as the timestamps revision of
GAIK-project/whisper-large-v3-fi-dialects
(card: release/TIMESTAMPS_CARD.md), and the main model is tagged
v1.0. Use the variant when you need sentence timestamps, or long audio transcribed without cutting
it yourself, and always with the fallback settings: without them it looped in 3 of the 10 interviews.
import soundfile as sf
from transformers import pipeline
asr = pipeline(
"automatic-speech-recognition",
model="GAIK-project/whisper-large-v3-fi-dialects",
revision="timestamps",
dtype="float16",
device="cuda",
)
audio, sr = sf.read("interview.wav", dtype="float32") # 16 kHz mono, any length
out = asr(
{"raw": audio, "sampling_rate": sr},
return_timestamps=True,
generate_kwargs={
"language": "fi",
"task": "transcribe",
"temperature": (0.0, 0.2, 0.4, 0.6, 0.8, 1.0),
"compression_ratio_threshold": 1.35,
"logprob_threshold": -1.0,
"no_speech_threshold": 0.6,
"condition_on_prev_tokens": False,
},
)
for chunk in out["chunks"]:
print(f"[{chunk['timestamp'][0]:7.2f} s] {chunk['text'].strip()}")
The judges are language models and they are noisy. On the clip, two arms with an identical
transcript, the Haaga-Helia WhisperX server's and HY's own run of the same set-up (a transcript HY
has not published, so it is not in this repository), got 4.5 and 3.5 points from Claude and 8.7 and
10.2 from Gemini, so read differences under about 2 points as ties. The same shows in this
repository on LA-murre: the same transcripts judged again in round 3, in other company and on 577 of
the 583 segments, got 24.2 instead of 25.9 points (base Whisper) and 13.7 instead of 13.6 (the
published model)
(summary-r2.json,
summary-r3.json). Gemini is stricter
throughout. Gemini also judges Gemini's own transcripts; Claude, which made no transcript, puts
Gemini 3.5 Flash first as well.
Is the LA-murre result Claude's opinion only? Open models served by CSC's Aitta service (CSC's
OpenAI-compatible LLM API for Finnish research) judged round 2 again with the same prompt, answer
schema, labels and order as Claude (judge_dialect.py --provider aitta,
compared by judge_agreement.py in
agreement-r2.json):
| Judge | Segments | Whisper large-v3 | Round 1 | Published | Internal | Published vs base (fewer / more points) | Same preference as Claude |
|---|---|---|---|---|---|---|---|
| Claude Opus 5.5 (reference) | 583 | 25.9 | 13.8 | 13.6 | 11.4 | 398 / 89 | – |
| gpt-oss-120b (Aitta) | 583 | 20.5 | 13.4 | 13.2 | 12.9 | 275 / 105 | 80.7 % of 1 535 pairs |
| Gemma 4 31B (Aitta), sample | 198 | 17.3 | 10.1 | 10.5 | 9.0 | 100 / 27 | 92.1 % of 544 pairs |
| Qwen3.6 27B (Aitta), sample | 197 | 23.0 | 11.8 | 11.3 | 9.3 | 121 / 27 | 92.8 % of 643 pairs |
Points are meaning-error points per 100 reference words. "Same preference" counts the pairs of
models in a segment where both judges see a difference, and how often they prefer the same one.
All four judges rank the models the same way: the internal model best, the published model and round
1 about level, base Whisper far behind; gpt-oss-120b also finds the published model clearly better
than base Whisper (sign test p < 0.001) and separates the three fine-tunes less than Claude does.
Gemma 4 31B and Qwen3.6 27B were run on a sample, not on all 583 segments: both judged the same seeded
sample of 200 segments (--per-locality 20: 20 per locality, seed 0; Gemma answered 198, Qwen 197),
and Gemma puts round 1 just ahead of the published model. Point scales differ between judges, so compare
models within one judge.
This clip cannot say how the models handle dialects. What it does show is the method: a dialect test needs dialect audio with a transcript a person has checked, and a decision on whether the transcript should keep the dialect forms or normalise them, because WER punishes whichever the model does not do. The LLM judge is built to ignore that difference and count only meaning.
Data that could serve as a dialect test set and as fine-tuning data:
| Dataset | What | Access |
|---|---|---|
| Lahjoita puhetta (Donate Speech) | About 3 600 h of spontaneous Finnish from over 20 000 speakers across Finland, part of it transcribed, with dialect metadata | Kielipankki; licence agreement, a fee for commercial use |
| Suomen kielen näytteitä (SKN) | Dialect recordings from 50 localities, with both dialect and normalised transcripts | Kielipankki, CC BY 4.0: used here for the dialect test and fine-tune |
| Lauseopin arkiston murrekorpus (LA-murre) | 142 dialect samples from 132 localities, dialect transcripts only | Kielipankki, CC BY-ND 4.0: used here as the second dialect test and for the internal model |
| Common Voice, FLEURS, VoxPopuli (Finnish) | Read or formal speech, the sets the RASMUS model card reports | Open |
What was held out: the published model trained on all of the 38 training municipalities' segments. The 2 validation and 10 test municipalities stayed out of training entirely, and the test uses 60 segments from each test municipality, so the rest of those 12 interviews (4 601 segments, 19.5 h) was not used at all.
What was tried next (15 Oct 2026):
all-skn branch
(card) for audio that is mostly dialect.Still open:
| Path | What it holds |
|---|---|
scripts/ | Runners, scorer, judges, table and chart builders (see Run it again). provenance.py writes each run's _run_metadata*.json; asr_scoring.py is the clip scorer with HY's normalisation; get_clip.py fetches the clip. |
tests/ | Offline tests (uv run pytest): no keys, no downloads, no GPU. |
lumi/ | How the open models were run and trained on LUMI: environment setup, Slurm jobs, and stubs that hide three container libraries NeMo cannot use. |
release/ | The Hugging Face cards, uploaded by publish_hf.py: MODEL_CARD.md (the main model), TIMESTAMPS_CARD.md (its timestamps revision), ALL_SKN_CARD.md (its all-skn revision) and DATASET_CARD.md. |
docs/ | DIALECT-EXAMPLES.md (20 SKN test segments side by side and WER per municipality), story/ (the walk-through pages, one self-contained HTML file per language, built from story/src/) and img/ (the dialect chart from dialect_chart.py, the front page's figures from readme_figures.py, the banner, an AI illustration made with GPT Image 2.5, and the sources' logos in logos/, their owners' trademarks, from svgl). video/ holds the front page's 21-second video: shots made with Runway Gen-4.5 and music with ElevenLabs Music (both through Replicate), no people, no voice. The story pages are also served at gaik-project.github.io/finnish-asr. |
results/transcriptions/ | One JSON per model and run on the clip: transcript, settings, library versions, timing. qad1/ holds GAIK's earlier QAD-1 runs (OpenAI, Gemini, the gaik transcriber, the Haaga-Helia WhisperX server, Whisper on CSC Roihu). |
results/judgements/ | Per judge and model: every meaning error quoted from the reference and the transcript, the name checks, reference doubts, scores. |
results/scores.json, scores.reviewed.json | WER, CER and error counts for every clip transcript, against HY's and the reviewed reference. |
results/clip/ | gt_alignment.json: the reference's sentences, word times and QAD-1's 42 reference doubts, frozen from QAD-1 for the listening page. decoding_check.json and decoding_scores.json: the published model from the Hub decoded six ways, the model card's settings table. |
results/benchmarks/ | SKN, FLEURS and VoxPopuli: one line per recording (reference, transcript, seconds) and scores.json. LA-murre: scores.json and run metadata only. |
results/judgements_dialect/ | The LA-murre meaning judges: summary*.json (rounds 1–3 and the Aitta judge), agreement-r2.json and run metadata (numbers only). |
results/finetune/ | The training log of every fine-tune: per-epoch validation loss, data counts, the epoch kept. No weights. |
results/labels/ | SKN training targets with LLM punctuation, SKN sentence times per segment, the long-form windows; for LA-murre only the statistics. |
results/timestamps/ | Raw decodes with timestamps and long-form windows (base Whisper, the published model, the timestamp variant), and their scores. |
results/gpu_run/ | GPU, host and library versions of the last clip job per environment (hf, nemo, qwen) on LUMI. |
Not here, on purpose: the clip and its references (download them from gaik-toolkit with
get_clip.py; data/ is gitignored), any LA-murre text (per-segment benchmark files, judge records,
rewritten training texts: licence), HY's own tool transcripts (not public), model weights (the
published model is on Hugging Face) and LUMI job logs. The LA-murre outputs the public scripts can
regenerate are listed in .gitignore.
Input. Every model gets the same 180 s file with Finnish set as the language where the model takes one, and no prompt or word list. Whisper and Qwen3-ASR take the whole clip. Models limited to short audio (Canary, Parakeet, Orukeet, wav2vec2, Chirp 3) get the same eight chunks, cut at the quietest 100 ms between 15 and 25 s.
WER and CER. The scorer of QAD-1 and the toolkit's transcription_eval, with HY's normalisation,
vendored in asr_scoring.py and unchanged, so the numbers match HY's and
QAD-1's published ones. The public test sets use the usual model-card normalisation (lower-case,
punctuation removed, whitespace collapsed) and corpus-level WER (score_benchmark.py).
LLM judges on the clip. Claude Opus 5.5 and Gemini 3.1 Pro each read the reference and one
transcript, without the model's name (judge_asr.py). The prompt tells them
to report only what would mislead a reader: a wrong word, name or number, missing content, invented
content. Fillers, spoken against written forms, compound spacing, punctuation and digits
against number words are explicitly not errors. Each judge also checks 14 names and
terms against their correct spelling (the reference itself misspells Dentsply Sirona)
and lists places where the reference looks wrong; those are reported, not scored.
SKN data and training. prepare_skn.py cuts SKN by municipality (2–25 s, seed 0);
prepare_targets.py punct adds LLM punctuation and keeps an answer only if its words are identical;
finetune_whisper.py trains a LoRA adapter on LUMI in a plain PyTorch
loop and keeps the epoch with the lowest validation loss.
The LA-murre judge. judge_dialect.py gives one judge the dialect reference and every model's
transcript of a segment at once, labelled with letters in an order shuffled per segment, and counts
meaning errors only. Rounds judge different sets of models (round 1: base Whisper, round 1's model,
Finnish-NLP; round 2: base Whisper and our three round-1 and round-2 models; round 3: base Whisper,
the published model and the timestamp variant); --provider aitta runs the same judgement on an open
model on CSC Aitta, and judge_agreement.py compares judges segment by segment. The
published scores were made with prompt dialect-judge-v1; this repository's copy is v2, identical
except that its three example word pairs come from SKN instead of LA-murre (the docstring explains).
Timestamps. prepare_timestamps.py turns SKN's sentence annotation into sentence times inside
every published segment and into the 10 long-form windows; finetune_whisper.py --timestamps 0.5
trains with them; eval_timestamps.py decodes on LUMI (--fallback adds the temperature-fallback
decode) and scores locally.
Decoding settings. score_asr.py --decoding scores the published model's six decodings of the
clip in results/clip/decoding_check.json against both references; the model card's table of
settings to avoid comes from it.
Provenance. Every runner that writes results writes a secret-free _run_metadata*.json beside
them: commit, runner, dataset path, models, arguments, and which keys were set (never their values).
Files made before this repository existed carry gaik_evals_commit and paths of GAIK's private
evaluation repository, where the work was first done; their content is unchanged.
Where it ran. Open models and training: one GPU on LUMI (AMD MI250X) in CSC's PyTorch container. ElevenLabs, Chirp 3, OpenAI and Gemini: their APIs. LLM judges: Claude through Azure Foundry, Gemini through Vertex AI, open models through CSC Aitta.
From the repo root, with uv:
uv sync --all-extras # Python deps for every local step
uv run pytest # offline tests: no keys, no downloads, no GPU
uv run python scripts/get_clip.py --prepare-audio # the clip and both references from gaik-toolkit (ffmpeg on PATH)
# the clip: API models, scores, judges, the README table
uv run python scripts/run_api_asr.py --repeats 3 # ElevenLabs, Chirp 3
uv run python scripts/score_asr.py # WER/CER, no keys; --reviewed for the reviewed reference
uv run python scripts/score_asr.py --decoding # the model card's decoding-settings table
uv run python scripts/judge_asr.py # both judges (--claude-provider api for an Anthropic key)
uv run python scripts/make_readme_table.py # the clip table above
# open models, public test sets and training on LUMI, one wrapper for every step (details: lumi/README.md)
export LUMI_USER=<your CSC user name> LUMI_PROJECT=project_<number>
L=lumi/lumi.sh
bash $L push && bash $L setup && bash $L download clip && bash $L download bench # once
for e in hf qwen nemo; do bash $L run clip $e; bash $L run bench $e; done # six jobs
bash $L status # until the queue is empty
bash $L fetch
uv run --extra api-asr --extra judges --extra hub python scripts/run_api_benchmark.py \
--arm vertex_gemini-3.5-flash --arm vertex_gemini-3.5-flash-std --arm google_chirp-3 # Gemini 3.5 Flash (two prompts), Chirp 3 on SKN
uv run python scripts/score_benchmark.py # SKN/FLEURS/VoxPopuli table, no keys
# the dialect model: SKN, punctuated targets, training, its tests
bash $L download skn && bash $L prepare skn
bash $L fetch-labels # prepare_skn.py's segment lists -> results/labels/ (gitignored)
uv run python scripts/prepare_targets.py punct results/labels/skn_train.jsonl
bash $L push-labels && bash $L download train
bash $L run train hf --name whisper-large-v3-fi-skn-punct-best --data skn+open --skn-text punct --epochs 3 --lora-r 64
bash $L run clip hf --arm ft_whisper-large-v3-fi-skn-punct-best-chunks
bash $L run bench hf --bench skn_fi && bash $L fetch
uv run python scripts/dialect_chart.py # docs/img/dialect-finetune.svg, no keys
uv run python scripts/dialect_examples.py # docs/DIALECT-EXAMPLES.md, no keys
uv run python scripts/build_story.py # docs/story/story.{en,fi}.html, no keys
uv run python scripts/readme_figures.py # the front page's figures in docs/img/, no keys
uv run --extra hub python scripts/build_listen.py # docs/listen.html and its SKN clips, no keys
uv run python scripts/build_compare.py # results/compare.html (gitignored); fetches the SKN test parquet
# timestamps and long-form (prepare_timestamps.py needs SKN's *_full.eaf files)
uv run python scripts/prepare_timestamps.py <dir with SKN *_full.eaf>
bash $L push-labels && bash $L run timestamps hf --arm hf_whisper-large-v3 --arm hub_whisper-large-v3-fi-dialects
# the timestamp variant: training, its tests, and the timestamp evaluation with the fallback decode
bash $L run train hf --name whisper-large-v3-fi-skn-punct-ts --data skn+open --skn-text punct --epochs 3 --lora-r 64 --timestamps 0.5
bash $L run clip hf --arm ft_whisper-large-v3-fi-skn-punct-ts --arm ft_whisper-large-v3-fi-skn-punct-ts-chunks
for b in skn_fi fleurs_fi voxpopuli_fi; do bash $L run bench hf --bench $b --arm ft_whisper-large-v3-fi-skn-punct-ts; done
bash $L run timestamps hf --arm ft_whisper-large-v3-fi-skn-punct-ts
bash $L run timestamps hf --fallback --arm hf_whisper-large-v3 --arm hub_whisper-large-v3-fi-dialects --arm ft_whisper-large-v3-fi-skn-punct-ts
bash $L fetch && uv run python scripts/eval_timestamps.py score
# LA-murre (CC BY-ND): downloaded from Kielipankki; every per-segment output stays local (gitignored)
bash $L prepare lamurre && bash $L run bench hf --bench lamurre_fi && bash $L fetch
uv run python scripts/judge_dialect.py --round r2 --arms hf_whisper-large-v3,ft_whisper-large-v3-fi-skn,ft_whisper-large-v3-fi-skn-punct-best,ft_whisper-large-v3-fi-skn-lamurre-best
uv run python scripts/judge_dialect.py --round r2 --arms <the same> --provider aitta --judge-model openai/gpt-oss-120b
uv run python scripts/judge_agreement.py --round r2
uv run python scripts/judge_dialect.py --round r3 --arms hf_whisper-large-v3,ft_whisper-large-v3-fi-skn-punct-best,ft_whisper-large-v3-fi-skn-punct-ts
# the API services on every test set (FLEURS, VoxPopuli and LA-murre WAVs: bash $L fetch-audio <bench>)
for b in skn_fi fleurs_fi voxpopuli_fi lamurre_fi; do uv run --extra api-asr python scripts/run_api_benchmark.py --bench $b --arm azure_gpt-4o-transcribe --arm elevenlabs_scribe-v2 --arm google_chirp-3; done
# all of SKN: every segment of the 12 held-out municipalities joins the training data
bash $L prepare skn-heldout && bash $L fetch-labels
uv run python scripts/prepare_targets.py punct results/labels/skn_heldout.jsonl && bash $L push-labels
bash $L run train hf --name whisper-large-v3-fi-skn-all --data skn-all+open --skn-text punct --epochs 3 --lora-r 64
for b in fleurs_fi voxpopuli_fi lamurre_fi; do bash $L run bench hf --bench $b --arm ft_whisper-large-v3-fi-skn-all; done
uv run python scripts/judge_dialect.py --round r4 --arms hf_whisper-large-v3,ft_whisper-large-v3-fi-skn-punct-best,ft_whisper-large-v3-fi-skn-all,ft_whisper-large-v3-fi-skn-lamurre-best,azure_gpt-4o-transcribe,vertex_gemini-3.5-flash-std
uv run python scripts/judge_dialect.py --round r4 --arms <the same> --provider aitta --judge-model openai/gpt-oss-120b # Gemma 4 and Qwen3.6: --per-locality 20
# the ten dental-webinar clips (HY's private references in data/dental10/, gitignored)
bash $L push-dental && bash $L run clip hf --bench dental10_fi
uv run --extra judges python scripts/judge_dental.py # blind judges; only summary.json is committed
Each step skips what it has already done; --force redoes it (bash $L run clip hf --force).
To add a model, add a line to ARMS in run_open_models.py and run_benchmark.py
and run push, download and run for its environment. bash $L clean removes the
project from LUMI's scratch when the work is over. Keys go in a .env at the repo root;
.env.example lists their names per script. publish_hf.py needs a GAIK-project
write token and is for maintainers.
reference.reviewed.txt; see
Reviewed reference); the rest is HY's unverified text. The judges flag
0–3 reference doubts per transcript.prepare_lamurre.py) and rerunning the models and the judge
reproduces them. The public judge prompt differs from the one that made them in three example
word pairs.gaik_evals_commit and paths in GAIK's private evaluation repository, where the work
was first done.scripts/, tests/, lumi/): MIT, LICENSE, copyright Haaga-Helia
University of Applied Sciences (GAIK project).results/, the chart, the story pages and these texts):
CC BY 4.0. Credit: GAIK project, Haaga-Helia
University of Applied Sciences.timestamps revision are
Apache 2.0, as the base model
openai/whisper-large-v3; the published dataset is CC BY 4.0.get_clip.py downloads it, with its
reference transcripts (by the University of Helsinki team, and the reviewed one), from
gaik-toolkit,
where they are public.Details in NOTICE.md.
Developed by Akseli Leskinen (laguagu), Haaga-Helia University of Applied Sciences, in the GAIK project (Generative AI-enhanced Knowledge Management in Business), led by Haaga-Helia with the University of Helsinki and Tampere University, co-funded by the European Union (ERDF). The Ajokortti reference transcript was made by the University of Helsinki team.
LUMI. We acknowledge CSC – IT Center for Science, Finland for awarding this project access to the LUMI supercomputer, owned by the EuroHPC Joint Undertaking, hosted by CSC (Finland) and the LUMI consortium through CSC – IT Center for Science, Finland.
Finnish speech-to-text evaluations and a Whisper large-v3 fine-tune for Finnish dialects (GAIK project)
See the code
We taught OpenAI's Whisper to understand Finnish dialects. On dialect speech it gets about half as many words wrong as before (40 % → 21 %), far fewer than any other model we tried, Gemini 3.5 Flash included. Read and parliament speech stay about as good as before; on standard Finnish full of brand names it gets more of the names wrong. The model and its training data are free to use.
🤗 Model · 🗂️ Training data · 🎬 Video · 🔊 Listen and compare · 📖 The story: suomeksi · in English · 🧰 gaik-toolkit · 🌐 gaik.ai
Made by Akseli Leskinen (laguagu), Haaga-Helia University of Applied Sciences, in the GAIK project, co-funded by the European Union (ERDF).
| Our model against the model it started from | Whisper large-v3 | Our model |
|---|---|---|
| 🗣️ Dialect speech: words wrong | 40.4 % | 21.1 % |
| ⚖️ Another dialect corpus: meaning-error points per 100 words | 25.9 | 13.6 |
| 📖 Read Finnish (FLEURS): words wrong | 7.4 % | 7.6 % |
| 🏛️ Parliament speech (VoxPopuli): words wrong | 12.7 % | 10.8 % |
You give it Finnish speech, dialect or not. It writes standard Finnish, with punctuation.
🔊 Listen to this sentence and two more
Training ran on LUMI, the EuroHPC supercomputer hosted by CSC
in Kajaani. Every script is in scripts/; Run it again lists the commands.
Round by round:
Why is the best version not published?
Published (v1.0) | Internal version | |
|---|---|---|
| Training data | SKN + FLEURS + VoxPopuli, all open licences | the same + 56 h of LA-murre interviews |
| What the language model did | added punctuation | also rewrote LA-murre's dialect spelling in standard Finnish |
| Dialect test, words wrong | 21.1 % | 19.7 % |
| Can it be shared? | ✅ yes, Apache 2.0 | ❌ no: LA-murre's licence (CC BY-ND) does not allow sharing changed texts, so the model trained on them stays internal |
The audio and the words are people's work. For the published model a language model, Claude Opus 5.5, helped in two places (for the internal version it also rewrote LA-murre's dialect spelling). First, it added punctuation to the training text and nothing else:
Second, it judged the test transcripts blind. A real example from the webinar:
The test municipalities were kept out of training, so the test is speech the model had never heard:
Two scores: word error rate counts every word that differs, the blind judge only errors that change the meaning:
Across the open test sets, our model leads on dialect speech and parliament speech; on read sentences gpt-4o-transcribe and Gemini are better:
On ten dental webinars (30 minutes), the judges agree: Gemini 3.5 Flash and ElevenLabs make the fewest meaning errors.
So: use our model for dialect speech, and Gemini 3.5 Flash or plain Whisper large-v3 for standard business Finnish with many names. 🔊 Hear the differences · the single webinar clip and all 26 set-ups on it are in the full details.
The fourth judging round put six transcripts of every segment of the second dialect corpus side by side: base Whisper, two API services and our three models. Claude Opus 5.5 and OpenAI's GPT-6.1-sol (through GAIK's Azure) judged all 583 segments, gpt-oss-120b on CSC's Aitta a seeded sample of 200. Each judge is stricter or milder, but all three put our models far ahead, and Gemini 3.5 Flash and gpt-4o-transcribe behind base Whisper:
GPT-6.1-sol prefers the same transcript as Claude in 92 % of pairs, gpt-oss-120b in 86 %. The second round's open judges (Gemma 4 31B, Qwen3.6 27B) agreed with Claude in 92–93 % of pairs (below); in this round they were too slow on Aitta to finish and are left out.
import soundfile as sf
from transformers import pipeline # pip install "transformers>=5" torch soundfile
asr = pipeline("automatic-speech-recognition", model="GAIK-project/whisper-large-v3-fi-dialects")
audio, sr = sf.read("clip.wav", dtype="float32") # 16 kHz mono, up to 30 s
print(asr({"raw": audio, "sampling_rate": sr}, generate_kwargs={"language": "fi", "task": "transcribe"})["text"])
For longer audio, the model card has code that cuts it into pieces at quiet moments.
Everything below is the full report: every model, every number, how each was measured, and the caveats.
Organisations ask whether there are speech-to-text models that understand Finnish, dialects included, and can run locally. This repository runs 20 current speech-to-text set-ups, 12 of them open models that run on your own GPU, on one Finnish webinar clip and scores them two ways: word error rate, and two blind LLM judges that count only the errors that change the meaning. It then measures the open models on four public test sets, two of them dialect corpora, and fine-tunes Whisper large-v3 for Finnish dialects: that model and its training data are published on Hugging Face.
The clip is Ajokortti: a 180 s excerpt of a public QAdental dental-education webinar (QAdental,
Suomen Kotilääkäripalvelu Oy, is a GAIK project partner), two speakers, standard spoken Finnish with
many names and brand names. It and the University of Helsinki (HY) team's reference transcript are
published in gaik-toolkit's
transcription_eval;
get_clip.py downloads them from there, they are not copied here. Some
transcripts come from QAD-1, GAIK's earlier re-run of HY's evaluation of this clip. The clip is
not dialect speech: dialects are measured on held-out parts of two dialect corpora
(Public test sets,
Our dialect model).
timestamps revision; the main
model stays as it was
(Timestamps and long-form audio).all-skn branch (next steps).v1.0; variants on its timestamps and all-skn branches) and its SKN segments
GAIK-project/skn-finnish-dialects-asr
(CC BY 4.0). This repository holds the method, the code and the results behind them.| Model | Type | WER % | CER % | Claude pts | Gemini pts | Names (Claude / Gemini) |
|---|---|---|---|---|---|---|
| Gemini 3.5 Flash | API | 14.2 (14.2–17.9) | 5.0 | 0.8 | 1.2 | 14 / 14 of 14 |
| Google Chirp 3 | API | 19.6 | 6.6 | 1.2 | 3.7 | 14 / 11 of 14 |
| Whisper large-v3, chunked decoding | open | 15.7 | 6.7 | 1.7 | 5.5 | 14 / 14 of 14 |
| Whisper large-v3 | open | 14.7 | 6.0 | 2.7 | 5.2 | 14 / 13 of 14 |
| ElevenLabs Scribe v2 | API | 18.2 (17.2–18.4) | 4.9 | 1.2 | 7.0 | 14 / 12 of 14 |
| This project, round 2 internal (+ LA-murre), chunked | open | 15.9 | 4.7 | 3.0 | 6.2 | 14 / 12 of 14 |
| Whisper large-v3 turbo | open | 18.4 | 7.7 | 1.5 | 8.5 | 14 / 13 of 14 |
| This project's pilot: Whisper large-v3 + LoRA on FLEURS and VoxPopuli | open | 17.9 | 8.3 | 2.2 | 8.2 | 14 / 13 of 14 |
| OpenAI gpt-transcribe | API | 18.4 (18.4–19.1) | 8.5 | 4.2 | 8.5 | 12 / 12 of 14 |
| WhisperX + Finnish-NLP whisper-large-finnish-v3 (Haaga-Helia WhisperX server) | open | 11.9 | 3.5 | 4.5 | 8.7 | 13 / 11 of 14 |
| This project, round 1, chunked decoding | open | 14.9 | 5.3 | 3.7 | 9.7 | 14 / 13 of 14 |
| OpenAI gpt-4o-transcribe | API | 16.9 (16.2–18.4) | 6.7 | 6.0 | 7.5 | 11 / 10 of 14 |
| OpenAI whisper-1 | API | 23.1 | 11.2 | 3.0 | 10.4 | 14 / 14 of 14 |
| Finnish-NLP whisper-large-finnish-v3 (plain transformers) | open | 13.2 | 4.3 | 5.2 | 8.5 | 13 / 11 of 14 |
| This project, round 1: Whisper large-v3 + LoRA on SKN dialects, FLEURS and VoxPopuli | open | 17.9 | 5.6 | 4.5 | 9.4 | 13 / 12 of 14 |
| OpenAI gpt-4o-mini-transcribe | API | 13.9 (13.9–14.7) | 4.4 | 3.2 | 11.4 | 13 / 10 of 14 |
| Orukeet (Parakeet v3 fine-tune) | open | 26.4 | 10.2 | 6.2 | 10.7 | 13 / 11 of 14 |
| This project, round 2 (published), chunked decoding | open | 13.9 | 5.2 | 6.7 | 10.9 | 11 / 10 of 14 |
| RASMUS Finnish-ASR-Canary-v2, greedy | open | 23.9 | 8.1 | 6.5 | 13.7 | 11 / 8 of 14 |
| RASMUS Finnish-ASR-Canary-v2 + KenLM | open | 22.1 | 8.5 | 8.5 | 15.4 | 11 / 7 of 14 |
| Gemini 3.1 Pro | API | 22.4 (15.4–22.4) | 9.3 | 7.0 | 18.4 | 12 / 10 of 14 |
| NVIDIA Canary 1B v2 | open | 24.1 | 10.3 | 9.0 | 17.9 | 13 / 10 of 14 |
| NVIDIA Parakeet TDT 0.6B v3 | open | 31.1 | 13.8 | 12.7 | 17.4 | 13 / 12 of 14 |
| Finnish-NLP wav2vec2 XLS-R 1B (no LM) | open | 31.6 | 9.1 | 6.2 | 26.1 | 13 / 10 of 14 |
| Qwen3-ASR 1.7B | open | 58.0 | 23.2 | 20.6 | 33.6 | 9 / 7 of 14 |
| Qwen3-ASR 0.6B | open | 77.9 | 33.9 | 28.1 | 41.0 | 7 / 4 of 14 |
Results on the Ajokortti clip, sorted by the mean of the two judges. pts = meaning-error points
per 100 reference words (critical 10, major 5, minor 1), lower is better. Names = how many of the
clip's 14 names and terms each judge found right or recognisable. WER and CER use HY's normalisation
(lower-case, every Unicode punctuation character removed, runs of whitespace collapsed; see the
toolkit's What the metrics mean);
the range is over three runs where a model was run three times. "open" = weights can be downloaded
and run on your own GPU. All numbers per model are in results/scores.json.
File names in results/ start with where the model ran: openai_, vertex_ (Gemini),
elevenlabs_ and google_ (Chirp) are APIs, gaik_ the gaik toolkit's transcriber, hh_ the
Haaga-Helia WhisperX server, hf_, nemo_ and qwen_ open models on a GPU, ft_ our fine-tunes.
The clip's reference is HY's transcript, which looked like a corrected WhisperX draft. On 14 Oct
2026 Akseli Leskinen listened to the 42 spans where most of seven systems hear something else and
corrected 14 of them: mostly spoken forms HY had written in standard Finnish, two missing words and a
brand name. The reviewed text is published next to the original in gaik-toolkit
(reference.reviewed.txt).
Against it (results/scores.reviewed.json, score_asr.py --reviewed):
| Model | WER %, HY's reference | WER %, reviewed |
|---|---|---|
| OpenAI gpt-4o-mini-transcribe | 13.9 | 13.2 |
| Whisper large-v3 | 14.7 | 13.7 |
| Gemini 3.5 Flash | 14.2 | 13.7 |
| Our model, round 2 (published), chunked | 13.9 | 14.0 |
| ElevenLabs Scribe v2 | 18.2 | 14.2 |
| WhisperX + Finnish-NLP whisper-large-finnish-v3 (Haaga-Helia WhisperX server) | 11.9 | 15.7 |
| Finnish-NLP whisper-large-finnish-v3 (plain transformers) | 13.2 | 15.7 |
The WhisperX-based arms lose their lead and ElevenLabs, which writes what is said, gains four points: the review confirms that HY's text favoured WhisperX output. HY's own 2025 WhisperX file shows the same shift (10.4 → 14.0 %, reported in gaik-toolkit; that transcript is not part of this repository). The leading set now sits within about one point (13.2–14.2 %), which a 408-word reference cannot separate. Only the 42 disputed spans were checked, so read every WER here with the rest of the reference still unverified. The tables above and the judges' scores are against HY's text and were not re-run.
The clip above is one recording. Four open Finnish test sets:
prepare_skn.py.
The reference is SKN's normalised transcript: standard Finnish word forms, no punctuation. A model
is right when it hears the dialect and writes standard Finnish. The test segments are the test
split of the published dataset.prepare_lamurre.py. No published model was
trained on any of LA-murre; only the internal model was, on recordings from other localities,
never these 10. Its reference is a dialect transcript, so WER here charges a model for writing standard
Finnish: every model lands at 52–73 %. Read this column with the
meaning judge, not on its own. Only the numbers are published
(results/benchmarks/lamurre_fi/scores.json): the
licence does not allow sharing the modified references, the transcripts paired with them or the
judge records, and prepare_lamurre.py rebuilds the test from Kielipankki.FLEURS and VoxPopuli are prepared or read speech, the sets model cards report; SKN is spontaneous speech from mostly elderly speakers, recorded in the 1960s–70s.
| Model | SKN dialects WER % | LA-murre dialects WER % | FLEURS WER % | VoxPopuli WER % | FLEURS CER % | s / clip |
|---|---|---|---|---|---|---|
| This project's pilot: Whisper large-v3 + LoRA †‡ | – | – | 7.1 | 10.8 | 1.6 | 0.95 |
| This project, trained on all of SKN (its test included, so no SKN score) †‡§ | – | 57.8 | 7.5 | 11.9 | 1.7 | 0.96 |
| Orukeet (Parakeet v3 fine-tune) | – | – | 11.6 | 15.9 | 2.8 | 0.04 |
| NVIDIA Parakeet TDT 0.6B v3 | – | – | 13.0 | 17.5 | 3.1 | 0.18 |
| Finnish-NLP wav2vec2 XLS-R 1B (no LM) | – | – | 16.2 | 9.4 | 6.2 | 1.50 |
| This project, round 2 internal: + LA-murre †‡§¶ | 19.7 | 57.4 | 7.8 | 11.4 | 1.8 | 0.96 |
| This project, round 2: + punctuated SKN targets (published) †‡§ | 21.1 | 58.1 | 7.6 | 10.8 | 1.7 | 0.98 |
This project, round 2 + timestamp tokens (timestamps revision) †‡§ | 21.4 | 58.1 | 7.4 | 11.2 | 1.7 | 0.98 |
| This project, round 1: Whisper large-v3 + LoRA incl. SKN dialects †‡§ | 22.2 | 57.9 | 7.8 | 10.6 | 1.9 | 0.96 |
| Gemini 3.5 Flash (API, asked for standard Finnish) | 34.7 | 61.8 | – | – | – | – |
| Whisper large-v3 | 40.4 | 62.5 | 7.4 | 12.7 | 1.7 | 0.98 |
| Gemini 3.5 Flash (API, asked to write as spoken) | 40.8 | 60.2 | 5.1 | 11.5 | 1.9 | 2.24 |
| Google Chirp 3 (API) | 41.3 | – | 4.8 | 11.6 | 1.6 | 1.82 |
| Whisper large-v3 turbo | 42.5 | 62.7 | 8.4 | 13.5 | 2.2 | 0.19 |
| ElevenLabs Scribe v2 (API) | 44.9 | – | 3.3 | 9.1 | 1.7 | 3.09 |
| NVIDIA Canary 1B v2 | 45.6 | 65.6 | 8.7 | 11.4 | 2.1 | 0.30 |
| RASMUS Finnish-ASR-Canary-v2 + KenLM †‡ | 47.6 | 62.7 | 7.6 | 7.7 | 2.5 | 0.71 |
| RASMUS Finnish-ASR-Canary-v2, greedy †‡ | 50.1 | 58.7 | 8.4 | 6.8 | 2.4 | 0.10 |
| Finnish-NLP whisper-large-finnish-v3 † | 56.6 | 51.6 | 7.1 | 6.6 | 1.7 | 0.89 |
| OpenAI gpt-4o-transcribe (API, via Azure) | 62.5 | 72.6 | 4.0 | 11.4 | 1.4 | 0.88 |
| Qwen3-ASR 1.7B | 66.7 | 72.9 | 25.1 | 27.5 | 5.8 | 0.34 |
Corpus WER and CER after lower-casing and removing punctuation. Seconds per utterance
are FLEURS GPU time in batches of 16 on one AMD MI250X, model loading excluded. † trained
on FLEURS training data, ‡ on VoxPopuli training data, § on SKN training data from
other municipalities, ¶ on LA-murre training data from other localities (model cards, or this
project), so those numbers are on familiar material. A dash: not run on that set. The API services
(run_api_benchmark.py) ran with the clip runs' models and settings,
one request per segment; OpenAI's gpt-4o-transcribe through GAIK's Azure. Seconds per clip for them
are request wall time, which varies with the service's load. "As spoken" is the clip's
prompt (write exactly what is said); "standard Finnish" asks for standard word forms, which is what the
SKN reference has. Chirp 3 returned 1 262 words of a repeated "se on" for one 50-word segment
(SKN15b_Rautu_0133), which costs it 6.3 points (35.0 % without that segment), and heard no speech in 8
segments, counted as deletions. The API services' training data is unknown, so for them the test
municipalities cannot be called held out.
VoxPopuli references keep digits and abbreviations as the parliament wrote them, which costs every
model some WER. The RASMUS model card reports 8.39 % (greedy) and 7.75 % (KenLM) on FLEURS; this run
gives 8.4 % and 7.6 %.
The Ajokortti clip is one of ten. The other nine come from the same QAdental webinar material: ten 180 s clips (30 minutes of speech), each with the University of Helsinki's Finnish transcript (QAD-1 set A in HY's study). The files were checked against HY's originals: the same audio and the same text byte for byte.
Read the word error rate here with care. HY's transcripts leave out fillers ("niinku", "tota",
"no niin") and were not checked against the audio by a listener. A spot check found missing fillers,
a wrong word form ("yrittäjien" where the speaker says "yrittäjän") and a word missing at a clip's
start; the Ajokortti review above found the same. So models that also leave fillers out (Whisper,
gpt-4o-transcribe) get about 2 % of inserted words, and models that write what is said (ElevenLabs,
Chirp, Gemini, our model) 11–14 %. The blind judges of judge_dental.py
(the Ajokortti prompt, Claude Opus 5.5 and Gemini 3.1 Pro) ignore fillers and count only what
changes the meaning; on that measure Gemini 3.5 Flash and ElevenLabs Scribe v2 are best, and our
model, built for dialects, is last of the six judged.
| Model | WER % | Inserted words % | Meaning points, Claude Opus 5.5 | Meaning points, Gemini 3.1 Pro |
|---|---|---|---|---|
| Whisper large-v3 | 15.7 | 2.7 | 3.6 | 6.4 |
| Whisper large-v3, in 15–25 s pieces | 17.4 | 2.2 | – | – |
| OpenAI gpt-4o-transcribe (API, via Azure) | 19.1 | 2.4 | 5.3 | 9.1 |
| Whisper large-v3 turbo | 19.2 | 2.2 | – | – |
| OpenAI Whisper (API, via Azure) | 22.1 | 2.6 | – | – |
| Orukeet (Parakeet v3 fine-tune) | 24.4 | 2.3 | – | – |
| NVIDIA Canary 1B v2 | 24.5 | 2.2 | – | – |
| ElevenLabs Scribe v2 (API) | 25.4 | 11.4 | 1.6 | 2.8 |
| This project, all of SKN, in pieces †‡§ | 25.7 | 12.5 | 4.8 | 9.7 |
This project, timestamps revision, in pieces †‡§ | 25.8 | 12.9 | – | – |
| Finnish-NLP whisper-large-finnish-v3 † | 25.8 | 12.7 | – | – |
| This project, published, in 15–25 s pieces †‡§ | 25.9 | 12.8 | 5.1 | 10.0 |
| Google Chirp 3 (API) | 25.9 | 11.3 | 2.3 | 3.7 |
| This project, internal, in pieces †‡§¶ | 26.5 | 13.4 | – | – |
| Gemini 3.5 Flash (API, asked for standard Finnish) | 26.5 | 13.7 | – | – |
This project, timestamps revision, long audio in one go †‡§ | 27.0 | 13.0 | – | – |
| Gemini 3.1 Pro (API) | 28.5 | 11.4 | – | – |
| RASMUS Finnish-ASR-Canary-v2 + KenLM †‡ | 28.6 | 9.9 | – | – |
| Gemini 3.5 Flash (API, asked to write as spoken) | 28.6 | 14.0 | 1.2 | 2.1 |
| RASMUS Finnish-ASR-Canary-v2, greedy †‡ | 29.7 | 8.8 | – | – |
This project, round 2 + timestamp tokens (timestamps revision) †‡§, whole clip in one go | 29.8 | 15.8 | – | – |
| NVIDIA Parakeet TDT 0.6B v3 | 32.3 | 1.8 | – | – |
| Finnish-NLP wav2vec2 XLS-R 1B (no LM) | 42.1 | 5.9 | – | – |
| This project, round 2 internal: + LA-murre †‡§¶, whole clip in one go | 60.0 | 44.9 | – | – |
| This project, round 2: + punctuated SKN targets (published) †‡§, whole clip in one go | 60.2 | 44.8 | – | – |
| Qwen3-ASR 1.7B | 93.7 | 35.4 | – | – |
Meaning points: errors per 100 reference words over all ten clips (critical 10, major 5, minor 1).
Our models were run on 15–25 s pieces cut at quiet points; fed a whole 180 s clip in one go, the
published model repeats itself (45 % inserted words), and only the timestamps revision with its
fallback decode copes. HY's transcripts are private, so only the numbers are committed
(results/benchmarks/dental10_fi/scores.json,
results/judgements_dental/summary.json).
Can the open training data improve the best open model? A LoRA fine-tune of Whisper large-v3 (1 % of the weights, rank 32 on the attention query and value projections) on the FLEURS and VoxPopuli Finnish training sets: 10 208 recordings, 2 epochs, 51 minutes on one MI250X. Validation loss fell from 0.48 to 0.17.
| Test | Whisper large-v3 | After fine-tuning |
|---|---|---|
| FLEURS WER % | 7.4 | 7.1 |
| VoxPopuli WER % | 12.7 | 10.8 |
| Ajokortti clip WER % | 14.7 | 17.9 |
| Ajokortti, Claude / Gemini error points | 2.7 / 5.2 | 2.2 / 8.2 |
It got better on speech like its training data and not on the webinar: WER on the
clip rose and the two judges disagree on the direction. Finnish-NLP's and RASMUS's
models, trained on the same kind of data, show the same pattern (good on FLEURS and
VoxPopuli, behind base Whisper on the clip). Open read and parliamentary speech is
not the data that makes a model better at spontaneous Finnish; that needs spontaneous
and dialect speech (see below). The training log is in
results/finetune/;
the adapter is not published. lumi.sh run train hf --name <name> repeats it
with other settings.
Walk-through for readers new to this: the story page, in English and suomeksi (sources:
docs/story/), explains the data, the four places where Claude Opus 5.5 was used, the training, both measurements, the three open judges, the checked webinar reference and the timestamp variant, with two audio examples. The files also work offline. To hear the models side by side, open the listening page (built bybuild_listen.py);build_compare.pybuilds a fuller local comparison that embeds the clip, so it is not committed.
The pilot showed that read speech does not teach spontaneous Finnish. The second run adds dialect interviews: Suomen kielen näytteitä (SKN), Kotus / Kielipankki, CC BY 4.0 (urn:nbn:fi:lb-2020112937), about two hours of interview from each of 50 municipalities, every sentence time-aligned with a dialect transcript and a normalised standard-Finnish one.
Data. prepare_skn.py joins sentences into segments of
2–25 s and splits by municipality, so no test locality or informant is heard in
training: 38 municipalities for training (15 042 segments, 62 h), 2 for validation
(Pälkäne, Sotkamo; 200 segments) and every fifth SKN booklet, in publication order, for the
test (SKN05, 10 … 50; 600 segments). By Kielipankki's classification the test covers the
south-western, south-western transitional, Häme, Savo and south-eastern dialects; all nine
Ostrobothnian and Peräpohjola municipalities are in training. Interviewer speech is kept, and
some SKN fieldworkers recorded both test and training municipalities, so a few test segments
may contain an interviewer's voice heard in training. The target is the normalised text,
so the model learns to write standard Finnish from dialect speech.
Training. The same LoRA recipe as the pilot (rank 32, 2 epochs, learning rate 1e-4)
on SKN plus FLEURS and VoxPopuli train: 25 250 segments, 2 h 2 min on one MI250X (training with
per-epoch validation, from its train_log.json).
Validation loss (SKN and the open sets together) fell from 0.55 to 0.21.
| Test | Whisper large-v3 | Pilot (open data) | + SKN dialects |
|---|---|---|---|
| SKN dialect test WER % (10 held-out municipalities) | 40.4 | – | 22.2 |
| SKN dialect test CER % | 19.1 | – | 9.0 |
| FLEURS WER % | 7.4 | 7.1 | 7.8 |
| VoxPopuli WER % | 12.7 | 10.8 | 10.6 |
| Ajokortti clip WER % | 14.7 | 17.9 | 17.9 (14.9 chunked) |
| Ajokortti, Claude / Gemini error points | 2.7 / 5.2 | 2.2 / 8.2 | 4.5 / 9.4 (3.7 / 9.7 chunked) |
WER on the dialect test fell in every one of the 10 municipalities:
| Municipality | Alastaro | Artjärvi | Eurajoki | Heinola | Hinnerjoki | Ikaalinen | Jaala | Mikkeli | Rautu | Vihti |
|---|---|---|---|---|---|---|---|---|---|---|
| Whisper large-v3 | 35.7 | 44.4 | 43.7 | 36.1 | 51.8 | 31.3 | 37.3 | 38.5 | 50.4 | 32.8 |
| + SKN dialects | 19.7 | 23.3 | 23.4 | 20.6 | 30.6 | 16.2 | 19.6 | 19.9 | 33.4 | 15.3 |
What it means. Dialect data is what moves dialect accuracy: the error rate nearly
halves, and no ready-made model, including the Finnish fine-tunes trained on read
speech (48–57 %), comes close. FLEURS moved by 0.4 points the wrong way and VoxPopuli
improved, so general Finnish held. On the webinar clip the higher WER came from Whisper's
long-form decoding loop, not from the model: with the clip cut into the same chunks the
short-audio models get, it is 14.9 % against 15.7 % for base Whisper
(round 2). The judges still count more meaning
errors than for base Whisper (brand names, people's names and dental terms such as "oikoja" and
"etualue"); that clip is standard spoken Finnish with brand names, which SKN does not contain. Part
of the gain is learning SKN's own conventions (numbers as words, its normalised spellings), which
WER rewards; the second corpus below separates the two.
The training log is in
results/finetune/; round 1 was not
published (round 2 is). docs/DIALECT-EXAMPLES.md shows 20 test
segments side by side for the published model.
Attribution. Suomen kielen näytteitä, Kotimaisten kielten keskus (Kotus), via
Kielipankki – The Language Bank of Finland, CC BY 4.0. The segments and transcripts
quoted in results/benchmarks/skn_fi/ and
results/labels/ come from it.
Is the gain dialect skill, or only SKN's own conventions? The fine-tuned model was run on a corpus it has never seen any part of: the Finnish Dialect Syntax Archive (LA-murre), recorded and transcribed by other people, 10 localities outside SKN's 50 municipalities, one per dialect region.
WER cannot answer it. LA-murre's reference is written in dialect, so a model that writes a dialect word in its standard form is charged an error, and the model that copies dialect spellings best (Finnish-NLP, 51.6 %) has the lowest WER. The fine-tune still improves on base Whisper (62.5 → 57.9 % WER, 22.9 → 17.0 % CER) and leaves far less out (deleted words 12.9 → 2.6 %).
So a blind judge read the meaning. Claude Opus 5.5 got the dialect reference and three
transcripts per segment, labelled A, B, C in shuffled order, and counted only meaning
errors (wrong word, name or number, missing or invented content); standard against
dialect forms is not an error (judge_dialect.py).
| Model | Error points / 100 words | Critical | Major | Minor | Segments with no error |
|---|---|---|---|---|---|
| Whisper large-v3 + SKN dialects (this project) | 13.9 | 16 | 309 | 654 | 108 |
| Finnish-NLP whisper-large-finnish-v3 | 21.6 | 38 | 482 | 864 | 49 |
| Whisper large-v3 | 22.7 | 41 | 527 | 798 | 55 |
564 of the 583 segments (the judge left a verdict out for 19 long ones, three attempts each). Critical 10, major 5, minor 1 point. Segment by segment the fine-tune has fewer points than base Whisper in 350, more in 101 and the same in 113 (sign test p < 0.001), and it is ahead in every locality:
| Locality | Tammela | Posio | Muolaa | Laukaa* | Iitti* | Sodankylä* | Laihia | Temmes | Nilsiä* | Rauma |
|---|---|---|---|---|---|---|---|---|---|---|
| Whisper large-v3 | 14.0 | 21.7 | 22.0 | 20.0 | 26.2 | 32.9 | 19.0 | 16.0 | 30.9 | 33.7 |
| + SKN dialects | 10.9 | 12.7 | 16.7 | 7.4 | 14.4 | 19.2 | 13.2 | 10.7 | 16.1 | 27.8 |
| Finnish-NLP fine-tune | 15.3 | 20.0 | 21.4 | 20.8 | 25.3 | 25.1 | 15.8 | 18.0 | 28.7 | 30.8 |
* read from the corpus file code (kess_lau1, kyml_iit1, perpl_sod1, savp_nil1); the other
six place names are also said in the interviews.
The gain carries over to dialect speech from another corpus: about 40 % fewer meaning-error points than base Whisper in this judging (47 % when the same transcripts were judged again in round 2 below), about the size of the WER gain on SKN. One judge, itself a model, and one fine-tune run; Rauma stays hard for every model.
Attribution. Finnish Dialect Syntax Archive (Lauseopin arkiston murrekorpus),
University of Turku and Kotimaisten kielten keskus, via Kielipankki, CC BY-ND 4.0
(urn:nbn:fi:lb-2020112935). No LA-murre text is in
this repository: the references were modified (two annotation marks removed, as
prepare_lamurre.py describes) and the licence is no-derivatives, so only numbers made from them
are published (scores.json, the judges' summary*.json and agreement-r2.json).
Round 1 showed that dialect data is what moves dialect accuracy. Round 2 asked what else helps, with the same base model and method, and picked the model to keep by validation loss, never by test scores.
flowchart LR
SKN["SKN, 50 municipalities<br/>CC BY 4.0"] --> CUT["prepare_skn.py<br/>2-25 s segments,<br/>split by municipality"]
LA["LA-murre, 142 interviews<br/>CC BY-ND 4.0"] --> CUTL["prepare_lamurre.py<br/>test: 10 localities<br/>train: the others"]
CUT --> P["Opus 5.5 adds punctuation<br/>(words must stay identical)"]
CUTL --> N["Opus 5.5 rewrites dialect<br/>in standard Finnish"]
OPEN["FLEURS + VoxPopuli<br/>train"] --> FT
P --> FT["LoRA r64 on Whisper large-v3<br/>LUMI MI250X, 3 epochs,<br/>keep the best epoch"]
N -.internal model only.-> FT
FT --> T["Tests: SKN, LA-murre,<br/>FLEURS, VoxPopuli, clip"]
T --> J["Blind Opus 5.5 judge<br/>on LA-murre meaning"]
1. Training text with punctuation. SKN's standard-Finnish text has no punctuation and
almost no capitals, so round 1 learned to write almost none (1 of its 600 SKN test outputs has a
full stop or comma; base Whisper 97.5 %). prepare_targets.py
had Claude Opus 5.5 add punctuation and capitals to every training and validation segment,
20 per call, and threw away any answer whose words were not exactly the input's: 15 024 of
15 042 passed, the other 18 kept their plain text. The text-only LLM never hears audio and
never touches a test set. The targets are in
results/labels/.
2. More dialect speech. LA-murre's other recordings, without the 10 test localities, a second recording from one of them and the five localities that are also SKN test municipalities, give 15 532 segments (56 h). Their transcripts are in dialect spelling, so Opus rewrote each one in standard Finnish, word for word; 15 529 kept a word count within 30 % of the source. LA-murre is CC BY-ND, so the model trained on them is an internal comparison only and its rewritten texts are not published.
3. Training. LoRA rank 64 on the attention query and value projections (2 % of the
weights), learning rate 1e-4, batch 8, on one MI250X. A four-epoch run showed overfitting:
validation loss was lowest after epoch 2 and rose after it (0.206, 0.208, 0.215), so the
final runs train three epochs and merge the epoch with the lowest validation loss, which
was epoch 2 in both. Published model: 25 250 segments, 3 h 8 min. Internal model:
40 779 segments, 4 h 57 min (training with per-epoch validation, from the train_log.json files).
| Model | Training data | SKN WER | LA-murre judge pts | FLEURS | VoxPopuli | Clip WER (chunked) | Clip Claude / Gemini pts |
|---|---|---|---|---|---|---|---|
| Whisper large-v3 | none | 40.4 | 25.9 | 7.4 | 12.7 | 15.7 | 1.7 / 5.5 |
| Round 1 (r32, 2 epochs) | SKN + open | 22.2 | 13.8 | 7.8 | 10.6 | 14.9 | 3.7 / 9.7 |
| Round 2, published | SKN with punctuation + open | 21.1 | 13.6 | 7.6 | 10.8 | 13.9 | 6.7 / 10.9 |
| Round 2, internal | + LA-murre rewritten | 19.7 | 11.4 | 7.8 | 11.4 | 15.9 | 3.0 / 6.2 |
WER in %, lower is better. LA-murre judge: meaning-error points per 100 reference words,
Claude Opus 5.5, all four models judged together on each of the 583 segments in shuffled,
unlabelled order (round 2 of judge_dialect.py). Its scale
differs from the round-1 judging above, which compared other models: base Whisper gets 22.7
points in round 1 and 25.9 in round 2 (critical errors 41 and 74), the round-1 model 13.9 and
13.8. Compare models only within one round. The four-epoch variants are in
results/benchmarks/*/scores.json (-r64e4, -punct, -lamurre); their last epoch is slightly
better on SKN and worse on VoxPopuli.
What it shows.
run_open_models.py, recipe whisper_chunks)
removes it. The model card's table of decoding settings is the published model downloaded back
from the Hub and run on the clip six ways
(results/clip/decoding_check.json, scored by
score_asr.py --decoding into decoding_scores.json):
quiet-point pieces 13.7 % WER, the pipeline's chunk_length_s=20 14.7 %, chunk_length_s=30
52.5 % (it loops) and 14.4 % with no_repeat_ngram_size=6; against the reviewed reference 13.5,
13.5, 51.5 and 14.2 %.Cost. Opus 5.5 for the training text, about 100 USD (punctuation 52, LA-murre rewriting
49); the blind judging of rounds 1 and 2 about 25 USD (round 3, for the timestamp variant below, used
about as many tokens as each of them); about 40 GCD-hours of LUMI GPU time for every job in both
rounds (about 20 billed GPU-hours). The published model (tag v1.0), its timestamp variant (branch
timestamps) and its SKN segments are on Hugging Face
(model,
dataset); their cards are
in release/ and publish_hf.py uploads them. The internal
LA-murre model is not published: it is kept in the private Hugging Face repository
GAIK-project/whisper-large-v3-fi-dialects-internal for use inside the project, and its rewritten
LA-murre training texts are not published either.
Attribution. SKN as above. LA-murre: Lauseopin arkiston murrekorpus, University of Turku and Kotus, via Kielipankki, CC BY-ND 4.0, used for testing and for the internal model only.
The published model was trained on text without Whisper's timestamp tokens. Does that break its
timestamps and its long-form decoding, and does training with them help?
eval_timestamps.py measured both against SKN's own sentence times
(prepare_timestamps.py):
return_timestamps=True. For each of the
791 sentence starts inside a segment (after its first sentence), the distance to the nearest
timestamp the model wrote there.results/labels/skn_longform.jsonl, window edges in pauses
no sentence crosses), decoded three ways: Whisper's own sequential long-form mode with timestamps;
the same with OpenAI's temperature fallback (temperatures 0.0–1.0, compression-ratio threshold
1.35, log-probability threshold −1.0, no-speech threshold 0.6, condition_on_prev_tokens=False),
which decodes a 30 s window again at a higher temperature when its text compresses too well or its
log-probability is too low; and 15–25 s pieces cut at the quietest 100 ms (the model card's code).
A decode counts as a loop when one 5-word sequence occurs four times or more and more often than
in the reference.| Model | SKN sentence starts within 0.5 s / 1 s | Median distance | Long-form WER, sequential (loops) | Long-form WER, with fallback (loops) | Long-form starts within 1 s, with fallback | Long-form WER, pieces |
|---|---|---|---|---|---|---|
| Whisper large-v3 | 53.5 % / 60.8 % | 0.18 s | 37.5 % (0 of 10) | 37.5 % (0) | 62.6 % | 39.4 % |
| Published model | 0.8 % / 0.8 % | 5.33 s | 21.5 % (1 of 10) | 19.8 % (0) | 18.9 % | 17.3 % |
| Timestamp variant | 56.4 % / 57.8 % | 0.04 s | 25.2 % (3 of 10) | 18.1 % (0) | 49.0 % | 17.1 % |
The published model's timestamps are unusable: it writes them, but almost never where a sentence
starts. Its text is still good, and in pieces it is far ahead of base Whisper (17.3 % against
39.4 %). So decode it in pieces and take times from the cut points, as the model card's code does.
WER with timestamps on the short segments is 21.2 % (base 41.0 %), level with the scores above.
Results: results/timestamps/scores.json, raw decodes beside it.
Can training put the timestamps back? finetune_whisper.py --timestamps 0.5 repeats the published
model's recipe, but half of the SKN items, drawn again every epoch, carry one timestamp pair per SKN
sentence (<|0.00|> text<|1.84|>, times from
results/labels/skn_parts.jsonl); 15 040 of the 15 042 training
items had usable times. Epoch 2 was kept, with validation loss 0.2158 (not comparable with the
published model's 0.206, because about half of its SKN validation segments carry timestamp tokens), after 3 h
6 min on one MI250X (train_log.json).
| Test | Whisper large-v3 | Published model | Timestamp variant |
|---|---|---|---|
| SKN dialect test, WER % | 40.4 | 21.1 | 21.4 |
| FLEURS, WER % | 7.4 | 7.6 | 7.4 |
| VoxPopuli, WER % | 12.7 | 10.8 | 11.2 |
| LA-murre, WER % (dialect reference) | 62.5 | 58.1 | 58.1 |
| LA-murre, blind judge round 3, points per 100 words | 24.2 | 13.7 | 13.7 |
| Clip, long-form with timestamps, WER % (HY's / reviewed reference) | 14.7 / 13.7 | 62.7 / 62.2 (loops) | 16.7 / 16.2 |
| Clip, quiet-point pieces, WER % (HY's / reviewed reference) | 15.7 / 15.7 | 13.9 / 14.0 | 15.2 / 14.5 |
| SKN sentence starts within 1 s | 60.8 % | 0.8 % | 57.8 % |
| Long-form interviews with fallback, WER % | 37.5 | 19.8 | 18.1 |
The blind judge's round 3 (judge_dialect.py --round r3,
summary-r3.json) judged base Whisper, the
published model and the variant together on 577 of the 583 LA-murre segments: 24.18, 13.72 and 13.68
points. The variant has fewer points than the published model in 98 segments and more in 100 (sign
test p = 0.94): no difference.
Decision. The published model stays the main model. The rule set before the run was that the
variant replaces it only if SKN gets at most about 0.5 points worse, FLEURS and VoxPopuli get no
worse, and the timestamps are usable. VoxPopuli got 0.4 points worse, so the variant did not pass; the
clip in pieces was also 0.5–1.3 points worse. The variant is published as the timestamps revision of
GAIK-project/whisper-large-v3-fi-dialects
(card: release/TIMESTAMPS_CARD.md), and the main model is tagged
v1.0. Use the variant when you need sentence timestamps, or long audio transcribed without cutting
it yourself, and always with the fallback settings: without them it looped in 3 of the 10 interviews.
import soundfile as sf
from transformers import pipeline
asr = pipeline(
"automatic-speech-recognition",
model="GAIK-project/whisper-large-v3-fi-dialects",
revision="timestamps",
dtype="float16",
device="cuda",
)
audio, sr = sf.read("interview.wav", dtype="float32") # 16 kHz mono, any length
out = asr(
{"raw": audio, "sampling_rate": sr},
return_timestamps=True,
generate_kwargs={
"language": "fi",
"task": "transcribe",
"temperature": (0.0, 0.2, 0.4, 0.6, 0.8, 1.0),
"compression_ratio_threshold": 1.35,
"logprob_threshold": -1.0,
"no_speech_threshold": 0.6,
"condition_on_prev_tokens": False,
},
)
for chunk in out["chunks"]:
print(f"[{chunk['timestamp'][0]:7.2f} s] {chunk['text'].strip()}")
The judges are language models and they are noisy. On the clip, two arms with an identical
transcript, the Haaga-Helia WhisperX server's and HY's own run of the same set-up (a transcript HY
has not published, so it is not in this repository), got 4.5 and 3.5 points from Claude and 8.7 and
10.2 from Gemini, so read differences under about 2 points as ties. The same shows in this
repository on LA-murre: the same transcripts judged again in round 3, in other company and on 577 of
the 583 segments, got 24.2 instead of 25.9 points (base Whisper) and 13.7 instead of 13.6 (the
published model)
(summary-r2.json,
summary-r3.json). Gemini is stricter
throughout. Gemini also judges Gemini's own transcripts; Claude, which made no transcript, puts
Gemini 3.5 Flash first as well.
Is the LA-murre result Claude's opinion only? Open models served by CSC's Aitta service (CSC's
OpenAI-compatible LLM API for Finnish research) judged round 2 again with the same prompt, answer
schema, labels and order as Claude (judge_dialect.py --provider aitta,
compared by judge_agreement.py in
agreement-r2.json):
| Judge | Segments | Whisper large-v3 | Round 1 | Published | Internal | Published vs base (fewer / more points) | Same preference as Claude |
|---|---|---|---|---|---|---|---|
| Claude Opus 5.5 (reference) | 583 | 25.9 | 13.8 | 13.6 | 11.4 | 398 / 89 | – |
| gpt-oss-120b (Aitta) | 583 | 20.5 | 13.4 | 13.2 | 12.9 | 275 / 105 | 80.7 % of 1 535 pairs |
| Gemma 4 31B (Aitta), sample | 198 | 17.3 | 10.1 | 10.5 | 9.0 | 100 / 27 | 92.1 % of 544 pairs |
| Qwen3.6 27B (Aitta), sample | 197 | 23.0 | 11.8 | 11.3 | 9.3 | 121 / 27 | 92.8 % of 643 pairs |
Points are meaning-error points per 100 reference words. "Same preference" counts the pairs of
models in a segment where both judges see a difference, and how often they prefer the same one.
All four judges rank the models the same way: the internal model best, the published model and round
1 about level, base Whisper far behind; gpt-oss-120b also finds the published model clearly better
than base Whisper (sign test p < 0.001) and separates the three fine-tunes less than Claude does.
Gemma 4 31B and Qwen3.6 27B were run on a sample, not on all 583 segments: both judged the same seeded
sample of 200 segments (--per-locality 20: 20 per locality, seed 0; Gemma answered 198, Qwen 197),
and Gemma puts round 1 just ahead of the published model. Point scales differ between judges, so compare
models within one judge.
This clip cannot say how the models handle dialects. What it does show is the method: a dialect test needs dialect audio with a transcript a person has checked, and a decision on whether the transcript should keep the dialect forms or normalise them, because WER punishes whichever the model does not do. The LLM judge is built to ignore that difference and count only meaning.
Data that could serve as a dialect test set and as fine-tuning data:
| Dataset | What | Access |
|---|---|---|
| Lahjoita puhetta (Donate Speech) | About 3 600 h of spontaneous Finnish from over 20 000 speakers across Finland, part of it transcribed, with dialect metadata | Kielipankki; licence agreement, a fee for commercial use |
| Suomen kielen näytteitä (SKN) | Dialect recordings from 50 localities, with both dialect and normalised transcripts | Kielipankki, CC BY 4.0: used here for the dialect test and fine-tune |
| Lauseopin arkiston murrekorpus (LA-murre) | 142 dialect samples from 132 localities, dialect transcripts only | Kielipankki, CC BY-ND 4.0: used here as the second dialect test and for the internal model |
| Common Voice, FLEURS, VoxPopuli (Finnish) | Read or formal speech, the sets the RASMUS model card reports | Open |
What was held out: the published model trained on all of the 38 training municipalities' segments. The 2 validation and 10 test municipalities stayed out of training entirely, and the test uses 60 segments from each test municipality, so the rest of those 12 interviews (4 601 segments, 19.5 h) was not used at all.
What was tried next (15 Oct 2026):
all-skn branch
(card) for audio that is mostly dialect.Still open:
| Path | What it holds |
|---|---|
scripts/ | Runners, scorer, judges, table and chart builders (see Run it again). provenance.py writes each run's _run_metadata*.json; asr_scoring.py is the clip scorer with HY's normalisation; get_clip.py fetches the clip. |
tests/ | Offline tests (uv run pytest): no keys, no downloads, no GPU. |
lumi/ | How the open models were run and trained on LUMI: environment setup, Slurm jobs, and stubs that hide three container libraries NeMo cannot use. |
release/ | The Hugging Face cards, uploaded by publish_hf.py: MODEL_CARD.md (the main model), TIMESTAMPS_CARD.md (its timestamps revision), ALL_SKN_CARD.md (its all-skn revision) and DATASET_CARD.md. |
docs/ | DIALECT-EXAMPLES.md (20 SKN test segments side by side and WER per municipality), story/ (the walk-through pages, one self-contained HTML file per language, built from story/src/) and img/ (the dialect chart from dialect_chart.py, the front page's figures from readme_figures.py, the banner, an AI illustration made with GPT Image 2.5, and the sources' logos in logos/, their owners' trademarks, from svgl). video/ holds the front page's 21-second video: shots made with Runway Gen-4.5 and music with ElevenLabs Music (both through Replicate), no people, no voice. The story pages are also served at gaik-project.github.io/finnish-asr. |
results/transcriptions/ | One JSON per model and run on the clip: transcript, settings, library versions, timing. qad1/ holds GAIK's earlier QAD-1 runs (OpenAI, Gemini, the gaik transcriber, the Haaga-Helia WhisperX server, Whisper on CSC Roihu). |
results/judgements/ | Per judge and model: every meaning error quoted from the reference and the transcript, the name checks, reference doubts, scores. |
results/scores.json, scores.reviewed.json | WER, CER and error counts for every clip transcript, against HY's and the reviewed reference. |
results/clip/ | gt_alignment.json: the reference's sentences, word times and QAD-1's 42 reference doubts, frozen from QAD-1 for the listening page. decoding_check.json and decoding_scores.json: the published model from the Hub decoded six ways, the model card's settings table. |
results/benchmarks/ | SKN, FLEURS and VoxPopuli: one line per recording (reference, transcript, seconds) and scores.json. LA-murre: scores.json and run metadata only. |
results/judgements_dialect/ | The LA-murre meaning judges: summary*.json (rounds 1–3 and the Aitta judge), agreement-r2.json and run metadata (numbers only). |
results/finetune/ | The training log of every fine-tune: per-epoch validation loss, data counts, the epoch kept. No weights. |
results/labels/ | SKN training targets with LLM punctuation, SKN sentence times per segment, the long-form windows; for LA-murre only the statistics. |
results/timestamps/ | Raw decodes with timestamps and long-form windows (base Whisper, the published model, the timestamp variant), and their scores. |
results/gpu_run/ | GPU, host and library versions of the last clip job per environment (hf, nemo, qwen) on LUMI. |
Not here, on purpose: the clip and its references (download them from gaik-toolkit with
get_clip.py; data/ is gitignored), any LA-murre text (per-segment benchmark files, judge records,
rewritten training texts: licence), HY's own tool transcripts (not public), model weights (the
published model is on Hugging Face) and LUMI job logs. The LA-murre outputs the public scripts can
regenerate are listed in .gitignore.
Input. Every model gets the same 180 s file with Finnish set as the language where the model takes one, and no prompt or word list. Whisper and Qwen3-ASR take the whole clip. Models limited to short audio (Canary, Parakeet, Orukeet, wav2vec2, Chirp 3) get the same eight chunks, cut at the quietest 100 ms between 15 and 25 s.
WER and CER. The scorer of QAD-1 and the toolkit's transcription_eval, with HY's normalisation,
vendored in asr_scoring.py and unchanged, so the numbers match HY's and
QAD-1's published ones. The public test sets use the usual model-card normalisation (lower-case,
punctuation removed, whitespace collapsed) and corpus-level WER (score_benchmark.py).
LLM judges on the clip. Claude Opus 5.5 and Gemini 3.1 Pro each read the reference and one
transcript, without the model's name (judge_asr.py). The prompt tells them
to report only what would mislead a reader: a wrong word, name or number, missing content, invented
content. Fillers, spoken against written forms, compound spacing, punctuation and digits
against number words are explicitly not errors. Each judge also checks 14 names and
terms against their correct spelling (the reference itself misspells Dentsply Sirona)
and lists places where the reference looks wrong; those are reported, not scored.
SKN data and training. prepare_skn.py cuts SKN by municipality (2–25 s, seed 0);
prepare_targets.py punct adds LLM punctuation and keeps an answer only if its words are identical;
finetune_whisper.py trains a LoRA adapter on LUMI in a plain PyTorch
loop and keeps the epoch with the lowest validation loss.
The LA-murre judge. judge_dialect.py gives one judge the dialect reference and every model's
transcript of a segment at once, labelled with letters in an order shuffled per segment, and counts
meaning errors only. Rounds judge different sets of models (round 1: base Whisper, round 1's model,
Finnish-NLP; round 2: base Whisper and our three round-1 and round-2 models; round 3: base Whisper,
the published model and the timestamp variant); --provider aitta runs the same judgement on an open
model on CSC Aitta, and judge_agreement.py compares judges segment by segment. The
published scores were made with prompt dialect-judge-v1; this repository's copy is v2, identical
except that its three example word pairs come from SKN instead of LA-murre (the docstring explains).
Timestamps. prepare_timestamps.py turns SKN's sentence annotation into sentence times inside
every published segment and into the 10 long-form windows; finetune_whisper.py --timestamps 0.5
trains with them; eval_timestamps.py decodes on LUMI (--fallback adds the temperature-fallback
decode) and scores locally.
Decoding settings. score_asr.py --decoding scores the published model's six decodings of the
clip in results/clip/decoding_check.json against both references; the model card's table of
settings to avoid comes from it.
Provenance. Every runner that writes results writes a secret-free _run_metadata*.json beside
them: commit, runner, dataset path, models, arguments, and which keys were set (never their values).
Files made before this repository existed carry gaik_evals_commit and paths of GAIK's private
evaluation repository, where the work was first done; their content is unchanged.
Where it ran. Open models and training: one GPU on LUMI (AMD MI250X) in CSC's PyTorch container. ElevenLabs, Chirp 3, OpenAI and Gemini: their APIs. LLM judges: Claude through Azure Foundry, Gemini through Vertex AI, open models through CSC Aitta.
From the repo root, with uv:
uv sync --all-extras # Python deps for every local step
uv run pytest # offline tests: no keys, no downloads, no GPU
uv run python scripts/get_clip.py --prepare-audio # the clip and both references from gaik-toolkit (ffmpeg on PATH)
# the clip: API models, scores, judges, the README table
uv run python scripts/run_api_asr.py --repeats 3 # ElevenLabs, Chirp 3
uv run python scripts/score_asr.py # WER/CER, no keys; --reviewed for the reviewed reference
uv run python scripts/score_asr.py --decoding # the model card's decoding-settings table
uv run python scripts/judge_asr.py # both judges (--claude-provider api for an Anthropic key)
uv run python scripts/make_readme_table.py # the clip table above
# open models, public test sets and training on LUMI, one wrapper for every step (details: lumi/README.md)
export LUMI_USER=<your CSC user name> LUMI_PROJECT=project_<number>
L=lumi/lumi.sh
bash $L push && bash $L setup && bash $L download clip && bash $L download bench # once
for e in hf qwen nemo; do bash $L run clip $e; bash $L run bench $e; done # six jobs
bash $L status # until the queue is empty
bash $L fetch
uv run --extra api-asr --extra judges --extra hub python scripts/run_api_benchmark.py \
--arm vertex_gemini-3.5-flash --arm vertex_gemini-3.5-flash-std --arm google_chirp-3 # Gemini 3.5 Flash (two prompts), Chirp 3 on SKN
uv run python scripts/score_benchmark.py # SKN/FLEURS/VoxPopuli table, no keys
# the dialect model: SKN, punctuated targets, training, its tests
bash $L download skn && bash $L prepare skn
bash $L fetch-labels # prepare_skn.py's segment lists -> results/labels/ (gitignored)
uv run python scripts/prepare_targets.py punct results/labels/skn_train.jsonl
bash $L push-labels && bash $L download train
bash $L run train hf --name whisper-large-v3-fi-skn-punct-best --data skn+open --skn-text punct --epochs 3 --lora-r 64
bash $L run clip hf --arm ft_whisper-large-v3-fi-skn-punct-best-chunks
bash $L run bench hf --bench skn_fi && bash $L fetch
uv run python scripts/dialect_chart.py # docs/img/dialect-finetune.svg, no keys
uv run python scripts/dialect_examples.py # docs/DIALECT-EXAMPLES.md, no keys
uv run python scripts/build_story.py # docs/story/story.{en,fi}.html, no keys
uv run python scripts/readme_figures.py # the front page's figures in docs/img/, no keys
uv run --extra hub python scripts/build_listen.py # docs/listen.html and its SKN clips, no keys
uv run python scripts/build_compare.py # results/compare.html (gitignored); fetches the SKN test parquet
# timestamps and long-form (prepare_timestamps.py needs SKN's *_full.eaf files)
uv run python scripts/prepare_timestamps.py <dir with SKN *_full.eaf>
bash $L push-labels && bash $L run timestamps hf --arm hf_whisper-large-v3 --arm hub_whisper-large-v3-fi-dialects
# the timestamp variant: training, its tests, and the timestamp evaluation with the fallback decode
bash $L run train hf --name whisper-large-v3-fi-skn-punct-ts --data skn+open --skn-text punct --epochs 3 --lora-r 64 --timestamps 0.5
bash $L run clip hf --arm ft_whisper-large-v3-fi-skn-punct-ts --arm ft_whisper-large-v3-fi-skn-punct-ts-chunks
for b in skn_fi fleurs_fi voxpopuli_fi; do bash $L run bench hf --bench $b --arm ft_whisper-large-v3-fi-skn-punct-ts; done
bash $L run timestamps hf --arm ft_whisper-large-v3-fi-skn-punct-ts
bash $L run timestamps hf --fallback --arm hf_whisper-large-v3 --arm hub_whisper-large-v3-fi-dialects --arm ft_whisper-large-v3-fi-skn-punct-ts
bash $L fetch && uv run python scripts/eval_timestamps.py score
# LA-murre (CC BY-ND): downloaded from Kielipankki; every per-segment output stays local (gitignored)
bash $L prepare lamurre && bash $L run bench hf --bench lamurre_fi && bash $L fetch
uv run python scripts/judge_dialect.py --round r2 --arms hf_whisper-large-v3,ft_whisper-large-v3-fi-skn,ft_whisper-large-v3-fi-skn-punct-best,ft_whisper-large-v3-fi-skn-lamurre-best
uv run python scripts/judge_dialect.py --round r2 --arms <the same> --provider aitta --judge-model openai/gpt-oss-120b
uv run python scripts/judge_agreement.py --round r2
uv run python scripts/judge_dialect.py --round r3 --arms hf_whisper-large-v3,ft_whisper-large-v3-fi-skn-punct-best,ft_whisper-large-v3-fi-skn-punct-ts
# the API services on every test set (FLEURS, VoxPopuli and LA-murre WAVs: bash $L fetch-audio <bench>)
for b in skn_fi fleurs_fi voxpopuli_fi lamurre_fi; do uv run --extra api-asr python scripts/run_api_benchmark.py --bench $b --arm azure_gpt-4o-transcribe --arm elevenlabs_scribe-v2 --arm google_chirp-3; done
# all of SKN: every segment of the 12 held-out municipalities joins the training data
bash $L prepare skn-heldout && bash $L fetch-labels
uv run python scripts/prepare_targets.py punct results/labels/skn_heldout.jsonl && bash $L push-labels
bash $L run train hf --name whisper-large-v3-fi-skn-all --data skn-all+open --skn-text punct --epochs 3 --lora-r 64
for b in fleurs_fi voxpopuli_fi lamurre_fi; do bash $L run bench hf --bench $b --arm ft_whisper-large-v3-fi-skn-all; done
uv run python scripts/judge_dialect.py --round r4 --arms hf_whisper-large-v3,ft_whisper-large-v3-fi-skn-punct-best,ft_whisper-large-v3-fi-skn-all,ft_whisper-large-v3-fi-skn-lamurre-best,azure_gpt-4o-transcribe,vertex_gemini-3.5-flash-std
uv run python scripts/judge_dialect.py --round r4 --arms <the same> --provider aitta --judge-model openai/gpt-oss-120b # Gemma 4 and Qwen3.6: --per-locality 20
# the ten dental-webinar clips (HY's private references in data/dental10/, gitignored)
bash $L push-dental && bash $L run clip hf --bench dental10_fi
uv run --extra judges python scripts/judge_dental.py # blind judges; only summary.json is committed
Each step skips what it has already done; --force redoes it (bash $L run clip hf --force).
To add a model, add a line to ARMS in run_open_models.py and run_benchmark.py
and run push, download and run for its environment. bash $L clean removes the
project from LUMI's scratch when the work is over. Keys go in a .env at the repo root;
.env.example lists their names per script. publish_hf.py needs a GAIK-project
write token and is for maintainers.
reference.reviewed.txt; see
Reviewed reference); the rest is HY's unverified text. The judges flag
0–3 reference doubts per transcript.prepare_lamurre.py) and rerunning the models and the judge
reproduces them. The public judge prompt differs from the one that made them in three example
word pairs.gaik_evals_commit and paths in GAIK's private evaluation repository, where the work
was first done.scripts/, tests/, lumi/): MIT, LICENSE, copyright Haaga-Helia
University of Applied Sciences (GAIK project).results/, the chart, the story pages and these texts):
CC BY 4.0. Credit: GAIK project, Haaga-Helia
University of Applied Sciences.timestamps revision are
Apache 2.0, as the base model
openai/whisper-large-v3; the published dataset is CC BY 4.0.get_clip.py downloads it, with its
reference transcripts (by the University of Helsinki team, and the reviewed one), from
gaik-toolkit,
where they are public.Details in NOTICE.md.
Developed by Akseli Leskinen (laguagu), Haaga-Helia University of Applied Sciences, in the GAIK project (Generative AI-enhanced Knowledge Management in Business), led by Haaga-Helia with the University of Helsinki and Tampere University, co-funded by the European Union (ERDF). The Ajokortti reference transcript was made by the University of Helsinki team.
LUMI. We acknowledge CSC – IT Center for Science, Finland for awarding this project access to the LUMI supercomputer, owned by the EuroHPC Joint Undertaking, hosted by CSC (Finland) and the LUMI consortium through CSC – IT Center for Science, Finland.