Live ondertiteling · commit-heuristiek

De zin die twee keer komt

Een streamende spraakherkenner levert geen tekst, hij levert gissingen — en hij herziet het begin van een zin terwijl het eind al klopt. Wie op het verkeerde moment vastzet, laat dezelfde zin twee keer op het scherm staan.

Versta · engine Gevallen uit versta-log.txt Werkbank op de verzonden heuristiek

Bij een spraakherkenner die live meeschrijft is er geen moment waarop een zin "af" is. Elke paar honderd milliseconden komt er een nieuwe kandidaat binnen voor wat er zojuist gezegd is, en die kandidaat kan korter zijn dan de vorige, of anders beginnen. Toch moet er op enig moment besloten worden: dit deel blijft staan. Anders blijft de hele tekst tot in het oneindige heen en weer springen onder de ogen van de lezer.

Dat besluit heet vastzetten — committen. En het is precies daar dat het misgaat op een manier die je pas ziet als je de logs naast elkaar legt: dezelfde zin komt twee keer op het scherm. Deze pagina laat zien waar dat vandaan komt, wat de fix is, en waar die fix nog steeds tekortschiet.

1Het verschijnsel

Vier gelogde gevallen, stap voor stap afgespeeld — links wat er zonder deduplicatie op het scherm belandt, rechts wat er met deduplicatie staat.

Op 20 augustus 2026 werden negen transcript-blokken uit versta-log.txt nagelopen. Zeven ervan bevatten hetzelfde patroon: twee bijna-identieke varianten van dezelfde zin, direct achter elkaar. Hieronder de gevallen zoals ze in de log staan, afgespeeld als de stroom van tussenresultaten die ze veroorzaakte.

Kies een gelogd geval

stap 0 van 0
Zonder deduplicatie
Met deduplicatie

De oranje markering in de band hierboven wijst het moment aan waarop de oude versie werd vastgezet. Alles wat daarna nog binnenkomt, wordt eráchter geplakt in plaats van eroverheen geschreven — en dat is de dubbele zin.

2Waarom het misgaat

Eén regel van vier lijnen, met een aanname erin die niet klopt.

De beslissing om vast te zetten zat in deze functie. Ze wordt aangeroepen zodra er een nieuw tussenresultaat binnenkomt, met de vorige versie als old en de nieuwe als new:

// Zet het oude fragment vast als het nieuwe een ándere uiting lijkt.
private func shouldCommitBeforeReplacing(old: String, with new: String) -> Bool {
    guard !old.isEmpty, new.count < old.count else { return false }
    let prefixLen = min(5, min(old.count, new.count))
    guard prefixLen > 0 else { return true }
    return old.prefix(prefixLen) != new.prefix(prefixLen)
}

De redenering: een nieuwe uiting begint klein. Komt er dus iets binnen dat korter is dan wat er stond én anders begint, dan is de vorige zin kennelijk afgelopen en is dit een nieuwe. Zet het oude vast.

Reken het na op het eerste gelogde geval:

Eén teken is genoeg

"Daar kunnen we mooi proberen" is 28 tekens.
"Dan kunnen we mooi proberen" is 27 tekens.

27 < 28, dus de eerste voorwaarde is voldaan. De eerste vijf tekens zijn "Daar " tegen "Dan k" — verschillend, dus de tweede voorwaarde ook.

Uitkomst: de oude versie wordt vastgezet. Vervolgens komt diezelfde zin nog een keer binnen als tussenresultaat, en staat er op het scherm: "Daar kunnen we mooi proberen Dan kunnen we mooi proberen".

De aanname onder die regel is dat een herkenner die twijfelt opnieuw begint. Maar dat doet hij niet. Hij herziet juist het zinsbegin en laat het eind staan. Het begin is namelijk het deel waar hij de minste context voor had toen hij het uitsprak; naarmate er meer woorden bij komen, wordt duidelijker wat er in het begin gezegd moet zijn geweest. "Moeten wallen" wordt "Moet een bal" zodra "op z'n concert" erachter staat.

