<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Smallest AI</title>
    <description>The latest articles on DEV Community by Smallest AI (@smallestai).</description>
    <link>https://dev.to/smallestai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3854927%2F8dabc078-5e46-402d-ad31-71a95a7a510b.png</url>
      <title>DEV Community: Smallest AI</title>
      <link>https://dev.to/smallestai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smallestai"/>
    <language>en</language>
    <item>
      <title>Voice Assistants Are Designed in the Silence</title>
      <dc:creator>Smallest AI</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:17:29 +0000</pubDate>
      <link>https://dev.to/smallestai/voice-assistants-are-designed-in-the-silence-4f5m</link>
      <guid>https://dev.to/smallestai/voice-assistants-are-designed-in-the-silence-4f5m</guid>
      <description>&lt;p&gt;A voice assistant can be technically correct and still feel broken.&lt;br&gt;
The failure often begins after the user finishes speaking. Nothing dramatic happens. There is simply a pause: long enough for the user to wonder whether the system heard them, but short enough for each service dashboard to report an acceptable result.&lt;br&gt;
That pause is the product.&lt;br&gt;
Users do not experience speech recognition, a language model, a tool call, and speech synthesis as separate services. They experience one conversational turn. If any handoff is late, uncertain, or difficult to cancel, the whole interaction begins to feel like a phone tree rather than a conversation.&lt;br&gt;
The useful question is therefore not, "Which model is fastest?" It is:&lt;br&gt;
How quickly can the system begin a trustworthy, speakable response?&lt;br&gt;
This article develops that idea from the original voice-assistant architecture guide, but focuses on what developers can measure, instrument, and change in a production pipeline.&lt;br&gt;
The guide was published by Smallest.ai, whose work focuses on real-time speech models and voice-agent systems.&lt;br&gt;
Measure one turn, not four services&lt;br&gt;
A typical cascaded assistant looks simple:&lt;br&gt;
microphone&lt;br&gt;
  -&amp;gt; streaming speech-to-text (STT)&lt;br&gt;
  -&amp;gt; turn decision&lt;br&gt;
  -&amp;gt; language model and optional tools&lt;br&gt;
  -&amp;gt; stable text buffer&lt;br&gt;
  -&amp;gt; streaming text-to-speech (TTS)&lt;br&gt;
  -&amp;gt; interruptible playback&lt;br&gt;
