Skip to content

Realtime transcription - #705

Open
mxyyz wants to merge 7 commits into
Zackriya-Solutions:mainfrom
mxyyz:realtime-transcription
Open

Realtime transcription#705
mxyyz wants to merge 7 commits into
Zackriya-Solutions:mainfrom
mxyyz:realtime-transcription

Conversation

@mxyyz

@mxyyz mxyyz commented Aug 9, 2026

Copy link
Copy Markdown

Description

This PR adds a streaming transcription provider so Meetily can transcribe against a self-hosted realtime ASR websocket, with vLLM serving Voxtral Realtime as the worked example.

The existing TranscriptionProvider trait is request/response per VAD-segmented chunk and cannot express a persistent socket, so this adds a sibling StreamingTranscriptionProvider trait driven by its own session worker and fed by a continuous pre-VAD tap on the audio pipeline. Results are emitted on the existing transcript-update event, leaving transcript history, persistence and the UI unchanged. Dialects dispatch on a protocol field, so another streaming backend is a new module rather than pipeline changes.

A dropped socket reconnects with backoff and reports an actionable error once attempts are exhausted. Since a realtime server holds an entire session in one bounded context, sessions also roll over on an audio budget so long meetings keep transcribing.

Related Issue

Fixes #657

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Performance improvement
  • Code refactoring
  • Other (please describe)

Testing

  • Unit tests added/updated
  • Manual testing performed
  • All tests pass

Tested with:

  • cargo test --features vulkan --lib audio::transcription — 21 passed
  • cargo test --features vulkan --lib — 207 passed, plus one pre-existing unrelated failure (device_detection::test_calculate_buffer_timeout_bluetooth, a float-precision assertion in a file this PR does not touch)
  • bun test tests/ — 18 passed
  • npx tsc --noEmit — clean apart from the repo's existing bun:test type-resolution errors under tests/lib/

Also validated end-to-end against a self-hosted vLLM Voxtral-Realtime endpoint, including killing the server mid-recording to exercise reconnect, and confirming Whisper and Parakeet are unaffected.

Documentation

  • Documentation updated
  • No documentation needed

Checklist

  • Code follows project style
  • Self-reviewed the code
  • Added comments for complex code
  • Updated README if needed
  • Branch is up to date with devtest
  • No merge conflicts

Screenshots (if applicable)

2026-08-10_00-48-17

Additional Notes

Commits are split backend / frontend / docs. The API key is stored as plaintext JSON in SQLite, consistent with the existing customOpenAI summary config. Developed on Linux and untested on macOS and Windows, though no platform-specific audio code is touched. Exercising system audio on Linux additionally needs the PulseAudio capture backend, submitted separately.

sujithatzackriya and others added 7 commits June 5, 2026 19:23
Add a `StreamingTranscriptionProvider` abstraction alongside the existing
one-shot `TranscriptionProvider`, so Meetily can transcribe against a
self-hosted realtime ASR websocket (e.g. vLLM serving Voxtral Realtime)
instead of one request per VAD-segmented chunk.

- Add a persistent session worker fed by a continuous pre-VAD tap on the
  mixed audio pipeline, resampled 48k -> 16k mono
- Add the Voxtral realtime websocket client (session.update handshake,
  base64 PCM16-LE frames, incremental deltas, final commit on stop)
- Segment the provider's cumulative text into sentences and emit them on
  the existing `transcript-update` event, so history persistence and the
  UI are unchanged; a sentence keeps one `sequence_id` while it grows and
  finalizes under that id
- Reconnect with backoff when the socket drops mid-recording, and report
  an actionable `transcription-error` once attempts are exhausted, so a
  dead endpoint can never leave a meeting silently untranscribed
- Persist endpoint/model/API key via a `customTranscriptionConfig` column
  on `transcript_settings`, mirroring the customOpenAI summary provider
- Add `api_test_custom_transcription_connection` plus start-of-recording
  validation so an unreachable endpoint fails loudly

Dialects dispatch on a `protocol` discriminator, so another streaming
backend is a new module plus a match arm rather than pipeline changes.

Refs: Zackriya-Solutions#657

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Expose the streaming websocket backend in the UI as a "Custom Realtime
(WebSocket)" transcription provider.

- Add endpoint, model and optional API key fields with a Test Connection
  action; saving persists the config and activates the provider in one step
- Add configService wrappers over the api_*_custom_transcription_* commands
- Treat the provider as auto-detect-only in LanguageSelection, since the
  server detects language itself (mirrors the Parakeet handling)
- Extract transcript merging into `lib/transcript-merge` and upsert live
  segments by `sequence_id` instead of dropping repeats, so a segment
  refines in place while it streams; providers that emit fresh ids per
  segment keep the previous append-only behavior
- Throttle the transcript buffer flush rather than debouncing it, so rapid
  partials render continuously instead of stalling until a speech pause

Refs: Zackriya-Solutions#657

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cover configuring the provider, running a vLLM Voxtral Realtime endpoint,
what the pipeline does during a recording, and reconnect behavior. Document
the provider contract separately from the voxtral-realtime dialect, so the
wire protocol reads as one implementation rather than the abstraction.

Refs: Zackriya-Solutions#657

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fills

A realtime ASR server holds an entire session in one bounded context window.
Once it fills, the session stops emitting transcripts for the rest of the
recording — so a long meeting silently loses transcription partway through.

Feed each server session a bounded budget of audio and, when it is spent,
finalize that session and continue the recording on a fresh one. The rollover
is invisible in the transcript: the outgoing session's final text is emitted
before the new one takes over.

- Add `maxSessionSeconds` to the persisted config (`None` = 300s default,
  `0` = never roll over) plus `session_limit_seconds()`
- Add `api_detect_custom_transcription_limits`, which reads `max_model_len`
  from the endpoint's `/v1/models` and converts it to a session length using
  the audio/text token rates, keeping 15% headroom. Servers that publish no
  limit are reported as such rather than as an error
- Reject a configured length under 30 seconds

Refs: Zackriya-Solutions#657

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Surface the streaming session budget so users on a smaller context window can
match it to their backend.

- Add a "Max session length" field with a Detect button that reads the
  endpoint's context size and fills in a fitting value
- Validate before saving: a whole number of seconds, at least 30, or 0 to
  never split
- Explain in the field's help text why the setting exists, since the failure
  it prevents (transcription stopping mid-meeting) is otherwise mysterious

Refs: Zackriya-Solutions#657

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cover why a realtime session is bounded at all — the server holds the whole
session in one context window, and a meeting long enough to fill it stops being
transcribed with no error — and how Max session length, Detect, and 0 behave.

Note the token-rate assumptions behind Detect, since they are Voxtral-specific
and a different encoder needs the value set by hand, and distinguish a planned
rollover from a reconnect: one keeps the transcript intact, the other reacts to
a dead socket and drops the outage window.

Refs: Zackriya-Solutions#657

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@getong

getong commented Aug 13, 2026

Copy link
Copy Markdown

Not a maintainer, but your code seems to not realtime, after 34 seconds, the text does not appear here.
Hope to be more real time.
image

@mxyyz

mxyyz commented Aug 13, 2026

Copy link
Copy Markdown
Author

Could you provide some more information?

  • What OS are you running?
  • What model are you using?
  • Are you running vLLM?
  • What transcription provider are you using?
  • Did you set the max. session length according to your model/hardware capabilities?

@getong

getong commented Aug 13, 2026

Copy link
Copy Markdown

macos,
Large V3
not running vllm
I do not setup other options.

@mxyyz

mxyyz commented Aug 13, 2026

Copy link
Copy Markdown
Author

This PR does not enable non-realtime models to be used for realtime transcription. As far as I know, Whisper Large v3 is not a realtime capable model

@getong

getong commented Aug 13, 2026

Copy link
Copy Markdown

ok, I don't know about llm. Sorry about it.

@mxyyz

mxyyz commented Aug 13, 2026

Copy link
Copy Markdown
Author

ok, I don't know about llm. Sorry about it.

Speach-to-text / ASR models aren't LLMs either actually! llms are written text predictors, asr's analyze audio signals and turn them into text! :) (superficial knowledge here)

@getong

getong commented Aug 13, 2026

Copy link
Copy Markdown

#693

how about this ? This was coding by codex, split speech into 8s and tranform into text.

@mxyyz

mxyyz commented Aug 13, 2026

Copy link
Copy Markdown
Author

#693

how about this ? This was coding by codex, split speech into 8s and tranform into text.

Well, yeah, that makes transcription quicker but it is not realtime and not using a realtime model via a websocket, like THIS PR is.
Also, splitting the analyzed text into smaller chunks decreases transcription accuracy (at the cost of speed)

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.

[FEATURE] Streaming transcription provider for self-hosted realtime ASR (websocket)

3 participants