Standard ASR

Plugin entry points

Who should read this?

  • Plugin authors: Learn how to expose your models to the Standard ASR runtime.
  • Application developers: Understand how to discover models that have been installed.
  • Standard ASR maintainers: Ensure the ecosystem follows the naming and compliance rules.

Quick summary

  • New to Standard ASR? Read docs/content/engine-authors/adapt-an-asr-system.md first.
  • Entry point group: standard_asr.models.
  • Name format: <engine_id>/<model_name>.
  • engine_id should match your distribution name after PEP 503 normalization.
  • model_name identifies a preset within that engine. Use an empty string for a default model only when truly necessary.
  • Entry point value: a callable (function or class) that returns a StandardASR implementation.
  • You can test locally with any installed plugin (for example, std-faster-whisper).

Naming rules

ComponentAllowed charactersNotes
engine_ida-z, 0-9, ., _, -Must start with [a-z0-9]; / is forbidden. Upper case is rejected outright, but a non-canonical lowercase form using ./_ separators (for example, faster_whisper) is accepted and folded to its PEP 503 routing identity (faster-whisper), with a normalization hint logged. The asymmetry is deliberate. Distribution names on PyPI are lowercase by convention, so an upper-case engine id is a mistake to fix at the source, not something to silently rewrite. The ./_- separator equivalence is a pure PEP 503 routing fold. The declared form is retained on ModelSpec.declared_engine_id for diagnostics.
model_nameA-Za-z0-9, ., _, +, %, :, -Must start with [A-Za-z0-9]; / is forbidden. Empty string signals a default model and triggers a warning.

Multiple models per engine are encouraged. Give each preset its own entry point. Presets include quantized variants, multilingual or monolingual builds, and device specializations. A separate entry point per preset lets downstream users request the exact behavior they need.

Default models

Leaving model_name empty (key written as engine_id/) denotes the engine’s canonical default. The discovery API accepts empty names and logs a warning so authors remember to document what the default does. An explicit default is allowed but discouraged; if you publish one, document what it selects.

A plugin key MUST contain the /: only <engine_id>/<model_name> and the explicit default <engine_id>/ are valid declaration forms. A slash-less key (for example, faster-whisper instead of faster-whisper/) is not a third valid form — it is almost always a typo that dropped /<model_name>. Discovery rejects it. The library call discover_models(strict=True) raises EntrypointValidationError. standard-asr compliance entrypoints --strict-discovery reports it as an entrypoint_invalid compliance error and exits non-zero (a compliance check always returns a report, never raises). Default discovery logs a warning naming the fix and skips the key. The trailing slash is required only on the declaration side; the lookup helpers below accept the bare engine id as a convenience alias for its default model.

If you publish an explicit default (engine_id/), the factory MUST return an instance whose properties.model_id is exactly engine_id/. Compliance checks this invariant (properties_key_mismatch).

An engine_id MUST be unique across installed distributions: two distributions that provide the same id (even through PEP 503 folding, such as my_engine and my-engine) are an identity collision, reported as the compliance error engine_id_collision even on a default run.

Declaring entry points

[project.entry-points."standard_asr.models"]
"faster-whisper/large-v3" = "std_faster_whisper.entrypoint:create"
"faster-whisper/distil-large-v3" = "std_faster_whisper.entrypoint:create_distil_large_v3"
"faster-whisper/turbo" = "std_faster_whisper.entrypoint:create_turbo"

Your callable can be a function or a class constructor. Each preset selects its model by which class it instantiates — never by passing a size name through an init model field (spec IC.7). The model identity lives on the engine class so discovery can read it without instantiating:

# std_faster_whisper/entrypoint.py
from typing import Any

from .std_asr_faster_whisper import DistilLargeV3ASR, FasterWhisperASR, TurboASR


def create(**kwargs: Any) -> FasterWhisperASR:
    """Return the large-v3 multilingual preset."""

    return FasterWhisperASR(**kwargs)


def create_distil_large_v3(**kwargs: Any) -> DistilLargeV3ASR:
    """Return the distil-large-v3 preset."""

    return DistilLargeV3ASR(**kwargs)


def create_turbo(**kwargs: Any) -> TurboASR:
    """Return the large-v3-turbo preset."""

    return TurboASR(**kwargs)

Annotate the factory with your concrete engine class, not the StandardASR protocol. Discovery reads class-level metadata (declared_capabilities, properties, provider_params_type) without instantiating or authenticating the engine, by resolving the factory's return annotation (ModelRegistry.engine_class). A concrete class (-> FasterWhisperASR) exposes those ClassVars; the StandardASR protocol does not, so annotating the factory -> StandardASR breaks instantiation-free discovery. This applies to a factory function; an entry point that is the engine class itself needs no annotation, because the class is returned directly. Compliance checks the outcome either way, reporting class_metadata_unreadable when neither form resolves.

Discovery validates each declaration:

  • Invalid names raise EntrypointValidationError in strict mode.
  • Duplicate keys can keep the first declaration or replace with the latest, depending on on_conflict.
  • Factories are loaded lazily; heavy dependencies stay unloaded until the model is requested.

