Standard ASR

Discover and use an engine (app developers)

Authoritative reference: docs/content/specification/protocol.md. This guide shows the common app-developer flow on the current API.

1. Discover installed engines

Engines are pip-installable plugins discovered via entry points — zero config.

from standard_asr import discover_models

registry = discover_models()
for name in registry.names():
    spec = registry.spec(name)
    print(name, spec.engine_id, spec.model_name)

2. Create an engine

engine = registry.create("faster-whisper/large-v3", device="cpu")

3. Pass audio — whatever you have

transcribe accepts a discriminated AudioInput union; bare values are coerced. A bare str is always a local path (never a URL — wrap URLs explicitly).

from standard_asr import AudioArray, AudioUrl

engine.transcribe("meeting.mp3")  # -> AudioPath
engine.transcribe((samples, 16000))  # -> AudioArray(samples, sr)
engine.transcribe(AudioUrl("https://.../a.flac"))  # explicit URL (engine-fetched)

The standard layer negotiates and converts to whatever the engine accepts (decode, encode-to-WAV, read-file, resample) and reports any lossy step in result.diagnostics.

4. Per-request parameters (portable + escape hatch)

from standard_asr import DIARIZE, RuntimeParams, WordTimestampGranularity

result = engine.transcribe(
    "meeting.mp3",
    RuntimeParams(
        language="en",  # or "auto"
        word_timestamps=WordTimestampGranularity.WORD,
        diarization=DIARIZE,  # "who said what" (presence = enable)
        prompt="Q3 budget review.",  # free-text guidance
        phrase_hints=["Anthropic", "Claude"],  # term boosting
    ),
)

diarization is an on/off request marker: pass DIARIZE (or DiarizationRequest()) to enable it, leave it None to skip it. Gate it first with engine.supports("batch.diarization").

Engine-specific options go through provider_params (typed, swap-safe — passing the wrong engine's params raises InvalidProviderParamError).

5. Check capabilities before relying on a feature

if engine.supports("batch.word_timestamps"):
    ...

Missing capabilities are fail-closed (supports(...) returns False).

6. Use the result (constant shape)

print(result.text)
print(result.detected_language, result.duration)
for seg in result.segments or []:
    print(seg.start, seg.end, seg.speaker, seg.text)  # speaker: label when diarized, else None

from standard_asr import to_srt, to_vtt

with open("out.srt", "w", encoding="utf-8") as f:
    f.write(to_srt(result))

A segment renders as a visible cue only if it carries a measured start/end span. Some engines omit timestamps — check seg.timestamp_status. The span must also survive the output grid: a span that quantizes to zero milliseconds fails, because players silently drop T --> T cues. When a segment cannot render, the renderers raise SubtitleRenderingError. They do not silently drop text, hide text, or fabricate timing. To choose the loss yourself, pass on_unrenderable="omit" to keep only the renderable cues, or "collapse" for one whole-text cue.

segment.speaker carries the speaker label when diarization was requested and supported (word.speaker gives the same detail at word level). Engines whose diarization is always_on may label speakers even without a request. A None label means "not attributed", never "unsupported" — capabilities answer support.

7. Streaming

fmt = engine.recommended_wire_format()  # the engine's preferred PCM wire format
async with engine.start_transcription(audio_format=fmt) as session:
    session.feed(microphone)
    async for event in session:
        if event.type == "partial":
            show(event.segment_id, event.text)
        elif event.type == "final":
            commit(event.segment_id, event.text)
        elif event.type == "supersede":
            for old in event.old_ids:
                remove(old)

A synchronous bridge (SyncSession) is available if you cannot use async.

On this page