The diagram becomes misleading when every arrow is treated as a clean, serial boundary. In a responsive system, useful work overlaps. Audio is transcribed while the user is speaking. The model may begin after the turn is committed but before every transcript artifact is finalized. TTS can start when a stable, speakable clause exists rather than waiting for the complete answer.&lt;br&gt;
Do not compress all of this into a single ambiguous "time to first audio" metric. Record the boundaries separately:&lt;br&gt;
Turn-decision delay: from the user's last speech frame to the point at which the system commits the turn.&lt;br&gt;
STT finalization delay: the time needed to produce a transcript that is safe to send downstream.&lt;br&gt;
LLM time to first token (TTFT): from the model request to its first token.&lt;br&gt;
Time to first speakable chunk: from the model request to the first stable clause that TTS can render safely.&lt;br&gt;
TTS time to first audio: from the synthesis request to the first playable audio chunk.&lt;br&gt;
End-of-turn-to-playback: from the user's last speech frame to audio actually beginning at the client.&lt;br&gt;
The last measurement is the user-facing result. The others explain why it happened.&lt;br&gt;
Turn detection is not the same as VAD&lt;br&gt;
Voice activity detection answers a narrow question: does this audio frame contain speech?&lt;br&gt;
Turn detection answers a harder question: has the speaker finished the thought?&lt;br&gt;
VAD can contribute evidence, but silence alone is not enough. A production turn detector may also use:&lt;br&gt;
finalized and interim transcript timing;&lt;br&gt;
confidence or transcript stability;&lt;br&gt;
punctuation or semantic completeness;&lt;br&gt;
domain-specific patterns, such as a phone number that may continue;&lt;br&gt;
the user's speaking rate and recent pause behavior;&lt;br&gt;
the cost of interrupting versus waiting in the current workflow.&lt;br&gt;
The distinction matters because a fixed silence threshold creates opposite failure modes. Commit too quickly and the assistant cuts off a user who paused mid-sentence. Wait too long and every turn feels hesitant.&lt;br&gt;
Endpointing remains heuristic: background noise can prevent reliable silence detection, while transcript-based gap detection behaves differently and may be more useful for some utterances. The correct threshold is not an industry constant; tune it against real audio from the deployment environment.&lt;br&gt;
STT accuracy is about consequential errors&lt;br&gt;
Word error rate is useful, but it should not be treated as a universal pass/fail score for an assistant.&lt;br&gt;
An incorrect filler word may have no downstream effect. One wrong digit in an account number, a misspelled surname, or a reversed negation may change the entire action. Conventional WER gives equal weight to errors with very different effects on meaning and readability, so it should be paired with entity-level and task-level evaluation.&lt;br&gt;
For a voice agent, evaluate at least three layers:&lt;br&gt;
Transcription quality: WER or another suitable ASR metric.&lt;br&gt;
Entity accuracy: names, dates, amounts, identifiers, addresses, and domain terminology.&lt;br&gt;
Task success: whether the downstream system understood the intent and performed the correct action.&lt;br&gt;
Test with the audio users will actually produce: noisy rooms, phone codecs, weak connections, accents, hesitations, and overlapping speech. A clean studio recording is not a substitute for a deployment test set.&lt;br&gt;
Streaming ASR can reduce emission delay, but it introduces unstable partial hypotheses. Research such as FastEmit demonstrates that latency and recognition quality must be evaluated together; its measured gains belong to the tested models and datasets, not to every streaming implementation.&lt;br&gt;
The first token is not yet a spoken answer&lt;br&gt;
LLM TTFT is important, but it is not the end of the model stage.&lt;br&gt;
Suppose the model emits:&lt;br&gt;
Sure — let me...&lt;br&gt;
The first token arrived, but the assistant still has nothing useful to say. TTS may also need to wait for a stable clause so it does not synthesize an opening that later becomes awkward or incorrect.&lt;br&gt;
A better voice prompt front-loads the answer:&lt;br&gt;
Your appointment is confirmed for Thursday at 3 PM.&lt;br&gt;
I can also send a reminder if you want one.&lt;br&gt;
The first sentence is complete, useful, and independently speakable.&lt;br&gt;
That creates another metric worth tracking: time to first speakable chunk. It includes model TTFT plus the time needed to accumulate a safe synthesis boundary.&lt;br&gt;
A simple clause buffer might look like this:&lt;br&gt;
const boundary = /[.!?]\s$|[,;:]\s$/;&lt;br&gt;
let pending = "";&lt;/p&gt;