Discovering models programmatically

from standard_asr import discover_models

registry = discover_models()
print(registry.names())
asr = registry.create("faster-whisper/large-v3", device="cuda", compute_type="float16")
text = asr.transcribe("meeting.wav").text  # or AudioArray(samples, 16000) / (samples, 16000)

Helper APIs:

  • parse_entrypoint_name() splits a key into (engine_id, model_name).
  • pep503_normalize() lets authors compute the canonical engine id.
  • ModelRegistry.keys_by_engine(engine_id) lists all presets for a given engine.

Required metadata

Your factory MUST return a compliant engine (typically an EngineBase subclass) that exposes:

  • properties: a BaseProperties instance (class attribute / ClassVar).
  • declared_capabilities: a DeclaredCapabilities instance (ClassVar).
  • config: a BaseConfig instance (captured at initialization).
  • transcribe(audio, params) returning TranscriptionResult, where params is an optional RuntimeParams. Subclassing EngineBase gives you this transcribe template for free; you implement only _transcribe(prepared, params).
  • Engine-specific parameters live in a typed ProviderParams subclass declared as provider_params_type — never as extra top-level RuntimeParams fields (RuntimeParams is closed). See adapt-an-asr-system.md for the full contract.

These are validated by standard-asr compliance entrypoints.

CLI support

Install your plugin in the same environment and use the CLI. The transcript below was captured live against std-faster-whisper; nested JSON blocks are abridged with ... — run the commands yourself for the full output (exact values depend on the plugin version):

$ standard-asr list
Discovered models:
 - faster-whisper/base             engine=faster-whisper  model=base
 - faster-whisper/distil-large-v3  engine=faster-whisper  model=distil-large-v3
 - faster-whisper/large-v3         engine=faster-whisper  model=large-v3
 - faster-whisper/large-v3-turbo   engine=faster-whisper  model=large-v3-turbo
 - faster-whisper/medium           engine=faster-whisper  model=medium
 - faster-whisper/small            engine=faster-whisper  model=small
 - faster-whisper/tiny             engine=faster-whisper  model=tiny

