Skip to content

Chat input dictation (speech-to-text)#326044

Merged
meganrogge merged 24 commits into
mainfrom
megrogge/chat-input-stt-azure
Jul 16, 2026
Merged

Chat input dictation (speech-to-text)#326044
meganrogge merged 24 commits into
mainfrom
megrogge/chat-input-stt-azure

Conversation

@meganrogge

@meganrogge meganrogge commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Fixes #317172
Fixes #315508

demo.mov

Chat input dictation (speech-to-text)

Adds a microphone button to the chat input that dictates speech into the prompt, with the transcript rendered live as you speak.

Approach: on-device, mirroring the GitHub Copilot app

This mirrors what the GitHub Copilot desktop app does for its dictation feature: speech is captured and transcribed fully on-device, with local transcription-model management — no backend, no API keys, no audio ever leaving the machine. (Cloud is reserved for conversational Voice mode, which is a separate feature.)

  • A Whisper model (ONNX) is downloaded on first use and cached on disk, then runs locally for all subsequent dictation.
  • Inference runs in a utility process using @huggingface/transformers (transformers.js) with onnxruntime-node, so the renderer stays responsive.
  • The renderer captures PCM16 16kHz audio and streams it to the utility process over VS Code IPC; interim and final transcripts flow back onto a cumulative-transcript surface that drives the live insertion into the editor.

User experience

  • Mic button in the chat input (main window and the Agents/Sessions window) toggles dictation: click to start, click again to stop and insert. Also bound to Cmd/Ctrl+I while the chat input is focused.
  • Escape cancels an in-progress dictation, discarding what was recorded.
  • Live transcript is inserted into the input as you speak.
  • Model download progress is surfaced via a background notification on first use.
  • Accessibility signals play when recording starts and stops (reusing the existing voice-recording cues).
  • Shares the microphone device selected for Voice mode.

Settings

  • chat.speechToText.enabled — kill-switch for the feature (default true). The mic button appears when enabled on a platform with on-device support.
  • chat.speechToText.model — Whisper model size (tiny / base / small); larger is more accurate but slower and a larger download. Defaults to base.

Packaging

  • onnxruntime-node/bin is unpacked from node_modules.asar (its N-API addon dlopens sibling native libraries).
  • Non-target-arch onnxruntime binaries are stripped from the package to save ~170MB.

Platform support

On-device inference requires a native onnxruntime binary, so dictation is available on darwin/arm64, linux/{x64,arm64}, and win32/{x64,arm64}. Where on-device inference is unavailable (e.g. web, or arches without a prebuilt binary), the mic button is hidden.

Notes

  • The earlier prototype used an external WebSocket/Azure transcription backend. That transport (and the chat.speechToText.serverUrl setting / product endpoint) has been fully removed in favor of the on-device path.

meganrogge and others added 8 commits July 15, 2026 13:50
Adds a microphone button to the chat input toolbar that records audio
and transcribes it into the input using an Azure OpenAI transcription
deployment (e.g. gpt-4o-mini-transcribe). Client-only: captures mic
audio with MediaRecorder and POSTs it directly to the
audio/transcriptions endpoint; no backend required.

- IChatSpeechToTextService: getUserMedia + MediaRecorder capture,
  webm upload to {endpoint}/openai/deployments/{deployment}/audio/transcriptions
- Toggle action in MenuId.ChatExecute; inserts transcript at the cursor
- chat.speechToText.azure.{endpoint,deployment,apiKey,apiVersion} settings;
  button appears only once endpoint + apiKey are configured
