Skip to content

feat(transcription): AssemblyAI with speaker labels - #19

Closed
bryangruneberg wants to merge 1 commit into
jamespember:mainfrom
bryangruneberg:pr/4-assemblyai-transcription
Closed

bryangruneberg wants to merge 1 commit into
jamespember:mainfrom
bryangruneberg:pr/4-assemblyai-transcription

Conversation

@bryangruneberg

Copy link
Copy Markdown
Contributor

(Keeps Whisper as an optional extra)

Adds a second transcription backend next to local Whisper, selected with transcriber: whisper | assemblyai (default unchanged: whisper).

AssemblyAITranscriber has the same interface as WhisperTranscriber and adds:

  • speaker diarisation: segments carry a speaker, the transcript renders as [mm:ss] Speaker A: ..., and the summariser is given "Speaker A: ..." lines (summary_input) so action items can name an owner;
  • upload as 16 kHz mono FLAC via ffmpeg. The recorder's 48 kHz WAV is ~11 MB/min, and a 34-minute meeting (377 MB) had its upload cut off mid-TLS; FLAC is lossless for what the model hears and ~10x smaller;
  • upload retries on a dropped connection;
  • submit()/wait() split so a caller can keep the transcript id and resume polling instead of re-uploading; AssemblyAIError carries status_code / transient / remote_failed for callers deciding whether to retry;
  • a missing API key is reported at transcription time, so the app still opens and records without one.

build_transcriber(config) replaces the two duplicated construction blocks in app.py (startup and settings-saved). Settings gains a transcription picker.

Packaging: openai-whisper moves from core dependencies to a whisper extra (torch is several GB and not always available for the newest Python). all still includes it, so setup.sh's pip install -e .[all] installs exactly what it did before; a new assemblyai extra is the cloud-only set.

CI installs requests and runs tests/test_assemblyai.py.

…tional extra

Adds a second transcription backend next to local Whisper, selected with
`transcriber: whisper | assemblyai` (default unchanged: whisper).

AssemblyAITranscriber has the same interface as WhisperTranscriber and adds:
- speaker diarisation: segments carry a speaker, the transcript renders as
  **[mm:ss] Speaker A:** ..., and the summariser is given "Speaker A: ..."
  lines (summary_input) so action items can name an owner;
- upload as 16 kHz mono FLAC via ffmpeg. The recorder's 48 kHz WAV is
  ~11 MB/min, and a 34-minute meeting (377 MB) had its upload cut off
  mid-TLS; FLAC is lossless for what the model hears and ~10x smaller;
- upload retries on a dropped connection;
- submit()/wait() split so a caller can keep the transcript id and resume
  polling instead of re-uploading; AssemblyAIError carries status_code /
  transient / remote_failed for callers deciding whether to retry;
- a missing API key is reported at transcription time, so the app still
  opens and records without one.

build_transcriber(config) replaces the two duplicated construction blocks in
app.py (startup and settings-saved). Settings gains a transcription picker.

Packaging: openai-whisper moves from core dependencies to a `whisper`
extra (torch is several GB and not always available for the newest Python).
`all` still includes it, so setup.sh's `pip install -e .[all]` installs
exactly what it did before; a new `assemblyai` extra is the cloud-only set.

CI installs requests and runs tests/test_assemblyai.py.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@jamespember

Copy link
Copy Markdown
Owner

This is really strong work @bryangruneberg. Reading it end-to-end there's a lot of care in the parts that matter: uploading, retries, the split between submit/wait, and the packaging trade-offs.

What I like:

  • Speaker labels flowing all the way through. TranscriptSegment.speaker is optional (Whisper leaves it None and renders unchanged), speaker_text() produces the Speaker A: … form that goes to the summariser, and format_segments renders **[mm:ss] Speaker A:** … in the saved transcript. Clean data flow, no leaky enums.
  • The FLAC compression choice. 16 kHz mono FLAC really is what AssemblyAI listens to internally, so this is lossless for recognition and ~10× smaller — the 377 MB → 37 MB range makes the difference between "upload finishes" and "TLS cuts out at 198 MB" over a flaky connection. Falling back to the original recording when ffmpeg isn't there is the right graceful-degrade.
  • AssemblyAIError carrying status_code / transient / remote_failed. Lets callers make sensible retry decisions instead of matching on error strings.
  • submit() / wait() split. Genuinely useful — a callback restart or later poll can resume rather than re-uploading a multi-hundred-MB meeting. Not used from app.py today, but the interface is right for when it is.
  • Deferring ValueError for missing key to transcribe(), not __init__. So the app opens and records without a key configured, and the failure is loud when it matters.
  • build_transcriber(config) — kills the duplicated construction block that was in both __init__ and handle_settings_closed. Long-overdue tidy.

Packaging change is a breaking change for existing installs, but handled well:

Moving openai-whisper from core deps to a whisper extra is the right call — torch is several GB and often lags on the newest Python — and setup.sh uses pip install -e .[all] which still pulls it in. Anyone who ran pip install -e . by hand without an extra will need to re-run with .[whisper] or .[all]. Worth a line in the README changelog when we next cut a release; not blocking.

Tests are excellent. Injected FakeSession for the transcriber, FakeResponse shaped like requests, no network. Coverage includes:

  • upload → transcript request → poll cycle (test_transcribe_uploads_requests_speakers_and_polls)
  • speaker text formatting
  • API-side error surfacing
  • HTTP error status included in message
  • build_transcriber routing
  • Whisper's speaker-less rendering unchanged
  • FLAC upload path (with @pytest.mark.skipif when ffmpeg is missing — nice)
  • upload retry on SSLError
  • upload give-up after 3 attempts
  • submit → wait split
  • redaction of assemblyai_api_key in to_safe_dict()

Minor nits (not blocking):

  • AssemblyAIError.transient and .remote_failed are set but not read anywhere yet. Fine — they're part of the API the split enables.
  • _check uses response.text[:200] for the fallback when JSON parsing fails; if the server ever streams a huge HTML error page that's a nice cap.

Rebased locally on top of #18 which just merged — clean rebase, 147 tests pass, ruff clean. Merging.

@jamespember

Copy link
Copy Markdown
Owner

Merged as commit 9aa486e on main (squashed locally and pushed because my gh token doesn't carry the workflow scope, so the automated gh pr merge refused to touch .github/workflows/ci.yml). GitHub sees the branch as closed rather than merged for the same reason, but the change is live on main. Thanks again @bryangruneberg — this landed with full commit attribution via Co-authored-by:.

@bryangruneberg

Copy link
Copy Markdown
Contributor Author

Woop Woop! Glad Assembly made it in 😁

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.

2 participants