BU
Backudden

Från 25 sekunder till 1,8: allt var lokalt, ändå svårt att använda

person
Daniel Andreasson
11 aug. 2026
schedule16 min läsning
Från 25 sekunder till 1,8: allt var lokalt, ändå svårt att använda

Mitt mål var aldrig bara röststyrning. Det var en assistent som svarar tillräckligt snabbt för att kännas som ett samtal, allt för att slippa stå i köket och vänta på att ta fram ett recept medan jag tittar på en snurrande ikon i tjugo sekunder.

Det här är andra delen i min serie om samma maskin. Den första handlade om hur långt 16 GB VRAM räcker när jag testar att en lokal modell ska ersätta molnbaserad AI som familjeassistent. Den här handlar om vad som hände när samma modell skulle få en röst.

Efter att Gemma 4 12B blivit familjeassistentens lokala hjärna var nu hela kedjan local first. Tal till text mot grafikkortet, modellen på samma kort, text till tal på processorn. Allt i en lokal setup.

Det första riktiga testet visade resultat på mellan 15 och 25 sekunder innan Freya började prata. Detta var inte det resultatet jag hade väntat mig.

Att få den att lyssna var enkelt

Den lokala speech-to-text var det som fungerade direkt. faster-whisper large-v3 på RTX 5060 Ti transkriberade 7,3 sekunder engelskt tal på 0,40 sekunder, ungefär arton gånger realtid. Svenska landade på runt femton gånger. Modellen tar cirka 8 sekunder att ladda och håller sedan ungefär 3 GB VRAM.

En detalj som blev viktig senare: när svenska transkriptioner ibland stavade fel på barnens namn var felet inte Whispers. Det var källjudet, alltså den TTS-röst som hade läst upp testmeningen, som uttalade namnet fel.

Att lyssna var alltså aldrig problemet, och rösten i sig var det inte heller fel på. Piper på CPU körs ungefär tjugofyra gånger snabbare än realtid.

Diagnosen: det var inte modellen

Det första man vill göra när en LLM-app känns långsam är att byta till en mindre, snabbare modell. Men efter att ha läst loggarna visade det sig inte vara rätt väg att gå.

Varje röstsegment gick in i röstagenten. Det innebar en kompilerad systemprompt på 52 719 tecken, ungefär 18 400 input-tokens, nio exponerade verktyg och thinking satt till high. Klienten väntade dessutom på hela svaret och syntetiserade en enda komplett WAV-fil på slutet. Modellens egen completion-tid låg mellan 9 och 36 sekunder.

Testet som avgjorde saken var enkelt. Samma varma gemma4:12b, fortfarande med produktionens allokering på 65 536 tokens, svarade på 0,85 sekunder när den fick

  • en prompt på 45 tokens,
  • thinking avstängt,
  • inga verktyg,
  • och ett tak på 80 output-tokens.

Det var alltså aldrig kapaciteten som var problemet, utan vad jag faktiskt skickade in i den. En stor context-allokering är inte samma sak som en stor prompt, och det är värt att skilja på dem: att ändra allokeringen tvingar fram en modellomladdning som i sig kostar sekunder.

Om du känner igen symptomet, alltså snabba delar men med en trög helhet som resultat, är det här testet värt mer än resten av artikeln. Det tar en minut och avgör om modellen över huvud taget är problemet.

curl -s http://127.0.0.1:11434/api/chat -d '{
  "model": "gemma4:12b",
  "think": false,
  "stream": false,
  "options": { "num_ctx": 65536, "num_predict": 80 },
  "messages": [
    { "role": "system", "content": "Du är en röstassistent. Svara med en kort mening." },
    { "role": "user",   "content": "Vad är huvudstaden i Frankrike?" }
  ]
}' | jq '{svar: .message.content, prompt_eval_count, eval_count}'

Kör ovanstående mot samma varma modell som din app använder, med samma num_ctx. Får du svar på ungefär en sekund är modellen okej och problemet ligger i det du skickar in eller i hur du väntar på svaret. Jag körde om exakt det här när jag skrev artikeln, tre veckor efter originalmätningen: 42 prompt-tokens, 12 genererade tokens, drygt en sekund inklusive processtart.

prompt_eval_count är i det här fallet den viktigaste siffran. Ställ den mot vad din app faktiskt skickar. Hos mig var skillnaden 42 mot ungefär 18 400, och då började saker att fungera som jag önskade.

Den snabba vägen

Röstportalen fick en egen väg som aldrig går via agent-bootstrap:

  • direkt mot Ollamas /api/chat, utan automatisk molnfallback,
  • en kompakt identitetsprompt i stället för hela dess personlighet,
  • think: false,
  • fyra senaste rundorna i minnet, inget mer,
  • 96 tokens tak och instruktion att hålla sig under ungefär 45 talade ord,
  • samma num_ctx=65536 som Telegram, just för att undvika omladdning när jag växlar kommunikationsplattform.

Så vad gjorde mest skillnad för upplevelsen? Text strömmas nu från modellen och delas vid naturliga klausul- och meningsgränser på minst tolv tecken. Varje klausul syntetiseras för sig och skickas som NDJSON-händelser. Webbläsaren köar och spelar den första klausulen medan resten fortfarande genereras.

Resultatet, när det lossnade: 0,40 sekunder STT, 0,60 sekunder till Gemmas första token, 1,64 sekunder till första spelbara ljud och 2,41 sekunder till ett färdiggenererat svar med två klausuler. Den svenska stacken som körs i dag ligger på 1,77 sekunder för en vanlig runda och 527 millisekunder för en deterministisk hjälpfråga som aldrig går via modellen alls.

Låt oss vara ärliga med vad siffrorna visar. Det är syntetiska end-to-end-mätningar med inspelat ljud, efter att tjänsten värmt Whisper och talsyntesen vid uppstart, och mätningen på 1,64 sekunder är dessutom första rundan efter en omstart. De bevisar serverns väg, men ingenting om hur det låter i ett kök med diskmaskinen igång. Skillnaden mellan de två sakerna är vad avsnittet om vardagen längre ner handlar om.

Men inom det de faktiskt mäter är resultatet entydigt: samma grafikkort och samma vikter, ungefär en tiopotens skillnad i upplevelse, och hela den skillnaden kom från orkestrering och strömning.

Jämförelse av tid till första hörbara ord före och efter den snabba vägen.
Whisper och talsyntesen var aldrig problemet. Väntan låg i hur mycket som skickades in i modellen och i att svaret inte fick börja förrän det var färdigt.

Priset för en mager prompt

Nästa dag testade jag att fråga vilken dag det var idag. Freya svarade "måndag den 20 maj" och upprepade det med samma självsäkerhet även när jag ifrågasatte svaret.

Den snabba vägen hade tagit bort agentens runtime-context utan att ersätta den del som faktiskt var sann. Med bara fyra rundors historik behandlade modellen sina egna hallucinationer som bevis.

Lösningen för detta har två lager. Varje vanlig röstinput får numera en liten auktoritativ systemrad med rätt tid i Europe/Stockholm. Direkta frågor om dag, datum eller klockslag går aldrig till modellen alls, utan besvaras från värdens klocka innan svaret går in i samma strömmande TTS-väg.

Lärdomen här är inte att magra promptar är farliga, utan att en mager prompt fortfarande måste innehålla de små bitar av auktoritativ runtime-kunskap som applikationen lovar. Att begränsa vad en modell får läsa räcker alltså inte, den måste också medvetet matas med det den omöjligt kan veta. För exakta fakta är deterministisk kod både snabbare och mer pålitlig än inferens.

Tre routes, ingen LLM-router

Den fasta portalen kunde först bara prata. Den kunde inte tända en lampa, starta en timer eller läsa mina anteckningar. Det är då lätt att falla för frestelsen att lösa det med en routermodell som klassificerar varje mening. Detta hade återinfört exakt den latens jag slitit för att bli av med.

I stället körs en billig matchning av fraser före modellen på varje runda, och det växte till tre routes:

  • Kommandon är verifierade actions. Lampor och minnesskrivning går aldrig via modellen. De kör samma fail-closed dispatchers som Telegram-verktygen, via en CLI-ingång på samma plugin. Ett säkerhetslager, två ytor. En neutral kvittering sägs först, och framgång sägs först när read-back verifierat att det faktiskt hände.
  • Frågor är grundad retrieval. Till exempel om jag säger "kolla mina anteckningar om X" görs en deterministisk sökning och resultatet matas in som tydligt märkt referens i samma smala strömmande runda. Modellen väljer aldrig verktyg. Den sammanfattar bara det som kod redan har hämtat. Tom träff sägs deterministiskt, utan att modellen ens tillfrågas.
  • Allt annat är prat, och då används den oförändrade snabba vägen.

Den tredje punkten är den viktigaste. Att aldrig be modellen svara när det inte finns något att svara på är samma lärdom som klockan, fast generaliserad.

Det var inte bara jag

Ungefär samtidigt cirkulerade ett inlägg på Reddit som återberättade en fallstudie där en agent gick från omkring 90 sekunder till omkring 4 per svar utan att byta modell, med tesen att modellen är det sista stället man ska leta på. De fyra bovarna var: retrieval som gör tungt arbete inline, för stor context, tool calls som staplas på varandra, och ingenting i cache.

Alla fyra motsvarar nästan exakt det jag hade landat i på egen hand. Morgonsammanfattningen genereras i förväg av en timer istället för under samtalet. Sessionen nollställs dagligen, max tokens är låst till samma 65 536 tokens som Ollama faktiskt kör, och policyn med sex verktyg finns just därför att schemat för varje aktiverat verktyg annars åker med i varje anrop. De deterministiska rutterna föregriper inferens helt. Och modellen bor kvar i VRAM, vilket är skälet till att en varm uppföljning i Telegram kan komma på 946 millisekunder.

Till och med deras riktade diagnos, att kolla processorkolumnen i ollama ps efter tysta GPU/CPU-delningar, står ordagrant i min egen omstartschecklista sedan en boot-race i juli fick modellen att starta helt på processorn. Den såg frisk ut i alla mätningar utom när man upplevde hastigheten.

Att någon annan kommer fram till samma sak är värt mer än mina egna siffror. Det visar att mönstret är generellt och inte en egenhet i min uppsättning av modell och hårdvara.

På två ställen går den här historien längre än den tidigare fallstudien.

Det första handlar om kapacitet. Rådet "byt inte modell för att få snabbare svar" är sant, men jag upplever det som ofullständigt. Gemma svarar på under en sekund när den är varm, och klarade ändå inte en flerstegsuppgift som krävde att läsa flera filer och syntetisera dem: nio generiska verktygsanrop, noll faktiska läsningar, och ett förvirrat ickesvar. Ingen mängd arbete för att minska latens hade hjälpt där.

Routing behöver alltså en kapacitetsgräns per uppgift, vilket är en annan axel än snabbhet, och det är därför den svåra syntesen numera är pinnad till en cloud-modell medan samtalet inte är det.

Det andra är att arbetet med latens också blev ett säkerhetsarbete. Fail-closed dispatchers, verifierad skrivning med read-back och deduplicering av retries infördes alla för att minska onödiga turer fram och tillbaka, men de är också det som gör det försvarbart att låta en lokal 12B röra riktig hårdvara i den här setupen. Det är ovanligt att prestanda och säkerhet drar åt samma håll.

En invändning mot originalinlägget: "allt syns i dina loggar" förutsätter att alla verktygsanrop loggas. Mina är medvetet maskerade, så det är sessionsdatan som faktiskt går att följa.

Två gånger som språket inte fungerade

Första gången var mikrofontestet av det första kommandot. snus ut kom tillbaka från den engelskspråkiga Whisper-modellen som "snusui" och matchade ingenting. Ett uttalat kommando på två ord räckte för att avslöja att funktionsord måste bo i samma språk som taligenkänningen. Den tillfälliga lösningen blev att rekommendera "snus break", två engelska ord som engelsk STT hör varje gång.

Andra gången blev lite mer utmanande. Hela det varma TTS-ekosystemet är engelskt. Valet stod mellan svenska med hanterbar kvalitet (Piper), engelska med genuint varm kvalitet (Kokoro), eller svenska med varm och hög kvalitet i molnet mot betalning (ElevenLabs). Jag valde först engelska lokalt, och skrotade sedan molnalternativet helt.

Sedan vände det. Ett riktigt familjetest gjorde utmaningen konkret på ett sätt ingen demo gör: en röst kan låta hur fin som helst och ändå vara underkänd om den säger ett barns namn fel. Hela den snabba röstytan flyttades tillbaka till svenska med lokala Piper Lisa.

Fixen var varken omträning eller ett uttalslexikon. Namnet har en alternativ stavning som är precis lika vanlig, och den stavningen råkar bli rätt uttalad. Den ligger numera enbart i den text som skickas till talsyntesen. Sidan, transkriberingen, minnet och loggarna visar fortfarande den riktiga stavningen, så det är bara högtalaren som hör skillnaden.

Skillnaden var alltså en bokstav. Innan den vann testade jag fem varianter blint, inklusive råa fonem i både svensk och engelsk stil, med samma röst och samma modell. Den enklaste lät bäst, vilket jag inte hade gissat.

Namnet står inte i den här artikeln. Barnen har inte bett om att bli exempel i en teknisk text, och längre fram argumenterar jag för hela bygget med att deras röster aldrig lämnar hemmet.

Det språkbytet var inte en promptändring. Det var en pipeline-migration. Datum, kommandogrammatik, lampor, Sonos, hämtning av minne, deterministiska felmeddelanden, hjälptexten, testerna och gränssnittet innehöll alla engelska. En flerspråkig modell gör inte det deterministiska systemet runtomkring i detta fall flerspråkigt.

Gränssnittet blev ärligt på köpet

Portalen har ett klot på en nordisk natthimmel som också är knappen som aktiverar talfunktionen. Klotet reagerar på riktigt ljud och inte på en timer. Uppspelningen går genom WebAudio med en analysnod, så klotet pulserar med den faktiska amplituden i Freyas röst, och en andra analysnod driver det från mikrofonnivån medan man håller ner knappen.

Den ärligheten föll ut gratis ur latensarkitekturen. Eftersom servern strömmar varje klausul som text och ljud tillsammans kan transkriberingen markera exakt den klausul som just sägs, i takt. En batchpipeline som returnerar en färdig WAV hade bara kunnat fejka det med en animationsloop.

Strömningen gjorde alltså assistenten snabbare och lät gränssnittet visa vad assistenten faktiskt gör just nu.

Varför inte bara OpenAI Realtime?

Den rimliga invändningen mot hela det här bygget är att molnet redan löst problemet. Ett realtids-API med tal in och tal ut skulle förmodligen kännas bättre än min lösning för grundläggande samtal, med möjlighet att avbryta mitt i en mening, mjukare betoning och inga skarvar mellan ord. Det är ingen idé att påstå att min lösning är bättre.

För mig handlar skälen att ändå göra det lokalt mindre om tekniska utmaningar och mer om att barnens röster aldrig lämnar hemmet. Det finns heller ingen kostnad per samtal, och ett sådant API faktureras separat från den prenumeration jag redan betalar. Assistenten fungerar om internet skulle ligga nere. Och ingen leverantörs kvot eller drifttid står mellan familjen och möjligheten att kontrollera belysningen i hemmet.

