{"slug": "how-to-fix-punctuation-in-speech-to-text-and-llm-output-without-calling-a-model", "title": "How to fix punctuation in speech-to-text and LLM output without calling a model", "summary": "A developer built sentencify, a lightweight JavaScript library that adds punctuation to speech-to-text and LLM output without calling a model. The library uses ordered regular expressions to classify sentences as interrogative, exclamatory, or declarative, and supports six languages including English, Spanish, French, and Japanese. The developer chose a first-match rule approach for debuggability, though acknowledges it can be fragile.", "body_md": "If you've ever wired up the Web Speech API and rendered the result straight to the page, you've seen this:\n\n``` js\nconst recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)()\nrecognition.lang = 'en-US'\n\nrecognition.onresult = (event) => {\n    const transcript = event.results[0][0].transcript\n    console.log(transcript)  // 'can you send me the report'\n}\n\nrecognition.start()\n```\n\nThe transcription is correct. The formatting isn't. No leading capital, no question mark. The same thing happens with streamed LLM tokens that get cut off before the final punctuation lands, and with any form field where users type fast and don't bother.\n\nThe instinctive fix is to send it back through a model. \"Clean up this text.\" It works, and it's a strange amount of machinery for the problem: a network round trip, tokens billed, latency you can feel, and a nondeterministic result for a task that is almost entirely mechanical.\n\nCapitalizing the first letter is trivial. The actual question is **which mark goes at the end** — and that turns out to be the only interesting part.\n\nTo punctuate a sentence you first have to know what kind of sentence it is:\n\n`can you send me the report`\n\n→ interrogative → `?`\n\n`this is amazing`\n\n→ exclamatory → `!`\n\n`the meeting is at three`\n\n→ declarative → `.`\n\nThat's a three-way classification problem. And for unpunctuated English it's a problem with a lot of surface structure to exploit — interrogatives overwhelmingly start with an auxiliary or a wh-word, exclamatives have their own recognizable openers. You don't need a language model to notice `can you`\n\n.\n\nWhich is what I ended up building. `sentencify`\n\nis a small library that does the classification with ordered regular expressions, then applies the punctuation:\n\n```\nnpm install sentencify\njs\nimport { correctSentence } from 'sentencify'\n\ncorrectSentence('can you send me the report')  // 'Can you send me the report?'\ncorrectSentence('this is amazing')             // 'This is amazing!'\ncorrectSentence('the meeting is at three')     // 'The meeting is at three.'\n```\n\nNo await. No model. No dependencies.\n\nIf you're only shipping English, you could write the naive version yourself in an afternoon. The moment you add a second language, the assumptions break.\n\nSpanish opens a question *and* closes it — `¿Cuál es tu nombre?`\n\n— so you can't just append to the end, you have to prepend too. French typography puts a space before `?`\n\n, `!`\n\n, and `:`\n\n— `Comment vas-tu ?`\n\n— and text that omits it looks wrong to a French reader in the way `Hello ,world`\n\nlooks wrong to an English one. Japanese uses `。`\n\nand `？`\n\n, not the ASCII marks.\n\n```\ncorrectSentence('cuál es tu nombre', 'es')     // '¿Cuál es tu nombre?'\ncorrectSentence('comment vas-tu', 'fr')        // 'Comment vas-tu ?'\ncorrectSentence('kannst du mir helfen', 'de')  // 'Kannst du mir helfen?'\ncorrectSentence('すごい', 'ja')                  // 'すごい！'\ncorrectSentence('qual é o seu nome', 'pt')     // 'Qual é o seu nome?'\n```\n\nSix languages currently: English, Japanese, German, Spanish, French, Portuguese.\n\nEach language is an ordered array of rules:\n\n```\ntype SentenceTypeDetectExpressionSets = {\n    expression: RegExp\n    type: 'exclamatory' | 'interrogative' | 'declarative'\n}[]\n```\n\n`detectSentenceType`\n\nwalks the array top to bottom and returns the type of the **first** rule that matches. Nothing matches, it falls through to declarative.\n\nThis is worth being explicit about, because it's the main thing that makes the library easy to reason about and also the main thing that makes it fragile. First-match means a broad interrogative pattern placed above a narrow exclamatory one will silently swallow it. The bug is invisible until someone reports one specific sentence coming out wrong.\n\nThe alternative design is to score every match and take the highest confidence. More robust, considerably harder to debug when it misfires. I chose debuggable, and I'm not certain that was the right call.\n\nBecause it's debuggable, the rule sets are a public export rather than a hidden internal:\n\n``` js\nimport { expressionsByLanguage } from 'sentencify'\n\n// Read the actual ordered rules for a language and see which one fires\nconsole.log(expressionsByLanguage.en)\n```\n\nWhen a sentence classifies wrong, you can find the exact rule responsible instead of filing an issue against a black box.\n\nThis is the part that actually matters for production use:\n\n**Deterministic.** Same input, same output, forever. You can write assertions against it.\n\n**Synchronous.** No promise, no warm-up, no cold start. Cheap enough to run on every keystroke in a controlled input, or on every token in a stream.\n\n**Idempotent.** Already-punctuated text passes through untouched. You will not get `Hello world..`\n\nby calling it twice, which matters when the call sits somewhere in a pipeline you don't fully control.\n\n```\ncorrectSentence('Already punctuated.')  // 'Already punctuated.' — unchanged\n```\n\n**Offline.** No network call means no failure mode where your text formatting goes down because a provider had an incident.\n\n**Small.** Zero runtime dependencies, ESM-only, `sideEffects: false`\n\n, under 40 KB unpacked.\n\nIt is not a grammar checker. It won't fix spelling, agreement, or word choice. It won't split a run-on into sentences. It classifies and punctuates, and that's the whole scope.\n\nAnd regex rules will get sentences wrong that a model would get right. Indirect questions are the obvious failure class — `I wonder if you could send the report`\n\nis declarative but reads interrogative to a naive pattern. That's the trade. You give up ceiling accuracy and you get determinism, speed, inspectability, and no bill.\n\nFor a lot of pipelines that's the right trade, because the text was going to be displayed unpunctuated otherwise.\n\nThe pattern I'd suggest is treating this as a **finishing pass**, not a replacement for anything:\n\nIt runs after the expensive thing has already happened, and it costs nothing.\n\nWhich makes the fix to the example this article opened with a one-line change:\n\n``` js\nimport { correctSentence } from 'sentencify'\n\nrecognition.onresult = (event) => {\n    const transcript = event.results[0][0].transcript\n    const clean = correctSentence(transcript, recognition.lang)\n    // 'Can you send me the report?'\n}\n```\n\nNote that `recognition.lang`\n\nis a full locale — `'en-US'`\n\n, `'fr-FR'`\n\n, `'ja-JP'`\n\n— and gets passed straight through. Both `correctSentence`\n\nand `isPunctuationAvailable`\n\nmatch on the first two characters, so locale variants resolve to the base language and you don't have to slice it yourself. Unsupported languages still get capitalized, they just skip punctuation.\n\nIn TypeScript the `language`\n\nparameter is typed as a narrow union of the six supported codes, so a raw locale string needs a guard or a cast — `isPunctuationAvailable(recognition.lang)`\n\nis there for exactly that check.\n\n```\nnpm install sentencify\n```\n\nIt's new, and the English rules have had the most attention. If you throw a sentence at it that classifies wrong, [open an issue](https://github.com/SheikhAminul/sentencify/issues) — misclassified sentences are the single most useful thing anyone can send me right now.\n\nAdding a language is one file: the ordered rule array plus that language's punctuation conventions, no core changes. Arabic (`؟`\n\n), Hindi (`।`\n\n), and Greek (`;`\n\n) would all be interesting, since in each of them the question mark isn't `?`\n\n.", "url": "https://wpnews.pro/news/how-to-fix-punctuation-in-speech-to-text-and-llm-output-without-calling-a-model", "canonical_source": "https://dev.to/sheikaminul/how-to-fix-punctuation-in-speech-to-text-and-llm-output-without-calling-a-model-3ofl", "published_at": "2026-08-22 07:49:25+00:00", "updated_at": "2026-08-22 08:14:02.238353+00:00", "lang": "en", "topics": ["developer-tools", "natural-language-processing"], "entities": ["sentencify", "Web Speech API"], "alternates": {"html": "https://wpnews.pro/news/how-to-fix-punctuation-in-speech-to-text-and-llm-output-without-calling-a-model", "markdown": "https://wpnews.pro/news/how-to-fix-punctuation-in-speech-to-text-and-llm-output-without-calling-a-model.md", "text": "https://wpnews.pro/news/how-to-fix-punctuation-in-speech-to-text-and-llm-output-without-calling-a-model.txt", "jsonld": "https://wpnews.pro/news/how-to-fix-punctuation-in-speech-to-text-and-llm-output-without-calling-a-model.jsonld"}}