- chatSpeechToTextRecording context key drives the record/stop icon

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Ctrl/Cmd+Alt+V while focused in the chat input records for as long as
the key is held (via keybinding hold mode) and transcribes on release.
Releases under 500ms are discarded as accidental taps.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Rewire the client from calling Azure OpenAI directly to POSTing audio to a
transcription backend (chat.speechToText.serverUrl / product.chatSpeechToTextUrl)
with the user's GitHub bearer token, matching the voiceClientService pattern.
Add a Python FastAPI reference backend under chat-stt-backend/ that validates
the GitHub token and forwards audio to Azure OpenAI, hiding the Azure key.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Resolve the auth provider id from IDefaultAccountService (correct for GitHub
Enterprise) instead of hardcoding 'github', and move the product default URL
under IDefaultChatAgent.speechToTextUrl to group it with the other Copilot
service URLs (entitlementUrl, tokenEntitlementUrl, ...), matching how
chatEntitlementService and defaultAccount reach the backend with the user's
GitHub bearer token. Raw fetch + FormData is retained since the audio upload
is multipart (IRequestService is JSON-only).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
The VS Code renderer issues a cross-origin request with an Authorization
header, triggering a CORS preflight the backend previously rejected (OPTIONS
-> 405, surfacing as 'Failed to fetch'). Add CORSMiddleware and a .gitignore
so local .env/venv are never committed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
- Add a speech-to-text mic button to the agents/sessions welcome composer,
  placed before the voice controls; inserts the transcript into the composer.
- Strip filler words (um/uh/er/...) and collapse whitespace from transcripts.
- Order the chat-input dictation button before the voice-mode button.
- Untrack the committed __pycache__ .pyc build artifact.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Replace the record-then-upload flow with a realtime bridge so the transcript
appears while the user speaks instead of only after they stop.

- Backend: add a /transcribe/stream WebSocket endpoint that proxies the client
  to the Azure OpenAI realtime transcription API (server_vad segmentation);
  relays audio appends up and delta/segment transcripts down.
- Client: stream PCM16 (16kHz mono) over a WebSocket; expose
  onDidUpdateTranscript with the growing cumulative transcript.
- Add dictationSession helper that renders the live transcript into the target
  editor (replacing its own region on each update); shared by the toggle,
  hold-to-talk, and sessions composer entry points.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
- Move the Cmd+Shift+Space chord from hold-to-talk to the toggle action
  (press to start/stop, matching Voice Mode); gate it to only claim the chord
  when Voice Mode is not enabled so the two bindings are unambiguous.
- Resume the AudioContext after the async mic/socket handshake so
  onaudioprocess actually fires and audio streams to the backend.
- Backend: only commit the input buffer when audio is pending (server_vad may
  have already committed), avoiding input_audio_buffer_commit_empty; add light
  diagnostic logging.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Copilot AI review requested due to automatic review settings July 15, 2026 20:14
@meganrogge meganrogge self-assigned this Jul 15, 2026
@meganrogge meganrogge added this to the 1.130.0 milestone Jul 15, 2026
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Base: bba82250 Current: bd026096

No screenshot changes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds live speech-to-text dictation to chat inputs, including microphone capture, streaming transcription, editor insertion, UI actions, and a reference backend.

Changes:

  • Adds the dictation service, session lifecycle, commands, and configuration.
  • Integrates dictation into workbench and Agents window chat inputs.
  • Adds a local FastAPI/Azure reference backend.
Show a summary per file
File Description
src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts Adds recording state context.
src/vs/workbench/contrib/chat/browser/speechToText/dictationSession.ts Inserts live transcripts.
src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts Captures and streams audio.
src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts Registers configuration and service.
src/vs/workbench/contrib/chat/browser/actions/chatSpeechToTextActions.ts Adds dictation actions and shortcuts.
src/vs/sessions/contrib/chat/browser/newChatInput.ts Adds Agents composer control.
src/vs/sessions/contrib/chat/browser/media/chatInput.css Styles the microphone control.
src/vs/base/common/product.ts Adds hosted backend product URL.
chat-stt-backend/requirements.txt Declares backend dependencies.
chat-stt-backend/README.md Documents the backend.
chat-stt-backend/main.py Implements the reference backend.
chat-stt-backend/.gitignore Excludes local Python artifacts.
chat-stt-backend/.env.example Documents backend environment values.

