ALLM.TranscriptionStreamAdapter behaviour (allm v0.6.1)

Copy Markdown View Source

Realtime speech-to-text contract: transcripts arrive while audio is still being sent.

Layer B — runtime. A transcription adapter opts into streaming by implementing this behaviour in addition to ALLM.TranscriptionAdapter, in the same module. There is no separate engine slot: the engine's :transcription_adapter is checked with function_exported?/3.

stream_transcribe/3 takes an ALLM.TranscriptionStreamRequest (the session configuration) and an enumerable of audio chunks, and returns {:ok, enumerable} of ALLM.TranscriptionEvent values obeying the grammar stated in ALLM.TranscriptionEvent.

Input elements

Each element of the audio enumerable is either:

  • a binary of PCM16 little-endian mono samples at request.sample_rate, or
  • :commit, which forces the current segment to be committed under either commit strategy.

Minimum impl skeleton

defmodule MyRealtimeTranscription do
  @behaviour ALLM.TranscriptionAdapter
  @behaviour ALLM.TranscriptionStreamAdapter

  alias ALLM.Error.TranscriptionAdapterError

  @impl ALLM.TranscriptionAdapter
  def max_audio_bytes, do: 25 * 1024 * 1024

  @impl ALLM.TranscriptionAdapter
  def transcribe(request, opts), do: MyProvider.transcribe(request, opts)

  @impl ALLM.TranscriptionStreamAdapter
  def stream_sample_rates, do: [16_000]

  @impl ALLM.TranscriptionStreamAdapter
  def stream_transcribe(%ALLM.TranscriptionStreamRequest{} = request, audio, opts) do
    if request.sample_rate in stream_sample_rates() do
      key = ALLM.Keys.fetch!(:my_provider, opts)
      {:ok, MyProvider.realtime_stream(request, audio, key)}
    else
      # Invariant 2: before ALLM.Keys.fetch!/2.
      {:error,
       TranscriptionAdapterError.new(:invalid_request,
         metadata: %{sample_rate: request.sample_rate}
       )}
    end
  end
end

Invariants

  1. The synchronous return is exactly {:ok, enumerable} or {:error, %ALLM.Error.TranscriptionAdapterError{}}. The one documented exception is ALLM.Keys.fetch!/2, which raises %ALLM.Error.EngineError{reason: :missing_key} by design.
  2. Lazy. No I/O happens until the enumerable is reduced. Pre-flight gates run synchronously before ALLM.Keys.fetch!/2. The per-adapter gate is request.sample_rate in stream_sample_rates(), else :invalid_request with metadata.sample_rate.
  3. The enumerable obeys the ALLM.TranscriptionEvent grammar: one :transcription_started, then partial and committed transcripts, then one :transcription_completed; or a failure ending in one {:error, _}. Nothing follows a terminal event.
  4. Cleanup: halt-safe. A consumer halt releases the transport and stops the input pump within 500 ms, and leaves no stream-owned message in the consumer's mailbox. The input's own resources are released by process exit, not by its after functions.
  5. opts[:stream_timeout] (milliseconds of silence, default 60,000) is honoured. The timer resets on every transport message and every input-pump message. Expiry ends the stream with {:error, %ALLM.Error.TranscriptionAdapterError{reason: :timeout}}.
  6. opts[:request_id] appears on :transcription_started and :transcription_completed. request.metadata appears unchanged on :transcription_completed's :metadata.
  7. Input. An element that is neither a binary nor :commit, or an input that raises or crashes, ends the stream with :invalid_request and metadata.cause one of :invalid_input_chunk, :input_raised or :input_crashed. For the last two, the error's :cause field is a %{kind: kind, message: message} map (kind is :error, :throw or :exit, message a string), and the consuming process is never killed. Chunk boundaries are the caller's, not the protocol's: a chunk may split a 16-bit sample, and the adapter carries the odd byte into the next chunk. An input whose total length is odd ends the stream with :invalid_input_chunk at end of input; an odd-length chunk followed by one that completes the sample is not an error. The audio the provider receives, in order, equals the concatenated input.
  8. End of input. When the input is exhausted, the adapter commits any uncommitted audio and waits up to :stream_timeout for the final :committed_transcript, then emits :transcription_completed and closes.
  9. Span flags, committed segments. With request.timestamps or request.logprobs exactly true, every :committed_transcript is built with ALLM.TranscriptionEvent.committed_transcript/3. Its :spans is a list of ALLM.TranscriptSpan, or nil when the adapter has no trustworthy span data for that segment. Attributes are dropped as in ALLM.TranscriptionAdapter's span-flag invariant (times only under timestamps: true, the logprob only under logprobs: true), and an unsupported set flag is refused the same way, synchronously, before ALLM.Keys.fetch!/2.
  10. Span flags, completion. With either flag true, :transcription_completed carries an extra :spans key: the concatenation, in commit order, of every committed segment's non-nil :spans, or [] when there were none. ALLM.AudioStream.collect_transcription/1 copies it onto the response and does not fold the committed events.
  11. Flags off. With neither flag true, committed segments are built with committed_transcript/2 and no event carries a :spans key. (A scripted %ALLM.TranscriptionResponse{} with non-nil :spans, which ALLM.Providers.FakeTranscription streams as scripted, is the one exception.)

:transcription_completed's :text is the committed segments, each trimmed, empties dropped, joined with one space. Its :duration_seconds is computed from the bytes sent, bytes / (sample_rate * 2).

Input is reduced in another process

An adapter reduces the audio enumerable through ALLM.Providers.Support.InputPump, so the input runs in a helper process. An input that reads the caller's mailbox, such as a microphone relay built on receive, must be relayed from a process the caller controls.

Summary

Types

One element of the audio enumerable fed to stream_transcribe/3.

Callbacks

Return the PCM sample rates, in Hz, stream_transcribe/3 accepts.

Transcribe an enumerable of audio chunks as they arrive.

Types

audio_chunk()

@type audio_chunk() :: binary() | :commit

One element of the audio enumerable fed to stream_transcribe/3.

Callbacks

stream_sample_rates()

@callback stream_sample_rates() :: [pos_integer()]

Return the PCM sample rates, in Hz, stream_transcribe/3 accepts.

A caller checks it before opening a microphone, as it checks ALLM.TranscriptionAdapter.max_audio_bytes/0 before uploading a file.

stream_transcribe(t, t, keyword)

Transcribe an enumerable of audio chunks as they arrive.

Returns {:ok, enumerable} of ALLM.TranscriptionEvent values, or {:error, %ALLM.Error.TranscriptionAdapterError{}} from a pre-flight gate.