En norsk stemme, trent på en gaming-PC
Det er et hull i den norske open-source-TTS-floraen, og det trenger det ikke være. Så jeg finjusterte CosyVoice 3 på ~458 timer bokmål.
Eksempler
| gen | ref | navn |
|---|---|---|
| Atle Antonsen | ||
| Kari Nessa Nordtun | ||
| Håvard Grønlie | ||
| Jonas Gahr Støre | ||
| Barack Obama |
Hvorfor?
Ved flere anledninger har jeg sett etter norske tekst-til-tale-modeller. Da jeg begynte å lage læringsspill til mine barn, Learnloop, begynte jeg for alvor å se etter gode norske tekst-til-tale-modeller. Å ha opplest tekst viser seg å være essensielt når man enda ikke kan lese. Det viser seg at det ikke finnes noen moderne tekst-til-tale-modeller på norsk. Det er uaktuelt å betale stemmeskuespillere for et hobbyprosjekt. Det er kjipt å betale penger til store amerikanske techselskaper. Dette er lavthengende frukt vi alt burde ha på plass! Da er det bare å starte treningen.
Og det var det jeg gjorde. I dag publiserte jeg første versjon: AlexKjes/cosyvoice3-norwegian-lora. En LoRA-finjustering av Fun-CosyVoice3-0.5B-2512 — Alibabas CosyVoice 3-base — på ~458 timer norsk bokmål. Steg 20 880, etter cirka 50 timers trening på en RTX 3090.
Hvorfor CosyVoice 3
Før dette kjørte jeg F5-TTS i samme stack. F5 er fantastisk på engelsk og brukbar på norsk etter finjustering, men den har én strukturell begrensning jeg stadig støtte på: prosodien kommer utelukkende fra referansesporet. Modellen forstår ikke hva den sier. Gir du den et spørsmål med en monoton referansestemme, får du tilbake et monotont spørsmål. For spilldialog — der samme stemmeskuespiller skal levere både rolig eksposisjon, dramatisk vending og stille kommentar — holder ikke det.
CosyVoice 3 er neste generasjon i samme linje, og et mye mer interessant stykke arkitektur. Det er en tostegs-modell: en Qwen2-0.5B LLM-frontend leser teksten og produserer semantiske tale-tokens, og en flow-matcher dekoder dem til lyd. Oppstrøms-teamet selger den som “designed for zero-shot multilingual speech synthesis in the wild”, med state-of-the-art-tall på “content consistency, speaker similarity, and prosody naturalness”. Det maper rett på de to tingene F5 ikke kunne gi meg:
- Ekte semantisk prosodi. Fordi første steg er en faktisk språkmodell, intoneres et spørsmål som et spørsmål og trykket legges på riktig ord uten at referansesporet må demonstrere det først. Det alene er forskjellen på “en robotisk forteller” og “noe som høres ut som dialog”.
- Prompt-styrt syntese. Oppstrøms beskriver det som støtte for “various instructions such as languages, dialects, emotions, speed, volume, etc.” I praksis betyr det at du kan fortelle modellen hvordan en linje skal leses — “si dette stille og ettertenksomt”, “med entusiasme” — og den hører etter. For interaktive spillfigurer er det akkurat den styringen jeg vil ha.
Den støtter også bi-streaming-inferens — tekst inn, lyd ut, parallelt — med en oppgitt ende-til-ende-latency helt ned mot 150 ms. Jeg har ikke målt mine egne tall ennå, men selv en størrelsesorden unna ville fortsatt være forskjellen på en merkbar pause og noe som føles som ekte tale.
Trening: LoRA, ikke full finjustering
Selv om CosyVoice 3 “bare” er en halv milliard parametere etter dagens LLM-mål, er det utelukket å finjustere alle på en enkelt RTX 3090 — optimizer-staten alene får ikke plass. Så dette er en LoRA-finjustering: istedenfor å oppdatere modellens vekter direkte, trener jeg en liten low-rank “delta” som legges på toppen av den frosne originalen. Omtrent 13 millioner trenbare parametere av ~500 millioner totalt — cirka 2,6 % av modellen.
LoRA-en sitter på Qwen2-0.5B LLM-frontenden med rank 24, anvendt på alle 24 transformer-blokker. Flow-matcher-dekoderen nedstrøms er urørt; den vet allerede hvordan tale-tokens skal renderes til lyd, og det som måtte læres var “hvordan høres norsk ut” i steget før. Total tid: ~50 timer på en 3090.
Har du aldri støtt på LoRA før, skrev jeg en kort illustrert intro: Hva er LoRA, og hvorfor bruker alle det? To diagrammer, ingen matte utover matriseformer.
Datasettet
~458 timer norsk bokmål, fra to åpne korpus publisert av Nasjonalbibliotekets AI-lab:
| Kilde | Klipp | Timer | Lisens |
|---|---|---|---|
NbAiLab/NST | ~219 000 | ~540 | Apache 2.0 |
NbAiLab/NPSC | ~32 000 | ~140 | CC-0 |
Datapipelinen kjører på hjemmeklyngen og har gått trofast i nesten et år nå. Hvert klipp går gjennom samme behandling: Demucs fjerner musikk og bakgrunnsstøy, nb-whisper-large gir første transkripsjonsrunde, pyannote 3.1 beholder bare énperson-segmenter, og en andre runde med nb-wav2vec2-1b-bokmaal produserer CTC-labels som treneren faktisk konsumerer. Filtre på lengde og ord-konfidens trimmer langhalen — for korte klipp, for støyete klipp, og klipp der transkripsjonene er uenige med seg selv.
NST er ryggraden — nær-mikrofon, stort volum. NPSC supplerer med parlamentstale, mer naturlig rytme og turtaking. Begge er åpent lisensiert, så modellen er ren på datasiden.
Prøv den
Den ligger på https://huggingface.co/AlexKjes/cosyvoice3-norwegian-lora. Utgivelsen er CC BY-NC 4.0 for nå — jeg vil evaluere modellen ordentlig før jeg lemper på lisensen.
Installer og kjør
LoRA-en er bare et checkpoint som legger seg oppå CosyVoice 3-rammeverket, så oppsettet er “klon CosyVoice, hent basemodellen, last vektene mine på toppen.”
git clone --recursive https://github.com/FunAudioLLM/CosyVoice.git
cd CosyVoice
git submodule update --init --recursive
conda create -n cosyvoice -y python=3.10
conda activate cosyvoice
pip install -r requirements.txt
Så hent basemodellen og det norske LoRA-checkpointet:
from huggingface_hub import snapshot_download
snapshot_download(
"FunAudioLLM/Fun-CosyVoice3-0.5B-2512",
local_dir="pretrained_models/Fun-CosyVoice3-0.5B",
)
snapshot_download(
"AlexKjes/cosyvoice3-norwegian-lora",
local_dir="pretrained_models/cosyvoice3-norwegian-lora",
)
Last basemodellen, slå LoRA-ens EMA-vekter inn i Qwen2-frontenden, og kjør zero-shot-inferens:
import torch
from cosyvoice.cli.cosyvoice import CosyVoice3
cosy = CosyVoice3("pretrained_models/Fun-CosyVoice3-0.5B", fp16=True)
state = torch.load(
"pretrained_models/cosyvoice3-norwegian-lora/model_20880_ema.pt",
map_location="cpu",
weights_only=False,
)
state = {k: v for k, v in state.items() if k not in ("step", "epoch")}
cosy.model.llm.load_state_dict(state, strict=False)
audio_chunks = []
for chunk in cosy.inference_zero_shot(
tts_text="Norsk talesyntese skal være tilgjengelig for alle.",
prompt_text="You are a helpful assistant.<|endofprompt|>" + ref_transcript,
prompt_speech_16k=ref_audio_16k,
):
audio_chunks.append(chunk["tts_speech"])
"You are a helpful assistant.<|endofprompt|>"-prefikset på prompt_text er det som forteller Qwen2-frontenden at den er i inferensmodus. Uten det driver modellen.
Hva som kommer
- Round-trip-eval på det kuraterte testsettet (TTS →
nb-whisper-medium→ WER). Tallene havner i samme TensorBoard som loss-kurven, så jeg kan se kvalitet og konvergens side om side. - Få den koblet inn i Learnloop og hørt den i faktisk spillkontekst — den eneste testen som egentlig betyr noe til slutt.
- Bedre dekning av nynorsk og dialekter. Begge er underrepresentert i datasettet i dag, modellen vet det, og du hører det. Datapipelinen har plass til å vokse.
Sakte, men det går framover.