Review details

  • Files reviewed: 13/13 changed files
  • Comments generated: 24
  • Review effort level: Medium

Comment thread src/vs/workbench/contrib/chat/browser/actions/chatSpeechToTextActions.ts Outdated
Comment thread src/vs/sessions/contrib/chat/browser/newChatInput.ts Outdated
Comment thread src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts Outdated
Comment thread src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts Outdated
Comment thread chat-stt-backend/main.py Outdated
Comment thread chat-stt-backend/README.md Outdated
Comment thread src/vs/sessions/contrib/chat/browser/newChatInput.ts Outdated
meganrogge and others added 12 commits July 15, 2026 16:33
- Add audio-reactive listening glow to chat inputs while dictating, across
  the main window, sessions ChatView, and new-session composer. Uses
  dictation-specific classes so the Voice Mode mic icon is not highlighted.
- Expose an AnalyserNode from the STT service to drive the glow.
- Prefetch the auth token so the auth frame is sent synchronously on socket
  open, fixing an intermittent "Expected auth frame" rejection.
- Backend: ignore benign input_audio_buffer_commit_empty (server VAD already
  committed) instead of surfacing it as a failed transcription.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
…t endpoint

- Backend now signals `done` and closes promptly after the final transcript
  instead of blocking on the always-open Azure realtime session, so the input
  no longer sits in the transcribing state (stop icon) for ~8s after stop.
- Client resolves the stop wait on `done`; timeout reduced to a 5s safety net.
- Introduce a bound `chatSpeechToTextConfigured` context key set from the
  `chat.speechToText.serverUrl` setting OR the product's `speechToTextUrl`, so
  the dictation button/keybinding appear in prod where the URL ships in
  product.json. Service is eager so gating is correct at startup.
- Expose `isConfigured`; composer gates on it. Keybinding switched to Cmd+I in
  the chat input, outranking the legacy Start Voice Chat binding.
- Clarify the setting as a developer override for the product default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Emit a chatSpeechToText.session telemetry event (outcome, duration,
segments, transcript length, error code) on completion/cancel/error, and
register the chat.speechToText.enabled kill-switch that gates the mic
button alongside a configured backend.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Add the transformers.js dependency (with onnxruntime-node's ABI-stable
napi-v6 prebuilt binaries) to power local, downloaded-model Whisper
transcription for chat dictation — no backend required, matching the
GitHub app's on-device dictation.