$ standard-asr show faster-whisper/large-v3
Model: faster-whisper/large-v3
  Engine ID   : faster-whisper
  Model name  : large-v3
  Module      : std_faster_whisper.entrypoint
  Attribute   : create_large_v3
  Value       : std_faster_whisper.entrypoint:create_large_v3
  Capabilities:
    {
      "batch": {
        "diarization": { ... },
        "guidance": {
          "phrase_hints": {
            "constraints": {
              "max_chars_per_term": 40,
              "max_terms": 50,
              "max_words_per_term": null
            },
            "supported": true
          },
          "prompt": { ... },
          "supported": true
        },
        "language": { ... },
        "supported": true,
        "word_timestamps": {
          "granularities": [
            "word",
            "segment"
          ],
          "supported": true
        }
      },
      "self_resamples": {
        "supported": false
      },
      "streaming": { ... },
      "streaming_input": {
        "supported": true
      },
      "streaming_output": {
        "supported": true
      }
    }
  Config schema:
    {
      "additionalProperties": false,
      "description": "Init configuration for the faster-whisper engine. ...",
      "properties": {
        "compute_type": { ... },
        "default_language": { ... },
        "device": { ... },
        "hf_token": {
          "anyOf": [
            {
              "format": "password",
              "type": "string",
              "writeOnly": true
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Hugging Face access token for gated/private model repos (secret).",
          "format": "password",
          "secret": true,
          "title": "Hf Token",
          "writeOnly": true
        },
        ...
      },
      "title": "FasterWhisperConfig",
      "type": "object"
    }

$ standard-asr compliance entrypoints
[OK] Entry point compliance checks passed.

$ standard-asr compliance run faster-whisper/large-v3
[OK] Entry point compliance checks passed.
[INFO] Two checks are not run here (each needs recorded data the CLI cannot synthesize): check_event_sequence for a streaming engine's event stream, and check_transcription_result for a batch result. Cover them with standard_asr.compliance in your tests (see docs/content/engine-authors/plugin-entry-points.md).
[OK] Compliance run passed.

Local testing with a plugin

Install a plugin (for example, std-faster-whisper) and run the checks end‑to‑end:

pip install "std-faster-whisper @ git+https://github.com/standard-voice/std-faster-whisper.git"
standard-asr list
standard-asr compliance entrypoints

Flags of interest:

  • --strict-discovery reports malformed entry points as entrypoint_invalid errors (non-zero exit; the report still covers the valid engines).
  • --no-instantiate skips smoke-instantiation. A missing credential already downgrades to a graceful skip (factory_requires_config); use this flag to avoid instantiation cost or side effects entirely.
  • --on-conflict replace helps debug when multiple packages expose the same model id.

Compliance testing

The standard_asr.compliance.check_entrypoints() helper powers the compliance suite and the CLI. It guarantees:

  1. Entry points exist (no silent typos).
  2. Factories load successfully.
  3. Factories that can be invoked without arguments produce an object exposing transcribe.
  4. properties.model_id matches the entry point key.

Plugin authors can integrate the check into their CI:

from standard_asr.compliance import check_entrypoints

report = check_entrypoints()
if not report.passed:
    for issue in report.issues:
        print(issue.level, issue.model, issue.message)
    raise SystemExit(1)

The Standard ASR compliance suite imports this helper to keep the ecosystem predictable. The checker already verifies capability declarations alongside entry-point metadata (see the full surface below), and grows additively as the metadata contract expands (supported locales, etc.) while keeping the API stable for you.

The full compliance surface

check_entrypoints() covers entry-point metadata and class-level declarations. The standard defines seven compliance dimensions; the remaining checks are also importable from standard_asr.compliance:

CheckWhat it assertsHow to run
check_entrypointsEntry-point metadata, capability declarations, the optional prepare() contractstandard-asr compliance entrypoints / compliance run
check_provider_params_swap_safety(engine)An engine rejects another engine's provider_params rather than silently misreading them (spec Runtime R3 / §5.4)standard-asr compliance run (per zero-arg engine)
check_streaming_param_gating(engine)A streaming engine gates an unsupported standard parameter per its strict/best_effort policystandard-asr compliance run (per zero-arg streaming engine)
check_recommended_wire_format(engine)recommended_wire_format() returns AudioFormat | None and any returned format passes the engine's own session-establishment rule — the member is unconditional (spec §3.1: Properties-pure, capability-blind), so this holds for every engine, batch-only includedstandard-asr compliance run (per zero-arg engine, inside the entrypoint instance checks)
check_sync_bridge(session_factory)The async→sync bridge terminates without deadlock or a leaked threadstandard-asr compliance run --include-bridge (opens a session)
check_event_sequence(events)A recorded streaming event stream obeys the segment/event-order contractlibrary API only — drive it from your own tests with recorded events
check_transcription_result(result, capabilities=...)A recorded batch result carries no speaker labels beyond the declared batch.diarization capability (code result_exceeds_diarization)library API only — drive it from your own tests with a recorded result

standard-asr compliance run orchestrates every check except check_event_sequence and check_transcription_result for you. It runs the entrypoint instance checks (including the wire-format round-trip) and check_provider_params_swap_safety for each zero-arg engine, then check_streaming_param_gating for each streaming engine (no-billing probes). It also runs check_sync_bridge when you opt in via --include-bridge (it opens a session). check_event_sequence needs an author-recorded event stream, and check_transcription_result an author-recorded batch result. The CLI cannot synthesize these, so wire them into your test suite:

import pytest

from standard_asr.compliance import (
    check_entrypoints,
    check_event_sequence,
    check_provider_params_swap_safety,
    check_recommended_wire_format,
    check_streaming_param_gating,
    check_sync_bridge,
    check_transcription_result,
)
from my_engine import create_engine  # your zero-arg factory


def test_entrypoints_compliant() -> None:
    report = check_entrypoints()
    assert report.passed, [i.message for i in report.issues]


def test_provider_params_swap_safe() -> None:
    report = check_provider_params_swap_safety(create_engine())
    assert report.passed, [i.message for i in report.issues]


def test_streaming_gating_compliant() -> None:
    report = check_streaming_param_gating(create_engine())
    assert report.passed, [i.message for i in report.issues]


def test_recommended_wire_format_consistent() -> None:
    report = check_recommended_wire_format(create_engine())
    assert report.passed, [i.message for i in report.issues]


def test_sync_bridge_no_deadlock() -> None:
    engine = create_engine()
    fmt = ...  # an AudioFormat using one of your declared wire_encodings
    report = check_sync_bridge(lambda: engine.start_transcription(audio_format=fmt))
    assert report.passed, [i.message for i in report.issues]


def test_event_sequence_contract() -> None:
    events = [...]  # a recorded list[TranscriptionEvent] from a real session
    report = check_event_sequence(events)
    assert report.passed, [i.message for i in report.issues]


def test_batch_result_within_capabilities() -> None:
    engine = create_engine()
    result = ...  # a recorded TranscriptionResult from a real transcribe() call
    report = check_transcription_result(result, capabilities=engine.declared_capabilities)
    assert report.passed, [i.message for i in report.issues]

Checklist for plugin authors

  • Choose a PEP 503–friendly engine id (ideally your package name).
  • List every shipped preset as <engine_id>/<model_name>.
  • Skip the explicit default (engine_id/) unless you need one; if you publish it, document what it selects.
  • Ensure factories accept keyword arguments for configurable options.
  • Run standard-asr compliance run before publishing (and cover the recorded-data checks in your tests: check_event_sequence for a streaming engine, check_transcription_result for a batch engine — see The full compliance surface above).

Following this guide gives downstream users a consistent discovery experience and keeps the Standard ASR catalog healthy.

On this page