Standard ASR

standard_asr

The top-level standard_asr namespace is the application-developer surface. Import what you need to discover engines, pass audio, read results, and stream:

from standard_asr import discover_models, RuntimeParams, TranscriptionResult

For the engine-author surface (building a plugin), see standard_asr.engine.

Standard ASR -- the open interface between applications and ASR engines.

This top-level namespace is the application-developer surface: discover an engine, hand it audio, read a constant-shape result, and (optionally) stream. That is the whole 80% path:

from standard_asr import discover_models, RuntimeParams

registry = discover_models()
engine = registry.create("faster-whisper/large-v3")
result = engine.transcribe("meeting.wav", RuntimeParams(language="en"))
print(result.text)

The deeper surfaces live in dedicated, audience-signaling submodules so the names you reach for are never buried under names you don't:

  • standard_asr.engine -- everything an engine author implements and declares (EngineBase, the config/properties surface, the full capability vocabulary).
  • standard_asr.compliance -- the compliance checks an engine author runs against their plugin.
  • granular modules (standard_asr.audio.wire, standard_asr.audio.negotiation, ...) expose the framework internals for advanced use.

AudioArray#classSource

class AudioArray

AudioArray(
    samples: NDArray[np.floating],
    sample_rate: int | None = None,
)

An already-decoded raw waveform.

Unlike the other variants, an array does not self-describe its sample rate. When sample_rate is None the global strict / best_effort policy decides whether to raise or assume the canonical 16 kHz.

eq is disabled because NumPy arrays do not support scalar equality; instances therefore compare by identity.

Parameters
NameTypeDescription
samplesNDArray[np.floating]

Waveform samples. Canonical form is float32 mono in [-1, 1]; multi-channel is (n_samples, n_channels).

sample_rate
= None
int | None

Sample rate in Hz, or None if unknown.

__post_init__#methodSource

def AudioArray.__post_init__() -> None

Reject a non-floating dtype or a non-positive sample rate at construction.

Both downstream paths assume floating samples in [-1, 1]: array passthrough delivers them unscaled, and WAV encoding scales by full int16 range. An integer array (for example, int16 PCM) would be silently mis-scaled by either path -- a wrong-audio result, the cardinal sin -- so it is rejected at construction with an actionable message.

A sample_rate of 0 or a negative value is likewise rejected here. Otherwise an engine declaring accepted_sample_rates="any" would be handed the bogus rate verbatim (silently, the cardinal sin), while an engine declaring a concrete list would crash deep in resampling (ZeroDivisionError on the duration check, or a bare ValueError) -- the failure mode drifting with engine metadata. Failing at construction makes the application bug (a unit/shape mix-up) loud and engine-independent, matching AudioFormat.sample_rate's gt=0 and the dtype check above. None (rate unknown) stays valid: it is resolved by the strict/best_effort sample-rate policy downstream. The number of samples is not constrained here -- an empty array can be a legitimate passthrough boundary input, so emptiness is handled (where it actually matters) at resample time, not rejected at construction.

Raises
  • TypeError

    If samples does not have a floating dtype.

  • ValueError

    If sample_rate is not None and not strictly positive.

AudioBase64#classSource

class AudioBase64

AudioBase64(
    value: str,
)

Base64-encoded (or data-URI) encoded audio.

Parameters
NameTypeDescription
valuestr

Base64 string or data: URI.

AudioBytes#classSource

class AudioBytes

AudioBytes(
    data: bytes,
    container: str | None = None,
)

Encoded audio held in memory.

The sample rate is self-describing via the file header.

Parameters
NameTypeDescription
databytes

Encoded audio bytes (for example, the contents of an MP3/WAV file).

container
= None
str | None

Optional container/format hint (for example, "wav", "mp3").

AudioFormat#classSource

class AudioFormat(BaseModel)

Declared wire format for raw PCM frames fed to a streaming session.

Raises
  • ValueError

    If validation fails.

Attributes
NameTypeDescription
encodingstr

Wire encoding of the PCM frames (for example, "pcm_s16le", "mulaw"). MUST be one of the engine's wire_encodings.

sample_rateint
gt=0

Sample rate of the frames in Hz.

channels
= 1
int
gt=0

Number of interleaved channels. Defaults to 1 (mono).

AudioInput#attributeSource

AudioInput = Union[AudioPath, AudioBytes, AudioArray, AudioUrl, AudioBase64, AudioStorageUri]

AudioInputLike#attributeSource

AudioInputLike = Union[AudioInput, str, 'os.PathLike[str]', bytes, 'NDArray[np.floating]', 'tuple[NDArray[np.floating], int]']

AudioPath#classSource

class AudioPath

AudioPath(
    value: str | os.PathLike[str],
)

A local audio file on disk.

The sample rate is self-describing via the file header.

Parameters
NameTypeDescription
valuestr | os.PathLike[str]

Path to the audio file.

AudioProcessingError#classSource

class AudioProcessingError(StandardASRError)

Raised when an error occurs during audio loading or processing.

The audio loading and conversion functions in standard_asr.audio raise this.

AudioStorageUri#classSource

class AudioStorageUri

AudioStorageUri(
    value: str,
)

A provider cloud-storage URI the engine resolves with its own credentials.

Whole engine classes are addressable only by a provider-native storage URI: AWS Transcribe batch requires an S3 URI (Media.MediaFileUri) and Google STT v2 requires a gs:// URI. These are not HTTPS-fetchable public URLs: the engine resolves them with its own cloud-SDK credentials, so -- unlike AudioUrl -- the standard MUST NOT run the HTTPS public-IP SSRF validator over them. The standard never fetches a storage URI itself; it only forwards it to an engine that declares "storage_uri" support.

Like AudioUrl / AudioBase64, this variant requires explicit construction: a bare str always coerces to AudioPath, never to a storage URI (the same SSRF safety stance against attacker-controlled strings). The scheme is validated against STORAGE_URI_SCHEMES at construction; file://, http(s)://, an empty value, or an unknown scheme is rejected with a clear error.

Parameters
NameTypeDescription
valuestr

The storage URI, for example, "s3://bucket/key.wav" or "gs://bucket/key.flac". The sample rate is self-describing at the remote/server side once the engine resolves it.

Raises
  • ValueError

    If the URI is empty, has no scheme, or uses a scheme outside STORAGE_URI_SCHEMES.

__post_init__#methodSource

def AudioStorageUri.__post_init__() -> None

Validate the URI scheme against the storage-scheme allowlist.

Raises
  • ValueError

    If the URI is empty, schemeless, or uses a scheme that is not an allowlisted provider storage scheme.

AudioUrl#classSource

class AudioUrl

AudioUrl(
    value: str,
)

A remote URL the engine or cloud service fetches server-side.

The semantics are "the server can fetch this". Security constraints (HTTPS-only, private/loopback/link-local-address rejection) are enforced before the URL is forwarded to an engine, by standard_asr.audio.negotiation.validate_fetchable_url at plan execution. In v1 the standard never fetches the URL itself.

Parameters
NameTypeDescription
valuestr

The remote URL.

ChannelResult#classSource

class ChannelResult(BaseModel)

Per-channel transcription for multi-channel audio.

Raises
  • ValueError

    If field validation fails.

Attributes
NameTypeDescription
channelint
ge=0

Channel index.

textstr

Full transcript for this channel.

segments
= None
list[Segment] | None

Optional segment-level details for this channel.

words
= None
list[Word] | None

Optional flattened word-level details for this channel.

ConfigError#classSource

class ConfigError(StructuredError, ValueError)

Raised when the CONFIGURATION -- supplied or ambient -- is invalid.

The type asserts fault ownership: the configuration's SUPPLIER can fix it (a bad init-config field, a default_language not in selectable_languages, a malformed --config / --set). Who that supplier is depends on the surface, and each surface maps the SAME error accordingly:

  • CLI: the invoking user owns the config -- the flags AND the env vars -- so every ConfigError is caller-actionable there: usage exit 2, with the sanitized message naming the field to fix.
  • Reference server: a wire client cannot supply engine config at all (construction is zero-arg; options are the portable WireRuntimeParams), so a ConfigError reaching the server -- at construction, transcription, or session establishment -- is a deployment-side fault and maps to a scrubbed 500 (WS internal_error); the client-fixable rejections have their own types (UnsupportedFeatureError -> 422, request validation -> 422). See ConfigurationRequiredError for the absent-config 503 state.

Engine-DECLARATION defects (a malformed declared language tag, an unsatisfiable prepare shape, an IC.6 violation) are NOT this type: they raise EngineContractError, because no configuration value fixes them. An engine that raises ConfigError (or lets a construction-time ValidationError, which ModelRegistry.create wraps into one, escape its factory) for a fault that is NOT about the supplied configuration mis-asserts this ownership contract -- the compliance suite's zero-arg construction check fails such engines (engine_construction_failed); consumers do not second-guess the type. The ValueError mixin serves IN-PROCESS callers, who genuinely can pass a bad config value to a constructor.

The one machine-distinguishable sub-state is ABSENT required configuration: raise (or catch) ConfigurationRequiredError for that -- consumers such as the compliance suite treat "config missing from this environment" (skip) differently from "config invalid" (fail).

ConfigurationRequiredError#classSource

class ConfigurationRequiredError(ConfigError)

Raised when required runtime configuration is ABSENT, not invalid.