&lt;p&gt;function onModelToken(token: string) {&lt;br&gt;
  pending += token;&lt;/p&gt;

&lt;p&gt;const enoughText = pending.trim().length &amp;gt;= 24;&lt;br&gt;
  const hasBoundary = boundary.test(pending);&lt;/p&gt;

&lt;p&gt;if (enoughText &amp;amp;&amp;amp; hasBoundary) {&lt;br&gt;
    tts.enqueue(pending);&lt;br&gt;
    pending = "";&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;function onModelComplete() {&lt;br&gt;
  if (pending.trim()) tts.enqueue(pending);&lt;br&gt;
}&lt;br&gt;
This is intentionally simple. A production buffer should also consider abbreviations, numbers, markup, pronunciation hints, language-specific punctuation, maximum wait time, and whether already-enqueued speech can be cancelled.&lt;br&gt;
Tool latency only hurts when it blocks the critical path&lt;br&gt;
Tool calls are often described as an additive delay: a 300 ms lookup supposedly adds exactly 300 ms to the turn.&lt;br&gt;
That is true only when the lookup sits on the serial critical path.&lt;br&gt;
Some work can be hidden or reduced through:&lt;br&gt;
prefetching likely context after the session starts;&lt;br&gt;
caching results with a clear freshness policy;&lt;br&gt;
running independent tools in parallel;&lt;br&gt;
starting a safe verbal acknowledgement while a slow operation continues;&lt;br&gt;
using deterministic routing when a model decision is unnecessary;&lt;br&gt;
cancelling stale work when the user changes direction.&lt;br&gt;
Do not use filler speech to disguise arbitrary latency. An acknowledgement such as "I'll check the live inventory" is useful only when it communicates real progress and does not make an unsupported promise.&lt;br&gt;
Trace every tool call with its queue time, network time, server time, result size, cache status, retry count, and whether it blocked the first speakable chunk. That final field is more informative than duration alone.&lt;br&gt;
Streaming is controlled overlap&lt;br&gt;
"Stream every stage" sounds attractive but is too absolute.&lt;br&gt;
Partial output is valuable only when it is useful and safely reversible. Some tools return atomic results. Partial transcripts can change. Premature TTS can speak text the model would have revised. Speculation can also increase cost and make cancellation harder.&lt;br&gt;
A better rule is:&lt;br&gt;
Stream where partial output is useful, and overlap work only when errors can be contained or reversed.&lt;br&gt;
Incremental TTS research demonstrates how synthesis can begin from partial text while later segments are still being generated. The prefix-to-prefix TTS study is useful evidence for the architecture, but it does not justify a universal millisecond saving for every model, sentence, device, or network.&lt;br&gt;
The engineering work behind low-latency voice systems extends beyond inference. Media transport, jitter, packet loss, session ownership, routing, WebRTC behavior, and infrastructure placement all affect the pause and belong in the same design discussion.&lt;br&gt;
Build an explicit latency budget&lt;br&gt;
A latency budget is a design constraint, not a chart created after launch. Start with an end-of-turn-to-playback target, then assign provisional limits to the stages on the critical path.&lt;br&gt;
Here is an illustrative, deliberately aggressive 800 ms budget. It is an example for planning, not an industry benchmark:&lt;br&gt;
Stage&lt;br&gt;
Example budget&lt;br&gt;
Turn decision&lt;br&gt;
150 ms&lt;br&gt;
Transcript stabilization&lt;br&gt;
100 ms&lt;br&gt;
First speakable model chunk&lt;br&gt;
300 ms&lt;br&gt;
First playable TTS audio&lt;br&gt;
150 ms&lt;br&gt;
Transport and client buffering&lt;br&gt;
100 ms&lt;br&gt;
Total&lt;br&gt;
800 ms&lt;/p&gt;

&lt;p&gt;Your allocation will change with the product. A hands-free command may prioritize speed. A medical intake flow may tolerate a longer wait to avoid interrupting the speaker. A tool-heavy transaction may need an acknowledgement before the final answer.&lt;br&gt;
Measure distributions rather than averages. At minimum, report p50, p95, and p99 by:&lt;br&gt;
interaction type;&lt;br&gt;
geography and network;&lt;br&gt;
language;&lt;br&gt;
device or telephony provider;&lt;br&gt;
tool-free versus tool-dependent turns;&lt;br&gt;
successful, interrupted, cancelled, and retried turns.&lt;br&gt;
An acceptable p50 can hide a painful p95.&lt;br&gt;
Instrument the complete turn&lt;br&gt;
The following TypeScript example records the event boundaries without tying the implementation to a specific STT, model, or TTS provider:&lt;br&gt;
const marks = new Map();&lt;/p&gt;

&lt;p&gt;function mark(name: string, at = performance.now()) {&lt;br&gt;
  marks.set(name, at);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;function duration(start: string, end: string) {&lt;br&gt;
  const a = marks.get(start);&lt;br&gt;
  const b = marks.get(end);&lt;br&gt;
  return a === undefined || b === undefined ? undefined : b - a;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;function voiceTurnMetrics() {&lt;br&gt;
  return {&lt;br&gt;
    turnDecisionMs: duration("speech_last_frame", "turn_committed"),&lt;br&gt;
    transcriptReadyMs: duration("turn_committed", "stt_final"),&lt;br&gt;
    llmTtftMs: duration("llm_started", "llm_first_token"),&lt;br&gt;
    firstSpeakableMs: duration("llm_started", "first_speakable_chunk"),&lt;br&gt;
    ttsFirstAudioMs: duration("tts_started", "tts_first_audio"),&lt;br&gt;
    clientBufferMs: duration("tts_first_audio", "playback_started"),&lt;br&gt;
    endToEndMs: duration("speech_last_frame", "playback_started"),&lt;br&gt;
  };&lt;br&gt;
}&lt;br&gt;
Call mark() from the actual callbacks in the pipeline:&lt;br&gt;
mark("speech_last_frame", vad.lastSpeechTimestamp());&lt;br&gt;
mark("turn_committed");&lt;br&gt;
mark("stt_final");&lt;br&gt;
mark("llm_started");&lt;br&gt;
mark("llm_first_token");&lt;br&gt;
mark("first_speakable_chunk");&lt;br&gt;
mark("tts_started");&lt;br&gt;
mark("tts_first_audio");&lt;br&gt;
mark("playback_started");&lt;br&gt;
Use one trace ID across the browser or phone gateway, STT, orchestration layer, tools, model, TTS, and playback client. Without cross-service correlation, teams often optimize the service with the most visible dashboard rather than the stage responsible for the user's wait.&lt;br&gt;
If you are comparing a managed stack with a custom pipeline, Smallest.ai's voice-agent platform is one integrated implementation to evaluate. Apply the same event boundaries and p50, p95, and p99 measurements to it—or any other platform—rather than assuming that an integrated stack is automatically low latency.&lt;br&gt;
Barge-in must cancel the old turn&lt;br&gt;
In a full-duplex assistant, inbound audio capture and speech detection normally remain active while the assistant speaks. When new user speech is confirmed, the system should stop treating the old response as current.&lt;br&gt;
The control path may resemble:&lt;br&gt;
async function onUserSpeechStarted() {&lt;br&gt;
  generation.abort();&lt;br&gt;
  await tts.cancel();&lt;br&gt;
  playback.stopAndFlush();&lt;br&gt;
  tools.cancelNonReusableWork();&lt;/p&gt;

&lt;p&gt;conversation.truncateAssistantMessage({&lt;br&gt;
    toPlayedAudioTimestamp: playback.lastPlayedTimestamp(),&lt;br&gt;
  });&lt;br&gt;
}&lt;br&gt;
The exact APIs will differ, but the responsibilities are stable:&lt;br&gt;
cancel model generation;&lt;br&gt;
cancel synthesis;&lt;br&gt;
stop and flush queued playback;&lt;br&gt;
discard tool results that are no longer relevant;&lt;br&gt;
preserve only the portion of the assistant response the user actually heard;&lt;br&gt;
continue processing the incoming speech without clipping its beginning.&lt;br&gt;
Barge-in cannot be added cleanly as a final UI feature. It changes audio capture, state management, model context, tool cancellation, and playback architecture.&lt;br&gt;
Production starts where the demo ends&lt;br&gt;
A convincing demo proves that the happy path can answer. Production requires tests for human behavior and imperfect infrastructure.&lt;br&gt;
Before launch, include:&lt;br&gt;
quiet, noisy, reverberant, and low-bitrate audio;&lt;br&gt;
short commands and long, hesitant utterances;&lt;br&gt;
mid-sentence pauses and self-corrections;&lt;br&gt;
names, phone numbers, dates, amounts, and domain terminology;&lt;br&gt;
tool-free, cached, slow, failed, and retried tool calls;&lt;br&gt;
barge-in during early and late playback;&lt;br&gt;
packet loss, jitter, reconnects, and duplicated events;&lt;br&gt;
multiple languages and code-switching;&lt;br&gt;
long conversations with context compaction;&lt;br&gt;
p95 latency under realistic concurrency.&lt;br&gt;
Also test the failure semantics. What happens if the tool succeeds after the user interrupts? If TTS emits audio after cancellation? If two final transcripts arrive? If the client reconnects while audio is queued? A low-latency system that produces stale actions is not a good system.&lt;br&gt;
When reviewing an implementation, study how it represents processors, transports, interruption, events, cancellation, and session state. Do not copy defaults blindly; validate each design choice against your own latency traces and failure modes.&lt;br&gt;
The pause is the architecture&lt;br&gt;
The fastest model will not rescue a pipeline with slow turn detection, serial tool calls, unstable partial transcripts, or excessive audio buffering.&lt;br&gt;
Measure the complete turn from the user's last speech frame to audible playback. Separate that duration into named stages. Optimize the stage that dominates p95, then test the change against accuracy, cancellation behavior, and task success.&lt;br&gt;
A natural voice assistant is not a collection of fast components. It is one coordinated participant whose timing, state, and failure modes have been designed as a whole.&lt;br&gt;
Ready to test this architecture in a working stack? Open Smallest.ai's self-serve application, build a voice agent, instrument the event boundaries above, and compare its p50 and p95 end-to-end latency with your current pipeline.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>performance</category>
    </item>
    <item>
      <title>Your First Voice AI App Shouldn’t Start With a WebSocket</title>
      <dc:creator>Smallest AI</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:16:46 +0000</pubDate>
      <link>https://dev.to/smallestai/your-first-voice-ai-app-shouldnt-start-with-a-websocket-3ie0</link>
      <guid>https://dev.to/smallestai/your-first-voice-ai-app-shouldnt-start-with-a-websocket-3ie0</guid>
      <description>&lt;p&gt;The first successful voice feature usually looks unimpressive: one audio file goes in, and one useful transcript comes out.&lt;/p&gt;

&lt;p&gt;That is not a toy result. It is the shortest route to the questions that decide whether a voice product will work. Does the transcript preserve names and numbers? Are speaker turns separated? Are timestamps useful downstream? Can the system handle the audio your users will actually produce?&lt;/p&gt;

&lt;p&gt;It is tempting to begin with live microphones, persistent connections, partial transcripts, browser permissions, and animated interfaces. Those pieces feel like voice AI. They also make failures harder to isolate. A safer rule is simple: prove the transcript’s value first, then earn the complexity of streaming.&lt;/p&gt;

&lt;p&gt;Start with the product’s clock&lt;/p&gt;

&lt;p&gt;The important architectural choice is not Python versus another language. It is batch versus real time.&lt;/p&gt;

&lt;p&gt;If you are processing a voicemail, podcast, meeting recording, or archive, the entire file already exists. An HTTP request matches the problem: send the audio, wait for the result, and store the transcript. There is no benefit in pretending a completed file is a live conversation.&lt;/p&gt;

&lt;p&gt;A voice assistant, live-captioning tool, or call workflow runs on a different clock. It needs partial results while the speaker is talking. The system must receive small audio frames, interpret interim hypotheses, decide when an utterance is final, and recover when the connection drops. That is where a WebSocket belongs.&lt;/p&gt;

&lt;p&gt;Smallest.ai publishes a roughly 64 ms time-to-first-transcript figure for Pulse streaming. Treat that as a provider specification, not a guarantee for your application: network path, audio framing, region, load, and transcript-stability requirements all affect what users experience. The Pulse speech-to-text product page is the current product reference, while your own p50 and p95 traces should make the architecture decision.&lt;/p&gt;

&lt;p&gt;The smallest useful Python proof&lt;/p&gt;

&lt;p&gt;A batch prototype should stay intentionally boring. Put the API key in an environment variable, read a representative audio file, request only the metadata you need, and inspect the returned JSON.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.smallest.ai/waves/v1/stt/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pulse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;word_timestamps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;diarize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SMALLEST_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/octet-stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meeting.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transcription&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;words&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This uses the current unified endpoint documented in the pre-recorded STT API reference. In production, also handle timeouts, retries, rate limits, response-schema changes, and request identifiers. Do not log the authorization header or raw audio by default.&lt;/p&gt;

&lt;p&gt;That short loop gives you a useful evaluation surface. Test several accents and speaking styles. Use clean and noisy recordings. Include telephony audio if production will receive it. Check the JSON your next component must consume, not only the sentence printed to the terminal.&lt;/p&gt;

&lt;p&gt;Measure the transcript you need, not the demo you want&lt;/p&gt;

&lt;p&gt;A single successful file proves connectivity. A small evaluation set begins to prove usefulness.&lt;/p&gt;

&lt;p&gt;Use audio captured from the microphones, codecs, channels, and environments your users will have.&lt;/p&gt;

&lt;p&gt;Record failures involving names, numbers, abbreviations, product terms, and code-switching—not only average text quality.&lt;/p&gt;

&lt;p&gt;Inspect speaker labels and timestamp drift if downstream features depend on them.&lt;/p&gt;

&lt;p&gt;Track request latency at p50 and p95; one fast example says little about production behavior.&lt;/p&gt;

&lt;p&gt;Define what happens when the API times out, rejects a file, or returns an empty transcript.&lt;/p&gt;

&lt;p&gt;This stage also tells you whether streaming is necessary. If users upload recordings and return later, a persistent connection may add operational cost without improving the product. If the transcript drives a live response, batch processing will eventually reveal its limit.&lt;/p&gt;

&lt;p&gt;Streaming is not a faster POST request&lt;/p&gt;

&lt;p&gt;Moving to streaming changes the application, not merely the transport. A live client sends raw audio frames and receives partial and final transcript messages while the connection remains open. Sending and receiving must happen concurrently.&lt;/p&gt;

&lt;p&gt;Batch waits for the complete recording; streaming turns a moving signal into usable partial results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlencode&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websockets&lt;/span&gt;

&lt;span class="n"&gt;CHUNK_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;

&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encoding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;linear16&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;16000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;word_timestamps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://api.smallest.ai/waves/v1/stt/live?model=pulse&amp;amp;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;urlencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SMALLEST_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CHUNK_SIZE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;close_stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_transcripts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;final&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_final&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;partial&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transcript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_last&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transcribe_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;websockets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;additional_headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;send_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;receiver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;receive_transcripts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receiver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;transcribe_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio.pcm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current real-time WebSocket documentation recommends 4096-byte chunks and a &lt;code&gt;close_stream&lt;/code&gt; control message when a one-off stream is complete. The sample above expects headerless, 16-bit linear PCM at 16 kHz mono. A WAV file normally includes a container header, so do not rename a WAV file to &lt;code&gt;.pcm&lt;/code&gt; and assume the bytes are equivalent.&lt;/p&gt;

&lt;p&gt;The concurrency model matters more than the socket&lt;/p&gt;

&lt;p&gt;A production microphone flow should separate audio capture, network sending, transcript receiving, and UI updates. If one task blocks the others, buffers grow and the interface feels stale even when recognition is fast.&lt;/p&gt;

&lt;p&gt;Audio producer: reads fixed-size frames and applies bounded buffering.&lt;/p&gt;

&lt;p&gt;Network sender: writes frames, handles backpressure, and stops cleanly.&lt;/p&gt;

&lt;p&gt;Transcript receiver: distinguishes revisable partial text from final text.&lt;/p&gt;

&lt;p&gt;Application state: stores only stable transcript segments and exposes connection status.&lt;/p&gt;

&lt;p&gt;Recovery path: reconnects deliberately and prevents duplicate or out-of-order segments.&lt;/p&gt;

&lt;p&gt;Partial transcripts are a preview, not an append-only log. Replace the current partial segment when a new hypothesis arrives; persist it only when the server marks it final. Otherwise, readers see duplicated words and downstream systems process text that the recognizer later corrected.&lt;/p&gt;

&lt;p&gt;Keep the browser simple—and the key off it&lt;/p&gt;

&lt;p&gt;A browser can capture microphone audio and render transcript updates, but it should not receive a long-lived service API key. Put the authenticated upstream connection behind your server. The browser connects to your application, and your server authorizes the user, opens the provider connection, applies limits, and forwards only the events the interface needs.&lt;/p&gt;

&lt;p&gt;That boundary gives you a place to enforce session duration, input format, concurrency, logging policy, and abuse controls. It also creates a clean progression: file uploads can use a normal server route, while microphone audio uses a streaming proxy. Batch and live transcription remain two deliberate modes instead of one over-engineered pipeline.&lt;/p&gt;

&lt;p&gt;A transcript is rarely the final product&lt;/p&gt;

&lt;p&gt;Raw text is enough for a connectivity demo. Production workflows usually need structure. Word timestamps support captions and review tools. Speaker diarization separates participants. Redaction can reduce sensitive information passed downstream. Each feature should exist because a downstream workflow needs it, not because a parameter is available.&lt;/p&gt;

&lt;p&gt;If your product depends on speaker separation, read the speaker diarization implementation guide before choosing a transcript schema. For conversational systems, the STT–LLM–TTS latency-budget guide explains why a fast recognition stage does not guarantee a fast audible response.&lt;/p&gt;

&lt;p&gt;Know when the prototype has earned real time&lt;/p&gt;

&lt;p&gt;Move from batch to streaming when at least one requirement cannot be met by waiting for a complete file:&lt;/p&gt;

&lt;p&gt;The user needs words on screen while speaking.&lt;/p&gt;

&lt;p&gt;A conversational system must begin reasoning before the entire recording exists.&lt;/p&gt;

&lt;p&gt;Turn-taking, interruption, or live routing depends on partial results.&lt;/p&gt;

&lt;p&gt;The recording is long enough that upload-then-process creates unacceptable delay.&lt;/p&gt;

&lt;p&gt;When you cross that boundary, measure the complete path: audio capture, frame queueing, network transit, first usable partial, finalization, downstream processing, and—if the system speaks—first audible response. Optimizing only the model’s headline latency can hide the stage users are actually waiting on.&lt;/p&gt;

&lt;p&gt;Build outward from one trustworthy transcript&lt;/p&gt;

&lt;p&gt;A voice product does not become serious when it opens a WebSocket. It becomes serious when every added layer solves a problem the simpler version exposed.&lt;/p&gt;

&lt;p&gt;Start with representative audio. Produce a transcript. Verify the structure and failure modes. Add streaming only when the user’s clock demands it. Then keep the API key behind your server and measure the whole path under realistic load.&lt;/p&gt;

&lt;p&gt;If you want to test the same batch-to-streaming path with Smallest.ai, open the self-serve application and create an API key—no demo booking required. The complete Python implementation guide provides the broader walkthrough. What requirement in your product genuinely forces the architecture to become real time?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>software</category>
    </item>
  </channel>
</rss>
