standard_asr.engine
The engine-author facade: everything you need to build a compliant ASR plugin, in a single import path.
from standard_asr.engine import (
EngineBase,
BaseConfig,
BaseProperties,
DeclaredCapabilities,
BatchCapabilities,
FlagCap,
LanguageCaps,
PreparedAudio,
RuntimeParams,
TranscriptionResult,
)Engine-author surface: everything you need to build a compliant ASR plugin.
This module is the single import path for engine authors. Where the top-level
standard_asr namespace is curated for application developers (discover an
engine, pass audio, read a result), standard_asr.engine aggregates the types
an engine author implements and declares against:
- the base class and protocol (
EngineBase,StandardASR), plus the standard session-establishment wire-format rule (ensure_wire_format_supported) for structural engines that implement their own establishment guard; - the typed config surface (
BaseConfig, the applicability mixins,secret_field); - static metadata (
BaseProperties,SampleRateRange,InputKind), and thesample_rate_accepted/nearest_accepted_sample_ratehelpers an engine reuses so itsaccepted_sample_ratesmembership and resample-target choices match the standard's; - the full capability vocabulary (
DeclaredCapabilitiesand every*Cap/*Constraintsnode); - language resolution and download-policy helpers (
effective_language,AUTO,resolve_download_root); - the result and streaming types an engine constructs and emits, plus the
wire projection helper (
to_json_value) for values headed into a wire-visible slot.
Exceptions an engine raises live in standard_asr.contract.exceptions (and are also
re-exported at the package top level). Compliance helpers for testing your plugin
live in standard_asr.compliance.
>>> from standard_asr.engine import (
... EngineBase,
... BaseConfig,
... BaseProperties,
... DeclaredCapabilities,
... BatchCapabilities,
... LanguageCaps,
... FlagCap,
... )AUTO#attributeSource
AUTO = 'auto'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 |
BaseConfig#classSource
class BaseConfig(BaseModel, Generic[EngineNameT])Base class for ASR engine init configuration models.
ValueErrorIf validation fails.
| Name | Type | Description |
|---|---|---|
engine | EngineNameT | Discriminator equal to the entrypoint-derived |
strict= True | bool | Global policy for unsupported standard parameters. |
allow_private_urls= False | bool | Opt-in to relax the SSRF policy so an AudioUrl may target a private/loopback/link-local address (HTTPS is still required). False by default; set True only for a trusted internal endpoint. |
__pydantic_init_subclass__#class methodSource
@classmethod
def BaseConfig.__pydantic_init_subclass__(**kwargs: Any) -> NoneEnforce definition-time invariants: secret annotations, flat aliases.
Definition-time guards, so a credential leak or an unclassifiable construction failure can never reach runtime:
- The input surface stays CLOSED, at every depth. The config itself
must keep
extra="forbid"(BaseConfig's default;allowstores undeclared caller data and emits it frompublic_dump,ignoresilently drops a mistyped key), and every nested input container its schema reaches -- a submodel, aTypedDict, a pydantic dataclass -- must forbid undeclared keys too: pydantic's default for all three silently DROPS them, so a typo'd nested option reads as applied while the engine runs on the field's default. - A field marked
secret=True(viasecret_field) must resolve to exactly ONE masking carrier (_secret_carrier):SecretStrorSecretBytes, optionally unioned withNoneand nothing else. A plain annotation (str | None) or a plaintext union member (SecretStr | int) would be hidden from REST/auto-UI while leaking plaintext inrepr/str/model_dump/public_dump; two carriers make raw-string wrapping ambiguous; a container of secrets (for example,list[SecretStr]) is only half-protected. 1b. The field's DEFAULT must uphold the same contract (pydantic does not validate defaults): required,None(only when the annotation admits it), or an instance of the carrier itself. A plain-string default would live on instances as rawstr-- plaintextrepr/model_dumpand a crash inmodel_dump_json/public_dump. Adefault_factoryis rejected outright (it runs at construction, unvetted by this guard). - A secret marker on a field of a nested submodel (the standard
encourages per-model-family submodels) is rejected outright. The secret
pipeline -- the enforcement here, the whitespace-preserving validator,
and
public_dump's masking -- only operates on aBaseConfig's own scalar fields, so a secret nested one level down is silently unprotected and leaks plaintext throughpublic_dump/repr/model_dump. Credentials MUST therefore be modeled as top-level scalarSecretStrfields on the config, not buried in a submodel. - A non-string validation alias --
AliasPath, or anAliasChoicescarrying one -- is rejected. The whole config surface is built on FLAT single-token field resolution: the env convention maps oneSTANDARD_ASR_<ENGINE>__<FIELD>variable to one field name (spec IC.4), and the absent-vs-invalid classifier behindConfigurationRequiredErrorresolves each errorlocback to a single string token. A path alias populates a field from NESTED input, which neither surface can express -- pre-guard it produced an environment-dependent compliance verdict (pure absence misclassified as a plugin defect). AnAliasChoicesof plain strings is fine: each choice resolves like a string alias. - The flat input-key vocabulary is UNIQUE across fields: a field's
alias (or
AliasChoiceschoice) colliding with another field's name or alias would let one caller key silently populate two independent settings (populate_by_namefills both) and makes loc-token resolution ambiguous. Every key has exactly one owning field. - The config's serialization surface is CLOSED: author-defined
serialization hooks --
@computed_field,@model_serializer,@field_serializer(declared here or inherited), andPlainSerializer/WrapSerializermetadata on a field -- are rejected outright. They run INSIDEmodel_dump, that is, insidepublic_dump's "masked" serialization, where they can rematerialize a sibling secret in plaintext (a computedauthorizationproperty, afield_serializerreadingself.api_key) under keys the by-name mask never touches -- and they break the dump-is-the-declared-input-surface contract (G1.3/G3.1: auto-UI rendersmodel_fields;extra="forbid"rejects a computed key on reload). Derived values belong on a plain@propertyor in the engine, never in the dump. - A field whose annotation contains a secret CARRIER
(
SecretStr/SecretBytes, anywhere in it) must carry the secret MARKER: an unmarked carrier is half-protected -- pydantic masks its dumps, but the whitespace-preserving validator skips it (the raw string input is silently stripped: a padded credential is rewritten with no diagnostic), and the schema never renders it as a password/write-only input. - The security-owned serialization methods keep their IDENTITY:
public_dump/reveal_dump(owned here) andmodel_dump/model_dump_json/__iter__(owned byBaseModel) MUST NOT be overridden -- directly or through a mixin / intermediate base. Guard 5 closes the serializer SCHEMA, but an ordinary Python override of these is the same customization one DISPATCH entry over: it never enters the decorator registry or the core schema, yet it runs insidepublic_dumpand can rematerialize a sibling secret under a key the by-name mask never touches. Resolved statically over the MRO; the runtime dumps also call the base implementations unbound (defense in depth). See_reject_security_method_override.
| Name | Type | Description |
|---|---|---|
**kwargs= {} | Any | Forwarded subclass keyword arguments. |
TypeErrorIf a secret-marked field's annotation does not resolve to exactly one carrier (
SecretStr/SecretBytes, optionally withNone), if its default violates the carrier contract (plain value,Noneon a non-optional annotation, or adefault_factory), if a nested submodel reachable from any field carries a secret-marked or carrier-annotated field, if a field's annotation contains an unresolved forward reference the nested-secret scan cannot vet, if a field declares a non-string validation alias (AliasPath/AliasChoiceswith a non-string entry), if two fields claim the same flat input key, if the class declares or inherits an author serialization hook, if the class overrides a security-owned serialization method (public_dump/reveal_dump/model_dump/model_dump_json/__iter__), if a carrier-annotated field lacks the secret marker, if the class reopens its own input surface (extra != "forbid"), if a nested input container reachable from the schema does not forbid undeclared keys, or if a field's env value has no defined reading -- its schema reaches both a scalar and a structured shape, or the core schema cannot be introspected at all (Guard 7,_env_codecs).
model_validate_json#class methodSource
@classmethod
def BaseConfig.model_validate_json(
json_data: str | bytes | bytearray,
*,
strict: bool | None = None,
context: Any | None = None,
**kwargs: Any,
) -> _ConfigTValidate a JSON document against this config (python-mode delegation).
Overridden because pydantic's native JSON pipeline is incompatible
with the whitespace-preserving secret wrap: the wrap replaces a raw
secret string with its carrier INSTANCE (SecretStr), which
JSON-mode field validation rejects outright (its secret schema
accepts only the JSON string form) -- so model_validate_json
failed for EVERY config document carrying a secret value. Skipping
the wrap in JSON mode is not an option either:
str_strip_whitespace applies in JSON mode too, so the padded
credential would be silently trimmed -- the exact rewrite the wrap
exists to prevent. The one path that preserves both contracts is to
parse the document and validate the resulting mapping in python
mode, where the wrap is defined.
Grammar parity holds because json.loads and pydantic's
parser accept the same token set for the config value space (both
accept NaN/Infinity tokens; field validation then treats the
resulting float identically on both routes -- pinned by test). A
document json.loads rejects is delegated to pydantic's own
JSON parser so the canonical json_invalid error surfaces; that
delegation is REQUIRED to raise -- if the parsers ever diverged and
pydantic accepted such a document, its JSON field pipeline would
run without the wrap (the silent trim again), so a delegation that
returns is refused outright (fail closed) rather than handed back.
| Name | Type | Description |
|---|---|---|
json_data | str | bytes | bytearray | The JSON document. |
strict= None | bool | None | Field strictness, applied by the python-mode validation of the parsed document. |
context= None | Any | None | Validation context, forwarded unchanged. |
**kwargs= {} | Any | Any further |
_ConfigTThe validated config.
ValidationErrorIf the document is malformed JSON (pydantic's canonical
json_invaliderror), is not a JSON object (the config mapping-required error), or fails field validation.ConfigErrorIf pydantic's parser accepted a document
json.loadsrejected (a parser divergence this override refuses to validate around).
model_validate_strings#class methodSource
@classmethod
def BaseConfig.model_validate_strings(
obj: Any,
*,
strict: bool | None = None,
context: Any | None = None,
**kwargs: Any,
) -> _ConfigTValidate string-valued data against this config (env-grammar delegation).
Overridden for the same reason as model_validate_json:
pydantic's native strings pipeline is incompatible with the
whitespace-preserving secret wrap -- the wrap replaces a raw secret
string with its carrier INSTANCE (SecretStr), which strings-mode
field validation rejects (its secret schema accepts only the string
form) -- so model_validate_strings failed for EVERY config
supplying a secret value, blaming the caller's own credential field
for a wrong type it never passed. Skipping the wrap is not an option
either: str_strip_whitespace applies in strings mode too, so the
padded credential would be silently trimmed.
The string grammar is the config surface's OWN, shared with
from_env / env_overrides rather than pydantic's
strings mode: a scalar field takes the raw string (python-mode lax
coercion handles "4" / "true"), a STRUCTURED field (list /
mapping / submodel -- the fields _ENV_CODECS marks "json")
takes a JSON document, and on a JSON error the raw string is kept so
construction still fails loudly. One grammar for every string-valued
source (env vars, CLI key=value pairs, query params), not two
subtly different ones.
| Name | Type | Description |
|---|---|---|
obj | Any | The string-valued mapping; keys may use any of a field's
flat input keys (canonical name, string alias/
|
strict= None | bool | None | Field strictness, applied by the python-mode validation
of the decoded mapping. |
context= None | Any | None | Validation context, forwarded unchanged. |
**kwargs= {} | Any | Any further |
_ConfigTThe validated config.
ValidationErrorIf
objis not a mapping (the config mapping-required error) or the decoded mapping fails field validation.
public_dump#methodSource
def BaseConfig.public_dump() -> dict[str, Any]Return a serialization with secrets masked (the default path).
This is the masked half of the secret-serialization contract and is
the serialization to use for /v1/models, persistence, and telemetry.
SecretStr / SecretBytes fields are rendered as
SECRET_MASK (never plaintext). As a defensive measure, any
secret-marked field is masked by name, so even a value that
(hypothetically) slipped through as plaintext is never emitted. The
default pydantic serializers (model_dump / model_dump_json)
likewise mask the secret carriers; use reveal_dump only when
plaintext is genuinely required in-process.
Trusting model_dump here rests on the definition-time guards
that reject the authored serializer shapes -- the three serializer
decorators, Annotated serializer metadata, nested credential
carriers, fields excluded from the dump (Guards 5/5c/6) -- so the
dump contains the declared input fields, serialized by pydantic's
own machinery. Per the trust model (AGENTS.md) this is an
enumeration, not a proof: the channels only a core-schema prover
could see (SerializeAsAny, a custom
__get_pydantic_core_schema__) require an author actively
smuggling a credential past the mask -- an adversary, out of scope.
The guards bound what the schema installs in the dump, not the
CONTENTS of the values author code constructed. Config code that
copies a secret out of its carrier into non-secret state -- an
after-validator writing self.api_key.get_secret_value() into a
declared str field, or onto display state a trusted serializer
renders by builtin dispatch (a validator-returned Path SUBCLASS
whose __str__ embeds the credential; pydantic's own audited
ser_path calls str() on the runtime value) -- emits that
plaintext here, and no serialization mechanism can prevent it: both
shapes are the same author act, and the leaking value passes every
type or dispatch audit (a plain str whose content nothing can
classify as secret). The value-level envelope is and remains the
carrier contract (IC.3): credentials live in secret-marked
SecretStr/SecretBytes fields, are masked here by name, and
MUST NOT be copied out of the carrier into any other field or
object state. The boundary is pinned executable in
test_secret_extraction_is_the_closure_boundary; moving it means
updating this contract, the spec, and that test together.
dict[str, Any]A JSON-safe dict with credentials masked.
reveal_dump#methodSource
def BaseConfig.reveal_dump() -> dict[str, Any]Return a serialization with secrets materialized as plaintext.
This is the reveal half of the secret-serialization contract: the
explicit, named counterpart to public_dump. Use it only for
in-process calls into an engine SDK that needs the raw credential (for example,
an Authorization header). The result contains plaintext secrets and
MUST NEVER be logged, persisted, sent to /v1/models, or emitted as
telemetry -- those paths use public_dump.
SecretStr / SecretBytes fields are unwrapped via
get_secret_value(); all other fields keep their Python values (no
JSON coercion), so a credential is returned as the engine SDK expects it.
dict[str, Any]A dict with secret-marked fields materialized to plaintext.
from_env#class methodSource
@classmethod
def BaseConfig.from_env(
engine_id: str,
*,
environ: Mapping[str, str] | None = None,
**explicit: Any,
) -> _ConfigTConstruct a config, filling unset fields from the environment.
Applies the normative priority explicit > env > (required-missing
error): each field that is not supplied in explicit under any
of its flat input keys (canonical name, string alias/
validation_alias, or an AliasChoices choice -- the
_flat_input_keys vocabulary) is filled from its
STANDARD_ASR_<NORMENGINE>__<NORMFIELD> environment variable
(collision detected), and the merged mapping is then passed to the
constructor. Alias-awareness is what makes "explicit wins" true for
aliased fields: a caller passing apiKey=... suppresses the
api_key env fallback instead of colliding with it under
extra="forbid". Because construction does the field coercion,
SecretStr credentials are wrapped (and so masked in
repr/str/public_dump) instead of being handed around as raw
plaintext -- avoiding the leak footgun of passing a plaintext
{field: secret} dict through application code.
Note on explicit None: "absent" means the key is not present in
explicit, not "present with value None". A key passed explicitly
as None is a value and wins over env (priority is "explicit wins",
not "explicit-non-None wins"). A wrapper that forwards optional kwargs
with None defaults therefore disables the env fallback for those
fields; drop None keys before calling ({k: v for k, v in kwargs if v is not None}) if env fallback should apply.
The engine discriminator is never read from the environment; it is
the entrypoint-derived identity and defaults on each engine's subclass.
The strict safety policy is likewise excluded so the environment can
never silently downgrade fail-loud to best_effort (see
_ENV_EXCLUDED_FIELDS).
| Name | Type | Description |
|---|---|---|
engine_id | str | The engine identifier used to build env var names. |
environ= None | Mapping[str, str] | None | Environment mapping (defaults to |
**explicit= {} | Any | Explicitly supplied field values (highest priority). |
_ConfigTA validated config instance.
ConfigErrorIf two field names collide on the same env var, or if construction fails (an invalid value, or a required field missing from both
explicitand the environment) -- wrapped from pydantic'sValidationErrorwith the offending input scrubbed.ConfigErroris aValueErrorsubclass, so existingexcept ValueErrorhandlers keep working.
env_overrides#class methodSource
@classmethod
def BaseConfig.env_overrides(
engine_id: str,
*,
environ: Mapping[str, str] | None = None,
) -> dict[str, Any]Collect config overrides from environment variables.
Only fields absent from explicit config should be filled from these; the caller applies priority (explicit > env). Collisions (two fields normalizing to the same env var) are rejected.
Security note: the returned dict holds raw plaintext values,
including any credential fields, because SecretStr wrapping happens
only at construction. Prefer from_env, which merges and
constructs in one step (so secrets are wrapped/masked); treat the dict
returned here as sensitive and never log it.
| Name | Type | Description |
|---|---|---|
engine_id | str | The engine identifier used to build env var names. |
environ= None | Mapping[str, str] | None | Environment mapping (defaults to |
dict[str, Any]A dict of
{field_name: value}discovered in the environment.
ConfigErrorIf two field names collide on the same env var.
BaseProperties#classSource
class BaseProperties(BaseModel)Base class for ASR engine static properties.
ValueErrorIf validation fails.
| Name | Type | Description |
|---|---|---|
engine_id | str | Engine identifier (surface syntax checked here; PEP 503 canonicalization happens at discovery). |
model_name | str | Model preset name within the engine. |
protocol_version | str | Standard ASR protocol version supported by the engine. |
accepted_input | set[InputKind] | Audio shapes the engine accepts (MUST be non-empty). |
native_sample_rate | intgt=0 | The model's native sample rate in Hz. |
accepted_sample_rates | list[int] | SampleRateRange | Literal['any'] | Sample rates the engine accepts, or |
required_input_sample_rate= None | int | Nonegt=0 | Sample rate the wire protocol hard-requires (for example, 24000 for OpenAI Realtime), if any. |
max_file_size= None | int | Nonegt=0 | Maximum file/payload size in bytes, if any. |
max_audio_duration= None | float | Nonegt=0 | Maximum audio duration in seconds, if any. |
wire_encodings= None | list[str] | None | Wire encodings supported for streaming, if any. |
selectable_languages= list() | list[str] | Languages the application may explicitly select
(BCP-47 tags plus optional |
detectable_languages= list() | list[str] | Languages detectable in |
description= None | str | None | Optional human-readable, display-only description. MUST NOT
carry machine-readable negotiation/gating data -- that belongs in
Capabilities (the free-form |
has_language_axis | bool | Whether the engine exposes a language axis. |
supports_auto | bool | Whether automatic language detection is selectable. |
accepts_any_sample_rate | bool | Whether the engine accepts any input sample rate. Renamed from |
model_id | str | Return the fully qualified model identifier (engine/model). |
BatchCapabilities#classSource
class BatchCapabilities(_Container)Capability tree for the batch mode domain.
| Name | Type | Description |
|---|---|---|
language= LanguageCaps() | LanguageCaps | Language capabilities. |
word_timestamps= WordTimestampsCap() | WordTimestampsCap | Word-timestamp capability. |
guidance= GuidanceCaps() | GuidanceCaps | Guidance-family capabilities. |
diarization= DiarizationCap() | DiarizationCap | Diarization capability. |
CandidateLanguagesCap#classSource
class CandidateLanguagesCap(_FlagLikeNode)Bounded capability for candidate languages.
| Name | Type | Description |
|---|---|---|
constraints= None | CandidateLanguagesConstraints | None | Limits (for example, |
supported | bool | Whether candidate languages are supported. |
CandidateLanguagesConstraints#classSource
class CandidateLanguagesConstraints(_JsonExtraModel)Constraints for the candidate-languages capability.
| Name | Type | Description |
|---|---|---|
max | intgt=0 | Maximum number of candidate languages accepted. |
ChannelResult#classSource
class ChannelResult(BaseModel)Per-channel transcription for multi-channel audio.
ValueErrorIf field validation fails.
CredentialsConfigMixin#classSource
class CredentialsConfigMixin(BaseModel)Applicability mixin: cloud credentials and endpoint routing.
Credentials (api_key) are secret; endpoint routing fields
(base_url / region / org_id) are not secret and may be logged.
| Name | Type | Description |
|---|---|---|
api_key= secret_field(description='Secret API key / token.') | SecretStr | None | Secret API key / token. |
base_url= None | str | None | Non-secret API base URL. |
region= None | str | None | Non-secret service region. |
org_id= None | str | None | Non-secret organization id. |
DIARIZE#attributeSource
DIARIZE: Final[DiarizationRequest] = DiarizationRequest()DeclaredCapabilities#classSource
class DeclaredCapabilities(_Container)The full capability tree declared by an engine.
Mode domains are optional: omitting a domain means the mode is not supported (fail-closed). Engine-global orthogonal flags live at the top level.
| Name | Type | Description |
|---|---|---|
batch= None | BatchCapabilities | None | Batch-mode capabilities, or |
streaming= None | StreamingCapabilities | None | Streaming-mode capabilities, or |
streaming_input= FlagCap() | FlagCap | Whether the engine accepts incremental audio. May only
be supported when a |
streaming_output= FlagCap() | FlagCap | Whether the engine returns results incrementally. May
only be supported when a |
self_resamples= FlagCap() | FlagCap | Whether the engine resamples audio internally. This is
one of the behavioral facts the spec declares in Capabilities
rather than Properties -- alongside the per-mode
It is purely informational: |
supports#methodSource
def DeclaredCapabilities.supports(dot_path: str) -> boolReturn whether the capability at dot_path is supported.
The only standard way to query capabilities. Walks the tree segment by
segment; any missing segment returns False (fail-closed). Resolving
a present mode-domain or container also returns True.
| Name | Type | Description |
|---|---|---|
dot_path | str | Dotted capability path without the |
boolTrueif supported, otherwiseFalse.
node_at#methodSource
def DeclaredCapabilities.node_at(dot_path: str) -> _CapNode | NoneReturn the typed capability node at dot_path, or None.
Unlike supports (which returns a bool), this returns the leaf
node object itself so callers can inspect its constraints / enums (for example,
a WordTimestampsCap to validate a requested granularity against
WordTimestampsCap.granularities). Returns None if the path
is absent or does not resolve to a capability leaf node.
| Name | Type | Description |
|---|---|---|
dot_path | str | Dotted capability path without the |
_CapNode | NoneThe capability leaf node, or
None.
iter_supported_paths#methodSource
def DeclaredCapabilities.iter_supported_paths() -> Iterator[str]Yield every dot-path in the tree whose node is supported.
Only the children of a supported typed node are descended into, so an
unsupported feature's constraint sub-containers (which are always
present, never None) do not appear. A raw x_* dict subtree is
walked unconditionally (see _iter_paths) so nested explicit
vendor capabilities stay discoverable. Used to verify the
effective ⊆ declared invariant.
strDot-paths of supported capability nodes and present containers.
iter_queryable_paths#methodSource
def DeclaredCapabilities.iter_queryable_paths() -> Iterator[str]Yield the dot-path of every NODE in the tree -- supported or not.
The node set is pinned by the two-layer isomorphism: exactly the
paths at which canonical_json renders a JSON object and at
which supports resolves a model/dict -- capability leaves,
containers, constraint submodels, and x_* extension subtrees
(typed or raw-dict; model extras pass the same x_* gate as every
other traversal, so a non-extension unknown key is not a node).
Scalar field values (a supported bool, a mode token, a
granularities list) are field internals, not nodes: neither
yielded nor descended. None children (an absent mode domain, a
constraints=None) are skipped.
Unlike iter_supported_paths (the supported-only view behind
effective ⊆ declared), UNSUPPORTED nodes are yielded and
descended, so a consumer can verify the fail-closed False answers
too -- for example, an unsupported feature's constraints submodel MUST
probe False. The compliance suite sweeps this set to assert a
hand-written supports() agrees with the tree on every node.
strDot-paths of every capability node, container, submodel, and
strextension subtree in the tree.
covers#methodSource
def DeclaredCapabilities.covers(other: DeclaredCapabilities) -> boolReturn whether other is a valid narrowing of this tree.
Enforces the normative effective ⊆ declared invariant:
the effective set may only close declared capabilities, never widen
them. This checks two things:
- Set containment -- every supported path in
otheris also supported here (no feature is enabled that this tree did not declare). - Constraint narrowing -- where both trees support a bounded or
enum/mode node,
other's limits MUST be no looser than this tree's (for example, a smaller-or-equalmax, a subset ofgranularities, amodethat is the same or a reduction). A widening (declaredmax=2-> effectivemax=999) is rejected.
| Name | Type | Description |
|---|---|---|
other | DeclaredCapabilities | A (typically narrowed, effective) capability tree. |
boolTrueifotheris a subset narrowing of this tree.
canonical_json#methodSource
def DeclaredCapabilities.canonical_json() -> dict[str, Any]Serialize to canonical JSON with a derived supported at every node.
Cross-language clients read capabilities from this JSON. Flag and bounded
nodes carry supported as a real field, but enum/mode nodes derive it
from mode (a Python property, absent from model_dump). This method
injects the uniform boolean at every capability node and present
container so a client never has to special-case archetypes or know the
"none"/"unsupported" sentinels (enum/mode nodes'
supported is server-injected). The root object itself carries no
supported key (it is the container of all modes, not a capability);
an absent mode domain serializes as null (fail-closed).
dict[str, Any]A JSON-serializable capability tree with
supportedon each node.
DeviceConfigMixin#classSource
class DeviceConfigMixin(BaseModel)Applicability mixin: compute-device selection.
| Name | Type | Description |
|---|---|---|
device= None | str | None | Compute device (for example, |
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. |
DiarizationCap#classSource
class DiarizationCap(_FlagLikeNode)Capability for speaker diarization (requested via RuntimeParams.diarization).
always_on is a behavioral fact, in the same family as
self_resamples (and the streaming behavior flags emits_partials /
re_segments / word_stability): it describes what the engine does
-- its architecture cannot DISABLE diarization, so speaker labels may appear
even when diarization is not requested -- and grants nothing an application
could request.
It is nonetheless a regular queryable flag node (a FlagCap),
uniform with self_resamples: supports("<mode>.diarization.always_on")
works, it appears in iter_supported_paths when
supported, canonical_json injects a uniform
supported boolean for it, and covers treats
a declared-unsupported -> effective-supported change as a rejected widening
(declaration drift) by plain set containment.
The one thing that distinguishes always_on from every other flag is a
semantic inversion: for every other flag True means "you MAY request
this", whereas for always_on True means "this is imposed on you" --
speaker labels may appear even when you did not ask for them (the documented
exemption to request-gated diarization). That inversion is documented prose,
not a difference in representation.
It is NOT placed inside constraints (constraints are machine-checkable
request limits, and always_on is a behavioral fact, not a limit). It is
reserved for architecturally non-disableable engines: an engine that CAN
disable diarization MUST disable it when diarization is not requested
(can-disable-must-disable), and MUST NOT declare always_on for adapter
convenience.
| Name | Type | Description |
|---|---|---|
always_on= FlagCap() | FlagCap | Whether diarization is architecturally non-disableable
(labels may appear unrequested). May only be supported when
|
constraints= DiarizationConstraints() | DiarizationConstraints | Limits when supported. |
supported | bool | Whether diarization is supported. |
DiarizationConstraints#classSource
class DiarizationConstraints(_JsonExtraModel)Constraints for the diarization capability.
| Name | Type | Description |
|---|---|---|
max_speakers= None | int | Nonegt=0 | Optional maximum number of speakers. |
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").
DownloadConfigMixin#classSource
class DownloadConfigMixin(BaseModel)Applicability mixin: model download / cache location.
| Name | Type | Description |
|---|---|---|
download_root= None | Path | None | Root directory for model artifacts. Priority: explicit >
|
EngineBase#classSource
class EngineBase(ABC)Abstract base implementing the standard transcribe pipeline.
Subclasses MUST set properties and declared_capabilities as
class attributes, assign config in __init__ (which MUST stay
pure -- no filesystem, GPU, or network access), and implement
_transcribe. Streaming engines additionally override
_start_transcription (the streaming template hook); the public
start_transcription runs the standard gating pipeline for them.
| Name | Type | Description |
|---|---|---|
properties | BaseProperties | |
declared_capabilities | DeclaredCapabilities | |
provider_params_type= None | type[ProviderParams] | None | |
config_type= None | type[BaseConfig[str]] | None | |
config | BaseConfig[str] | |
effective_capabilities | DeclaredCapabilities | Runtime-effective capabilities (default: the declared set). Engines that narrow capabilities based on configuration override this;
the result MUST satisfy |
supports#methodSource
def EngineBase.supports(dot_path: str) -> boolReturn whether a capability is supported at runtime (fail-closed).
| Name | Type | Description |
|---|---|---|
dot_path | str | A capability dot-path. |
boolTrueif supported by the effective capabilities.
prepare#methodSource
def EngineBase.prepare() -> NoneWarm up the engine (download / load weights) without transcribing.
The optional, synchronous, idempotent pre-warm hook,
invoked by standard-asr prepare and by production / CI
pre-warming to move the lazy side effects
(weight download / model load) off the first transcription to one
billing-free, transcription-free call. The base implementation is a
no-op: an engine with nothing to warm up inherits it unchanged and the
toolchain reports a no-op rather than failing.
Engines that load weights MUST override this to materialize them (for example,
call _ensure_model_loaded), and that path MUST honor the same
download gate as transcription: check
allow_downloads and raise
DiscoveryError when downloads are
disabled and weights are missing. An override MUST remain a zero-argument
synchronous method -- never an async def (a coroutine function would
be called but never awaited, silently reporting a false success); the
compliance suite and the CLI reject a coroutine prepare.
NoneNone.
DiscoveryErrorAn override SHOULD raise this when downloads are disabled and the weights are not already present (the base no-op never raises).
transcribe#methodSource
def EngineBase.transcribe(
audio: AudioInputLike,
params: RuntimeParams | None = None,
) -> TranscriptionResultTranscribe a complete audio input (template method).
Runs the standard pipeline, fail-fast first: validate the language config -> gate parameters (provider_params + capability gating, which needs no audio) -> resolve & validate the effective language axis -> coerce -> negotiate -> convert/resample -> call the engine -> synthesize missing segment speakers from word speakers (the pinned synthesis rule) -> attach diagnostics.
Parameter validation runs before the (potentially expensive) audio
decode/resample so a swapped-engine provider_params bug or an
unsupported parameter is rejected before any audio is touched (fail fast
on provider_params first).
| Name | Type | Description |
|---|---|---|
audio | AudioInputLike | The audio to transcribe. |
params= None | RuntimeParams | None | Per-request runtime parameters. |
TranscriptionResultThe transcription result with gating / language / conversion
TranscriptionResultdiagnostics attached.
ConfigErrorIf the engine's
default_languageVALUE is malformed or not inselectable_languages-- 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 (non-HTTPS, or a private/loopback/link-local target).AudioProcessingErrorOn an audio failure surfaced by the conversion pipeline -- a decode failure, an over-
max_file_sizepayload, or (in strict mode) a bare array with no sample rate.UnsupportedFeatureErrorIn strict mode, on an unsupported parameter, a requested
languagenot selectable by the engine, or a valid-but-unreachable candidate list (a non-detectable candidate or one over the declaredmax).InvalidProviderParamErrorOn wrong provider params.
ValueErrorOn a malformed candidate tag or one containing
auto-- a caller code bug, raised always (independent of strict/best_effort).TranscriptionErrorOn an engine-execution failure inside
_transcribe-- including a pydanticValidationErrorescaping it (an invalid result construction is an engine fault; the template wraps it here so it can never masquerade as a client-input validation error).
transcribe_async#async methodSource
async def EngineBase.transcribe_async(
audio: AudioInputLike,
params: RuntimeParams | None = None,
) -> TranscriptionResultAsynchronously transcribe (default: run transcribe in a thread).
| Name | Type | Description |
|---|---|---|
audio | AudioInputLike | The audio to transcribe. |
params= None | RuntimeParams | None | Per-request runtime parameters. |
TranscriptionResultThe transcription result.
EngineContractErrorIf
transcribe(overridden by the subclass) violated the synchronous protocol contract -- returned an awaitable (async def) or a value that is not aTranscriptionResult-- or propagated a declaration defect fromtranscribe(a missing IC.6 default, a malformed declared tag).ExceptionThe same exception set as
transcribe(it runs that method):ConfigError,IncompatibleAudioInputError,UnsafeAudioUrlError,AudioProcessingError,UnsupportedFeatureError,InvalidProviderParamError,ValueError, andTranscriptionError.
ensure_stream_inputs_exclusive#static methodSource
@staticmethod
def EngineBase.ensure_stream_inputs_exclusive(
audio_format: AudioFormat | None,
audio: AudioInputLike | None,
) -> NoneEnforce the audio_format / audio mutual-exclusion.
audio_format (incremental PCM feeding) and audio (whole-input
streaming output) are mutually exclusive; passing both MUST raise. This
shared guard lets every streaming engine enforce the rule with one call
instead of reimplementing it; the base start_transcription
invokes it before raising the unsupported-streaming error.
| Name | Type | Description |
|---|---|---|
audio_format | AudioFormat | None | The wire format for incremental frames, if any. |
audio | AudioInputLike | None | A complete audio input for whole-input streaming, if any. |
ValueErrorIf both
audio_formatandaudioare provided.
ensure_stream_format_supported#methodSource
def EngineBase.ensure_stream_format_supported(audio_format: AudioFormat) -> NoneValidate a declared streaming wire format at session establishment.
Shared session-establishment guard for streaming engines: call it first
(like ensure_stream_inputs_exclusive) when opening a
audio_format=... session. It is fail-closed on the wire sample
rate and the channel count unconditionally, and on the wire encoding
when wire_encodings is declared.
Wire encoding: when the engine declares wire_encodings, an
encoding not among them is rejected up front rather than misframed as PCM
and silently mistranscribed. When wire_encodings is None
("unconstrained") the encoding cannot be validated and the
check is skipped -- the engine is then trusted to accept any encoding
(typically a self-managed-wire-format engine). The compliance suite
emits a warning for a streaming_input engine that leaves
wire_encodings unset, since that skip is where a forgotten
declaration would let a non-PCM frame be misframed.
Wire sample rate: the standard's v1 implementation note is explicit
that v1 does NOT resample streaming bare frames in the standard layer
(unlike the batch transcribe path, which resamples). Therefore, until
standard-layer streaming resampling lands, a wire sample_rate that the
engine does not accept MUST be rejected here rather than forwarded as
frames the engine never declared -- a loud error beats a silent
mistranscription. When required_input_sample_rate is set, the wire
rate MUST equal it -- even when accepted_sample_rates is "any"
(that combination is constructible; the declaration-time reachability
validator only checks concrete lists). Otherwise the rate is accepted
when accepted_sample_rates is "any" or when it is in that
concrete list.
| Name | Type | Description |
|---|---|---|
audio_format | AudioFormat | The wire format the session declared. |
UnsupportedFeatureErrorIf
wire_encodingsis declared and the requested encoding is not among them, if the wirechannelsis not1(v1 streaming wire is mono-only), or if the wire sample rate is not reachable for the engine (fail-closed; v1 does not resample streaming wire frames).
recommended_wire_format#methodSource
def EngineBase.recommended_wire_format() -> AudioFormat | NoneReturn a minimal wire AudioFormat to open a streaming session.
Single source of truth for the legal bare-frame wire format the standard
layer uses when it must open a streaming_input session but has no
application-chosen format -- the CLI sync-bridge runner and the streaming
gating probe both rely on it. They previously derived one independently and
disagreed (which sample-rate source to use, and what to do with no declared
wire_encodings); this unifies them. The format is built from the
engine's own Properties so ensure_stream_format_supported accepts
it (the compliance suite asserts that round-trip):
sample_rate=required_input_sample_ratewhen the engine hard-requires one, elsenative_sample_rate(the reachability invariant guarantees the native rate is accepted).encoding= the first declaredwire_encodingsentry, else the canonicalpcm_s16le(used only whenwire_encodingsis unconstrained, where the engine accepts any encoding).channels= 1 (v1 streaming wire is mono-only).
The derivation is deliberately capability-blind (Properties only):
whether a bare-frame session can be opened at all is decided by the
streaming_input gate in start_transcription, not here --
so the recommendation stays a pure, class-level static fact and the
compliance round-trip (format ⊆ ensure_stream_format_supported)
holds for every engine, streaming or not.
AudioFormat | NoneA wire format the engine's session-establishment guard accepts, or
AudioFormat | NoneNonewhen the engine declares no usable (positive) sample rate, soAudioFormat | Noneno bare-frame streaming format can be recommended.
start_transcription#methodSource
def EngineBase.start_transcription(
*,
audio_format: AudioFormat | None = None,
params: RuntimeParams | None = None,
audio: AudioInputLike | None = None,
deadlines: StreamDeadlines | None = None,
) -> TranscriptionSessionOpen a streaming transcription session (template method).
Symmetric to transcribe: the base runs the standard streaming
pipeline and delegates only the engine-specific session construction to
_start_transcription. The pipeline enforces input
mutual-exclusion, validates the language config, validates the wire
format, gates parameters against the streaming capabilities,
resolves the language axis, prepares whole-input audio through the
standard audio pipeline, and attaches the resulting diagnostics to the
session.
Because gating now runs here, provider_params
swap-safety is enforced on the streaming path too: a swapped-engine
provider_params type-mismatch always raises
InvalidProviderParamError (no longer
undefined behavior), and an unsupported standard parameter is rejected
(strict) or dropped + diagnosed (best_effort) exactly as for batch.
The streaming input/output capability axis is checked before the hook override defense, so an engine that implements the hook but does not declare the requested session mode fails on the missing capability rather than reaching parameter or audio gating. The hook override defense still runs before parameter gating, so a batch-only engine reports "does not support streaming" rather than a confusing parameter error -- while still running the input mutual-exclusion guard first, exactly as before.
Streaming param freeze: the already-gated, frozen
RuntimeParams is handed to the hook
as gated_params; the engine uses that for the whole session and MUST
NOT re-accept raw params mid-stream.
| 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. Applied by this template after the engine hook constructed the session, so explicitly set fields always win over the engine's construction-time choices -- precedence: application explicit > engine choice > standard default. Unset fields are left untouched. |
TranscriptionSessionA streaming session with gating / language diagnostics attached.
ValueErrorIf both
audio_formatandaudioare provided, or on a malformed/autocandidate-language entry (a caller code bug; always raises, independent of strict/best_effort).ConfigErrorIf the engine's
default_languageVALUE is malformed or not inselectable_languages.EngineContractErrorOn an engine-declaration defect -- a declared language axis with no
default_language(IC.6), or a malformed declared selectable/detectable tag.UnsupportedFeatureErrorWhen the requested streaming input/output axis is unsupported, when streaming 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_start_transcriptionhook (an invalid model construction is an engine fault; wrapped here so it can never masquerade as a client-input validation error).
FinalityCap#classSource
class FinalityCap(_CapNode)Streaming finality level the engine can guarantee.
| Name | Type | Description |
|---|---|---|
mode= 'final' | Literal['final', 'closed'] |
|
is_supported | bool | Whether a finality level is guaranteed (always |
FlagCap#classSource
class FlagCap(_FlagLikeNode)A simple supported / not-supported flag.
GuidanceCaps#classSource
class GuidanceCaps(_Container)Guidance-family capabilities for one mode.
| Name | Type | Description |
|---|---|---|
prompt= PromptCap() | PromptCap | Free-text prompt channel. |
phrase_hints= PhraseHintsCap() | PhraseHintsCap | Phrase-hint channel. |
InputKind#classSource
class InputKind(str, Enum)Closed enumeration of the audio shapes an engine can accept.
Engines declare the set of shapes they accept via
Properties.accepted_input. The negotiation layer matches the variant an
application provides against this set.
| Name | Type | Description |
|---|---|---|
ARRAY= 'array' | An already-decoded waveform (NumPy array). | |
ENCODED_BYTES= 'encoded_bytes' | Encoded audio held in memory (for example, MP3/WAV bytes). | |
ENCODED_FILE= 'encoded_file' | An encoded audio file on disk. | |
FETCHABLE_URL= 'fetchable_url' | A URL the engine/cloud service fetches server-side. | |
STORAGE_URI= 'storage_uri' | A provider cloud-storage URI (for example, |
LanguageCaps#classSource
class LanguageCaps(_Container)Language capabilities for one mode.
| Name | Type | Description |
|---|---|---|
runtime_override= FlagCap() | FlagCap | Whether per-request language override is allowed. |
candidate_languages= CandidateLanguagesCap() | CandidateLanguagesCap | Candidate-language support and limits. |
LanguageConfigMixin#classSource
class LanguageConfigMixin(BaseModel)Applicability mixin: default language selection.
| Name | Type | Description |
|---|---|---|
default_language= None | str | None | Default language (BCP-47 or |
default_candidate_languages= None | list[str] | None | Default candidate languages. |
Mode#attributeSource
Mode = ModeNamePhraseHintsCap#classSource
class PhraseHintsCap(_FlagLikeNode)Guidance channel: phrase-hint term boosting.
| Name | Type | Description |
|---|---|---|
constraints= PhraseHintsConstraints() | PhraseHintsConstraints | Limits when supported. |
supported | bool | Whether phrase hints are supported. |
PhraseHintsConstraints#classSource
class PhraseHintsConstraints(_JsonExtraModel)Constraints for the phrase-hints guidance channel.
| Name | Type | Description |
|---|---|---|
max_terms= None | int | Nonegt=0 | Optional maximum number of phrase-hint terms. |
max_chars_per_term= None | int | Nonegt=0 | Optional maximum characters per term. |
max_words_per_term= None | int | Nonegt=0 | Optional maximum words per term. |
PreparedAudio#classSource
class PreparedAudio
PreparedAudio(
kind: InputKind,
array: NDArray[np.float32] | None = None,
sample_rate: int | None = None,
data: bytes | None = None,
container: str | None = None,
path: str | None = None,
url: str | None = None,
storage_uri: str | None = None,
diagnostics: list[Diagnostic] = _empty_diagnostics(),
)Audio negotiated into exactly one engine-accepted shape.
Exactly one payload slot is populated according to kind.
| Name | Type | Description |
|---|---|---|
kind | InputKind | The accepted shape this payload represents. |
array= None | NDArray[np.float32] | None | Waveform (for |
sample_rate= None | int | None | Sample rate of |
data= None | bytes | None | Encoded bytes (for |
container= None | str | None | Optional container hint for |
path= None | str | None | File path (for |
url= None | str | None | Remote URL (for |
storage_uri= None | str | None | Provider cloud-storage URI (for |
PromptCap#classSource
class PromptCap(_FlagLikeNode)Guidance channel: free-text prompt.
| Name | Type | Description |
|---|---|---|
constraints= PromptConstraints() | PromptConstraints | Limits when supported. |
supported | bool | Whether prompt guidance is supported. |
PromptConstraints#classSource
class PromptConstraints(_JsonExtraModel)Constraints for the prompt guidance channel.
| Name | Type | Description |
|---|---|---|
max_tokens= None | int | Nonegt=0 | Optional maximum prompt length in tokens. The standard layer
has no engine tokenizer, so it enforces this bound against a
conservative, script-aware approximation -- whitespace-delimited
words plus one unit per space-less (CJK, kana, Hangul, Thai, and so on)
codepoint -- not the engine's exact token count. Honest scope of the
guarantee: the approximation never under-counts relative to that
whitespace + no-space-script tokenization, but it MAY under-count an
engine's subword (BPE) tokenization of long Latin words / URLs /
digit runs (counted as 1 here, often 6-17 BPE tokens), so such a
prompt can exceed the engine's true budget despite passing the gate.
Declare |
ProviderParams#classSource
class ProviderParams(BaseModel)Base class for an engine's typed, non-portable parameter model.
Engines publish a subclass (for example, OpenAIParams) and declare it as their
expected provider_params type. Passing one engine's params model to a
different engine is a validation error (swap-safe), raised as
InvalidProviderParamError by the engine
layer regardless of the strict / best_effort policy.
The swap-safety match is exact (type(provided) is <EngineParams>),
not isinstance: every engine MUST publish a distinct terminal params
type, because honoring a subclass would let one engine silently accept
another's params and drop the extra fields. Inheritance is
therefore not a way to declare cross-engine compatibility. This bare base is
never a valid concrete params model -- declaring it as an engine's
provider_params type, or passing a bare instance, is rejected (the latter
at RuntimeParams construction).
ReconnectCap#classSource
class ReconnectCap(_CapNode)Streaming reconnect capability.
| Name | Type | Description |
|---|---|---|
mode= 'unsupported' | Literal['seamless', 'lossy', 'unsupported'] |
|
is_supported | bool | Whether reconnect is supported. |
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 |
SampleRateRange#classSource
class SampleRateRange(BaseModel)A continuous inclusive range of accepted input sample rates.
The third accepted_sample_rates variant (besides an explicit list[int]
and "any"), for engines whose I/O boundary is a range rather than a
discrete set -- for example, AWS Transcribe accepts any rate in [8000, 48000]
(research 4). Without it such an engine must either enumerate a few points
(forcing the standard to needlessly resample an in-range rate it would accept,
losing quality) or declare "any" (over-declaring, so an out-of-range rate
is passed through and fails vendor-side instead of being negotiated). Both
betray the standard's "negotiate before the call" promise. On the
wire it serializes as {"min": 8000, "max": 48000}.
| Name | Type | Description |
|---|---|---|
min | intgt=0 | Lowest accepted rate in Hz (inclusive, |
max | intgt=0 | Highest accepted rate in Hz (inclusive, |
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.
StreamTimestampsCap#classSource
class StreamTimestampsCap(_CapNode)Source of streaming timestamps.
| Name | Type | Description |
|---|---|---|
mode= 'none' | Literal['native_frame_aligned', 'post_align', 'none'] |
|
is_supported | bool | Whether streaming timestamps are provided. |
StreamingCapabilities#classSource
class StreamingCapabilities(_Container)Capability tree for the streaming mode domain.
| Name | Type | Description |
|---|---|---|
language= LanguageCaps() | LanguageCaps | Language capabilities (MAY differ from batch). |
word_timestamps= WordTimestampsCap() | WordTimestampsCap | Word-timestamp capability. |
diarization= DiarizationCap() | DiarizationCap | Diarization capability (MAY differ from batch). |
guidance= StreamingGuidanceCaps() | GuidanceCaps | Guidance-family capabilities (MAY differ from batch); the
streaming variant additionally exposes |
emits_partials= FlagCap() | FlagCap | Whether partial events are emitted. |
re_segments= FlagCap() | FlagCap | Whether supersede events may occur. |
word_stability= FlagCap() | FlagCap | Whether a meaningful |
reconnect= ReconnectCap() | ReconnectCap | Reconnect capability mode. |
finality_level= FinalityCap() | FinalityCap | Finality level guaranteed. |
timestamps= StreamTimestampsCap() | StreamTimestampsCap | Source of streaming timestamps. |
StreamingGuidanceCaps#classSource
class StreamingGuidanceCaps(GuidanceCaps)Streaming guidance-family capabilities (adds mid-stream mutability).
Identical to GuidanceCaps plus mutable_mid_stream -- the
declaration site for the "guidance may change mid-stream" flag. It lives
only on the streaming guidance family because mid-stream
mutability is meaningless for batch (a single shot); batch guidance keeps the
plain GuidanceCaps.
A supported mutable_mid_stream means the engine MAY accept updated guidance
after start_transcription (otherwise RuntimeParams is frozen for the
whole session). v1 reserves the flag as the standard query path
(supports("streaming.guidance.mutable_mid_stream")) and does NOT promise an
update_guidance() method; default supported=False coincides with the
fail-closed "session-locked" semantics, so the compliance suite requires no
behavior for it. Modeled as a FlagCap (not a bare bool) so it
derives a uniform supported and covers() set-containment auto-rejects a
declared=false -> effective=true widening.
| Name | Type | Description |
|---|---|---|
mutable_mid_stream= FlagCap() | FlagCap | Whether guidance may be updated mid-session. |
prompt | PromptCap | Free-text prompt channel. |
phrase_hints | PhraseHintsCap | Phrase-hint channel. |
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.
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). |
WordTimestampGranularityName#attributeSource
WordTimestampGranularityName = Literal['word', 'segment', 'char']WordTimestampsCap#classSource
class WordTimestampsCap(_FlagLikeNode)Capability for word-level timestamps.
A supported word-timestamp capability MUST enumerate at least one
granularity: an engine that declares supported=True but lists no
granularities is ambiguous -- gating could not tell whether a
requested granularity is offered, and silently honoring an unlisted one is
the cardinal sin. Requiring explicit enumeration makes the "supported but
unenumerated" state unrepresentable, so gating always validates against a
real set. When supported=False the list is ignored (it defaults to
empty and carries no meaning).
| Name | Type | Description |
|---|---|---|
granularities= lambda: cast('list[WordTimestampGranularityName]', [])() | list[WordTimestampGranularityName] | Supported granularities ( |
supported | bool | Whether word timestamps are supported. |
allow_downloads#functionSource
def allow_downloads(env_var: str = 'STANDARD_ASR_ALLOW_DOWNLOAD') -> boolReturn whether model downloads are allowed at runtime.
Implements the download-policy contract: 1/true/yes
enable downloads, 0/false/no disable them, an unset variable
defaults to enabled, and any other value (including an empty string,
for example, a VAR= line in docker-compose, or a typo like on) disables them
fail-closed -- an unrecognized value must never silently enable downloads.
Because the engine that later raises DiscoveryError cannot see that the
toggle held an unrecognized value (it only sees this boolean), an
unrecognized non-disable value is logged here on every read so the operator can trace
a surprising "downloads disabled" back to the real cause -- the explicit,
logged explanation the philosophy requires instead of a silent degrade.
Note the empty-string asymmetry with _env_override (used by the cache
helpers) is deliberate: for a path override an empty value is meaningless
and treated as unset, whereas for this safety toggle an empty value is an
unrecognized value and must fail closed to disabled.
| Name | Type | Description |
|---|---|---|
env_var= 'STANDARD_ASR_ALLOW_DOWNLOAD' | str | Environment variable name that controls download policy. |
boolTruewhen downloads are allowed, otherwiseFalse.
effective_candidate_languages#functionSource
def effective_candidate_languages(
effective_lang: str | None,
request_candidates: list[str] | None,
default_candidates: list[str] | None,
*,
candidate_supported: bool,
detectable_languages: Collection[str],
max_count: int | None,
strict: bool,
mode: ModeName | None = None,
) -> tuple[list[str] | None, list[Diagnostic]]Resolve the candidate languages in effect for a request.
| Name | Type | Description |
|---|---|---|
effective_lang | str | None | The resolved effective language. |
request_candidates | list[str] | None | Per-request candidate languages, if any. |
default_candidates | list[str] | None | The engine's default candidate languages. |
candidate_supported | bool | Whether candidate languages are supported. |
detectable_languages | Collection[str] | Languages detectable in |
max_count | int | None | Maximum candidate count, if constrained. |
strict | bool | Whether to raise (vs truncate/drop + diagnostic) on violations. |
mode= None | ModeName | None | The mode being resolved ( |
list[str] | NoneA
(candidates, diagnostics)pair;candidatesisNonewhen notlist[Diagnostic]applicable. A
candidate_languages_ignoreddiagnostic is emitted onlytuple[list[str] | None, list[Diagnostic]]when a candidate list was actually provided (per request or per the
tuple[list[str] | None, list[Diagnostic]]engine default) but the engine/mode does not support candidate
tuple[list[str] | None, list[Diagnostic]]languages -- never when no list was provided at all (there is nothing to
tuple[list[str] | None, list[Diagnostic]]ignore), so an
autoengine without candidate support staystuple[list[str] | None, list[Diagnostic]]diagnostic-free on ordinary requests.
ValueErrorIndependent of
strict, if a candidate is a malformed BCP-47 tag or the reserved"auto"token -- once per-item validation is reached: per spec §LANG R3 the unsupported-capability short-circuit (step 3) runs FIRST, so when the engine/mode does not support candidate languages the provided list is ignored with a diagnostic and its items are never validated here. Direct callers relying on unconditional malformed-item rejection get it fromRuntimeParamsconstruction, which validates every candidate before any resolution runs (the engine pipeline is always covered by that). Also raised if adetectable_languagesentry is empty/whitespace (engine paths pre-validate this into aConfigError; the bare error is the direct-call contract). These are caller code bugs, never policy.UnsupportedFeatureErrorIn strict mode, on a valid-but-unreachable candidate list -- a candidate not in
detectable_languagesor a list overmax_count. This is the standard strict-gate rejection type (spec, Runtime Parameters R2), so every transport maps it to the same client-error verdict as any other strict rejection (the server's 422) instead of an internal-error 500.
effective_language#functionSource
def effective_language(
request_language: str | None,
default_language: str | None,
*,
has_language_axis: bool,
runtime_override_supported: bool,
) -> str | NoneResolve the language in effect for a request.
| Name | Type | Description |
|---|---|---|
request_language | str | None | The per-request language, if any. |
default_language | str | None | The engine's default language. |
has_language_axis | bool | Whether the engine exposes a language axis. |
runtime_override_supported | bool | Whether per-request override is supported. |
str | NoneThe effective language tag /
"auto", orNoneif the engine hasstr | Noneno language axis.
ensure_wire_format_supported#functionSource
def ensure_wire_format_supported(
properties: BaseProperties,
audio_format: AudioFormat,
) -> NoneValidate a streaming wire format against an engine's declared Properties.
The standard's session-establishment format rule as a PURE function of
(Properties, AudioFormat) -- the single owner shared by
EngineBase.ensure_stream_format_supported (the template's
establishment guard) and the compliance suite's
check_recommended_wire_format round-trip. Compliance validating through
this function instead of the EngineBase method keeps the check honest
for structural (non-EngineBase) engines: the guard method is NOT a
StandardASR protocol member, so calling it on a structural engine
raised AttributeError and mis-reported a fully compliant engine as
self-inconsistent.
See EngineBase.ensure_stream_format_supported for the full
normative semantics (fail-closed on sample rate and channels; fail-closed
on encoding only when wire_encodings is declared).
| Name | Type | Description |
|---|---|---|
properties | BaseProperties | The engine's declared static Properties. |
audio_format | AudioFormat | The wire format the session declared. |
UnsupportedFeatureErrorIf
wire_encodingsis declared and the requested encoding is not among them, if the wirechannelsis not1(v1 streaming wire is mono-only), or if the wire sample rate is not reachable for the engine (fail-closed; v1 does not resample streaming wire frames).
env_var_name#functionSource
def env_var_name(engine_id: str, field_name: str) -> strReturn the environment variable name for an engine config field.
The engine and field segments are joined by a double underscore
(STANDARD_ASR_<ENGINE>__<FIELD>) so the boundary between them is
unambiguous for the realistic name space. With a single-underscore separator
the engine/field split was not recoverable -- env_var_name("openai", "api_key") and env_var_name("openai-api", "key") both produced
STANDARD_ASR_OPENAI_API_KEY, so two different engines could silently read
each other's credentials. Because _normalize_segment collapses each
non-alphanumeric run to a single _, an interior single _ (folded
from - / .) can never be mistaken for the __ boundary. This
relies on engine_id being entrypoint-derived and PEP 503-normalized (no
leading/trailing separator) and field_name being a Python identifier:
a pathological engine_id ending in a separator combined with a
field_name starting with one is out of that space. Same-class collisions
(two fields of one config normalizing alike) are still caught by
BaseConfig.env_overrides.
| Name | Type | Description |
|---|---|---|
engine_id | str | The engine identifier. |
field_name | str | The standard config field name. |
strThe fully qualified environment variable name.
granularity_offers_all#functionSource
def granularity_offers_all(granularities: Sequence[str]) -> boolReturn whether a declared granularities list means "unbounded (all)".
An empty enumeration on a bounded node is the "engine did not enumerate" /
unbounded case, not "offers nothing" (on a bounded archetype an empty
enumeration list does not constrain). The only consumer is the
capability-narrowing comparison _node_narrows /
DeclaredCapabilities.covers, and there only on the raw dict /
x_* path: a typed WordTimestampsCap cannot reach this case
because its validator makes supported=True with an empty
granularities unrepresentable, so "empty == all" survives solely for
untyped JSON-sourced nodes.
This is deliberately NOT shared with runtime parameter gating:
runtime.gating._gate_granularity does not call this function and takes the
opposite stance (a supported WordTimestampsCap always enumerates its
granularities, so a requested value MUST be one of them -- no "empty => honor
anything"). The two modules agree because that typed-validator invariant
closes the empty case, not because they share this helper.
| Name | Type | Description |
|---|---|---|
granularities | Sequence[str] | The declared granularity list (possibly empty). |
boolTrueif the list is empty (unbounded -- every granularity offered).
nearest_accepted_sample_rate#functionSource
def nearest_accepted_sample_rate(accepted: AcceptedSampleRates, source: int) -> intReturn the accepted rate closest to source (resample target choice).
Only meaningful when source is NOT already accepted (the caller checks
that first). For a list, the nearest member preferring not to upsample (the
anti-upsampling spirit). For a range, source clamped into [min, max]
-- the closest reachable in-range rate, which never upsamples when source
is above the range. "any" accepts everything, so it can never reach here
with an unaccepted rate.
| Name | Type | Description |
|---|---|---|
accepted | AcceptedSampleRates | The declared accepted sample rates (a list or range). |
source | int | The input waveform's current sample rate in Hz. |
intAn accepted target sample rate in Hz.
ValueErrorIf
acceptedis"any"(no finite target to choose; an"any"engine acceptssourcedirectly and never resamples).
normalize_bcp47#functionSource
def normalize_bcp47(tag: str) -> strNormalize a BCP 47 language tag into a consistent, canonical form.
Trims/replaces separators, then applies BCP-47 canonical casing
(language lowercase, script Titlecase, region UPPERCASE) so values echoed
back to applications -- for example, detected_language and diagnostic
provided/effective fields -- read canonically (zh-Hans, not
zh-hans). Per RFC 5646 §2.1.1 the script/region conventions apply only
before the first singleton (a 1-char subtag); every subtag after it --
extension and private-use subtags -- stays lowercase
(zh-Hans-u-co-pinyin, never u-CO). Membership comparisons are
unaffected: both the declared set and the request are canonicalized through
this same function, so matching stays case-insensitive in effect.
| Name | Type | Description |
|---|---|---|
tag | str | Input language tag. |
strThe canonicalized tag (separators normalized to
-, canonical casing).
ValueErrorIf
tagis empty or only whitespace.
resolve_download_root#functionSource
def resolve_download_root(
explicit: Path | None = None,
*,
has_library_default: bool = False,
) -> Path | NoneResolve an engine's model download root per the standard precedence.
Implements the normative four-level chain engines MUST follow when picking
where model artifacts land: explicit config (the engine's
download_root field) > the STANDARD_ASR_MODEL_DIR environment
override > the underlying ASR library's own default cache (when it has one;
expressed as a None passthrough, see below) > the shared Standard ASR
cache directory (resolve_cache_dir, ~/.cache/standard-asr or
the platform equivalent). The environment tier inherits
resolve_cache_dir's reading of the variable: a whitespace-only
value is unset and a relative value resolves against the current working
directory at call time.
The library tier is a passthrough: ASR libraries typically express
"use my own default cache" as an unset download path (for example, faster-whisper's
WhisperModel(download_root=None) resolves via the HuggingFace hub
cache), so when the chain lands on that tier this returns None and the
engine forwards it unchanged. Substituting a concrete directory here would
delete the spec's third tier: every unconfigured install's models would
relocate away from the library's existing cache, breaking offline loads of
already-downloaded models and silently re-downloading them.
| Name | Type | Description |
|---|---|---|
explicit= None | Path | None | Explicitly configured download root (highest priority); a
leading |
has_library_default= False | bool | Whether the underlying ASR library has its own default
model cache (the spec's third tier). When |
Path | NoneThe resolved download root, or
Nonewhen the underlying ASR library's ownPath | Nonedefault cache applies (only possible with
has_library_default).
sample_rate_accepted#functionSource
def sample_rate_accepted(accepted: AcceptedSampleRates, rate: int) -> boolReturn whether rate is accepted by an accepted_sample_rates value.
The single membership predicate for all three variants, so every call site (reachability checks, batch resampling decision, streaming session validation) agrees on what "accepted" means.
| Name | Type | Description |
|---|---|---|
accepted | AcceptedSampleRates | The declared accepted sample rates. |
rate | int | A candidate sample rate in Hz. |
boolTrueifacceptedadmitsrate: always for"any",boolrate in acceptedfor a list,min <= rate <= maxfor a range.
secret_field#functionSource
def secret_field(default: Any = None, *, description: str = '') -> AnyBuild a Field for a write-only credential rendered as a password.
Use together with a SecretStr or SecretBytes annotation (exactly
one carrier, optionally unioned with None). The json_schema_extra
marks the field secret so auto-UI renders a password / write-only input and
REST exposes it POST-only. BaseConfig enforces the carrier
annotation AND the default's shape at class-definition time, masks the
value in BaseConfig.public_dump, and preserves the secret's exact
contents (no whitespace stripping) so a paste error is never silently
swallowed.
| Name | Type | Description |
|---|---|---|
default= None | Any | Field default. |
description= '' | str | Field description. |
AnyA configured pydantic
Field.
to_json_value#functionSource
def to_json_value(value: object) -> JsonValueProject a Python value into the wire value space.
Every wire-visible slot -- Diagnostic.provided / effective, every
extra mapping -- is declared JsonValue, because the
Python objects and the JSON documents are meant to be the same protocol
seen twice (G5.2). Declaring them Any admitted values with no JSON
representation at all, which then failed during the wire projection --
after an endpoint had already committed to a response.
Two things stand between an ordinary value and that declaration, and this helper is where both are handled:
- a structured value (a pydantic submodel such as a
DiarizationRequest) has a JSON form but is not itself JSON, so it is dumped; - a typed container (
list[str],dict[str, int]) IS JSON data, but a type checker does not accept it wherelist[JsonValue]is expected, becauselistis invariant. That is a static-analysis artifact, not a real mismatch, so it is absorbed here once instead of forcing acastat every call site.
Runtime validation is unaffected: the model still validates what it is given, so a value that is genuinely not JSON is rejected loudly at construction, naming the field.
| Name | Type | Description |
|---|---|---|
value | object | The value to hand to a wire-visible slot. |
JsonValueThe value's JSON projection.