feat(transcribe): add BW Labs STT as optional provider alongside ElevenLabs - #152
Open
isweluiz wants to merge 6 commits into
Open
feat(transcribe): add BW Labs STT as optional provider alongside ElevenLabs#152isweluiz wants to merge 6 commits into
isweluiz wants to merge 6 commits into
Conversation
…enLabs ElevenLabs Scribe remains the default — zero breaking changes for existing users. BW Labs STT (https://labs.bandwidth.com/) is now available as a second backend via --provider bw_stt, TRANSCRIBE_PROVIDER env var, or auto-detection from BW_STT_API_KEY. Uses the BW Labs STT WebSocket API directly (wss://api.labs.bandwidth.com/ audio/v1/listen) — no proprietary SDK required. Only dependency is websockets (pip install websockets), which is on PyPI. Key differences vs ElevenLabs: - WebSocket streaming (no 5-min HTTP upload limit) - No speaker diarization (all words tagged speaker_0) - No audio events (laughter, applause, etc.) - No language selection (auto-detect only) Provider selection priority: 1. --provider {elevenlabs,bw_stt} CLI flag 2. TRANSCRIBE_PROVIDER in .env or environment 3. Auto-detect from available API key (ElevenLabs wins if both set) Changes: - helpers/transcribe.py: add _find_env_value(), detect_provider(), resolve_provider(), _call_bw_stt() (direct WebSocket, lazy websockets import); restore call_scribe() and --language/--num-speakers flags; add provider param to transcribe_one() and main() - helpers/transcribe_batch.py: add --provider flag, pass through to workers - pyproject.toml: add bw-stt optional dep (websockets>=12, on PyPI) - .env.example: document BW_STT_API_KEY, TRANSCRIBE_PROVIDER, install step API docs: https://labs.bandwidth.com/docs/speech-to-text
There was a problem hiding this comment.
All reported issues were addressed across 4 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Addresses review feedback: a clean WebSocket close before the server sends SessionClosed ends the receive iterator without raising (websockets only raises on abnormal closures), so a truncated session returned a partial transcript that transcribe_one() then cached as final output — and the never-re-transcribe cache rule made the corruption permanent. Track whether SessionClosed was received and raise RuntimeError on an incomplete session, so nothing is written and the file can be retried. Sender-thread errors still take precedence as the root cause when present.
Addresses review feedback: the cache accepted any existing transcript JSON regardless of which backend produced it, so --provider bw_stt silently returned an ElevenLabs transcript and vice versa (e.g. re-running with --provider elevenlabs --num-speakers 2 for diarization would return the diarization-free BW file). Record "provider" in the transcript payload and validate it on cache hit: - same provider → cached, as before - different provider → note printed, re-transcribed, file overwritten - missing field (legacy files) → treated as elevenlabs, the only backend that existed before this field - unreadable/corrupt file → never matches, gets re-transcribed The filename stays provider-agnostic — render.py and pack_transcripts.py expect one <stem>.json per source, so provider identity lives inside the file rather than in the name. transcribe_batch.py applies the same check in its cache pre-filter so batch and single-file semantics agree; provider resolution now happens before the cache scan since it defines what counts as cached.
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
…_provider Addresses review feedback: read_text() raises UnicodeDecodeError (a ValueError, caught by neither OSError nor json.JSONDecodeError) on invalid UTF-8, so a binary-garbage cache file crashed transcription instead of falling through to "" and being re-transcribed as the docstring promises. Add UnicodeDecodeError to the except tuple.
Addresses review feedback: moving resolve_provider() ahead of the cache scan meant its auto-detection exited on a missing API key even when every transcript was already cached — a regression from the original behavior where a fully-cached rerun printed "nothing to do" and exited 0. Split provider resolution into two steps: - explicit_provider() (new, keyless): returns the provider requested via --provider or TRANSCRIBE_PROVIDER, or None. resolve_provider() now delegates to it before falling back to key auto-detection. - Batch checks the cache against the explicit provider when one is given (mismatches re-transcribe, as before). Without one, any readable transcript counts as cached — matching the original semantics and the never-re-transcribe rule — and auto-detection (with its key requirement) runs only once pending work exists.
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="helpers/transcribe_batch.py">
<violation number="1" location="helpers/transcribe_batch.py:106">
P2: When a cache JSON contains a non-string provider such as `null`, auto mode marks it cached because `src != ""` is true, so the batch never repairs the malformed or stale file. Require a nonempty string before accepting an auto-mode cache.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
…ipts Addresses review feedback: cached_provider() returned the "provider" value verbatim, so a malformed file with "provider": null (or any non-string / unknown value) passed batch auto mode's src != "" check and was never repaired — and the function violated its own -> str annotation by returning None or an int. Validate the value against PROVIDERS: known name → returned, anything else → "" (treated as corrupt, re-transcribed). Legacy files with no provider field still default to "elevenlabs".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds BW Labs STT as an optional second transcription provider alongside ElevenLabs Scribe.
websockets(PyPI), declared as an optional extra--provider {elevenlabs,bw_stt},TRANSCRIBE_PROVIDERenv var, or auto-detected from whichever API key is present (ElevenLabs wins if both are set)Usage
Differences vs ElevenLabs
speaker_0)(laughter), …)--language/--num-speakersstill work for ElevenLabs and print a note when ignored by BW STT.Implementation notes
_call_bw_stt()streams raw PCM over the socket from a background thread while the main thread drainsSegmentmessages — reading only after sending would stall long files once the client receive buffer fillswebsocketsis lazy-imported with a clear install hint, so ElevenLabs users are unaffectedpack_transcripts.pyalready consumes (type/text/start/end/speaker_id)Testing
pack_transcripts.pyTRANSCRIBE_PROVIDERoverride, and CLI flags on both helpershelpers/transcribe.pyimports fine withoutwebsocketsinstalled (ElevenLabs path untouched)Known API quirk
BW STT occasionally emits sub-word tokens in
words[](e.g."bel"+"ieve"). Timestamps remain correct so phrase-boundary detection works; noted here for anyone doing exact word matching.Files changed
helpers/transcribe.py— provider dispatch (resolve_provider,detect_provider),_call_bw_stt()helpers/transcribe_batch.py—--providerflag, passed through to workerspyproject.toml—bw-stt = ["websockets>=12"]optional extra.env.example— documentsBW_STT_API_KEYandTRANSCRIBE_PROVIDERSummary by cubic
Adds BW Labs STT as an optional transcription provider alongside ElevenLabs, which remains the default; existing ElevenLabs users are unaffected.
Behavior changes
--provider {elevenlabs,bw_stt},TRANSCRIBE_PROVIDER, or auto-detected from whichever API key is present (ElevenLabs wins if both are set).speaker_0), audio events, and language selection (auto-detect only).--languageand--num-speakersstill work for ElevenLabs and print a note when BW STT ignores them.pack_transcripts.pyalready consumes; BW STT may emit sub-word tokens inwords[]with correct timestamps.SessionClosed, the run raises an error instead of caching a partial transcript.Dependencies
bw-stt = ["websockets>=12"];websocketsis lazy-imported, so ElevenLabs users don't need it.Written for commit e1c754b. Summary will update on new commits.