En dat gebeurt vaker naarmate het akoestisch moeilijker is: bij achtergrondlawaai en bij een sprekerwissel. Wat precies de omstandigheden zijn waarvoor deze app gebouwd is.

3De werkbank

Wat de heuristiek wél en niet moet vangen, en wat er gebeurt als je aan de knoppen draait.

De voor de hand liggende reactie is: maak de regel strenger. Vergelijk niet vijf maar tien tekens, eis een groter lengteverschil. Dan zet hij minder vaak ten onrechte vast.

Dat is de verkeerde kant op. Een strengere regel mist échte nieuwe uitingen, en dan wordt de ene zin aan de andere geplakt of overschreven — tekstverlies. Voor iemand die het gesprek van het scherm moet lezen is een zin die dubbel staat vervelend maar leesbaar; een zin die er niet staat is weg. De twee fouten zijn niet even erg.

De gekozen fix laat het raadwerk daarom staan en filtert de overlap weg bij het samenvoegen. Herkennen dat iets een herziening is gaat op een ander signaal: de herkenner corrigeert het begin, dus beide versies eindigen op dezelfde woorden en de lengtes liggen dicht bij elkaar.

// Is `candidate` een herziening van het vorige vastgezette segment,
// of een nieuwe uiting? Herziening = gedeelde staart + vergelijkbare lengte.
private func isRevision(of previous: String, by candidate: String) -> Bool {
    let a = Self.normalizedWords(previous)
    let b = Self.normalizedWords(candidate)
    guard a.count >= 3, b.count >= 3 else { return false }
    let longer = max(a.count, b.count)
    let shorter = min(a.count, b.count)
    guard Double(shorter) / Double(longer) >= 0.5 else { return false }
    var shared = 0
    while shared < shorter, a[a.count - 1 - shared] == b[b.count - 1 - shared] { shared += 1 }
    return shared >= 3 || Double(shared) / Double(shorter) >= 0.6
}

Hieronder staan die vijf getallen als schuifjes. Ze sturen zowel de gevallen in deze paragraaf als de speler in paragraaf 1 aan — zet een schuif verkeerd en je ziet de dubbele zin daarboven terugkomen.

Parameters
Uitkomst op de gevallen
Waar deze gevallen vandaan komen

De gevallen met het label uit de log staan letterlijk in versta-log.txt of in de commit-boodschap van 20 augustus 2026. De gevallen met nagebouwd zijn hier bijgemaakt, en bewust lastiger gekozen dan de vijf tegenvoorbeelden waarop de fix destijds getoetst is. Dat is geen oordeel over die toets — het is de reden dat deze pagina bestaat: zien waar de grens ligt.

4De twee fouten wegen niet even zwaar

Een dubbele zin kost leesruimte. Een weggefilterde zin kost het gesprek.

Elke instelling van de schuifjes is een keuze tussen twee soorten fout. Te laks: dubbele tekst blijft staan. Te streng: een echte, nieuwe zin wordt aangezien voor een herziening en overschrijft de vorige — die is dan weg, en de lezer weet niet dat er iets miste.

Geef beide fouten een prijs en het wordt één getal. De standaardverhouding hieronder — tekstverlies vijf keer zo duur als een dubbele zin — is de weging die achter het oorspronkelijke besluit zat: liever een keer dubbel dan een keer weg.

Wat kost een fout?
Vervelend, maar de lezer ziet beide versies en kan zelf kiezen.
Een echte zin verdwijnt geruisloos. De lezer merkt niet dat er iets ontbrak.
Kosten per instelling van de staartlengte

Alle andere schuifjes blijven staan zoals je ze nu hebt; alleen gedeelde staart in woorden loopt van 1 tot 6. Lager is beter.

5Waar het niet uitkomt

Twee gevallen die geen enkele instelling goed krijgt.

Als de herkenner ook het láátste woord herziet

Het hele idee rust op de gedeelde staart. Corrigeert de herkenner nu net óók het slotwoord, dan is die staart weg en ziet de regel twee onafhankelijke zinnen. In de log staat zo'n geval: "normale boer man" tegenover "malle boer van". Drie woorden elk, het middelste identiek — maar man is geen van, dus de gedeelde staart is nul woorden lang en de dubbele tekst blijft gewoon staan. Speel dat geval af in paragraaf 1: rechts staat hetzelfde als links.

Een gedeelde staart míddenin meetellen zou dit vangen, maar dat maakt de regel meteen veel gretiger op zinnen die toevallig een paar woorden delen — en gretiger betekent hier tekstverlies.

Als de zin ná de herziening doorgroeit

Speel het eerste geval hierboven af tot en met stap 7. Bij stap 6 vangt de deduplicatie de herziening en staat er één zin op het scherm. Bij stap 7 groeit diezelfde zin met één woord — "om" — en komt de dubbele tekst terug.

De oorzaak is de manier waarop de gedeelde staart geteld wordt: vanaf het eind, woord voor woord. Voegt de herkenner achteraan iets toe, dan schuift die uitlijning één plaats op en is de gedeelde staart in één klap nul woorden lang. De regel ziet dan weer twee losse zinnen. En omdat dat bij de eerstvolgende vastzetting nog steeds zo is, wordt de dubbeling daarna niet meer weggewerkt maar vastgelegd.

Dit is niet met de schuifjes te herstellen. De gedeelde staart is nul; geen enkele stand van gedeelde staart in woorden of van het aandeel maakt van nul een treffer. Wie dit wil vangen moet niet de staart vergelijken maar de langste gedeelde reeks woorden zoeken, waar die ook begint — met precies het risico op gretigheid dat hierboven al de prijs bleek.

Als iemand zich echt herhaalt

Het tweede geval is fundamenteler. Staat er twee keer exact dezelfde zin van vijf woorden, dan is dat óf een herkenner die dezelfde zin opnieuw aanbiedt, óf een spreker die zichzelf herhaalt omdat de eerste keer niet aankwam. Dat is bij een slechthorende luisteraar bepaald geen zeldzaamheid — het is juist wat er gebeurt als het verstaan misgaat.

Niet oplosbaar in de tekst

Twee identieke zinnen zijn twee identieke zinnen. Geen enkele regel die alleen naar de tekst kijkt kan een herhalende herkenner van een herhalende spreker onderscheiden — de invoer is letterlijk gelijk. Dit is een geaccepteerde beperking van de huidige fix: zegt iemand tweemaal exact hetzelfde van drie woorden of langer, dan wordt één exemplaar weggefilterd. Wie dit wél wil oplossen moet buiten de tekst kijken: naar de tijd tussen de segmenten, of naar het geluid zelf.

Wat de fix waarschijnlijk óók verklaart

Drie eerdere meldingen gingen over "de tweede spreker wordt slechter opgepikt" (29 april 2026), multi-speaker-problemen (30 april 2026) en tekst die haperde "zodra een ánder begon te spreken" (20 juli 2026). Bij een sprekerwissel herziet de herkenner juist het zinsbegin — precies de trigger van deze bug. Het mechanisme staat vast; dat het déze drie meldingen verklaart is een vermoeden, geen meting.

6Verantwoording

De twee functies op deze pagina zijn de code zoals die in SpeechManager.swift staat, hier in JavaScript nagebouwd. Alle oordelen in de werkbank worden ter plekke uitgerekend — er staat geen enkele uitkomst voorgeschreven in de pagina.

Wat hier niet in zit: de fix is op het toestel gezet als losse build en op het moment van schrijven nog niet in een openbare versie bevestigd. De uitkomsten hieronder zijn dus rekenwerk op de code, geen veldmeting.

Gebouwd door de Nachtelijke Maker · 25 augustus 2026 · alles wordt in je eigen browser uitgerekend, er gaat niets naar een server.