Dessutom: de funktioner som bär den faktiska nyttan, alltså lampor, timers och minne, måste röra saker i huset. De hade varit lokala oavsett vilken modell som förde samtalet. Molnet hade bara ersatt den delen som redan är snabb.

Jämförelsen ligger kvar som ett medvetet uppskjutet beslut, inte som en blind tro på lokala lösningar. Rätt tillfälle att göra den är när den lokala baslinjen faktiskt har godkänts av familjen, inte före det godkännandet.

Vad vardagen hittade

Förbehållet från den snabba vägen gäller fortfarande: siffrorna beskriver servern, inte rummet. De säger ingenting om hur klausulskarvar låter över ett längre svar, hur autoplay beter sig på en telefon, hur mikrofonen klarar ett kök där någon lastar ur diskmaskinen, eller om fyra rundors kontinuitet räcker för ett riktigt samtal.

Sedan dess har rummet hunnit svara på en del av det. Jag startar och stoppar radion med rösten från telefonen varje dag, och det var den vanan, inte ett testfall, som hittade den enda riktiga bristen: Freya kunde säga att hon satt på radion utan att radion hade börjat spela.

Felet låg varken i taligenkänningen eller i modellen, för kommandovägen går inte via någon av dem. Det låg i vad jag hade räknat som framgång. Home Assistant svarar OK på att kommandot är mottaget, och det är ett annat påstående än att musiken spelar. Just den skillnaden är vad kommandovägen lovar att hålla reda på, och för Sonos gjorde den inte det.

Rättningen är att kvitteringen läser tillbaka det observerade läget i stället för svaret på anropet. Efter ett kommando pollas högtalarens state tills det faktiskt står playing, i upp till fem sekunder. Om det aldrig blir playing säger Freya att kommandot togs emot men att det nya läget inte kan bekräftas. Det beskedet är tråkigare, men det går att lita på. Beteendet ligger numera i ett test, så samma fel inte kan passera obemärkt igen.

Den intressantare halvan av samma fel låg åt andra hållet. Ett stopp får inte kräva att läget blir stopped, eftersom Home Assistants Sonos-integration rapporterar PAUSED efter media_stop på köksspelaren. En naiv verifiering hade alltså börjat neka något den nyss hade gjort helt rätt. Falsk framgång och falskt misslyckande har samma rot: en verifiering är inte värd något förrän man vet hur det sanna läget faktiskt ser ut.

Så vad återstår att göra? Kvar finns möjligheten att avbryta mitt i ett svar, en explicit överlämning för svåra uppgifter som får svara asynkront, ett wake word, integration med surfplattan i köket och ett riktigt acceptanstest av hela den svenska ytan, inte bara av de rutter som råkar ingå i vardagen.

Latens, kapacitet och säkerhet

"Allt lokalt" och "snabbt" är två olika påståenden. Tiden till första hörbara svar betyder mer än tiden till sista token, och input-tokens och dold reasoning kan dominera helt över själva genereringen på GPU:n. Den mest kapabla agenten är därför oftast fel ändpunkt för ett samtal, och det som fungerade här är inte två modeller på varje runda utan en snabb väg med medvetna överlämningar till den starkare agenten när den faktiskt behövs.

Latens är ändå bara den ena axeln, och den som fått mest uppmärksamhet. Den andra är att en modell kan vara hur snabb som helst och ändå inte räcka till uppgiften, och det reparerar ingen orkestrering. Att veta var lägsta nivån ligger för varje sorts arbete har varit svårare, och mer användbart, än att veta hur många millisekunder pipelinen kostar.

Det jag inte hade räknat med är att nästan allt jag gjorde för att få ner tiden också gjorde systemet säkrare: färre rundturer, mindre context, deterministisk kod där det gick och verifiering före påstående. Det är sällan prestanda och trygghet drar åt samma håll, så jag ser det som en happy accident.