paprika-whisper-lt-v3 - 🌶️ Lithuanian ASR (LIEPA-3 fine-tune)
Third generation of a Lithuanian fine-tune of
whisper-large-v3-turbo.
Trained on ~3,281 h of LIEPA-3, warm-started from
paprika-whisper-lt.
Ready-to-run pipelines for real-time subtitles and long-file transcription with
punctuation and speaker labels:
https://github.com/kristijonasatpro/paprika
Read this before you use it
Use long-form decoding. Do not use chunk_length_s.
The chunked pipeline cuts audio at a fixed stride, transcribes each window
independently, then merges by matching text in the overlaps — and where the two
sides disagree it discards the span it cannot align. Measured 2026-08-15 on
clean audio, it silently dropped 30 words from one recording and 52 from
another, both at a seam. The output reads perfectly. There is no marker that
anything is missing.
It also invents text on non-speech. On 60 s of digital silence, 60 s of faint
hiss and 60 s of room tone, the chunked pipeline produced 24–164 characters of
confident Lithuanian; native long-form produced zero characters on all
three. This is a property of the chunked decoder, not of the weights — the
previous generation behaves the same way.
1import torch
2from transformers import WhisperForConditionalGeneration, WhisperProcessor
3
4m = "kristijonas/paprika-whisper-lt-v3"
5proc = WhisperProcessor.from_pretrained(m, language="lithuanian", task="transcribe")
6model = WhisperForConditionalGeneration.from_pretrained(m, dtype=torch.float16).to("mps").eval()
7
8feats = proc(audio, sampling_rate=16000, return_tensors="pt",
9 truncation=False, padding="longest", return_attention_mask=True)
10ids = model.generate(feats.input_features.to("mps", torch.float16),
11 attention_mask=feats.attention_mask.to("mps"),
12 language="lithuanian", task="transcribe",
13 return_timestamps=True, condition_on_prev_tokens=False,
14 temperature=(0.0, 0.2, 0.4, 0.6, 0.8, 1.0),
15 logprob_threshold=-1.0, compression_ratio_threshold=1.35,
16 no_speech_threshold=0.6)
17print(proc.batch_decode(ids, skip_special_tokens=True)[0])
Native long-form holds the whole feature sequence in memory (~18 GB for 22
minutes with word timestamps). For long recordings, cut into pause-aligned
blocks under 30 s and decode each independently — chunk_longform.py in the
repo above does this in bounded memory (5.4 GB flat regardless of duration).
Third-party tools take the chunked path. whisper-standalone-win,
faster-whisper and WhisperX do their own fixed-stride segmentation and do not
run a punctuation tagger, so you get ~30-second blocks of unpunctuated lowercase
text. That is the path described above, not a property of the weights. Measured
on a 57-minute press event: this model produced 6,681 words against stock
large-v3's 6,341, so 5.4% MORE content, but in 132 rigid ~30 s blocks with zero
punctuation, which reads far worse side by side. Use transcribe_file.py from
the repo for long files.
Output has no punctuation and no casing. That is by design: the LIEPA-3
labels have neither, and a separate tagger does the job better. The repo ships
one with a word-preservation contract (comma 84.7 / period 88.5 / casing 91.7
F1).
Results
| v1 | v2 | v3 |
|---|
| gold-11 WER (chunked) | 15.87 | 15.50 | 15.29 |
| gold-11 WER (long-form) | 17.94 | 17.68 | 17.25 |
| heldout-39 WER (chunked) | 5.45 | 5.13 | 5.16 |
| heldout-39 WER (long-form) | 8.44 | 7.67 | 6.42 |
| valid timestamp share | — | — | 1.00 |
The long-form column is where this generation earns its keep. v1's card advised
chunked-only inference because its long-form decoding was broken (+23 WER gap);
v3's gap is +1.26 to +1.96, so long-form is now the correct default.
Both benchmarks are in-domain — same sources as training. There is no valid
out-of-domain number: the sealed FLEURS set built for it turned out to be 44/44
digital silence, and every figure derived from it was withdrawn. The honest
out-of-domain evidence is A/B comparison against a commercial API on real
recordings (press conference, two-person call, phone recordings), where v3 was
competitive and visibly better on dialect speech.
Training
Warm-started from v2 (itself from v1, from svogunas/whisper-large-v3-turbo-lt).
37,500 steps, effective batch 32, one L40S, ~29 GPU-hours.
Mix: 50% spontaneous, 30% read, 12% phonetic, 5% dialect, 3% VoxPopuli LT.
The dialect slice spans all four regions (Aukštaitija, Žemaitija, Dzūkija,
Suvalkija). Its transcripts carry stress marks and non-standard vowels
(ɜ ə ɘ), which would teach the model to emit them, so they were normalised to
standard orthography before training — conservatively, leaving any word the
normaliser could not confidently map. That dialect data, not the raw volume, is
what this generation actually bought: tripling the hours moved WER ~0.2 points.
Limitations
- Lowercase, unpunctuated output (see above).
- Realized-speech convention: transcribes
turim, not normative turime,
because that is what LIEPA-3 labels do.
- 16 kHz mono. Parliamentary and spontaneous speech dominate the training mix.
- No out-of-domain benchmark. Test on your own audio before relying on it.
Attribution
- Data: LIEPA-3 garsynas (CC BY 4.0, VU / raštija.lt) — dėkojame. VoxPopuli (Meta AI).
- Base lineage:
svogunas/whisper-large-v3-turbo-lt (CC BY 4.0).
- Built for kalamo.ai — Lithuanian speech tooling.