mstar.api_server.openai.serving_realtime#
/v1/realtime (transcription intent): streaming speech-to-text over a WebSocket.
A subset of OpenAI’s Realtime transcription session, enough for a client that streams microphone audio and wants partial transcripts:
- Client events
transcription_session.update{"session": {"input_audio_transcription": {"model", "language", "prompt"}, "input_audio_format", "turn_detection", "mstar": {"chunk_seconds", "unfixed_chunks", "unfixed_tokens"}}}input_audio_buffer.append{"audio": <base64 PCM16 mono>}input_audio_buffer.commitend of the utterance: flush and finishinput_audio_buffer.cleardrop buffered audio and start over
input_audio_format is "pcm16" (16 kHz, the default) or OpenAI’s
{"type": "audio/pcm", "rate": 24000}; 24 kHz audio is resampled to the
model’s 16 kHz as it arrives, so partial audio_seconds stay on the
client’s timeline. The rate changes only with an empty buffer.
turn_detection null (the default) leaves ending an utterance to the
client’s commit. {"type": "server_vad", "threshold", "prefix_padding_ms",
"silence_duration_ms"} lets the server end it (realtime_vad), adding
input_audio_buffer.speech_started / .speech_stopped events with
audio_start_ms / audio_end_ms on the session’s audio timeline; a
turn that reaches the item cap is committed there instead of refused.
chunk_seconds below MIN_CHUNK_SECONDS is raised to it and must not
pass the item cap, and the unfixed counts are non-negative integers; numeric
strings count as numbers. language and prompt are strings (or null),
language at most MAX_LANGUAGE_CHARS characters. Any other value is an
error and the update changes nothing.
A failed step is an error too. A request error (a 4xx from the engine,
such as a model refusing the language) quotes its message, with server paths
reduced to their base names and cut to MAX_ERROR_CHARS; any other failure
says only that the step failed, with an error id that the server log carries
the details under.
- Server events
transcription_session.created/.updatedconversation.item.input_audio_transcription.deltaappend-only textmstar.transcription.partialthe whole current hypothesis, unstable tail includedconversation.item.input_audio_transcription.completed{"transcript"}error
The decoding follows the Qwen3-ASR SDK’s streaming algorithm: every
chunk_seconds of new audio, the audio heard so far is transcribed again
as one engine request whose assistant turn is prefilled with the previous
hypothesis minus its last unfixed_tokens tokens (once unfixed_chunks
chunks are in), so the tail can still be revised while the head is fixed.
The adapter decides how that prefix reaches its model
(realtime_step_request). Each step is an ordinary request, so sessions
batch on the engine like any other traffic.
Since every step decodes the whole item again, an item’s work grows with the
square of its length. An item is capped at MAX_ITEM_SECONDS (or the
adapter’s max_audio_seconds if shorter): an append past the cap is an
error and the client commits or clears. An append that completes several
chunks at once is decoded as one step.
Functions
|
Base64 PCM16 (little-endian, mono) -> (float32 in [-1, 1), the odd trailing byte). |
|
|
Classes
|
One WebSocket's transcription state (the SDK's |
|
Drives one WebSocket session against the API server. |
Exceptions
|
The engine's error for a step, with the HTTP status it carried: a 4xx is the request's fault (a model's ValueError), anything else the server's. |
- class mstar.api_server.openai.serving_realtime.RealtimeSession(session_id, request, chunk_seconds=2.0, unfixed_chunks=2, unfixed_tokens=5, max_item_seconds=300.0, pending=<factory>, pending_samples=0, heard=<factory>, chunk_id=0, raw_hypothesis='', emitted='', item_id=<factory>, failures=('', {}), odd_byte=b'', input_rate=16000, resampler=None, short=0, turn_detection=None, segmenter=None, stream_samples=0, clock_base=0, clock_in=0)[source]#
Bases:
objectOne WebSocket’s transcription state (the SDK’s
ASRStreamingState).- Parameters:
session_id (str)
request (TranscriptionRequest)
chunk_seconds (float)
unfixed_chunks (int)
unfixed_tokens (int)
max_item_seconds (float)
pending (list[ndarray])
pending_samples (int)
heard (list[ndarray])
chunk_id (int)
raw_hypothesis (str)
emitted (str)
item_id (str)
odd_byte (bytes)
input_rate (int)
resampler (StreamResampler | None)
short (int)
turn_detection (ServerVAD | None)
segmenter (TurnSegmenter | None)
stream_samples (int)
clock_base (int)
clock_in (int)
- add(audio)[source]#
Add SAMPLE_RATE audio to the item.
- Parameters:
audio (ndarray)
- Return type:
None
- clear()[source]#
Start a new item; the stream state (resampler, segmenter, odd byte) goes on.
- Return type:
None
- drain_resampler()[source]#
What the resampler still holds, as if silence followed; it starts over.
- Return type:
ndarray
- item_samples(extra=0)[source]#
SAMPLE_RATE samples in the item once
extramore input samples arrive, rounded once over the whole item (not per append), so the cap does not depend on how the audio was split.
- realign()[source]#
After the resampler starts over (a commit or a clear), put the timeline back on the input clock: each start-over rounds its output up by less than a sample, which would add up over many of them.
- Return type:
None
- request: TranscriptionRequest#
- resampler: StreamResampler | None = None#
- reset_stream()[source]#
A clear: drop what the resampler and the segmenter hold, keeping the timeline (the dropped audio still counts).
- Return type:
None
- segmenter: TurnSegmenter | None = None#
- take_chunk()[source]#
Every full chunk of pending audio as one array, or None until enough arrived. Output the resampler still holds for its lookahead counts toward the chunk, so a chunk of 24 kHz input is decoded on the append that completes it, not the one after; the chunk is then short by those samples (
short), which go with the next one.- Return type:
ndarray | None
- class mstar.api_server.openai.serving_realtime.RealtimeTranscription(api, model_name, adapter, websocket)[source]#
Bases:
objectDrives one WebSocket session against the API server.
- Parameters:
model_name (str)
websocket (WebSocket)
- async apply(ops)[source]#
Carry out segmenter operations in order (see
TurnSegmenter). Audio steps once, after the last operation, so one long append is one step, as without turn detection; a turn’s commit decodes the rest.
- async commit()[source]#
Finish the item: decode what is left, report it, start a new one.
- Return type:
None
- async emit(text, hold)[source]#
Send the part of
textpast what was already sent, minus the lastholdcharacters. A revision that reaches into sent text is not resent — deltas are append-only — the partial event carries it.
- exception mstar.api_server.openai.serving_realtime.StepError(message, status)[source]#
Bases:
RuntimeErrorThe engine’s error for a step, with the HTTP status it carried: a 4xx is the request’s fault (a model’s ValueError), anything else the server’s.
- mstar.api_server.openai.serving_realtime.pcm16_to_float(payload, carry=b'')[source]#
Base64 PCM16 (little-endian, mono) -> (float32 in [-1, 1), the odd trailing byte).
carryis the previous append’s odd byte, so a sample split across two appends is joined instead of shifting every sample after it. Strict decoding: a malformed payload is an error, not silence.