Back to Projects
Whisker
CompletedChromeJavaScript

Whisker

Whisper-powered speech-to-text input Chrome extension

Timeline

Nov 2025

Role

Solo

Team

Solo

Status
Completed

Technology Stack

Chrome
JavaScript

Key Challenges

  • Injecting a floating mic overlay into arbitrary page DOMs without breaking host-page styling or colliding with framework-controlled elements
  • Positioning the mic control with scroll/resize-aware geometry so it always tracks its host field across reflows and viewport changes
  • Reliably batching MediaRecorder chunks into a single webm blob and streaming it to Whisper while handling denied permissions, concurrent recordings, and missing API keys
  • Keeping the OpenAI API key isolated from page scripts even though the request itself is sent from the content script — the page DOM must never see the credential

Key Learnings

  • A direct content-script → OpenAI pipeline eliminates the need for a back-end entirely, cutting latency and the surface area for leaks
  • chrome.storage.sync (with chrome.storage.local as a fallback) is the right home for credentials in MV3 — it is isolated from the page context by design
  • Sharing one compiled Tailwind+DaisyUI bundle between the popup and the in-page modal keeps the design system consistent with zero style duplication
  • Releasing the MediaRecorder stream the instant recording stops is non-negotiable for trust — the browser mic indicator must turn off immediately when the user taps stop

The Problem

I type a lot. And half the time, my hands are doing something else — holding a coffee, holding a baby, holding a phone on a call. The OS-level dictation tools are good, but they live in a separate app or system surface. What I actually wanted was a small mic icon that just appears next to whatever field I was already typing into, on any website, and dumps clean text right where my cursor was.

Building that sounds easy. It is not. The mic overlay has to inject into third-party DOMs without breaking their CSS, follow the field around as the page scrolls and resizes, capture audio reliably across MediaRecorder quirks, and — most importantly — keep my OpenAI key completely isolated from the page's JavaScript context. A browser extension that leaks credentials into the host page is worse than no extension at all.

What I Built

Whisker is a Manifest V3 Chrome extension that adds a floating mic button beside any focused text field — regular <input>, <textarea>, and most rich-text editors. Tap it, speak, and the transcript appears at the cursor. The whole thing is a single content script and a popup; there is no back-end, no server, no proxy. Audio is captured with MediaRecorder, batched into a webm blob, and sent directly to OpenAI's Whisper transcription endpoint. The result is dropped back at the caret position.

It is not on the Chrome Web Store yet — installation is manual (chrome://extensionsLoad unpacked) — but the extension itself is fully functional and supports 50+ languages out of the box, handles punctuation and capitalization automatically, and runs with a setup modal that appears the first time you trigger a recording without a saved key.

How It Works

The architecture is deliberately thin:

Focused field (any webpage)
    │
    ├── content.js listens for focusin
    │       └── injects mic button, positions via scroll/resize-aware geometry
    │
    ├── user taps mic → MediaRecorder captures audio
    │       └── chunks batched into a single webm blob
    │
    ├── multipart POST → https://api.openai.com/v1/audio/transcriptions
    │       └── key read from chrome.storage (sync, with local fallback)
    │
    └── transcript inserted at the caret position

The content script is the entire feature surface. It listens for focusin events, validates that the target is actually editable, and shows a small floating control positioned relative to the field's bounding box. When the user records, audio is captured with the MediaRecorder API, chunked, and combined into one webm blob before being POSTed to Whisper. The transcript lands where the user was typing — no copy-pasting, no separate app to switch to.

The popup is just key management. It uses chrome.storage.sync so the key follows the user across signed-in devices, with chrome.storage.local as a fallback. There is a reveal/hide toggle for the key, inline error states, and copy that never touches the network.

Key Design Decisions

Zero back-end, by design

The naive design is a proxy server that holds the key and forwards audio. I rejected that on day one. A proxy means a server to host, secrets to rotate, logs to scrub, and a network hop on every recording. The content script can talk to OpenAI directly with the key in chrome.storage, the request never leaves the user's machine except for the audio itself, and there is no third party in the loop. The trade-off is that the key technically has to be in the extension (so it can make authenticated requests), but it lives in storage that is unreachable from page scripts — that is the security boundary I leaned on.

One shared bundle, two surfaces

The popup and the in-page onboarding modal look the same on purpose. Both consume the same compiled output.css — Tailwind utilities plus a custom DaisyUI theme. I regenerate the CSS locally and ship the bundle in the repo (the Tailwind/DaisyUI bundler files are intentionally excluded to keep the repo lean). The result is that the "missing key" modal feels like part of the extension, not a foreign widget bolted onto a page.

Source-Available License

Whisker ships under the Whisker Source-Available License v1.0. It is not open source in the OSI sense, but commercial redistribution is explicitly allowed as long as recipients get it for free and attribution is preserved. For a single-developer utility that runs entirely client-side, that felt like the right balance.

Security & Privacy

This is the part I cared about most, and it is the part most browser-extension tutorials get wrong.

  • Keys live in chrome.storage and never enter page context. The content script reads the key at request time; the host page's JavaScript cannot. That is the only reason this design is safe.
  • No proxy server, no third parties. Requests go directly from the content script to api.openai.com/v1/audio/transcriptions. There is no analytics endpoint, no telemetry, no error reporter — nothing phones home.
  • Microphone released immediately. The MediaRecorder stream is stopped and the tracks are released the moment recording ends. The browser's mic indicator turns off right away, not "eventually."
  • Errors are actionable, not leaky. Failures surface a short message ("permission denied", "no key configured", "network error") — never a stack trace, never a request body, never the key.

What's Next

The roadmap I have in the README:

  • A global hotkey so Whisker works without needing field focus
  • Streaming transcription so dictated text appears while audio is still uploading
  • Optional self-hosted Whisper endpoint configuration for users running local inference
  • A Firefox-compatible build pipeline alongside the current Chromium packaging

If you want to try it today, load the repo as an unpacked extension, set your OpenAI key in the popup, and focus any text field on any page. The mic button should be right there.

Links

Timeline

Built in a single focused session. The whole extension — content script, popup, and bundled UI — was designed, implemented, and pushed in one sitting.

  • Nov 2025 — Built in a focused session: Manifest V3 extension with content-script overlay, MediaRecorder → Whisper pipeline, popup key management, Tailwind+DaisyUI UI

Design & Developed by Sudip Ghosh
© 2026. All rights reserved.