transformers.js hard-imports the native `sharp` image library, which is
unused by the audio pipeline and fails to build against Electron; override
it with a pure-JS stub so no libvips native binary is needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Remove the audio-reactive "listening" glow shown while dictating; it is
unnecessary. Deletes dictationGlow.ts and its call sites, and drops the
now-unused analyser tap and service injections.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Repoint ChatSpeechToTextService from the external WebSocket backend to the
new on-device ILocalTranscriptionService (transformers.js + onnxruntime-node
in a utility process) when supported, keeping the cloud WebSocket as a web
fallback. Adds the platform IPC contract, node service, utility entry, and
renderer/web client singletons.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Unpack onnxruntime-node/bin from the node_modules ASAR (its N-API addon
dlopen's sibling shared libraries by on-disk path) and strip non-target
platform/arch binaries so each build only ships the one it needs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Surface ILocalTranscriptionService.onDidChangeModelStatus as a background
progress notification while the on-device Whisper model downloads/loads on
first use, and add the chat.speechToText.model setting (tiny/base/small) to
choose the model.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
The Agents/Sessions window is a separate workbench instance with its own DI
container; it registered services individually and lacked
ILocalTranscriptionService, so the eager ChatSpeechToTextService failed to
construct there and the dictation mic button never appeared. Register the
electron-browser client (and web null fallback) in the sessions mains.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
…Escape + a11y

- Force-instantiate the eager ChatSpeechToTextService at startup via a
  workbench contribution so it publishes the `chatSpeechToTextConfigured`
  context key that gates the mic button (registered singletons are created
  lazily, so the key was never set and the button never appeared).
- Remove the legacy WebSocket/cloud transport entirely (serverUrl setting,
  product speechToTextUrl endpoint, auth-token fetching, base64 streaming) in
  favor of the on-device model path only.
- Escape cancels an in-progress dictation.
- Play voiceRecordingStarted/Stopped accessibility signals on start/stop.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
…tt-azure

# Conflicts:
#	build/gulpfile.vscode.ts
@meganrogge meganrogge requested a review from Copilot July 16, 2026 00:20
@meganrogge meganrogge marked this pull request as ready for review July 16, 2026 00:22
@meganrogge meganrogge requested a review from alexr00 as a code owner July 16, 2026 00:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 23/24 changed files
  • Comments generated: 16
  • Review effort level: Medium

Comment thread src/vs/sessions/contrib/chat/browser/newChatInput.ts Outdated
Comment thread src/vs/workbench/contrib/chat/browser/speechToText/dictationSession.ts Outdated
Comment thread src/vs/platform/localTranscription/common/localTranscription.ts
Comment thread src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts Outdated
…n cancel

Show a spinning indicator (in both the standard chat input and the sessions
composer) in place of the mic button while the on-device speech-to-text model
is downloading or loading, driven by a new chatSpeechToTextPreparing context key
and onDidChangePreparingModel event. Also flip the voiceRecordingStopped signal
sound default to on so cancelling dictation is audible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
@meganrogge meganrogge marked this pull request as draft July 16, 2026 00:34
- Drop the model-download notification; the toolbar spinner is the only
  progress affordance (removes the double-spinner). Default the
  chat.speechToText.enabled setting to off for now.
- Fix voiceRecordingStopped signal default (was inherited 'auto' → never
  audible without a screen reader); now 'on' like voiceRecordingStarted.
- Escape now reverts the live transcript so no dictated text is left behind.
- Cancel dictation when the target editor is disposed; clear the active
  session when the service ends it (e.g. model load error).
- Surface model-load and audio-stream (pushAudio) failures instead of
  silently returning an empty transcript; guard capture-setup errors.
- Node service: await model preparation on stop() so first-use downloads
  aren't dropped; drop stale inference results across sessions (generation
  token); bound interim inference to avoid unbounded re-transcription.
- Gate isSupported by packaged OS/arch (onnxruntime-node targets) so the
  mic isn't shown where inference can't run.
- Sessions composer mic: constructor-inject the service, add Enter/Space
  and touch (Gesture) activation, and route Cmd/Ctrl+I to dictation.
- Add chat accessibility-help entries for Dictate and Cancel Dictation.
- Use the base codicon size token for the composer mic; refresh stale
  cloud-fallback docs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
@meganrogge meganrogge marked this pull request as ready for review July 16, 2026 00:55
@meganrogge meganrogge enabled auto-merge (squash) July 16, 2026 00:55

@alexr00 alexr00 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to check: is the new .js file actually needed?

meganrogge and others added 2 commits July 16, 2026 10:00
…Widget

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 02c7340-e851-429b-a94b-e5bae931ca4b
@meganrogge

meganrogge commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

@alexr00 Yes, it's needed.  sharp is a hard dependency of @huggingface/transformers and gets imported eagerly (it's on the ASR path via  utils/image.js ), even though we only use the speech pipeline. It needs the native libvips binary, which fails to build in CI/Electron. Transformers is loaded at runtime via a () in the utility process, and the bundled dist does a top-level require("sharp"), so a bundler external  can't intercept it. So I point sharp at this tiny no-op stub via an npm override, which throws a clear error if any image API is ever called.

@meganrogge meganrogge requested a review from alexr00 July 16, 2026 14:24
@meganrogge meganrogge merged commit 8526450 into main Jul 16, 2026
30 checks passed
@meganrogge meganrogge deleted the megrogge/chat-input-stt-azure branch July 16, 2026 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants