Skip to content

feat(examples): MoQT の audio track を STT へ流す FastAPI サーバーとブラウザ example を追加する - #353

Open
yuki-uchida wants to merge 5 commits into
feat/python-echo-examplefrom
feat/python-stt-example
Open

feat(examples): MoQT の audio track を STT へ流す FastAPI サーバーとブラウザ example を追加する#353
yuki-uchida wants to merge 5 commits into
feat/python-echo-examplefrom
feat/python-stt-example

Conversation

@yuki-uchida

@yuki-uchida yuki-uchida commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

概要

MoQT で届く audio track を音声認識(STT)へ流し、結果を transcript track として返す FastAPI サーバーと、マイク音声を publish して結果を表示するブラウザ example を追加しました。

やったこと

  • examples/python/stt-server: PUBLISH で届いた track は全て accept し catalog で audio track を判定。PUBLISH_NAMESPACE では <ns>/catalog を subscribe し、列挙された audio track を subscribe。payload は生 EncodedAudioChunk(ブラウザ)と長さ付き JSON メタ(live-ingest)の両対応で、Opus / AAC を PyAV でデコード
  • STT バックエンドは SpeechToText プロトコルで差し替え: wav(既定)、deepgramopenaiwhisper(faster-whisper をローカル実行、無音で区切った発話単位で文字起こし。無音のみの区間は渡さず、VAD と no_speech_prob で幻覚を抑制)。結果は <ns>/transcript への SUBSCRIBE を accept して 1 transcript = 1 group の JSON {track, text, final, at} で配信(他の SUBSCRIBE は reject)
  • examples/browser/examples/stt: getUserMedia → WebCodecs(Opus)→ PUBLISH_NAMESPACE で publish し、transcript track を subscribe して表示。50 chunk ごとに group を進める前に EndOfGroup を送る(wasm クライアントは EndOfGroup でのみストリームを閉じ、TrackReader は現在のストリームが終わるまで次の group を読まないため)

やらないこと

  • Deepgram / OpenAI の実サービスに対する動作確認(API キーなし)。LOC 拡張ヘッダの capture timestamp の利用。遅れて subscribe した相手への過去 transcript の配信

テスト

  • pytest 11 件: catalog 解析、両 payload 形式の展開、PUBLISH / PUBLISH_NAMESPACE 両フローで Opus 正弦波が wav バックエンドに PCM として届くこと、偽バックエンドの結果が transcript track で届くこと、transcript 以外の SUBSCRIBE が reject されること、whisper のセグメンタとバックエンド
  • 手動: live-ingest 経由の RTMP(AAC)6 秒 → 6.0 秒の WAV。macOS say の音声 → whisper(base)で 2 発話。Playwright の Chrome に擬似マイクとして音声 WAV を与え、whisper 経由でページに 2 件の書き起こしが表示されることを確認

備考

  • ブラウザ example は Chrome が信頼する証明書が必要。README に relay/keys の証明書を流用して make chrome で開く手順を記載

@yuki-uchida yuki-uchida changed the title feat(examples): MoQT の audio track を受け取り STT サービスへ流す FastAPI サーバーを追加する feat(examples): MoQT の audio track を STT へ流す FastAPI サーバーとブラウザ example を追加する Sep 8, 2026
… tracks

examples/python/stt-server accepts MoQT sessions inside a FastAPI process,
decodes the audio tracks it receives to PCM16 and streams them into a
speech-to-text backend. Audio arrives either as PUBLISH (every track is
accepted, the catalog names the audio tracks) or after PUBLISH_NAMESPACE
(the server subscribes to the catalog and to the audio tracks it lists).
Object payloads may be bare EncodedAudioChunk bytes (browser, LOC) or the
bridges/live-ingest length-prefixed JSON framing; Opus and AAC are decoded
with PyAV and resampled to the backend's PCM format.

Backends share one SpeechToText protocol: a WAV recorder for local runs,
Deepgram live (linear16 16 kHz) and OpenAI Realtime transcription (pcm16
24 kHz). Transcripts are logged and served at GET /transcripts.

Verified with the live-ingest bridge: an ffmpeg RTMP push with AAC audio
produced 282 objects that decoded to a 6.0 s 16 kHz WAV.
faster-whisper runs on this machine, so no service credentials are
needed. Whisper is a batch model: SpeechSegmenter cuts the PCM stream
into utterances at pauses (or at WHISPER_MAX_SEGMENT_SEC) and a worker
task transcribes each one on a thread, so the track reader never waits
for the model. The model dependencies live in the optional `whisper`
dependency group; onnxruntime is pinned below 1.24 because newer wheels
drop Python 3.10.

Verified with macOS `say` output: 7.8 s of speech transcribed in 2.0 s
with the base model, split into two utterances at the pause.
The STT server accepted audio but only exposed results over HTTP. It now
accepts SUBSCRIBE for <namespace>/transcript on any session and writes
every transcript there as its own group with a JSON payload
{track, text, final, at}, so a client that publishes audio can receive
the recognition result on the same MoQT session. Other SUBSCRIBEs are
still rejected.
…hone

examples/browser/examples/stt captures the microphone with getUserMedia,
encodes it to Opus with the shared WebCodecs audio encoder worker and
publishes it to examples/python/stt-server over WebTransport: it answers
the server's SUBSCRIBE for catalog and audio after PUBLISH_NAMESPACE,
starts a new group every 50 chunks, and subscribes to the transcript
track to render the results.

Each group is closed with an EndOfGroup status object before the next
one starts because the wasm client only closes the subgroup stream on
EndOfGroup and sequential readers such as moqt's TrackReader wait for
the stream to end before moving to the next group.

Verified headless with Playwright: Chrome's fake microphone fed a WAV of
synthesized speech; the local Whisper backend returned two transcripts
that appeared on the page.
With a real microphone the segmenter emitted a silence-only buffer every
second and Whisper answered each one with a stock phrase
("ご視聴ありがとうございました"). Three gates now suppress it: buffers whose
frames never exceed WHISPER_SILENCE_RMS are discarded instead of emitted,
faster-whisper's VAD filter is enabled (WHISPER_VAD_FILTER), and result
segments whose no_speech_prob exceeds WHISPER_NO_SPEECH_THRESHOLD
(default 0.6) are dropped. Verified with the base model: six seconds of
microphone noise produce no transcript, speech surrounded by noise is
still transcribed.
@yuki-uchida
yuki-uchida force-pushed the feat/python-stt-example branch from 302a367 to a057181 Compare September 8, 2026 14:02
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.

1 participant