The narrow, machine-distinguishable subtype of ConfigError for the one state that is a fact about the ENVIRONMENT rather than about any code or declaration: a required config field (typically a credential) was neither passed explicitly nor found in the environment. Consumers use the distinction to keep two very different verdicts apart:

  • the compliance suite SKIPS instantiation-level checks on this error (a credentialed engine on a clean CI is behaving correctly; the verdict must not depend on the runtime's credential state), while
  • any other ConfigError -- an invalid supplied value, an internally inconsistent declaration, a factory contract bug -- stays a compliance FAILURE (skipping those would let a broken plugin read as green-with-warning).

from_env raises this automatically when construction failed solely because required fields are missing, so engines following the documented explicit > env > raise pattern get the classification for free. An engine building its config another way should raise this type itself for the missing-credential state.

Transport mapping: the reference server maps this state to HTTP 503 (REST) / a service_unavailable frame (WS) with a stable generic detail -- whether it surfaces at zero-arg engine construction or lazily at transcription/session establishment (an engine deferring its credential check past __init__). An operator-side availability state, never the caller's 422, and never the absent field names (those are deployment detail, safe-logged for the operator only).

DIAG_SEGMENT_TIMESTAMPS_UNAVAILABLE#attributeSource

DIAG_SEGMENT_TIMESTAMPS_UNAVAILABLE = 'segment_timestamps_unavailable'

DIARIZE#attributeSource

DIARIZE: Final[DiarizationRequest] = DiarizationRequest()

Diagnostic#classSource

class Diagnostic(BaseModel)

A structured, non-fatal notification from the standard layer.

Diagnostics report lossy conversions, assumed parameters, best_effort degradations, and similar non-ideal paths.

Attributes
NameTypeDescription
level
= 'info'
Literal['info', 'warning']

Severity, "info" or "warning".

codestr

Stable machine-readable code (for example, "audio_conversion").

messagestr

Human-readable explanation.

param
= None
str | None

The parameter the diagnostic concerns, if any.

provided
= None
WireJsonValue

The value the application provided, if relevant.

effective
= None
WireJsonValue

The value that took effect, if relevant.

DiarizationRequest#classSource

class DiarizationRequest(BaseModel)

Request marker for speaker diarization ("who said what").

Presence enables diarization: RuntimeParams(diarization=DiarizationRequest()) (or the DIARIZE convenience constant) requests speaker labels; diarization=None (the default) means not requested. There is no []-analog -- a "requested-but-empty" state is meaningless for an on/off feature, so None vs an instance is the whole state space. On the wire the marker maps three ways: "diarization": {} -> DiarizationRequest(); "diarization": null -> None; key absent -> None.

The model is a deliberately empty frozen marker in v1: tuning parameters (num_speakers / min/max hints, granularity selection) are deferred because today's engine landscape cannot honor them portably -- they graduate additively onto this model once support is broad enough. extra="forbid" keeps that evolution honest: an unknown key (for example, a client guessing num_speakers) fails loudly (a 422 on the wire) instead of being silently ignored. An import-time assert in standard_asr.runtime.gating additionally forces sub-gating to be written the moment a field is added here.

Raises
  • ValueError

    If an unknown field is supplied (extra="forbid").

DiscoveryError#classSource

class DiscoveryError(StandardASRError)

Base class for discovery and plugin-related errors.

EngineContractError#classSource

class EngineContractError(StandardASRError)

Raised when a constructed engine breaks the protocol contract.

The runtime counterpart of a compliance failure, in two shapes:

  • Runtime behavior: a SYNCHRONOUS StandardASR member (transcribe / start_transcription / supports / recommended_wire_format / prepare) returned an awaitable (an async def implementation, or a sync wrapper delegating to one) or a value outside its protocol-pinned return type. Raised by standard_asr.runtime.protocol_boundary.require_sync_result at the consumer call sites (CLI, reference server) so the defect is loud at the boundary instead of surfacing as a confusing secondary AttributeError (or a silent misreading) deep inside another subsystem.
  • Declaration shape: the engine DECLARED something the contract forbids -- a prepare that is a coroutine function, non-callable, or parameter-requiring; a malformed selectable_languages / detectable_languages tag; a language axis without the IC.6 default_language obligation. No caller-side value can fix these, which is what separates them from ConfigError (an invalid configuration VALUE, fixable by whoever supplies the config).

An engine/plugin fault, never a caller mistake -- deliberately NOT a ValueError: transports and the CLI map the ValueError family to caller-fixable surfaces (HTTP 422 / usage exit 2), while this must land on the engine-fault surfaces (scrubbed HTTP 500 / internal_error frame / CLI exit 1). If you hit it as an application developer, report it to the engine's author. Messages carry type names only, never the offending value.

EntrypointValidationError#classSource

class EntrypointValidationError(DiscoveryError, ValueError)

Raised when an entry point name or metadata is invalid.

FFmpegNotFoundError#classSource

class FFmpegNotFoundError(AudioProcessingError, FileNotFoundError)

Raised when FFmpeg is required but not found in the system PATH.

FFprobeNotFoundError#classSource

class FFprobeNotFoundError(AudioProcessingError, FileNotFoundError)

Raised when FFprobe is required but not found in the system PATH.

FactoryLoadError#classSource

class FactoryLoadError(DiscoveryError, ImportError)

Raised when an entry point target cannot be imported or is not callable.

IncompatibleAudioInputError#classSource

class IncompatibleAudioInputError(AudioProcessingError)

IncompatibleAudioInputError(
    provided: str,
    accepted: object,
    hint: str,
)

Raised when no viable conversion path exists for the provided audio.

This happens when the shape an application provides cannot be negotiated into any shape the engine accepts (for example, a local array given to an engine that only accepts a server-fetchable URL).

Parameters
NameTypeDescription
providedstr

Human-readable description of the provided input shape.

acceptedobject

The engine's accepted input kinds.

hintstr

Actionable guidance for resolving the mismatch.

InvalidProviderParamError#classSource

class InvalidProviderParamError(StructuredError, ValueError)

Raised when provider_params are invalid for the target engine.

Unlike standard-set parameters, provider_params errors are always raised regardless of strict / best_effort -- they indicate a code-level bug (such as passing one engine's params model to another after a swap).

InvalidSessionUseError#classSource

class InvalidSessionUseError(StandardASRError, ValueError)

Raised when a streaming session is driven incorrectly while still live.

A caller-side programming error against an open session -- distinct from StreamClosedError (the stream genuinely ended). It covers the still-live-session usage breaches that are NOT lifecycle-close:

  • mixing managed feed() with manual send_audio / end_audio (only one input mode may own a session);
  • calling feed() more than once (a session owns at most one fed source);
  • iterating the event stream more than once (single-consumer contract).

The session is not closed in any of these cases -- the mistake is in how the application used it. Catching StreamClosedError here would lead an application to wrongly conclude the session terminated and rebuild it. Mixes in ValueError (like ConfigError / InvalidProviderParamError): it is a bad-call programming error. (It has no HTTP mapping: it fires only against an in-process session object, and the server drives its own sessions correctly by construction.)

UnrenderablePolicy#attributeSource

UnrenderablePolicy = Literal['error', 'omit', 'collapse']

ModelRegistry#classSource

class ModelRegistry

ModelRegistry(
    specs: Mapping[str, ModelSpec],
    *,
    shadowed_engine_ids: set[str] | None = None,
)

Container for discovered ASR engine factories.

ModelRegistry holds the results of plugin discovery and provides methods to list, query, and instantiate ASR engines. It does not perform discovery itself—use discover_models() to create a populated registry.

Typical Usage:

>>> from standard_asr import discover_models
>>> registry = discover_models()
>>>
>>> # List all available models
>>> registry.names()  # ['faster-whisper/large-v3', 'whisper/base', ...]
>>>
>>> # Create an ASR instance
>>> asr = registry.create("faster-whisper/large-v3", device="cuda")
>>> result = asr.transcribe(audio)

Key Methods:

  • names(): List all discovered model keys.
  • keys_by_engine(engine_id): List models for a specific engine.
  • create(name, **kwargs): Instantiate an ASR engine.
  • spec(name): Get metadata for a model.

Note

Use discover_models() to create a ModelRegistry. Do not instantiate directly unless you're providing custom entry points for testing.

Initialize with a mapping of model specs (internal use).

Parameters
NameTypeDescription
specsMapping[str, ModelSpec]

Mapping of engine_id/model_name keys to specs.

shadowed_engine_ids
= None
set[str] | None

Engine ids contributed by more than one distribution (an engine-identity collision). Routing on these is ambiguous; consumers may surface or reject them.

Attributes
NameTypeDescription
shadowed_engine_idsset[str]

Engine ids provided by more than one distribution.

names#methodSource

def ModelRegistry.names() -> list[str]

List all discovered model keys, sorted alphabetically.

Returns
  • list[str]

    List of model keys (for example, ['faster-whisper/large-v3', 'whisper/base']).

keys_by_engine#methodSource

def ModelRegistry.keys_by_engine(engine_id: str) -> list[str]

List all model keys for a specific engine.

The argument is PEP 503-normalized to the canonical routing identity before matching, so a non-canonical form (for example, my_engine) resolves to the same engine as spec / create -- the stored engine_id is already canonical.

Parameters
NameTypeDescription
engine_idstr

Engine identifier (for example, faster-whisper). Any PEP 503 equivalent form (Faster_Whisper, faster.whisper) matches.

Returns
  • list[str]

    List of matching model keys, sorted alphabetically.

Example
>>> registry.keys_by_engine("faster-whisper")
['faster-whisper/', 'faster-whisper/large-v3', 'faster-whisper/small']

spec#methodSource

def ModelRegistry.spec(name: str) -> ModelSpec

Get metadata for a model.

Parameters
NameTypeDescription
namestr

Model key in engine_id/model_name format.

Returns
  • ModelSpec

    ModelSpec containing entry point metadata.

Raises

get_factory#methodSource

def ModelRegistry.get_factory(name: str) -> ASRFactory

Get the factory callable for a model (without instantiating).

Parameters
NameTypeDescription
namestr

Model key in engine_id/model_name format.

Returns
  • ASRFactory

    Callable that creates a StandardASR instance.

Raises

engine_class#methodSource

def ModelRegistry.engine_class(name: str) -> type['StandardASR']

Resolve a model's engine class without instantiating it.

Use this to read class-level metadata (declared_capabilities, properties, provider_params_type) for discovery, UI generation, and REST endpoints without paying the cost (or auth requirements) of constructing the engine. See ModelSpec.engine_class.

Parameters
NameTypeDescription
namestr

Model key in engine_id/model_name format.

Returns
  • type['StandardASR']

    The engine class.

Raises

config_schema#methodSource

def ModelRegistry.config_schema(name: str) -> dict[str, Any] | None

Return the JSON Schema of a model's init config, without instantiation.

Resolves the engine class and reads its config_type ClassVar. This is the discovery path for settings UIs: an application can render a configuration form for an engine before constructing it -- construction may require the very values (credentials, default language) the form is meant to collect. Secret fields carry the format: password / writeOnly: true markers in the schema, so a schema-driven UI renders them safely.

Parameters
NameTypeDescription
namestr

Model key in engine_id/model_name format.

Returns
  • dict[str, Any] | None

    The config JSON Schema, or None when the engine does not

  • dict[str, Any] | None

    declare a class-level config_type.

Raises
  • EntrypointValidationError

    Model not found.

  • FactoryLoadError

    Entry point failed to load, the engine class cannot be determined without calling the factory, config_type is not a BaseConfig subclass, or its JSON Schema cannot be generated.

create#methodSource

def ModelRegistry.create(name: str, /, *args: Any, **kwargs: Any) -> StandardASR

Create an ASR engine instance.

This is the primary method for instantiating ASR engines. It loads the factory and invokes it with the provided arguments.

Parameters
NameTypeDescription
namestr

Model key (for example, "faster-whisper/large-v3").

*args
= ()
Any

Positional arguments passed to the factory.

**kwargs
= {}
Any

Keyword arguments passed to the factory (for example, device="cuda").

Returns
  • StandardASR

    StandardASR instance ready for transcription.

Raises
  • EntrypointValidationError

    Model not found.

  • FactoryLoadError

    Entry point failed to load or is not callable.

  • ConfigError

    The model needs configuration that was missing or invalid (a required credential, a bad default_language, and so on). A construction-time pydantic ValidationError -- whether from the bare constructor or an engine's own validator -- is wrapped into ConfigError with the offending input scrubbed, so a caller can except ConfigError uniformly. The wrap ASSERTS the type's ownership contract: a factory's construction-time ValidationError means the supplied configuration was rejected (the documented bare-constructor pattern). An engine whose factory lets a NON-config internal ValidationError escape construction mis-asserts that contract -- in-band the two are indistinguishable, so the compliance suite's zero-arg construction check (engine_construction_failed) polices it, not consumer-side guessing. (The reference server maps construction-time config faults by fault ownership: absent required config -> 503, anything else -> scrubbed 500 -- never a caller-blaming 422, since its construction is zero-arg; the CLI maps them to usage exit 2, since its invoker owns the config and the env.) Use config_schema to discover what configuration a model requires.

Example
>>> asr = registry.create("faster-whisper/large-v3", device="cuda")
>>> result = asr.transcribe(audio)

ModelSpec#classSource

class ModelSpec

ModelSpec(
    model_id: str,
    engine_id: str,
    model_name: str,
    entry_point: EntryPoint,
    declared_engine_id: str = '',
)

Metadata for a discovered ASR model entry point.

Note

Instances are created by discover_models(). Use load_factory() to get the callable that constructs the ASR engine.

Attributes
NameTypeDescription
model_idstr

Full routing key (engine_id/model_name), built from the canonical engine id.

engine_idstr

PEP 503 canonical engine identifier and routing identity (for example, faster-whisper). This is the unique engine discriminator.

model_namestr

Model preset name (for example, large-v3), or empty for default.

entry_pointEntryPoint

The underlying importlib.metadata.EntryPoint object.

declared_engine_id
= ''
str

The verbatim engine id as declared in the entry point (for example, faster_whisper, which canonicalizes to the faster-whisper routing engine_id). Kept for diagnostics only; never used for routing. Equals engine_id when already canonical. Note an upper-case form such as Faster_Whisper can never appear here: the declared id is validated before normalization and rejects upper case outright (plugin-entry-points.md naming table), unlike the non-canonical-but-valid _/. separators which are folded.

load_factory#methodSource

def ModelSpec.load_factory() -> ASRFactory

Load the factory callable for this entry point.

Returns
  • ASRFactory

    Callable that creates a StandardASR instance when invoked.

Raises

engine_class#methodSource

def ModelSpec.engine_class() -> type['StandardASR']

Resolve the engine class without instantiating it.

This enables reading class-level ClassVar metadata (declared_capabilities, properties, provider_params_type) without calling the factory -- which the standard requires to be possible "without instantiation or authentication". Instantiating a cloud engine would force credential resolution and a heavy __init__, turning an unauthenticated metadata read into a denial-of-service vector.

The entry-point target is loaded (its module is imported) but never called. Resolution rules:

  • If the target is itself a class, it is returned directly.
  • If the target is a function (the common factory pattern), only its return annotation is resolved and, if it names a concrete class, that class is returned. The resolver deliberately does not evaluate the whole annotation namespace (for example, via typing.get_type_hints): an unrelated parameter carrying an unresolvable forward reference must not turn a static metadata read into a FactoryLoadError (metadata must stay readable without instantiation).
Returns
  • type['StandardASR']

    The engine class declaring the static metadata.

Raises
  • FactoryLoadError

    The target failed to load, or the class cannot be determined without calling the factory (for example, a factory with no concrete return annotation). Callers SHOULD fall back to instantiation only when they explicitly accept that cost.

RuntimeParams#classSource

class RuntimeParams(BaseModel)

Closed per-request parameter container.

Raises
  • ValueError

    If field validation fails.

Attributes
NameTypeDescription
language
= None
str | None

Per-request language (BCP-47 or "auto") overriding the engine default. Gated by <mode>.language.runtime_override.

candidate_languages
= None
list[str] | None

Candidate languages, meaningful only in auto mode. Gated by <mode>.language.candidate_languages.

word_timestamps
= None
WordTimestampGranularity | None

Requested word-timestamp granularity. Gated by <mode>.word_timestamps.

diarization
= None
DiarizationRequest | None

Speaker-diarization request marker; presence = enable (None = not requested). Gated by <mode>.diarization.

prompt
= None
str | None

Free-text guidance prompt. Gated by <mode>.guidance.prompt.

phrase_hints
= None
list[str] | None

Phrase-hint boost terms. Gated by <mode>.guidance.phrase_hints.

on_unsupported
= 'fail'
Literal['fail', 'degrade_to_prompt']

Guidance degradation policy. "fail" (default) keeps the fail-closed contract; "degrade_to_prompt" opts into the one-way rich->prompt fallback (a diagnostic is emitted on degrade).

provider_params
= None
ProviderParams | None

Engine-specific typed parameters, or None.

Segment#classSource

class Segment(BaseModel)

Segment-level detail, shared between batch results and streaming events.

Note

start / end follow the same time frame as Word: non-negative finite float seconds with origin at the first submitted sample (t=0), end >= start (zero-duration allowed), and NaN / Inf rejected -- OR None when the engine measured no such time. None is data, not absence-of-field: the values themselves are the single source of timing truth (there is no side-channel marker), and the legal shapes are pinned by timestamp_status. An end without a start is unrepresentable (rejected at construction): no engine measures where speech stopped without knowing it started.

Ordering: within one channel, MEASURED segments are time-ordered, and the top-level TranscriptionResult.segments with a start are sorted by (start, channel, speaker) (cross-channel spans may overlap; speaker is the final tie-break for equal-(start, channel) overlapping segments, None sorting before any real label). A start=None segment has no time position: the producer keeps the list in READING order instead (list order is the reading order, and TranscriptionResult.text joins segment texts in list order), so a single unmeasured segment never scrambles -- or forces fabricated positions into -- an otherwise real timeline.

Raises
  • ValueError

    If field validation fails (incl. NaN/Inf, a negative time, end < start, or end without start).

Attributes
NameTypeDescription
startfloat | None
ge=0.0

Segment start time in seconds (origin = first submitted sample; non-negative, finite), or None when unmeasured.

endfloat | None
ge=0.0

Segment end time in seconds (non-negative, finite, >= start), or None when unmeasured. Requires start.

textstr

Segment transcript text.

words
= None
list[Word] | None

Optional word-level details for this segment.

speaker
= None
str | None

Optional speaker label (authoritative diarization shape).

channel
= None
int | None
ge=0

Optional channel index for provenance (>= 0).

avg_logprob
= None
float | None

Optional average log-probability.

no_speech_prob
= None
float | None

Optional no-speech probability.

temperature
= None
float | None

Optional decoding temperature.

compression_ratio
= None
float | None

Optional compression-ratio metric.

extra
= dict()
WireExtra

Engine-specific extra data (engine-owned; the standard reserves no keys here).

timestamp_statusLiteral['measured', 'start_only', 'unavailable']

The segment's timing shape, derived from start/end.

Derived, not stored: the nullable values are the single source of truth, so the status can never disagree with them (the previous design stored fabricated 0.0 spans guarded by a mutable side-channel marker -- two truths that could, and did, diverge).

StandardASR#classSource

class StandardASR(Protocol)

Structural protocol for a Standard ASR engine.

Any object exposing these members is a compliant engine, regardless of how it is implemented. The protocol describes the full public surface every engine exposes -- batch (transcribe / transcribe_async) and the streaming entry point (start_transcription). start_transcription is always present; streaming support itself is optional, so a batch-only engine raises UnsupportedFeatureError from it. Because the surface is complete, callers (for example, the server) can type an engine as StandardASR and call the streaming entry point without a cast.

Attributes
NameTypeDescription
propertiesBaseProperties
declared_capabilitiesDeclaredCapabilities
configBaseConfig[str]

The engine's runtime configuration.

Declared as a READ-ONLY property, not a mutable attribute: a mutable protocol member is invariant under strict typing, so a real engine annotating its own subtype (config: WhisperConfig) would not be structurally assignable to StandardASR without a cast -- defeating the protocol's own no-cast promise above. Read-only makes the member covariant (any engine's narrower config satisfies it), and matches intent: config is constructor-injected; callers never reassign it through the protocol. Implementations satisfy this with a plain (even mutable) instance attribute -- no @property required.

transcribe#methodSource

def StandardASR.transcribe(
    audio: AudioInputLike,
    params: RuntimeParams | None = None,
) -> TranscriptionResult

Transcribe a complete audio input.

Parameters
NameTypeDescription
audioAudioInputLike

The audio to transcribe (any AudioInput variant or a coercible bare value).

params
= None
RuntimeParams | None

Per-request runtime parameters.

Returns
Raises
  • ConfigError

    On an invalid language configuration VALUE (default_language malformed or not selectable) -- fixable by whoever supplies the config.

  • EngineContractError

    On an engine-declaration defect -- a declared language axis with no default_language (IC.6), or a malformed declared selectable/detectable tag.

  • IncompatibleAudioInputError

    If no conversion path exists.

  • UnsafeAudioUrlError

    If an AudioUrl fails the SSRF policy.

  • AudioProcessingError

    On a decode / size / missing-sample-rate failure in the conversion pipeline.

  • UnsupportedFeatureError

    In strict mode, on an unsupported parameter, a non-selectable language, or a valid-but-unreachable candidate list (non-detectable candidate / over-max).

  • InvalidProviderParamError

    On wrong provider_params (swap-safety).

  • ValueError

    On a malformed or "auto" candidate-language entry (a caller code bug; raises independent of strict/best_effort).

  • TranscriptionError

    On an engine-execution failure.

transcribe_async#async methodSource

async def StandardASR.transcribe_async(
    audio: AudioInputLike,
    params: RuntimeParams | None = None,
) -> TranscriptionResult

Asynchronously transcribe a complete audio input.

Parameters
NameTypeDescription
audioAudioInputLike

The audio to transcribe (any AudioInput variant or a coercible bare value).

params
= None
RuntimeParams | None

Per-request runtime parameters.

Returns
Raises

start_transcription#methodSource

def StandardASR.start_transcription(
    *,
    audio_format: AudioFormat | None = None,
    params: RuntimeParams | None = None,
    audio: AudioInputLike | None = None,
    deadlines: StreamDeadlines | None = None,
) -> TranscriptionSession

Open a streaming transcription session.

Always present on a compliant engine, but streaming itself is optional: a batch-only engine raises UnsupportedFeatureError here. Callers that need streaming should gate on supports("streaming_input") / supports("streaming_output") (or be ready to handle the unsupported-streaming error).

Parameters
NameTypeDescription
audio_format
= None
AudioFormat | None

Wire format for incremental PCM frames.

params
= None
RuntimeParams | None

Per-request runtime parameters.

audio
= None
AudioInputLike | None

A complete audio input for whole-input streaming output.

deadlines
= None
StreamDeadlines | None

Application overrides for the session's termination deadlines; explicitly set fields win over the engine's construction-time choices.

Returns
Raises
  • ValueError

    If both audio_format and audio are provided, or on a malformed/auto candidate-language entry (a caller code bug; always raises, independent of strict/best_effort).

  • ConfigError

    On an invalid language configuration VALUE (default_language malformed or not selectable).

  • EngineContractError

    On an engine-declaration defect -- a declared language axis with no default_language (IC.6), or a malformed declared selectable/detectable tag.

  • UnsupportedFeatureError

    When streaming (or the requested streaming input/output axis) is unsupported, when the wire format is unreachable, or, in strict mode, on an unsupported parameter or a valid-but-unreachable candidate list (non-detectable / over-max).

  • IncompatibleAudioInputError

    If no conversion path exists for a whole-input streaming audio value.

  • UnsafeAudioUrlError

    If a whole-input AudioUrl fails the SSRF policy.

  • AudioProcessingError

    On a decode / size / missing-sample-rate failure for a whole-input audio value.

  • InvalidProviderParamError

    On wrong provider_params (swap-safety).

  • TranscriptionError

    When a pydantic ValidationError escapes the engine's session-construction hook (an invalid model construction is an engine fault, never a request error).

supports#methodSource

def StandardASR.supports(dot_path: str) -> bool

Return whether the capability at dot_path is supported.

Parameters
NameTypeDescription
dot_pathstr

A capability dot-path.

Returns
  • bool

    True if supported.

recommended_wire_format#methodSource

def StandardASR.recommended_wire_format() -> AudioFormat | None

Return a wire AudioFormat this engine accepts for streaming.

Part of the protocol because it is the documented first step of the streaming journey (README / quickstart / streaming guide all start with it) and the toolchain's sync-bridge runner and gating probes rely on it -- a member every caller is taught to invoke MUST be part of the contract, or a structural engine (and every StandardASR-typed variable) breaks on the standard's own 80% path. The value is purely derivable from the engine's static Properties (see EngineBase.recommended_wire_format for the derivation EngineBase provides for free) -- deliberately capability-blind: whether a bare-frame session can be OPENED is the streaming_input capability gate's job inside start_transcription, so a batch-only or output-only engine still derives a format here (callers gate on supports("streaming_input") first, per the streaming guide).

Returns
  • AudioFormat | None

    A wire format the engine's session-establishment guard accepts, or

  • AudioFormat | None

    None when no bare-frame streaming format can be recommended.

StandardASRError#classSource

class StandardASRError(Exception)

Base class for every domain error the Standard ASR runtime raises.

It does not cover data-model construction, which is pydantic's: a malformed field on RuntimeParams, on a result model, or on a config raises pydantic.ValidationError. That is a ValueError, not a StandardASRError. Plain caller misuse likewise raises a built-in: ValueError for a bad value (two mutually exclusive arguments), TypeError for a wrong type (an unsupported input type).

except (StandardASRError, ValueError) catches the domain errors and the value mistakes. A TypeError stays outside both on purpose -- a wrong input type is a code bug to fix, not a state to handle.

StreamClosedError#classSource

class StreamClosedError(StandardASRError)

Raised when audio is delivered to a streaming session that is closed.

Strictly a lifecycle-close breach: the input side is over, so the audio can no longer be consumed. Covers send_audio after end_audio() and send_audio after the session already delivered a terminal event (the audio queue has no consumer anymore). It does NOT cover usage mistakes against a still-live session (mixing feed with manual input, calling feed twice, or iterating the event stream twice) -- those raise InvalidSessionUseError, so an application can tell "the session ended" apart from "my code drove the session incorrectly".

StreamDeadlines#classSource

class StreamDeadlines(BaseModel)

Application-side overrides for a session's termination deadlines.

Pass to StandardASR.start_transcription(deadlines=...). Only fields you explicitly set are applied; unset fields keep whatever the engine (or the standard default) chose. Precedence: application explicit > engine construction choice > standard default.

The fields mirror the TranscriptionSession deadline parameters -- see there for full semantics. Each accepts None to explicitly disable that deadline.

Attributes
NameTypeDescription
done_timeout
= DEFAULT_DONE_TIMEOUT
float | None
gt=0.0

The pipeline-inactivity hang backstop, reset by events AND by audio consumption.

max_idle
= DEFAULT_MAX_IDLE
float | None
gt=0.0

The opt-in content-stall detector.

max_session_seconds
= DEFAULT_MAX_SESSION_SECONDS
float | None
gt=0.0

The opt-in absolute wall-clock cap.

StructuredError#classSource

class StructuredError(StandardASRError)

StructuredError(
    message: str = '',
    *,
    param: str | None = None,
    hint: str | None = None,
    details: list[dict[str, Any]] | None = None,
)

Base for errors that carry machine-readable context beside the message.

Gives the error half of the contract the same "don't make me parse the message" property the diagnostics have: an application can read .param (the offending field/parameter), .hint (actionable guidance), and .details (structured context -- for example, the sanitized pydantic error entries behind a wrapped config failure) without scraping str(exc). Every context field is optional and keyword-only, so Error("message") keeps working while Error("message", param="base_url", hint="use https") is now valid too -- removing the asymmetry where only some exceptions accepted structured fields (spec: explicit > implicit; structured over stringly typed).

Parameters
NameTypeDescription
message
= ''
str

Human-readable description of the error.

param
= None
str | None

The offending field / parameter name, if applicable.

hint
= None
str | None

Actionable guidance for resolving the error, if any.

details
= None
list[dict[str, Any]] | None

Optional machine-readable context (for example, the sanitized validation-error entries from a wrapped pydantic ValidationError).

SubtitleRenderingError#classSource

class SubtitleRenderingError(StandardASRError, ValueError)

SubtitleRenderingError(
    message: str = '',
    *,
    unrenderable: int | None = None,
    total: int | None = None,
)

Raised by to_srt / to_vtt when segments cannot render as visible cues.

A subtitle cue is an interval claim -- "this text occurs at this time" -- and it must survive the output's millisecond grid to be seen at all. A segment is therefore UNRENDERABLE in either of two ways: it lacks a measured span (Segment.timestamp_status is "start_only" or "unavailable"), or its measured span quantizes to zero milliseconds on the output grid (end and start format to the same timestamp -- players silently drop such cues, so emitting one silently hides the text while the render call reports success). Neither dropping the text nor fabricating timing is the renderer's to choose silently: under the default policy (on_unrenderable="error") it raises this error, and the caller picks the loss explicitly ("omit" drops the unrenderable segments' text from the timed cues; "collapse" renders one whole-text cue with no real timeline). Mixes in ValueError: the caller can fix the call -- choose a policy, or supply renderable segments.

Parameters
NameTypeDescription
message
= ''
str

Human-readable description of the rejection.

unrenderable
= None
int | None

How many segments cannot render as visible cues, if known.

total
= None
int | None

How many segments the result carries, if known.

SyncSession#classSource

class SyncSession

SyncSession(
    session: TranscriptionSession,
    *,
    submit_timeout: float | None = 30.0,
)

Synchronous bridge over an async TranscriptionSession.

Runs a single background event loop in a dedicated thread (owned by this object and torn down on close), so applications can drive an async engine synchronously and authors only ever write async code.

Lifecycle submits (__enter__ / input calls / __exit__) carry a timeout: a hanging engine _open / _close can never deadlock the calling thread, and on every cooperative path the background loop + thread are torn down even on timeout (from an external thread, no deadlock). A truly blocking, non-awaiting engine can survive the join and leave the daemon thread alive with the loop unclosed -- see _shutdown; the sync-bridge compliance check reports that state as a thread leak.

Wrap an async session.

Parameters
NameTypeDescription
sessionTranscriptionSession

The async session to drive.

submit_timeout
= 30.0
float | None

Seconds to wait for a lifecycle submit (enter / feed / send / end / exit) before raising TimeoutError and tearing the loop down. None waits forever (not recommended).

__enter__#methodSource

def SyncSession.__enter__() -> SyncSession

Enter the async session's context.

Exception-safe: a context manager whose __enter__ raises never receives __exit__, so a failed enter MUST tear down the owned loop + thread started in __init__ itself -- otherwise an engine whose _open raises (bad credentials, unreachable host) would leak the bridge's background thread (no leak). The timeout path is already torn down inside _submit; the entered guard also covers a non-timeout raise (_open raising a regular exception).

Returns
Raises
  • TimeoutError

    If the engine _open hangs past submit_timeout.

__exit__#methodSource

def SyncSession.__exit__(*exc: object) -> None

Exit the async context and stop the owned loop.

feed#methodSource

def SyncSession.feed(source: Iterable[bytes] | bytes | bytearray) -> None

Feed audio from a managed source.

Parameters
NameTypeDescription
sourceIterable[bytes] | bytes | bytearray

A sync iterable of byte chunks, or a single bytes / bytearray chunk.

Raises
  • TypeError

    If source is a str (forwarded from the async session: a str is byte-chunks-shaped only by accident -- a whole file goes to start_transcription(audio=...)).

  • InvalidSessionUseError

    If manual input or a prior feed was already used (forwarded from the async session).

  • StreamClosedError

    If the bridge was already torn down (its with block exited, or a prior lifecycle call timed out).

  • TimeoutError

    If the call does not complete within the submit timeout; the bridge is torn down first (no-hang contract).

send_audio#methodSource

def SyncSession.send_audio(chunk: bytes) -> None

Manually send one audio chunk.

Parameters
NameTypeDescription
chunkbytes

The audio chunk.

Raises
  • InvalidSessionUseError

    If feed was already used (forwarded from the async session: mixing input modes).

  • StreamClosedError

    If the input was ended or the session already delivered a terminal event (forwarded from the async session), or the bridge was already torn down (its with block exited, or a prior lifecycle call timed out).

  • TimeoutError

    If the call does not complete within the submit timeout; the bridge is torn down first (no-hang contract).

end_audio#methodSource

def SyncSession.end_audio() -> None

Mark the end of manual audio input.

Raises
  • InvalidSessionUseError

    If feed was already used (forwarded from the async session: mixing input modes).

  • StreamClosedError

    If the bridge was already torn down (its with block exited, or a prior lifecycle call timed out).

  • TimeoutError

    If the call does not complete within the submit timeout; the bridge is torn down first (no-hang contract).

__iter__#methodSource

def SyncSession.__iter__() -> Iterator[TranscriptionEvent]

Iterate events synchronously.

Event waits are unbounded by design: a live, fed session may legitimately go arbitrarily long between events (user silence), so the pump must never manufacture a timeout for it -- a stuck pipeline already surfaces as a terminal event from the async side's own deadlines. The pump waits in short slices purely to detect the two failures those in-loop deadlines cannot report: the owned event-loop thread dying, and the loop being frozen by an engine running blocking (non-async) code. Brief blocking stalls are tolerated (several consecutive unresponsive probes are required), so only a persistently frozen loop tears the bridge down.

Yields
Raises
  • StreamClosedError

    If the bridge was already torn down (its with block exited, or a prior lifecycle call timed out), or if the session was never entered (no event stream exists to pump).

  • TimeoutError

    If the owned event-loop thread died or stayed unresponsive, so no further event (or in-loop deadline) can ever be delivered.

result#methodSource

def SyncSession.result() -> TranscriptionResult

Reduce the session so far into a transcription result.

SERIALIZED WITH THE PRODUCER while the bridge is live: the reduction walks reducer state the producer task mutates (a supersede pops segments mid-walk), so it is submitted to the owned loop like every other bridge member -- asyncio's run-to-completion between awaits is the mutual exclusion, and the wrapper coroutine never awaits around the call. Running it on the caller's thread instead read that state concurrently: a rendering loop (for ev in sync: render(sync. result())) crashed with a spurious KeyError or returned a torn result mixing pre- and post-supersede segments. After teardown (__exit__, or a timed-out lifecycle call) no producer runs anymore, so the direct call is safe -- and keeps the result-after-the-with-block pattern working.

Returns
Raises
  • TimeoutError

    If the live loop cannot run the reduction within the submit timeout (frozen by blocking engine code); the bridge is torn down first (no-hang contract).

diagnostics#methodSource

def SyncSession.diagnostics() -> list[Diagnostic]

Return the session's standard-layer and lifecycle diagnostics.

Mirrors TranscriptionSession.diagnostics so a synchronously- driven session exposes the same parameter-gating / language-resolution / lifecycle-suppression diagnostics as the async one. Without this the sync bridge would silently drop a first-class, compliance-checked part of the session surface (the sync bridge is a faithful mirror of the async session). Serialized with the producer exactly like result (the snapshot copies guard state the producer task appends to); direct once torn down.

Returns
Raises
  • TimeoutError

    If the live loop cannot run the snapshot within the submit timeout (frozen by blocking engine code); the bridge is torn down first (no-hang contract).

is_loop_alive#methodSource

def SyncSession.is_loop_alive() -> bool

Whether the owned background event-loop thread is still running.

The bridge starts a dedicated event-loop thread in __init__ and MUST tear it down on close (__exit__) or on a failed __enter__ (from an external thread). After a clean lifecycle this returns False; a True here once the session is closed is a leaked loop thread. Exposed so the sync-bridge compliance check can assert on this bridge's own thread rather than diffing the whole process thread set (which would mis-flag a dependency's benign daemon thread as a leak).

Returns
  • bool

    True if the owned loop thread is alive.

TranscriptionError#classSource

class TranscriptionError(StructuredError)

TranscriptionError(
    message: str = '',
    *,
    param: str | None = None,
    hint: str | None = None,
    details: list[dict[str, Any]] | None = None,
    retriable: bool | None = None,
)

Raised when an engine fails during batch transcription (the cardinal sin guard).

This is the portable batch error contract: when an engine's model inference, network call, or SDK fails inside _transcribe, the failure MUST surface as a TranscriptionError (with the original exception preserved as __cause__ via raise ... from) so an application can catch one type across every engine instead of each engine's native exception (RuntimeError, an SDK error, requests.HTTPError, and so on). It is the batch counterpart of the streaming error event's engine_error code. It denotes an engine/runtime fault, not a caller mistake (those raise ConfigError / UnsupportedFeatureError / InvalidProviderParamError / AudioProcessingError), so the server maps it to a generic 5xx.

Carries the StructuredError fields plus .retriable: when an engine knows a failure is transient (a 503 / timeout / rate-limit) it MAY pass retriable=True so an application can decide whether to retry. None (the default) means "unknown" -- the safe reading is do not assume it is safe to retry.

Parameters
NameTypeDescription
message
= ''
str

Human-readable description of the failure.

param
= None
str | None

The offending field / parameter name, if applicable.

hint
= None
str | None

Actionable guidance, if any.

details
= None
list[dict[str, Any]] | None

Optional machine-readable context.

retriable
= None
bool | None

True / False if the engine knows whether a retry may succeed; None when unknown.

TranscriptionEvent#classSource

class TranscriptionEvent(BaseModel)

A single streaming transcription event.

Attributes
NameTypeDescription
typeEventType

The event type.

segment_id
= None
str | None

Stable id of the segment this event concerns.

text
= None
str | None

The segment's complete current text (cumulative/replace).

stable_until
= None
int | None

Frozen-prefix length in codepoints (monotonic per segment while recognition is in progress; a terminal closed restatement may shrink it).

finality
= 'final'
Literal['final', 'closed']

For final events, "final" or "closed" (a closed final is the terminal restatement and may rewrite frozen text once).

words
= None
list[Word] | None

Optional word-level detail (shares the batch Word model).

speaker
= None
str | None

Segment-level speaker label, with the same inheritance rule as Segment.speaker (a non-None words[i].speaker overrides it at word level) and the same label-validity rule. A frozen segment's accepted speaker is protected across events by the lifecycle guard, not at construction.

start
= None
float | None
ge=0.0

Segment start time in seconds (origin = first session sample).

end
= None
float | None
ge=0.0

Segment end time in seconds.

audio_processed_until
= None
float | None
ge=0.0

Monotonic audio-time cursor in seconds.

old_ids
= list()
list[str]

For supersede, the retired segment ids.

new_ids
= list()
list[str]

For supersede, the replacement segment ids.

code
= None
str | None

For error, the error code.

recoverable
= None
bool | None

For error, whether the session may continue.

retriable_after
= None
float | None
ge=0.0

For error, suggested retry delay in seconds.

reconnect
= None
bool | None

For progress, whether this marks a reconnect.

gap_start
= None
float | None
ge=0.0

For a reconnect progress, the gap start time.

gap_end
= None
float | None
ge=0.0

For a reconnect progress, the gap end time.

detected_language
= None
str | None

The engine-detected language (BCP-47, never auto), validated like TranscriptionResult.detected_language; sticky per session (the last non-None value wins), and the reconnect-continuity carrier.

extra
= dict()
WireExtra

Engine-specific extra data.

stable_textstr

The frozen prefix of text (text[:stable_until]).

Guards against an invalid (negative or out-of-range) stable_until so a malformed frontier never produces a wrong or oversized prefix.

is_contentbool

Whether this event advances transcription content.

is_terminalbool

Whether this event ends the session.

partial#class methodSource

@classmethod
def TranscriptionEvent.partial(
    segment_id: str,
    text: str,
    **kw: Any,
) -> TranscriptionEvent

Build a partial event.

Parameters
NameTypeDescription
segment_idstr

The segment id.

textstr

The segment's complete current text.

**kw
= {}
Any

Additional event fields.

Returns

final#class methodSource

@classmethod
def TranscriptionEvent.final(
    segment_id: str,
    text: str,
    **kw: Any,
) -> TranscriptionEvent

Build a final event.

Parameters
NameTypeDescription
segment_idstr

The segment id.

textstr

The segment's final text.

**kw
= {}
Any

Additional event fields.

Returns

closed#class methodSource

@classmethod
def TranscriptionEvent.closed(
    segment_id: str,
    text: str,
    **kw: Any,
) -> TranscriptionEvent

Build a closed finality event (a final with finality=closed).

Parameters
NameTypeDescription
segment_idstr

The segment id.

textstr

The segment's possibly post-processed text.

**kw
= {}
Any

Additional event fields.

Returns

supersede#class methodSource

@classmethod
def TranscriptionEvent.supersede(
    old_ids: list[str],
    new_ids: list[str],
    **kw: Any,
) -> TranscriptionEvent

Build a supersede event replacing old segments with new ones.

Lineage is set-to-set: old_ids/new_ids express the cardinality of the re-segmentation (which ids retire, which appear) but not a per-old->per-new mapping. On a merge+split (many->many) a UI cannot tell which specific old segment a given new segment descends from. This is a documented v1 limitation; the spec does not require a pairwise mapping, and the frozen-prefix-preservation invariant is enforced over the concatenated prefixes, not per pair. Per-pair edit-ops/diffs are a possible future direction (additive later).

Parameters
NameTypeDescription
old_idslist[str]

The retired segment ids, in reading (time) order.

new_idslist[str]

The replacement segment ids, in reading (time) order (must be disjoint from old_ids).

**kw
= {}
Any

Additional event fields.

Returns
Raises
  • ValueError

    If old_ids is empty (a supersede MUST retire at least one segment), if old_ids and new_ids intersect, or if either list repeats a segment id.

progress#class methodSource

@classmethod
def TranscriptionEvent.progress(**kw: Any) -> TranscriptionEvent

Build a progress event (heartbeat / cursor / reconnect notice).

Parameters
NameTypeDescription
**kw
= {}
Any

Event fields (for example, audio_processed_until, reconnect).

Returns

done#class methodSource

@classmethod
def TranscriptionEvent.done(**kw: Any) -> TranscriptionEvent

Build a terminal done event.

Parameters
NameTypeDescription
**kw
= {}
Any

Additional event fields.

Returns

make_error#class methodSource

@classmethod
def TranscriptionEvent.make_error(
    code: str,
    *,
    recoverable: bool = False,
    **kw: Any,
) -> TranscriptionEvent

Build an error event.

Parameters
NameTypeDescription
codestr

The error code.

recoverable
= False
bool

Whether the session may continue.

**kw
= {}
Any

Additional event fields.

Returns

TranscriptionResult#classSource

class TranscriptionResult(BaseModel)

The constant-shape result returned by transcribe and stream reduction.

The top-level text / segments / words are always the complete, channel- and speaker-agnostic transcription. For multi-channel audio they are the time-merge of all channels (never channel-0-only), so ignoring channels is always safe and lossless.

Raises
  • ValueError

    If field validation fails (incl. NaN/Inf, a negative duration, or a malformed detected_language).

Attributes
NameTypeDescription
textstr

Full transcript (required).

detected_language
= None
str | None

Detected language as a well-formed BCP-47 tag in auto mode; None when not applicable.

language_confidence
= None
float | None
ge=0.0, le=1.0

Detection confidence in [0, 1].

duration
= None
float | None
ge=0.0

Audio duration in seconds, if known (non-negative, finite).

segments
= None
list[Segment] | None

Segments across all channels, if available. Segments WITH a start SHOULD be sorted by (start, channel, speaker) (monotonic within a channel; speaker is the final tie-break, None sorting first); a start=None segment has no time position, so the list stays in READING order instead (list order is the reading order; text joins segment texts in list order). The ordering is an engine obligation, neither enforced at construction nor checked by the compliance suite (the streaming reducer keeps arrival order whenever any retained segment lacks a start). The SRT/VTT renderers' defensive re-sort of measured cues is the only standard-layer safety net.

words
= None
list[Word] | None

Flattened word-level details, if available.

channels
= None
list[ChannelResult] | None

Per-channel results when channel separation was performed. Each channel index MUST be unique (one entry per channel), enforced at construction.

diagnostics
= lambda: cast('list[Diagnostic]', [])()
list[Diagnostic]

Conversion / best_effort / degradation diagnostics.

extra
= dict()
WireExtra

Engine-specific / experimental data (incl. provider formats).

TranscriptionSession#classSource

class TranscriptionSession(ABC)

TranscriptionSession(
    *,
    done_timeout: float | None = DEFAULT_DONE_TIMEOUT,
    max_idle: float | None = DEFAULT_MAX_IDLE,
    max_session_seconds: float | None = DEFAULT_MAX_SESSION_SECONDS,
    event_buffer_capacity: int = DEFAULT_EVENT_BUFFER_CAPACITY,
    audio_queue_maxsize: int = DEFAULT_AUDIO_QUEUE_MAXSIZE,
    audio_history_maxlen: int = DEFAULT_AUDIO_HISTORY_MAXLEN,
    strict_lifecycle: bool = False,
    max_guard_diagnostics: int = DEFAULT_MAX_GUARD_DIAGNOSTICS,
)

Async-first, full-duplex streaming session base.

Authors implement _produce (and optionally _open / _close), reading fed audio via audio_chunks and yielding TranscriptionEvent objects. The base manages input ownership, bounded backpressure, lifecycle enforcement, a bounded rolling audio buffer

  • reconnect scaffolding, termination deadlines, and result reduction.

Reconnect contract -- base vs engine:

  • The base owns a bounded rolling audio buffer (the most recent fed chunks) and exposes replay_buffer, the source replayable classification, and note_reconnect. It cannot detect a reconnect itself (it owns no network connection).
  • The engine detects the disconnect, re-establishes the connection, replays replay_buffer audio, keeps segment_id / timestamps / detected language / speaker-label mapping continuous, then calls note_reconnect. For speaker labels the safe default is mint-fresh: the engine MUST NOT reuse a pre-reconnect label for a post-reconnect cluster without identity evidence (blind clustering restarts from zero after a reconnect) -- it MUST mint fresh labels (speaker_2, speaker_3, and so on) and emit a speaker_labels_reset diagnostic via emit_diagnostic -- the fidelity-warning counterpart of content_lost, which is an error event -- because over-counting speakers is the safe direction while silently merging two people under one label is not. The base then emits the progress(reconnect=True, gap_start, gap_end) event and, iff the engine passed content_lost=True (its own determination that the reconnect + replay could not cover the gap), a trailing error(code="content_lost", recoverable=True) fidelity warning.

Initialize the session.

Parameters
NameTypeDescription
done_timeout
= DEFAULT_DONE_TIMEOUT
float | None

Seconds of total pipeline inactivity -- no event arriving AND no fed audio consumed via audio_chunks -- before synthesizing a done_timeout error. A hang backstop, not engine-liveness detection: a silently listening engine stays alive while it keeps consuming audio. After end_audio() it bounds the engine's flush-and-done window. None disables (explicit opt-out of the backstop).

max_idle
= DEFAULT_MAX_IDLE
float | None

Seconds without a content event (partial / final / supersede) before force-terminating with a stream_stalled error. NOT reset by progress heartbeats or audio consumption, so it detects an engine that consumes (or chats) without ever producing content. None (the default) disables: silence is a normal state for a live session.

max_session_seconds
= DEFAULT_MAX_SESSION_SECONDS
float | None

Absolute wall-clock cap; None disables.

event_buffer_capacity
= DEFAULT_EVENT_BUFFER_CAPACITY
int

Pending-event budget shared by every event kind -- drop-proof final / supersede / error / done slots consume it too, and a not-yet-delivered segment can hold two slots at once (its declaration partial plus its final). A new-segment partial or progress heartbeat arriving once the budget is spent overflows, which the session reports as a terminal backpressure error.

audio_queue_maxsize
= DEFAULT_AUDIO_QUEUE_MAXSIZE
int

Max pending audio chunks; bounds feed / send_audio so a slow engine exerts real backpressure.

audio_history_maxlen
= DEFAULT_AUDIO_HISTORY_MAXLEN
int

Capacity of the bounded rolling audio buffer used to replay recent audio after a reconnect.

strict_lifecycle
= False
bool

If True, raise on illegal lifecycle transitions instead of suppressing + diagnosing them.

max_guard_diagnostics
= DEFAULT_MAX_GUARD_DIAGNOSTICS
int

Cap on the bounded lifecycle-suppression diagnostics channel; further diagnostics are aggregated into a single overflow summary rather than growing without bound. Exposed alongside the other bounds so a session can size it; defaults to DEFAULT_MAX_GUARD_DIAGNOSTICS.

Raises
  • ValueError

    If a deadline is not positive (or None where allowed) or a buffer/queue bound is not positive. In particular audio_queue_maxsize=0 would mean an UNBOUNDED asyncio.Queue -- silently disabling the documented feed backpressure -- so it is rejected rather than passed through. Also if max_guard_diagnostics is not positive.

Attributes
NameTypeDescription
replayablebool

Whether the audio source can be re-read after a reconnect.

done_timeoutfloat | None

The configured pipeline-inactivity backstop in seconds.

max_idlefloat | None

The configured content-stall deadline in seconds.

max_session_secondsfloat | None

The configured absolute wall-clock cap in seconds.

audio_chunks#async methodSource

async def TranscriptionSession.audio_chunks() -> AsyncIterator[bytes]

Async-iterate fed audio chunks until the input ends.

Each yielded chunk is also retained in the bounded rolling audio buffer for possible replay after a reconnect.

Consuming from this iterator is the session's liveness anchor: every dequeue resets the done_timeout backstop, so an engine that legitimately emits no events during user silence stays alive for as long as it keeps reading audio here.

Yields
  • AsyncIterator[bytes]

    Raw audio chunks in the session's declared format.

emit_diagnostic#methodSource

def TranscriptionSession.emit_diagnostic(
    *,
    code: str,
    message: str,
    level: Literal['info', 'warning'] = 'info',
    param: str | None = None,
    provided: JsonValue | BaseModel = None,
    effective: JsonValue | BaseModel = None,
) -> None

Surface a structured diagnostic from _produce (the streaming channel).

The streaming counterpart of the batch path's result.diagnostics: call this from your _produce to report a non-fatal note -- a best-effort degradation, an assumed parameter, a lossy fallback -- through the session's own diagnostics channel, so a WS client (mid-stream diagnostics frame) or a sync caller (diagnostics()) sees WHY. For a fatal condition, emit an error event instead -- this is for non-fatal notes that must not end the stream.

Recorded through the same bounded channel as the guard's own diagnostics: past the cap entries aggregate into the overflow summary rather than growing without bound, so a chatty producer cannot exhaust memory.

Security -- this is engine-authored, client-facing output, like the transcript text itself. Every field (message / param / provided / effective) is forwarded to clients VERBATIM -- to a (possibly unauthenticated) WebSocket client as a diagnostics frame, and, for the batch counterpart result.diagnostics, in the REST response -- and is NOT redacted. The standard layer scrubs only the detail IT auto-captures (an error event's extra, which holds a scrubbed exception summary -- never a raw str(exc)); content you pass here is your contract to keep. NEVER pass a credential, API key, a URL with embedded auth, or raw exception text -- route sensitive operator detail to logging instead. (That asymmetry is deliberate: an error event's extra is auto-captured so the server drops it; this note you chose, so you own its safety.)

Parameters
NameTypeDescription
codestr

Stable, machine-readable diagnostic code (for example, "vad_fallback").

messagestr

Human-readable explanation (client-facing; no secrets).

level
= 'info'
Literal['info', 'warning']

"info" or "warning" (default "info").

param
= None
str | None

The parameter the diagnostic concerns, if any.

provided
= None
JsonValue | BaseModel

The value the application provided, if relevant (client-facing). A pydantic model is projected into its JSON form via to_json_value -- the same projection the standard's own diagnostics apply -- so passing a request submodel just works instead of raising out of a live _produce.

effective
= None
JsonValue | BaseModel

The value that took effect, if relevant (client-facing; projected like provided).

Raises
  • pydantic.ValidationError

    If provided / effective has no JSON form even after projection (for example, an arbitrary in-process object) -- an engine bug surfaced loudly at the call site, naming the field, instead of failing later in the transport. Raising from _produce terminates the session, so keep such objects in your own state (see the docs' wire-visible values section).

replay_buffer#methodSource

def TranscriptionSession.replay_buffer() -> list[bytes]

Return audio for re-feeding to a freshly re-established connection.

Engines call this on reconnect to re-send audio. For a replayable (list / tuple) source the COMPLETE source is returned -- replayability promises loss-free replay, so it must not be silently truncated to the rolling ring. For a non-replayable / live source only the bounded rolling window of recent chunks is available (older audio was evicted).

Returns
  • list[bytes]

    The audio chunks to replay, oldest first: the full source if

  • list[bytes]

    replayable, otherwise the bounded rolling window.

note_reconnect#methodSource

def TranscriptionSession.note_reconnect(
    gap_start: float | None = None,
    gap_end: float | None = None,
    *,
    content_lost: bool = False,
) -> None

Record that an internal reconnect bridged a gap (engine-driven).

The base ALWAYS emits a progress(reconnect=True, gap_start, gap_end) event in order with produced events, followed -- IMMEDIATELY -- by a trailing error(code="content_lost", recoverable=True, gap_start, gap_end) IFF the engine passes content_lost=True.

The events are flushed promptly, the moment this is called, through the drop-proof path (_CoalescingBuffer.put_forced, the same sync primitive the base uses for synthesized terminals). This matters because an engine typically calls note_reconnect and then BLOCKS while it re-establishes the connection without yielding another event; deferring the flush to the next produced event would leave the consumer staring at a timeout/silence during a slow reconnect -- the opposite of "transparent but honest". note_reconnect runs in the producer-task coroutine on the session loop, so a synchronous flush is correct and ordered (the events land after already-emitted events and before any subsequent one). The _run_producer / finally drains remain as a harmless safety net -- they simply find nothing pending.

Content loss is an explicit engine determination, not something the base infers from rolling-buffer eviction: a live ring is always evicting, so eviction is the wrong signal (it would falsely claim permanent loss on every long live session). The engine -- which alone knows whether its reconnect + replay_buffer replay actually covered the gap -- sets content_lost=True only when audio the engine had not yet processed could not be replayed and is therefore truly lost. This mirrors the existing contract where segment_id / timestamps / detected language / speaker-label mapping continuity across the reconnect is likewise the engine's responsibility (the base never rewrites them). For speaker labels the engine MUST NOT reuse a pre-reconnect label without identity evidence; the safe default is to mint fresh labels and emit a speaker_labels_reset diagnostic via emit_diagnostic (the fidelity-warning counterpart of the content_lost error event).

Parameters
NameTypeDescription
gap_start
= None
float | None

Start time (seconds) of the lossy gap, if known.

gap_end
= None
float | None

End time (seconds) of the lossy gap, if known.

content_lost
= False
bool

True if the reconnect could not cover the gap and unreplayable audio was permanently lost; queues a non-terminal content_lost fidelity warning after the progress.

feed#methodSource

def TranscriptionSession.feed(
    source: Iterable[bytes] | AsyncIterable[bytes] | bytes | bytearray,
) -> None

Feed audio from a managed source (mutually exclusive with manual).

A bare bytes / bytearray is treated as a single audio chunk (not an iterable of chunks -- iterating it would yield int byte values). A non-async, re-iterable collection (list / tuple, or a wrapped bytes-like) is classified as replayable for reconnect purposes; an async source or a one-shot generator/iterator is non-replayable. Any AsyncIterable (the Pythonic __aiter__ protocol, not only the stricter AsyncIterator) is accepted and normalized via aiter.

Parameters
NameTypeDescription
sourceIterable[bytes] | AsyncIterable[bytes] | bytes | bytearray

A sync or async iterable of byte chunks (Iterable / AsyncIterable of bytes), or a single bytes / bytearray chunk.

Raises
  • TypeError

    If source is a str. A str satisfies Iterable[str] and would be silently consumed one character at a time (or fail deep inside an engine as a confusing engine_error); passing a file path here is a common slip. A whole audio file goes to start_transcription(audio=...) and incremental input is raw PCM byte chunks.

  • InvalidSessionUseError

    If manual input was already used (mixing) or feed was already called once -- a usage error against a still-live session, NOT a lifecycle close.

  • TypeError

    If a subclass rebound a reserved base attribute.

send_audio#async methodSource

async def TranscriptionSession.send_audio(chunk: bytes) -> None

Manually send one audio chunk (mutually exclusive with feed).

Manual sources are always treated as non-replayable (live input).

Parameters
NameTypeDescription
chunkbytes

The audio chunk.

Raises
  • InvalidSessionUseError

    If feed was already used (mixing input modes is a usage error against a still-live session).

  • StreamClosedError

    If the input was ended (end_audio()) or the session already delivered a terminal event -- a genuine lifecycle close: the audio queue has no consumer anymore, so raising beats blocking forever on a dead queue.

  • TypeError

    If a subclass rebound a reserved base attribute.

end_audio#async methodSource

async def TranscriptionSession.end_audio() -> None

Mark the end of manual audio input (idempotent in manual mode).

Claims manual ownership if this is the first input call, so a later feed is correctly rejected as mixing.

Raises
  • InvalidSessionUseError

    If feed was used (mixing input modes is a usage error against a still-live session).

  • TypeError

    If a subclass rebound a reserved base attribute.

__aenter__#async methodSource

async def TranscriptionSession.__aenter__() -> TranscriptionSession

Open resources and start the producer.

Anchors the max_session_seconds wall-clock origin here, at session establishment, so the absolute cap measures from when the session opened -- not from the first __anext__ -- and a re-entered iterator cannot reset it.

Returns
Raises
  • TypeError

    If a subclass rebound a reserved base attribute.

__aexit__#async methodSource

async def TranscriptionSession.__aexit__(*exc: object) -> None

Tear down the producer, feed task, and engine resources.

Cancels the producer and feed tasks and AWAITS them (so no coroutine is still touching engine state when _close runs), then closes.

__aiter__#methodSource

def TranscriptionSession.__aiter__() -> AsyncIterator[TranscriptionEvent]

Return the event async iterator (single-consumer).

A session has exactly ONE event stream and exactly ONE consumer: the events live in a shared bounded buffer, and a second concurrent iterator would race the first for _CoalescingBuffer.get, silently splitting the stream between them (each iterator would see an arbitrary subset, so neither's result would match the stream -- breaking the stream == result invariant). Re-iterating would also reset the per-iteration deadline anchors. Both are programming errors, so the second call fails loudly instead of returning a competing iterator.

Returns
Raises
  • InvalidSessionUseError

    If the session is already being iterated -- a usage error against a still-live session, not a lifecycle close.

  • TypeError

    If a subclass rebound a reserved base attribute.

diagnostics#methodSource

def TranscriptionSession.diagnostics() -> list[Diagnostic]

Return the diagnostics accumulated for this session so far.

The lifecycle-suppression channel is bounded: a misbehaving engine that trips a clamp on (nearly) every event cannot grow this list without limit over a long session. Once DEFAULT_MAX_GUARD_DIAGNOSTICS entries accumulate, further suppressions are aggregated into a single trailing diagnostics_truncated summary (per-code counts) instead of being retained individually -- so the overflow is reported honestly, never silently dropped.

Returns
  • list[Diagnostic]

    The standard-layer parameter-gating / language diagnostics attached

  • list[Diagnostic]

    at session establishment, followed by the runtime's

  • list[Diagnostic]

    lifecycle-suppression diagnostics (suppressed illegal transitions or

  • list[Diagnostic]

    clamped stable_until values), capped as described above.

result#methodSource

def TranscriptionSession.result() -> TranscriptionResult

Reduce the session so far into a transcription result.

Returns

UnsafeAudioUrlError#classSource

class UnsafeAudioUrlError(IncompatibleAudioInputError)

UnsafeAudioUrlError(
    url: str,
    reason: str,
)

An AudioUrl failed the SSRF security policy and MUST NOT be forwarded.

Raised before a URL is handed to an engine when the URL fails the SSRF policy: a malformed URL or port, a non-HTTPS scheme, or a missing host. Unless allow_private_addresses opts out, it also covers a host that does not resolve, and any resolved address (in whole or in part) that is not globally routable -- private / loopback / link-local and their relatives. Under that opt-in the validator returns after the structural checks and never resolves the host, so an unresolvable name reaches the engine and fails there instead. Subclasses IncompatibleAudioInputError so existing audio-input error handling catches it, while remaining distinguishable.

Parameters
NameTypeDescription
urlstr

The offending URL.

reasonstr

A human-readable explanation of why it was rejected.

UnsupportedFeatureError#classSource

class UnsupportedFeatureError(StructuredError)

UnsupportedFeatureError(
    message: str,
    *,
    param: str | None = None,
    mode: str | None = None,
    hint: str | None = None,
)

Raised in strict mode when a requested standard feature is unsupported.

In best_effort mode the unsupported parameter is ignored and a structured diagnostic is returned instead of raising. The strict path carries the same structured context as that diagnostic so callers can inspect which feature was rejected without parsing the message.

Parameters
NameTypeDescription
messagestr

Human-readable description of the rejection.

param
= None
str | None

The offending standard parameter name, if applicable.

mode
= None
str | None

The mode ("batch" / "streaming") the rejection occurred in, if applicable.

hint
= None
str | None

Actionable guidance for resolving the rejection, if any.

Word#classSource

class Word(BaseModel)

Word-level detail, shared between batch results and streaming events.

Note

Time is measured in float seconds with the origin at the first submitted sample (audio time t=0), the same origin as the streaming cursor. start / end are therefore non-negative finite floats and end >= start (a zero-duration span is allowed). NaN / Inf are rejected (allow_inf_nan=False). Engines convert ms / protobuf-duration / ticks into this frame; a negative or inverted span is an engine bug, so the model refuses to represent one rather than let it surface as a silent wrong timestamp downstream.

Raises
  • ValueError

    If field validation fails (incl. NaN/Inf, a negative time, or end < start).

Attributes
NameTypeDescription
startfloat
ge=0.0

Word start time in seconds (origin = first submitted sample; non-negative, finite).

endfloat
ge=0.0

Word end time in seconds (non-negative, finite, >= start).

textstr

Word text.

probability
= None
float | None
ge=0.0, le=1.0

Optional confidence in [0, 1].

logprob
= None
float | None

Optional log-probability (kept separate from probability).

speaker
= None
str | None

Optional speaker label.

channel
= None
int | None
ge=0

Optional channel index for provenance (>= 0).

extra
= dict()
WireExtra

Engine-specific extra data.

WordTimestampGranularity#classSource

class WordTimestampGranularity(str, Enum)

Granularity for requested word timestamps.

The member values are the single source of truth shared with the declaration-side capability vocabulary WordTimestampGranularityName (a Literal). A module-level assertion (below) and a drift test bind the two sets so an additive change to one cannot silently desync the other.

Attributes
NameTypeDescription
WORD
= 'word'

Word-level timestamps.

SEGMENT
= 'segment'

Segment-level timestamps.

CHAR
= 'char'

Character-level timestamps (reserved, additive).

discover_models#functionSource

def discover_models(
    eps: Iterable[EntryPoint] | None = None,
    *,
    strict: bool = False,
    on_conflict: str = 'warn_keep_first',
) -> ModelRegistry

Discover all installed ASR plugins and return a registry.

This is the main entry point for the plugin discovery system. It scans the standard_asr.models entry point group and returns a ModelRegistry containing all discovered ASR engine factories.

Parameters
NameTypeDescription
eps
= None
Iterable[EntryPoint] | None

Custom entry points for testing. Leave None for normal discovery.

strict
= False
bool

If True, raise on invalid entry points. Default: False (warn only).

on_conflict
= 'warn_keep_first'
str

How to handle duplicate model keys:

  • "warn_keep_first": Keep first, warn about duplicates (default).
  • "replace": Use latest, warn about replacement.
Returns
Raises
Example
>>> from standard_asr import discover_models
>>> registry = discover_models()
>>> print(registry.names())
['faster-whisper/large-v3', 'whisper/base', ...]
>>> asr = registry.create("faster-whisper/large-v3")

to_srt#functionSource

def to_srt(
    result: TranscriptionResult,
    *,
    include_speakers: bool = False,
    on_unrenderable: UnrenderablePolicy = 'error',
) -> str

Render a transcription result as SRT.

Cue text is sanitized so it cannot forge cue structure (line terminators normalized, interior blank-line runs collapsed). Unlike to_vtt, SRT has no character-reference mechanism, so & and angle brackets in transcript text are emitted verbatim: an engine-leaked <unk> token or <i> is passed through as-is. Most SRT players render angle-bracket text literally, but some interpret a subset of HTML-like tags; if a downstream consumer must neutralize tags, do so on the transcript text before rendering. (WebVTT, which mandates escaping, is handled by to_vtt.)

Speaker rendering: SRT has no speaker syntax, so opting in mutates the cue text itself -- each labeled cue is prefixed with [<label>]: . The default is False on text-purity grounds (the renderer is a projection of the transcript; injecting labels uninvited would surprise every consumer that treats the cue text as speech), not for backward compatibility. Cues whose speaker is None are rendered unchanged; empty-text segments are skipped even when labeled (a label with no payload is not a cue).

Unrenderable segments: a segment without a measured span, or whose measured span quantizes to zero milliseconds on the output grid (its T --> T cue would be silently dropped by players), cannot render as a visible cue. By default that raises SubtitleRenderingError -- the render never silently drops, hides, or fabricates -- and the caller opts into a loss explicitly: on_unrenderable="omit" renders only the renderable cues (the other segments' text is absent from the file), "collapse" renders one synthetic whole-text cue [0, duration] (or [0, 3 s] when duration is unknown or quantizes to zero milliseconds; the fallback exists for the same player behavior). result.segments is None (segmentation not requested/applicable) uses the same whole-text fallback under every policy; segments == [] (requested but empty, for example, silence) ALWAYS yields no cues. The synthetic cue carries no speaker label -- a whole-text cue has no single attributable speaker -- so include_speakers has no effect on it. Pass a fully renderable result for time-accurate (and speaker-attributed) subtitles.

Parameters
NameTypeDescription
resultTranscriptionResult

The transcription result to render.

include_speakers
= False
bool

Render Segment.speaker labels as [<label>]: cue-text prefixes. Default False (pure transcript text).

on_unrenderable
= 'error'
UnrenderablePolicy

Policy for segments that cannot render as visible cues (unmeasured span, or a span that quantizes to zero milliseconds): "error" (default) raises; "omit" keeps only renderable cues; "collapse" renders one whole-text cue.

Returns
  • str

    The SRT document as a string.

Raises
  • SubtitleRenderingError

    Under the default "error" policy, when any segment cannot render as a visible cue.

  • ValueError

    If on_unrenderable is not one of the three policies (see _cues).

to_vtt#functionSource

def to_vtt(
    result: TranscriptionResult,
    *,
    include_speakers: bool = False,
    on_unrenderable: UnrenderablePolicy = 'error',
) -> str

Render a transcription result as WebVTT.

Cue text is escaped per the W3C WebVTT cue-text grammar (& -> &amp;, < -> &lt;, > -> &gt;) so payload text -- including engine-leaked <unk> / <|...|> tokens or "AT&T" -- is shown verbatim instead of being silently dropped by the browser's cue-span tokenizer. Cue structure is also protected (line terminators normalized, blank-line runs collapsed, --> neutralized by the > escape).

Speaker rendering: opting in wraps the whole cue body of each labeled cue in WebVTT's native voice span, <v <label>>. The default is False on text-purity grounds (the renderer is a projection of the transcript), not for backward compatibility. Cues whose speaker is None are rendered unchanged; empty-text segments are skipped even when labeled.

Unrenderable segments: identical policy contract to to_srt -- a segment without a measured span, or whose span quantizes to zero milliseconds on the output grid, raises by default (SubtitleRenderingError), and the caller opts into "omit" (renderable cues only) or "collapse" (one synthetic whole-text cue) explicitly. segments is None uses the whole-text fallback under every policy; segments == [] always yields zero cues; the synthetic cue carries no speaker label.

Parameters
NameTypeDescription
resultTranscriptionResult

The transcription result to render.

include_speakers
= False
bool

Render Segment.speaker labels as <v <label>> voice spans wrapping the cue body. Default False (pure transcript text).

on_unrenderable
= 'error'
UnrenderablePolicy

Policy for segments that cannot render as visible cues (unmeasured span, or a span that quantizes to zero milliseconds): "error" (default) raises; "omit" keeps only renderable cues; "collapse" renders one whole-text cue.

Returns
  • str

    The WebVTT document as a string.

Raises
  • SubtitleRenderingError

    Under the default "error" policy, when any segment cannot render as a visible cue.

  • ValueError

    If on_unrenderable is not one of the three policies (see _cues).

On this page