Skip to main content

Search

Search problems, hooks, and pages

Back to About

Speak Out Loud

built with: Web Speech API

Live speech-to-text for rehearsing answers, built on a browser API that TypeScript does not ship types for.

Typing an API the DOM lib forgot

`SpeechRecognition` is not in TypeScript's DOM library — it is still prefixed in most shipping browsers and never stabilised into the standard typings. Using it means either casting to `any` everywhere or writing the types once.

This app writes them once, in a shared module, covering only the surface actually used: the constructor, the three config flags, start/stop, and the three event handlers. Everything is named `*Like` as a reminder that these are hand-written approximations of a moving spec, not official types.

// src/lib/speech-recognition.ts
export interface SpeechRecognitionLike {
  continuous: boolean;
  interimResults: boolean;
  lang: string;
  start: () => void;
  stop: () => void;
  onresult: ((event: SpeechRecognitionEventLike) => void) | null;
  onend: (() => void) | null;
  onerror: ((event: SpeechRecognitionErrorEventLike) => void) | null;
}

export function getSpeechRecognition(): SpeechRecognitionConstructor | undefined {
  const w = window as unknown as {
    SpeechRecognition?: SpeechRecognitionConstructor;
    webkitSpeechRecognition?: SpeechRecognitionConstructor;
  };
  return w.SpeechRecognition || w.webkitSpeechRecognition;
}

Interim vs final results

With `interimResults` on, the API emits a running best guess that keeps getting revised, then marks a result `isFinal` once it settles. Rendering every event naively makes text flicker and duplicate.

The handler therefore reads from `event.resultIndex` forward and keeps final and interim text separate — finalised text is appended to the committed transcript, while the interim tail is rendered as provisional and replaced on the next event.

The same typed helper backs two features: Speak Up's rehearsal view and Mock Interview's live answer capture.

Important

Gotcha

The API needs a real microphone permission prompt and a genuine network path. Testing inside an embedded webview — VS Code’s Simple Browser, for instance — silently fails in ways that look exactly like a bug in the code.

Where it lives in the repo

  • src/lib/speech-recognition.ts
  • src/components/speak-up/speech-practice.tsx
  • src/components/mock-interview/chat/use-speech-input.ts