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, TranscriptionResultFor 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.
| Name | Type | Description |
|---|---|---|
samples | NDArray[np.floating] | Waveform samples. Canonical form is |
sample_rate= None | int | None | Sample rate in Hz, or |
__post_init__#methodSource
def AudioArray.__post_init__() -> NoneReject 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.
TypeErrorIf
samplesdoes not have a floating dtype.ValueErrorIf
sample_rateis notNoneand not strictly positive.
AudioBase64#classSource
class AudioBase64
AudioBase64(
value: str,
)Base64-encoded (or data-URI) encoded audio.
| Name | Type | Description |
|---|---|---|
value | str | Base64 string or |
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.
| Name | Type | Description |
|---|---|---|
data | bytes | Encoded audio bytes (for example, the contents of an MP3/WAV file). |
container= None | str | None | Optional container/format hint (for example, |
AudioFormat#classSource
class AudioFormat(BaseModel)Declared wire format for raw PCM frames fed to a streaming session.
ValueErrorIf validation fails.
| Name | Type | Description |
|---|---|---|
encoding | str | Wire encoding of the PCM frames (for example, |
sample_rate | intgt=0 | Sample rate of the frames in Hz. |
channels= 1 | intgt=0 | Number of interleaved channels. Defaults to |
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.
| Name | Type | Description |
|---|---|---|
value | str | 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.
| Name | Type | Description |
|---|---|---|
value | str | The storage URI, for example, |
ValueErrorIf the URI is empty, has no scheme, or uses a scheme outside
STORAGE_URI_SCHEMES.
__post_init__#methodSource
def AudioStorageUri.__post_init__() -> NoneValidate the URI scheme against the storage-scheme allowlist.
ValueErrorIf 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.
| Name | Type | Description |
|---|---|---|
value | str | The remote URL. |
ChannelResult#classSource
class ChannelResult(BaseModel)Per-channel transcription for multi-channel audio.
ValueErrorIf field validation fails.
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
ConfigErroris 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 aConfigErrorreaching the server -- at construction, transcription, or session establishment -- is a deployment-side fault and maps to a scrubbed 500 (WSinternal_error); the client-fixable rejections have their own types (UnsupportedFeatureError-> 422, request validation -> 422). SeeConfigurationRequiredErrorfor 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.
| Name | Type | Description |
|---|---|---|
level= 'info' | Literal['info', 'warning'] | Severity, |
code | str | Stable machine-readable code (for example, |
message | str | 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.
ValueErrorIf 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
StandardASRmember (transcribe/start_transcription/supports/recommended_wire_format/prepare) returned an awaitable (anasync defimplementation, or a sync wrapper delegating to one) or a value outside its protocol-pinned return type. Raised bystandard_asr.runtime.protocol_boundary.require_sync_resultat the consumer call sites (CLI, reference server) so the defect is loud at the boundary instead of surfacing as a confusing secondaryAttributeError(or a silent misreading) deep inside another subsystem. - Declaration shape: the engine DECLARED something the contract
forbids -- a
preparethat is a coroutine function, non-callable, or parameter-requiring; a malformedselectable_languages/detectable_languagestag; a language axis without the IC.6default_languageobligation. No caller-side value can fix these, which is what separates them fromConfigError(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).
| Name | Type | Description |
|---|---|---|
provided | str | Human-readable description of the provided input shape. |
accepted | object | The engine's accepted input kinds. |
hint | str | 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 manualsend_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).
| Name | Type | Description |
|---|---|---|
specs | Mapping[str, ModelSpec] | Mapping of |
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. |
| Name | Type | Description |
|---|---|---|
shadowed_engine_ids | set[str] | Engine ids provided by more than one distribution. |
names#methodSource
def ModelRegistry.names() -> list[str]List all discovered model keys, sorted alphabetically.
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.
| Name | Type | Description |
|---|---|---|
engine_id | str | Engine identifier (for example, |
list[str]List of matching model keys, sorted alphabetically.
>>> registry.keys_by_engine("faster-whisper")
['faster-whisper/', 'faster-whisper/large-v3', 'faster-whisper/small']spec#methodSource
def ModelRegistry.spec(name: str) -> ModelSpecGet metadata for a model.
| Name | Type | Description |
|---|---|---|
name | str | Model key in |
ModelSpecModelSpeccontaining entry point metadata.
EntrypointValidationErrorModel not found or invalid name format.
get_factory#methodSource
def ModelRegistry.get_factory(name: str) -> ASRFactoryGet the factory callable for a model (without instantiating).
| Name | Type | Description |
|---|---|---|
name | str | Model key in |
ASRFactoryCallable that creates a
StandardASRinstance.
EntrypointValidationErrorModel not found.
FactoryLoadErrorEntry point failed to load.
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.
| Name | Type | Description |
|---|---|---|
name | str | Model key in |
type['StandardASR']The engine class.
EntrypointValidationErrorModel not found.
FactoryLoadErrorEntry point failed to load, or the class cannot be determined without calling the factory.
config_schema#methodSource
def ModelRegistry.config_schema(name: str) -> dict[str, Any] | NoneReturn 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.
| Name | Type | Description |
|---|---|---|
name | str | Model key in |
dict[str, Any] | NoneThe config JSON Schema, or
Nonewhen the engine does notdict[str, Any] | Nonedeclare a class-level
config_type.
EntrypointValidationErrorModel not found.
FactoryLoadErrorEntry point failed to load, the engine class cannot be determined without calling the factory,
config_typeis not aBaseConfigsubclass, or its JSON Schema cannot be generated.
create#methodSource
def ModelRegistry.create(name: str, /, *args: Any, **kwargs: Any) -> StandardASRCreate an ASR engine instance.
This is the primary method for instantiating ASR engines. It loads the factory and invokes it with the provided arguments.
| Name | Type | Description |
|---|---|---|
name | str | Model key (for example, |
*args= () | Any | Positional arguments passed to the factory. |
**kwargs= {} | Any | Keyword arguments passed to the factory (for example, |
StandardASRStandardASRinstance ready for transcription.
EntrypointValidationErrorModel not found.
FactoryLoadErrorEntry point failed to load or is not callable.
ConfigErrorThe model needs configuration that was missing or invalid (a required credential, a bad
default_language, and so on). A construction-time pydanticValidationError-- whether from the bare constructor or an engine's own validator -- is wrapped intoConfigErrorwith the offending input scrubbed, so a caller canexcept ConfigErroruniformly. The wrap ASSERTS the type's ownership contract: a factory's construction-timeValidationErrormeans the supplied configuration was rejected (the documented bare-constructor pattern). An engine whose factory lets a NON-config internalValidationErrorescape 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.) Useconfig_schemato discover what configuration a model requires.
>>> 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.
| Name | Type | Description |
|---|---|---|
model_id | str | Full routing key ( |
engine_id | str | PEP 503 canonical engine identifier and routing identity
(for example, |
model_name | str | Model preset name (for example, |
entry_point | EntryPoint | The underlying |
declared_engine_id= '' | str | The verbatim engine id as declared in the entry
point (for example, |
load_factory#methodSource
def ModelSpec.load_factory() -> ASRFactoryLoad the factory callable for this entry point.
ASRFactoryCallable that creates a
StandardASRinstance when invoked.
FactoryLoadErrorEntry point failed to load or is not callable.
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 aFactoryLoadError(metadata must stay readable without instantiation).
type['StandardASR']The engine class declaring the static metadata.
FactoryLoadErrorThe 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.
ValueErrorIf field validation fails.
| Name | Type | Description |
|---|---|---|
language= None | str | None | Per-request language (BCP-47 or |
candidate_languages= None | list[str] | None | Candidate languages, meaningful only in |
word_timestamps= None | WordTimestampGranularity | None | Requested word-timestamp granularity. Gated by
|
diarization= None | DiarizationRequest | None | Speaker-diarization request marker; presence = enable
( |
prompt= None | str | None | Free-text guidance prompt. Gated by |
phrase_hints= None | list[str] | None | Phrase-hint boost terms. Gated by
|
on_unsupported= 'fail' | Literal['fail', 'degrade_to_prompt'] | Guidance degradation policy. |
provider_params= None | ProviderParams | None | Engine-specific typed parameters, or |
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.
ValueErrorIf field validation fails (incl. NaN/Inf, a negative time,
end < start, orendwithoutstart).
| Name | Type | Description |
|---|---|---|
start | float | Nonege=0.0 | Segment start time in seconds (origin = first submitted sample;
non-negative, finite), or |
end | float | Nonege=0.0 | Segment end time in seconds (non-negative, finite, |
text | str | 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 | Nonege=0 | Optional channel index for provenance ( |
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_status | Literal['measured', 'start_only', 'unavailable'] | The segment's timing shape, derived from 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 |
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.
| Name | Type | Description |
|---|---|---|
properties | BaseProperties | |
declared_capabilities | DeclaredCapabilities | |
config | BaseConfig[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 ( |
transcribe#methodSource
def StandardASR.transcribe(
audio: AudioInputLike,
params: RuntimeParams | None = None,
) -> TranscriptionResultTranscribe a complete audio input.
| Name | Type | Description |
|---|---|---|
audio | AudioInputLike | The audio to transcribe (any |
params= None | RuntimeParams | None | Per-request runtime parameters. |
TranscriptionResultThe transcription result.
ConfigErrorOn an invalid language configuration VALUE (
default_languagemalformed or not selectable) -- fixable by whoever supplies the config.EngineContractErrorOn an engine-declaration defect -- a declared language axis with no
default_language(IC.6), or a malformed declared selectable/detectable tag.IncompatibleAudioInputErrorIf no conversion path exists.
UnsafeAudioUrlErrorIf an
AudioUrlfails the SSRF policy.AudioProcessingErrorOn a decode / size / missing-sample-rate failure in the conversion pipeline.
UnsupportedFeatureErrorIn strict mode, on an unsupported parameter, a non-selectable
language, or a valid-but-unreachable candidate list (non-detectable candidate / over-max).InvalidProviderParamErrorOn wrong
provider_params(swap-safety).ValueErrorOn a malformed or
"auto"candidate-language entry (a caller code bug; raises independent of strict/best_effort).TranscriptionErrorOn an engine-execution failure.
transcribe_async#async methodSource
async def StandardASR.transcribe_async(
audio: AudioInputLike,
params: RuntimeParams | None = None,
) -> TranscriptionResultAsynchronously transcribe a complete audio input.
| Name | Type | Description |
|---|---|---|
audio | AudioInputLike | The audio to transcribe (any |
params= None | RuntimeParams | None | Per-request runtime parameters. |
TranscriptionResultThe transcription result.
ExceptionThe same exception set as
transcribe.
start_transcription#methodSource
def StandardASR.start_transcription(
*,
audio_format: AudioFormat | None = None,
params: RuntimeParams | None = None,
audio: AudioInputLike | None = None,
deadlines: StreamDeadlines | None = None,
) -> TranscriptionSessionOpen 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).
| Name | Type | Description |
|---|---|---|
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. |
TranscriptionSessionA streaming session.
ValueErrorIf both
audio_formatandaudioare provided, or on a malformed/autocandidate-language entry (a caller code bug; always raises, independent of strict/best_effort).ConfigErrorOn an invalid language configuration VALUE (
default_languagemalformed or not selectable).EngineContractErrorOn an engine-declaration defect -- a declared language axis with no
default_language(IC.6), or a malformed declared selectable/detectable tag.UnsupportedFeatureErrorWhen 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).IncompatibleAudioInputErrorIf no conversion path exists for a whole-input streaming
audiovalue.UnsafeAudioUrlErrorIf a whole-input
AudioUrlfails the SSRF policy.AudioProcessingErrorOn a decode / size / missing-sample-rate failure for a whole-input
audiovalue.InvalidProviderParamErrorOn wrong
provider_params(swap-safety).TranscriptionErrorWhen a pydantic
ValidationErrorescapes 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) -> boolReturn whether the capability at dot_path is supported.
| Name | Type | Description |
|---|---|---|
dot_path | str | A capability dot-path. |
boolTrueif supported.
recommended_wire_format#methodSource
def StandardASR.recommended_wire_format() -> AudioFormat | NoneReturn 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).
AudioFormat | NoneA wire format the engine's session-establishment guard accepts, or
AudioFormat | NoneNonewhen 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.
| Name | Type | Description |
|---|---|---|
done_timeout= DEFAULT_DONE_TIMEOUT | float | Nonegt=0.0 | The pipeline-inactivity hang backstop, reset by events AND by audio consumption. |
max_idle= DEFAULT_MAX_IDLE | float | Nonegt=0.0 | The opt-in content-stall detector. |
max_session_seconds= DEFAULT_MAX_SESSION_SECONDS | float | Nonegt=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).
| Name | Type | Description |
|---|---|---|
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 |
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.
| Name | Type | Description |
|---|---|---|
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.
| Name | Type | Description |
|---|---|---|
session | TranscriptionSession | 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 |
__enter__#methodSource
def SyncSession.__enter__() -> SyncSessionEnter 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).
SyncSessionThe sync session.
TimeoutErrorIf the engine
_openhangs pastsubmit_timeout.
__exit__#methodSource
def SyncSession.__exit__(*exc: object) -> NoneExit the async context and stop the owned loop.
feed#methodSource
def SyncSession.feed(source: Iterable[bytes] | bytes | bytearray) -> NoneFeed audio from a managed source.
| Name | Type | Description |
|---|---|---|
source | Iterable[bytes] | bytes | bytearray | A sync iterable of byte chunks, or a single |
TypeErrorIf
sourceis astr(forwarded from the async session: astris byte-chunks-shaped only by accident -- a whole file goes tostart_transcription(audio=...)).InvalidSessionUseErrorIf manual input or a prior feed was already used (forwarded from the async session).
StreamClosedErrorIf the bridge was already torn down (its
withblock exited, or a prior lifecycle call timed out).TimeoutErrorIf 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) -> NoneManually send one audio chunk.
| Name | Type | Description |
|---|---|---|
chunk | bytes | The audio chunk. |
InvalidSessionUseErrorIf
feedwas already used (forwarded from the async session: mixing input modes).StreamClosedErrorIf 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
withblock exited, or a prior lifecycle call timed out).TimeoutErrorIf 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() -> NoneMark the end of manual audio input.
InvalidSessionUseErrorIf
feedwas already used (forwarded from the async session: mixing input modes).StreamClosedErrorIf the bridge was already torn down (its
withblock exited, or a prior lifecycle call timed out).TimeoutErrorIf 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.
TranscriptionEventEvents from the underlying async session.
StreamClosedErrorIf the bridge was already torn down (its
withblock exited, or a prior lifecycle call timed out), or if the session was never entered (no event stream exists to pump).TimeoutErrorIf 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() -> TranscriptionResultReduce 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.
TranscriptionResultThe reduced result.
TimeoutErrorIf 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.
list[Diagnostic]The accumulated diagnostics.
TimeoutErrorIf 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() -> boolWhether 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).
boolTrueif 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.
| Name | Type | Description |
|---|---|---|
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 |
|
TranscriptionEvent#classSource
class TranscriptionEvent(BaseModel)A single streaming transcription event.
| Name | Type | Description |
|---|---|---|
type | EventType | 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 |
finality= 'final' | Literal['final', 'closed'] | For |
words= None | list[Word] | None | Optional word-level detail (shares the batch |
speaker= None | str | None | Segment-level speaker label, with the same
inheritance rule as |
start= None | float | Nonege=0.0 | Segment start time in seconds (origin = first session sample). |
end= None | float | Nonege=0.0 | Segment end time in seconds. |
audio_processed_until= None | float | Nonege=0.0 | Monotonic audio-time cursor in seconds. |
old_ids= list() | list[str] | For |
new_ids= list() | list[str] | For |
code= None | str | None | For |
recoverable= None | bool | None | For |
retriable_after= None | float | Nonege=0.0 | For |
reconnect= None | bool | None | For |
gap_start= None | float | Nonege=0.0 | For a reconnect |
gap_end= None | float | Nonege=0.0 | For a reconnect |
detected_language= None | str | None | The engine-detected language (BCP-47, never
|
extra= dict() | WireExtra | Engine-specific extra data. |
stable_text | str | The frozen prefix of Guards against an invalid (negative or out-of-range) |
is_content | bool | Whether this event advances transcription content. |
is_terminal | bool | Whether this event ends the session. |
partial#class methodSource
@classmethod
def TranscriptionEvent.partial(
segment_id: str,
text: str,
**kw: Any,
) -> TranscriptionEventBuild a partial event.
| Name | Type | Description |
|---|---|---|
segment_id | str | The segment id. |
text | str | The segment's complete current text. |
**kw= {} | Any | Additional event fields. |
TranscriptionEventA
partialevent.
final#class methodSource
@classmethod
def TranscriptionEvent.final(
segment_id: str,
text: str,
**kw: Any,
) -> TranscriptionEventBuild a final event.
| Name | Type | Description |
|---|---|---|
segment_id | str | The segment id. |
text | str | The segment's final text. |
**kw= {} | Any | Additional event fields. |
TranscriptionEventA
finalevent.
closed#class methodSource
@classmethod
def TranscriptionEvent.closed(
segment_id: str,
text: str,
**kw: Any,
) -> TranscriptionEventBuild a closed finality event (a final with finality=closed).
| Name | Type | Description |
|---|---|---|
segment_id | str | The segment id. |
text | str | The segment's possibly post-processed text. |
**kw= {} | Any | Additional event fields. |
TranscriptionEventA
finalevent markedfinality="closed".
supersede#class methodSource
@classmethod
def TranscriptionEvent.supersede(
old_ids: list[str],
new_ids: list[str],
**kw: Any,
) -> TranscriptionEventBuild 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).
| Name | Type | Description |
|---|---|---|
old_ids | list[str] | The retired segment ids, in reading (time) order. |
new_ids | list[str] | The replacement segment ids, in reading (time) order
(must be disjoint from |
**kw= {} | Any | Additional event fields. |
TranscriptionEventA
supersedeevent.
ValueErrorIf
old_idsis empty (a supersede MUST retire at least one segment), ifold_idsandnew_idsintersect, or if either list repeats a segment id.
progress#class methodSource
@classmethod
def TranscriptionEvent.progress(**kw: Any) -> TranscriptionEventBuild a progress event (heartbeat / cursor / reconnect notice).
| Name | Type | Description |
|---|---|---|
**kw= {} | Any | Event fields (for example, |
TranscriptionEventA
progressevent.
done#class methodSource
@classmethod
def TranscriptionEvent.done(**kw: Any) -> TranscriptionEventBuild a terminal done event.
| Name | Type | Description |
|---|---|---|
**kw= {} | Any | Additional event fields. |
TranscriptionEventA
doneevent.
make_error#class methodSource
@classmethod
def TranscriptionEvent.make_error(
code: str,
*,
recoverable: bool = False,
**kw: Any,
) -> TranscriptionEventBuild an error event.
| Name | Type | Description |
|---|---|---|
code | str | The error code. |
recoverable= False | bool | Whether the session may continue. |
**kw= {} | Any | Additional event fields. |
TranscriptionEventAn
errorevent.
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.
ValueErrorIf field validation fails (incl. NaN/Inf, a negative
duration, or a malformeddetected_language).
| Name | Type | Description |
|---|---|---|
text | str | Full transcript (required). |
detected_language= None | str | None | Detected language as a well-formed BCP-47 tag in
|
language_confidence= None | float | Nonege=0.0, le=1.0 | Detection confidence in |
duration= None | float | Nonege=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
|
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
|
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 sourcereplayableclassification, andnote_reconnect. It cannot detect a reconnect itself (it owns no network connection). - The engine detects the disconnect, re-establishes the connection,
replays
replay_bufferaudio, keepssegment_id/ timestamps / detected language / speaker-label mapping continuous, then callsnote_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 aspeaker_labels_resetdiagnostic viaemit_diagnostic-- the fidelity-warning counterpart ofcontent_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 theprogress(reconnect=True, gap_start, gap_end)event and, iff the engine passedcontent_lost=True(its own determination that the reconnect + replay could not cover the gap), a trailingerror(code="content_lost", recoverable=True)fidelity warning.
Initialize the session.
| Name | Type | Description |
|---|---|---|
done_timeout= DEFAULT_DONE_TIMEOUT | float | None | Seconds of total pipeline inactivity -- no event
arriving AND no fed audio consumed via |
max_idle= DEFAULT_MAX_IDLE | float | None | Seconds without a content event ( |
max_session_seconds= DEFAULT_MAX_SESSION_SECONDS | float | None | Absolute wall-clock cap; |
event_buffer_capacity= DEFAULT_EVENT_BUFFER_CAPACITY | int | Pending-event budget shared by every
event kind -- drop-proof |
audio_queue_maxsize= DEFAULT_AUDIO_QUEUE_MAXSIZE | int | Max pending audio chunks; bounds |
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 |
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 |
ValueErrorIf a deadline is not positive (or
Nonewhere allowed) or a buffer/queue bound is not positive. In particularaudio_queue_maxsize=0would mean an UNBOUNDEDasyncio.Queue-- silently disabling the documented feed backpressure -- so it is rejected rather than passed through. Also ifmax_guard_diagnosticsis not positive.
| Name | Type | Description |
|---|---|---|
replayable | bool | Whether the audio source can be re-read after a reconnect. |
done_timeout | float | None | The configured pipeline-inactivity backstop in seconds. |
max_idle | float | None | The configured content-stall deadline in seconds. |
max_session_seconds | float | 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.
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,
) -> NoneSurface 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.)
| Name | Type | Description |
|---|---|---|
code | str | Stable, machine-readable diagnostic code (for example, |
message | str | Human-readable explanation (client-facing; no secrets). |
level= 'info' | Literal['info', 'warning'] |
|
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 |
effective= None | JsonValue | BaseModel | The value that took effect, if relevant
(client-facing; projected like |
pydantic.ValidationErrorIf
provided/effectivehas 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_produceterminates 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).
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,
) -> NoneRecord 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).
| Name | Type | Description |
|---|---|---|
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 |
|
feed#methodSource
def TranscriptionSession.feed(
source: Iterable[bytes] | AsyncIterable[bytes] | bytes | bytearray,
) -> NoneFeed 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.
| Name | Type | Description |
|---|---|---|
source | Iterable[bytes] | AsyncIterable[bytes] | bytes | bytearray | A sync or async iterable of byte chunks ( |
TypeErrorIf
sourceis astr. AstrsatisfiesIterable[str]and would be silently consumed one character at a time (or fail deep inside an engine as a confusingengine_error); passing a file path here is a common slip. A whole audio file goes tostart_transcription(audio=...)and incremental input is raw PCM byte chunks.InvalidSessionUseErrorIf manual input was already used (mixing) or
feedwas already called once -- a usage error against a still-live session, NOT a lifecycle close.TypeErrorIf a subclass rebound a reserved base attribute.
send_audio#async methodSource
async def TranscriptionSession.send_audio(chunk: bytes) -> NoneManually send one audio chunk (mutually exclusive with feed).
Manual sources are always treated as non-replayable (live input).
| Name | Type | Description |
|---|---|---|
chunk | bytes | The audio chunk. |
InvalidSessionUseErrorIf
feedwas already used (mixing input modes is a usage error against a still-live session).StreamClosedErrorIf 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.TypeErrorIf a subclass rebound a reserved base attribute.
end_audio#async methodSource
async def TranscriptionSession.end_audio() -> NoneMark 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.
InvalidSessionUseErrorIf
feedwas used (mixing input modes is a usage error against a still-live session).TypeErrorIf a subclass rebound a reserved base attribute.
__aenter__#async methodSource
async def TranscriptionSession.__aenter__() -> TranscriptionSessionOpen 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.
TranscriptionSessionThe session.
TypeErrorIf a subclass rebound a reserved base attribute.
__aexit__#async methodSource
async def TranscriptionSession.__aexit__(*exc: object) -> NoneTear 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.
AsyncIterator[TranscriptionEvent]The session's event async iterator.
InvalidSessionUseErrorIf the session is already being iterated -- a usage error against a still-live session, not a lifecycle close.
TypeErrorIf 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.
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_untilvalues), capped as described above.
result#methodSource
def TranscriptionSession.result() -> TranscriptionResultReduce the session so far into a transcription result.
TranscriptionResultThe reduced result.
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.
| Name | Type | Description |
|---|---|---|
url | str | The offending URL. |
reason | str | 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.
| Name | Type | Description |
|---|---|---|
message | str | Human-readable description of the rejection. |
param= None | str | None | The offending standard parameter name, if applicable. |
mode= None | str | None | The mode ( |
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.
ValueErrorIf field validation fails (incl. NaN/Inf, a negative time, or
end < start).
| Name | Type | Description |
|---|---|---|
start | floatge=0.0 | Word start time in seconds (origin = first submitted sample; non-negative, finite). |
end | floatge=0.0 | Word end time in seconds (non-negative, finite, |
text | str | Word text. |
probability= None | float | Nonege=0.0, le=1.0 | Optional confidence in |
logprob= None | float | None | Optional log-probability (kept separate from |
speaker= None | str | None | Optional speaker label. |
channel= None | int | Nonege=0 | Optional channel index for provenance ( |
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.
| Name | Type | Description |
|---|---|---|
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',
) -> ModelRegistryDiscover 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.
| Name | Type | Description |
|---|---|---|
eps= None | Iterable[EntryPoint] | None | Custom entry points for testing. Leave |
strict= False | bool | If |
on_conflict= 'warn_keep_first' | str | How to handle duplicate model keys:
|
ModelRegistryModelRegistrycontaining all discovered models.
EntrypointValidationError(strict mode) Invalid entry points detected.
ValueErrorUnknown
on_conflictvalue.
>>> 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',
) -> strRender 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.
| Name | Type | Description |
|---|---|---|
result | TranscriptionResult | The transcription result to render. |
include_speakers= False | bool | Render |
on_unrenderable= 'error' | UnrenderablePolicy | Policy for segments that cannot render as visible
cues (unmeasured span, or a span that quantizes to zero
milliseconds): |
strThe SRT document as a string.
SubtitleRenderingErrorUnder the default
"error"policy, when any segment cannot render as a visible cue.ValueErrorIf
on_unrenderableis not one of the three policies (see_cues).
to_vtt#functionSource
def to_vtt(
result: TranscriptionResult,
*,
include_speakers: bool = False,
on_unrenderable: UnrenderablePolicy = 'error',
) -> strRender a transcription result as WebVTT.
Cue text is escaped per the W3C WebVTT cue-text grammar (& -> &,
< -> <, > -> >) 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.
| Name | Type | Description |
|---|---|---|
result | TranscriptionResult | The transcription result to render. |
include_speakers= False | bool | Render |
on_unrenderable= 'error' | UnrenderablePolicy | Policy for segments that cannot render as visible
cues (unmeasured span, or a span that quantizes to zero
milliseconds): |
strThe WebVTT document as a string.
SubtitleRenderingErrorUnder the default
"error"policy, when any segment cannot render as a visible cue.ValueErrorIf
on_unrenderableis not one of the three policies (see_cues).