Skip to content

feat(web): OpenAI-compatible STT endpoint for self-hosted transcription (whisper.cpp, faster-whisper) - #3523

Open
prosomoalexd wants to merge 4 commits into
CapSoftware:mainfrom
prosomoalexd:feat/openai-compatible-stt
Open

prosomoalexd wants to merge 4 commits into
CapSoftware:mainfrom
prosomoalexd:feat/openai-compatible-stt

Conversation

@prosomoalexd

@prosomoalexd prosomoalexd commented Oct 28, 2026 •

Copy link
Copy Markdown

Picks up #2397 by @kovashikawa, which was closed in favour of #2771 and then approved pending an update to main. This is that work rebased onto main, plus the fixes below. The author's later commit 54dcc0b (whisper.cpp --vad timeline re-anchoring) came after the reviewed head 8763a86, so it's new for review here.

Self-hosters can transcribe recorded videos with their own speech-to-text server instead of AssemblyAI:

STT_BASE_URL=http://whisper:9000/v1
STT_MODEL=large-v3-turbo     # optional, defaults to whisper-1
STT_API_KEY=                 # optional
  • With STT_BASE_URL set, recorded-video transcription and editable-transcript backfills use <STT_BASE_URL>/audio/transcriptions (verbose_json, word timestamps). Otherwise AssemblyAI is used as today.
  • isTranscriptionConfigured() replaces the ASSEMBLY_API_KEY checks, so an STT endpoint alone is enough. Live transcription of instant recordings stays AssemblyAI-only.
  • Changes on top of feat(web): OpenAI-compatible STT endpoint for self-hosted transcription (whisper.cpp, faster-whisper) #2397: AssemblyAI goes through createAssemblyAIClient() (keeps ASSEMBLY_BASE_URL from feat(self-hosting): add ASSEMBLY_BASE_URL for EU transcription #2998); STT_* passed through both compose files (empty values count as unset); a response body cut off after the headers is now retried instead of reported as "did not return JSON"; with AssemblyAI only, the backfill's "no spoken audio" case is no longer retried (it already ended in the same backfill error).
  • Docs: "Local Transcription" under AI Features, with whisper.cpp and speaches (as a Compose service) examples. Known limit: with --vad, words right after a long pause can still land early (about 7.5 s early after 8 s of silence in our test).

Coordination: #3520 adds ASSEMBLY_API_KEY guards in four places. Whichever merges second switches them to isTranscriptionConfigured(); STT-only tests here for those four entry points fail on a merge of the two as they stand.

Closes #1996. Refs #2161.

Tests

  • 48 new tests: feat(web): OpenAI-compatible STT endpoint for self-hosted transcription (whisper.cpp, faster-whisper) #2397's stt.test.ts and whisper.cpp fixture, plus STT-only cases for every entry point, retry classification (including a real loopback reset and timeout mid-body), and compose's empty values. The STT tests fail on main; the 7 regression controls pass on both.
  • Full apps/web suite on Node 24: main 4771 passed / 1 failed, this branch 4819 passed / 1 failed; the same homepage-seo test fails on both. bun run typecheck and biome ci --linter-enabled=false clean.
  • Against a local whisper-server (base.en, --convert --vad): mp3 and concatenated segment audio transcribe correctly; silence returns no words.

RetriggerView in GreptileConfidence Score: 4/5

The PR appears safe to merge, with a non-blocking improvement needed to reject unexpected JSON responses.

Findings

  1. P2 Wrong responses look like silence ▶
Fix with agent prompt
### Issue 1
apps/web/lib/stt.ts:236-241
`parseOpenAICompatibleTranscription` accepts objects without any transcript fields. If a misrouted endpoint returns `{"status":"ok"}`, the parser produces empty words. The workflow then marks the video `NO_AUDIO` and reports success, hiding the configuration error.

Require a recognized transcript shape before accepting an empty result, while keeping valid silent responses supported.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds an OpenAI-compatible speech-to-text provider for recorded videos and editable-transcript backfills.

  • Recorded videos can use a self-hosted speech-to-text server.
  • Transcription queues and transcript controls recognize local servers.
  • Self-hosters can configure the STT server and model in Compose.

Reviews (1) · Last reviewed commit: "docs(self-hosting): document local trans..." · Reviewed by Greptile

prosomoalexd and others added 3 commits October 28, 2026 13:04
…ndpoint

Adds STT_BASE_URL, STT_MODEL and STT_API_KEY. When STT_BASE_URL is set,
recorded-video transcription and editable-transcript backfills post the
extracted audio to <STT_BASE_URL>/audio/transcriptions (verbose_json with
word timestamps) instead of AssemblyAI, so self-hosters can use whisper.cpp
whisper-server, faster-whisper-server or speaches. The words go through the
existing createEditTranscript pipeline, so one pass still produces both the
caption VTT and the editable word transcript.

isTranscriptionConfigured() now decides whether transcription is available
in lib/transcribe, the transcription workflows, get-status, the share page
and finalize-desktop-recording, so an STT endpoint alone is enough. Live
transcription stays AssemblyAI-only.

5xx/408/429 responses, connection failures and a response body cut off
after the headers are retried; other errors, including a body that isn't
JSON, are fatal.

Co-authored-by: rafael kovashikawa <kovashikawaa@gmail.com>
Co-authored-by: rafael kovashikawa <kovashikawaa@gmail.com>
… speaches

Co-authored-by: rafael kovashikawa <kovashikawaa@gmail.com>
@vercel

vercel Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

@prosomoalexd is attempting to deploy a commit to the Cap Software Inc Team on Vercel.

A member of the Team first needs to authorize it.

Comment thread apps/web/lib/stt.ts
Comment on lines +236 to +241
const text = typeof body.text === "string" ? body.text.trim() : "";
if (words.length === 0 && text.length > 0) {
throw new FatalError(
"STT response has text but no word timestamps; the endpoint must support response_format=verbose_json with timestamp_granularities[]=word",
);
}

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.

P2 Wrong responses look like silence

parseOpenAICompatibleTranscription accepts objects without any transcript fields. If a misrouted endpoint returns {"status":"ok"}, the parser produces empty words. The workflow then marks the video NO_AUDIO and reports success, hiding the configuration error.

Require a recognized transcript shape before accepting an empty result, while keeping valid silent responses supported.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/lib/stt.ts
Line: 236-241

Comment:
**Wrong responses look like silence**

`parseOpenAICompatibleTranscription` accepts objects without any transcript fields. If a misrouted endpoint returns `{"status":"ok"}`, the parser produces empty words. The workflow then marks the video `NO_AUDIO` and reports success, hiding the configuration error.

Require a recognized transcript shape before accepting an empty result, while keeping valid silent responses supported.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e580f3b: a JSON object with no text, words or segments is now a fatal configuration error instead of silence; {"text":"","segments":[]} still maps to NO_AUDIO.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support custom base URLs for self-hosted AI providers (vLLM, self-hosted Whisper, etc.)

1 participant