Skip to content

Voice

CLI overview · Control plane & integrations · Full voice guide

Use the voice family to operate the reference speech topology without confusing proxy ports with model ownership. The declared topology owner runs the STT and TTS model serves. Companion Node may own the realtime proxy and loopback forwarders, but it is model-free by default.

Choose a workflow

Goal Start here Then
Operate one co-located managed stack voice up --dry-run Apply with --confirm; voice down stops it in reverse dependency order.
Inspect audio readiness voice audio status Read a bounded tail with voice audio logs.
Change STT/TTS lifecycle voice audio up --dry-run Apply with --confirm; down stops native processes and stops/removes managed containers.
Run the realtime proxy interactively voice proxy run Interrupt the foreground process when finished.
Manage the background proxy voice proxy status Preview up, down, or restart, then apply with --confirm.
Forward Mini-local audio ports voice proxy bridge --dry-run Run the bridge in the foreground after reviewing both routes.
Measure a voice candidate voice benchmark Retain structured evidence with --evidence-out.
Qualify an STT model voice corpus prepare Validate it, then run voice benchmark --scope stt.
Measure TTS generation over a text corpus voice benchmark --scope tts Retain audio outside Git, then obtain independent STT and human listening evidence.
Inspect configuration overlays voice profiles list Validate one resolved overlay with voice profiles validate.
Prepare an optional sidecar voice sidecar validate Render a host command or Compose skeleton; neither command launches it.

Command map

Operate one co-located managed stack

Command Purpose
voice up Start managed STT, TTS, then the managed realtime proxy.
voice down Stop the managed realtime proxy, then managed STT/TTS.

Operate topology-owned audio serves

Command Purpose
voice audio status Read bounded STT/TTS readiness from the declared owner.
voice audio logs Read a bounded log tail for each owned audio serve.
voice audio up Preview or start managed/native STT and TTS serves.
voice audio down Preview or stop native STT/TTS processes and stop/remove managed containers.

Operate the Mini realtime layer

Command Purpose
voice proxy run Run the authenticated realtime WebSocket proxy in the foreground.
voice proxy status Inspect owned background-proxy process state.
voice proxy logs Read a bounded background-proxy log tail.
voice proxy up Preview or start the owned background proxy.
voice proxy down Preview or stop the owned background proxy.
voice proxy restart Preview or restart the same owned proxy instance.
voice proxy bridge Forward Mini-local STT/TTS ports to Dark in the foreground.

Evaluate and inspect configuration

Command Purpose
voice benchmark Replay one end-to-end session, or a reusable STT/TTS corpus, against resolved endpoints.
voice corpus prepare Build the deterministic 24-human/6-synthetic English STT corpus.
voice corpus validate Fail closed on malformed JSONL, audio metadata, paths, or hashes.
voice profiles list List named overlays without contacting a service.
voice profiles validate Validate one fully merged profile offline.

Prepare the optional sidecar

Command Purpose
voice sidecar validate Validate sidecar URLs, models, image, and secret references.
voice sidecar command Render shell-safe host argv without executing it.
voice sidecar compose Render a loopback-bound Compose service without writing or running it.

Co-located stack lifecycle

voice up and voice down are deliberately narrow aggregates for one managed host. The STT, TTS, and realtime-proxy tables must all use lifecycle = "managed", reference the same serves manifest, and belong to its voice group. STT/TTS use host-relative 127.0.0.1 URLs for host callers, while the managed proxy joins the audio-owned anvil-voice Docker network and uses the stt / tts service names. The command refuses native, external, incomplete, or split-host configurations before lifecycle I/O.

Preview the complete dependency order before applying:

anvil-serving voice up --config ~/.anvil-serving/voice.toml --dry-run
anvil-serving voice up --config ~/.anvil-serving/voice.toml --confirm
anvil-serving voice down --config ~/.anvil-serving/voice.toml --dry-run
anvil-serving voice down --config ~/.anvil-serving/voice.toml --confirm

Bring-up is STT/TTS first, then the proxy. A failed audio bring-up skips the proxy. Tear-down is the reverse; a failed proxy stop leaves audio running so a still-live proxy does not lose its dependencies. Both commands emit one combined result covering the audio and proxy phases.

The aggregate is manifest-owned and does not require topology resolution. If topology options are supplied explicitly, the command verifies that STT, TTS, and the realtime proxy resolve to one Docker owner before importing the handler.

The reference Mini/Dark topology is intentionally split and must keep using the explicit voice audio ... and voice proxy ... commands below. The aggregate does not compose separate controller operations across owners.

Audio lifecycle

Every audio lifecycle command requires topology because loopback is host-relative and STT/TTS are model workloads. Inspect before changing state:

anvil-serving voice audio status --topology ~/.anvil-serving/operator-topology.toml --profile dark-audio
anvil-serving voice audio logs --topology ~/.anvil-serving/operator-topology.toml --profile dark-audio --tail 50

status uses a three-second readiness deadline per managed serve by default; --ready-timeout accepts 0.1 through 60 seconds. logs defaults to 200 lines per serve and accepts 1 through 5000. Native file reads are capped at 1 MiB.

Preview and apply use the same owner, runtime, and lifecycle resolution:

anvil-serving voice audio up --topology ~/.anvil-serving/operator-topology.toml --profile dark-audio --dry-run
anvil-serving voice audio up --topology ~/.anvil-serving/operator-topology.toml --profile dark-audio --timeout-seconds 300 --confirm
anvil-serving voice audio down --topology ~/.anvil-serving/operator-topology.toml --profile dark-audio --dry-run
anvil-serving voice audio down --topology ~/.anvil-serving/operator-topology.toml --profile dark-audio --confirm

The command refuses split STT/TTS ownership, local execution against a remote owner, and lifecycle/runtime mismatches. Managed endpoints require Docker; native endpoints require a native runtime. External endpoints are reported and left untouched. Mini runs model workloads only when topology explicitly marks an experimental same-host mode such as mini-audio.

Audio up and down share an overall 300-second lifecycle deadline. Override it with --timeout-seconds from 1 through 7200 seconds; the deadline covers both STT and TTS subprocess work rather than resetting for each serve.

Realtime proxy

The reference proxy profile keeps models on Dark while the realtime session layer runs on Mini:

anvil-serving voice proxy run --topology ~/.anvil-serving/operator-topology.toml --profile mini-dark-audio-proxy

run resolves the Mini owner, verifies that the manifest listener matches the topology endpoint, and probes the router, STT, and TTS endpoints before binding. It remains in the foreground until interrupted. A candidate overlay is scoped to that process:

anvil-serving voice proxy run --topology ~/.anvil-serving/operator-topology.toml --profile mini-dark-audio-proxy --candidate voice-candidate --candidate-overlay candidate.toml

For managed background operation, inspect and preview first:

anvil-serving voice proxy status --topology ~/.anvil-serving/operator-topology.toml --profile mini-dark-audio-proxy
anvil-serving voice proxy logs --topology ~/.anvil-serving/operator-topology.toml --profile mini-dark-audio-proxy --tail 50
anvil-serving voice proxy up --topology ~/.anvil-serving/operator-topology.toml --profile mini-dark-audio-proxy --dry-run
anvil-serving voice proxy up --topology ~/.anvil-serving/operator-topology.toml --profile mini-dark-audio-proxy --confirm
anvil-serving voice proxy restart --topology ~/.anvil-serving/operator-topology.toml --profile mini-dark-audio-proxy --confirm
anvil-serving voice proxy down --topology ~/.anvil-serving/operator-topology.toml --profile mini-dark-audio-proxy --confirm

The default owned records are ~/.anvil-serving/run/voice-proxy.pid and voice-proxy.log. Status and lifecycle commands reject stale, reused, or foreign PID state. Logs are a bounded tail rather than a follow stream.

The bridge is a separate foreground process. Mini-local ports 127.0.0.1:30110 and 127.0.0.1:30111 forward to Dark; they are not local model serves:

anvil-serving voice proxy bridge --topology ~/.anvil-serving/operator-topology.toml --profile mini-dark-audio-proxy --dry-run
anvil-serving voice proxy bridge --topology ~/.anvil-serving/operator-topology.toml --profile mini-dark-audio-proxy

Topology supplies listen ports and Dark target addresses. The listener must remain 127.0.0.1; localhost, wildcard targets, and public target IPs are rejected.

Benchmark

Benchmark the resolved manifest or one already-loaded candidate:

anvil-serving voice benchmark --profile mini-dark-audio-proxy --candidate current-fast
anvil-serving voice benchmark --candidate-base-url http://127.0.0.1:30001/v1 --candidate-model MODEL --evidence-out artifacts/voice/candidate.json

--candidate-overlay applies after the selected profile. A direct candidate URL and model must be supplied together; the optional token flag names an environment variable. These overrides never rewrite the manifest.

For a speech baseline, pass a local regular PCM16, mono, 16-kHz RIFF WAV and its verbatim transcript together. The file must be no longer than 30 seconds; a symbolic link in the input-file path's final component, non-regular files, compressed or malformed WAVs, stereo audio, other sample rates, and any header/data size mismatch are rejected before the benchmark contacts an endpoint:

anvil-serving voice benchmark --scope end-to-end \
  --input-wav artifacts/voice/utterance.wav \
  --reference-text "the recorded utterance, exactly as spoken" \
  --evidence-out artifacts/voice/speech-baseline.json

The evidence retains the input kind, source-WAV and PCM byte counts and SHA-256 values, audio format, duration, and transcript SHA-256. A valid WAV only establishes container properties: the input is labelled supplied-content-unverified, so retain corpus provenance separately before interpreting it as human speech. Supplying a WAV still measures the current serialized STT → LLM → TTS replay. Its structured measurement_scope records serialized-stage-replay, first-nonempty-yielded-TTS-chunk, realtime=false, and acoustic_playback=false. ttfa_ms is null when TTS yields no audio, and otherwise records the first nonempty chunk yielded after any stage-side buffering; it is not a first-response-byte or audible-playback measurement. Without these two options, the command retains compatibility by using a 220-Hz synthetic tone. Its input identity says synthetic-tone-not-speech and not-qualifying; it is only a wire-path smoke, never speech-baseline evidence.

The benchmark records resolved model and endpoint identity with its end-to-end STT, router, and TTS metrics. Evidence output is restricted to the workspace or configured evidence root. Evidence publication is atomic and refuses to overwrite an existing file, including when another writer publishes the same path concurrently. Select a new --evidence-out filename for every attempt. Unreachable dependencies return nonzero and do not create a successful measurement record.

STT corpus

Prepare the corpus into an evidence workspace:

anvil-serving voice corpus prepare --config ~/.anvil-serving/voice.audio-dark.toml --out artifacts/stt/corpus
anvil-serving voice corpus validate --manifest artifacts/stt/corpus/manifest.jsonl --expected-cases 30

The versioned JSONL manifest resolves audio paths against itself and accepts only 16-kHz mono WAV/FLAC with matching SHA-256. Run a candidate through an STT-only overlay:

anvil-serving voice benchmark --scope stt \
  --config ~/.anvil-serving/voice.audio-dark.toml \
  --corpus artifacts/stt/corpus/manifest.jsonl \
  --repetitions 3 --concurrency 1 \
  --stt-candidate-overlay examples/primary-node/stt-experiments/overlays/nemotron35-asr.toml \
  --evidence-out artifacts/stt/nemotron-sequential.json

Use a separate --repetitions 1 --concurrency 4 lane. --auto-language-probes 6 records six additional unconditioned human requests; it does not qualify multilingual behavior. Evidence uses stt-benchmark-evidence/v1, writes atomically, and exits nonzero with complete=false if an expected request fails.

Prepare a separate 120-human English holdout corpus with 60 utterances from each LibriSpeech split, excluding the canonical corpus:

anvil-serving voice corpus prepare --config ~/.anvil-serving/voice.toml \
  --out artifacts/stt/holdout --human-cases-per-split 60 --synthetic-cases 0 \
  --exclude-manifest artifacts/stt/corpus/manifest.jsonl
anvil-serving voice corpus validate --manifest artifacts/stt/holdout/manifest.jsonl --expected-cases 120

--human-cases-per-split accepts multiples of three from 3 through 300 and balances short, medium, and long source durations equally in each split. --synthetic-cases accepts 0–6 fixed phrases; zero sends no TTS requests. The default remains 12 human cases per split plus six synthetic cases. Exclusion validates the supplied manifest, filters exact case IDs and source identities before deterministic selection, and rejects normalized-audio hash overlap. Supplement preparation validates its exact requested count and rejects duplicate audio hashes or IDs before publishing the output directory. provenance.json retains the exclusion manifest hash and identities, actual duration-bucket counts, archive checksums, source FLAC hashes, and normalized WAV hashes; the command also returns its provenance hash. Failed preparation leaves no partial published corpus.

TTS corpus

Measure an already-running TTS endpoint using the selected voice manifest and profile. Store audio in a new directory outside every Git worktree; retain JSON under the workspace or a configured evidence root:

anvil-serving voice benchmark --scope tts \
  --config ~/.anvil-serving/voice.toml --profile dark-audio \
  --corpus artifacts/tts/prompts.jsonl --expected-cases 24 \
  --repetitions 3 --concurrency 1 \
  --audio-out ~/.local/share/anvil-serving/tts-audio/run-001 \
  --evidence-out artifacts/tts/run-001.json

Each contiguous JSONL line has exactly these fields:

{"schema_version":"tts-corpus/v1","id":"short-01","text":"Please cancel the reminder.","category":"short-correction","language":"en"}

The command rejects duplicate IDs or JSON fields, blank text, unsupported schemas, unsafe ID path tokens, extra fields, count mismatches, and oversized corpora before contacting the endpoint. It accepts 1–128 cases, 1–20 warm repetitions, and at most 1024 warm requests. TTS concurrency is currently 1. The optional --expected-cases supplies an exact count gate. The output directory must be new and contain no symbolic-link ancestors or parent traversal.

Use --tts-candidate-overlay candidate.toml to apply a TTS-only overlay after the profile. A candidate overlay contains only [voice.tts] and the optional [tts_benchmark.identity] table. Identity accepts served_name, checkpoint, revision, runtime, runtime_version, image, image_digest, hardware, and quantization, as nonempty strings. --candidate supplies a label; otherwise the overlay filename supplies it. These declarations describe the operator's pinned candidate and are not an endpoint attestation.

The supported endpoint contract is OpenAI HTTP /v1/audio/speech. The manifest's voice_id, when declared, is sent as voice; otherwise evidence labels voice selection server-default-unverified. Declared language is also sent without inferring language from each corpus case. Authentication uses the configured api_key_env reference and rejects an unset variable. The request preserves the exact model, voice, language, and response format, and refuses HTTP redirects. LLM/STT candidate options are rejected for this scope.

voice.tts.stream is an explicit Boolean request option and defaults to true. Set it to false for a declared buffered adapter; the same setting reaches both corpus benchmarks and the live voice pipeline. Evidence retains the selected setting and labels buffered first-audio timing accordingly: the first body data may arrive only after synthesis completes. The command does not retry a rejected streaming request as a buffered request.

voice.tts.max_tokens optionally sends an explicit OpenAI HTTP generation cap. It accepts integers from 1 through 8192; Boolean, fractional, zero, negative, and larger values are rejected. When absent, the request omits the field and retains the server default. A declared value applies identically to the live pipeline and corpus benchmark and is retained in the resolved configuration and its hash. Set it in the manifest or TTS candidate overlay; changing the cap defines a new tested configuration and does not modify historical comparison evidence. The server determines what one generation token represents for its model.

The evidence schema is tts-benchmark-evidence/v1. It retains the exact request endpoint and configuration, source manifest/overlay hashes, resolved TTS configuration hash, corpus hash and file order, a separate first-observed cold request, and every warm request in repetition then JSONL order. The cold label does not imply empty endpoint caches: the command never resets a model or operates its lifecycle. Failures remain in the evidence, and complete=true requires every expected request and zero failures. Incomplete runs write evidence atomically and exit nonzero.

Each request records response status, byte count and SHA-256, first body read, first complete audio frame observed in an HTTP read, request-through-EOF latency, source audio duration, and client read chunk timings. WAV headers are excluded from first-audio timing. Chunk timings describe HTTP reads and may include client buffering; they do not establish server synthesis boundaries or acoustic playback. The end-to-end metric excludes decoding and artifact writes. Chunk logs retain at most the first 64 reads per request and 16,384 reads across the complete run, including the cold request. Each request reports retained and omitted counts and explicit truncation. First-frame timing remains independent of these log limits.

summary.warm_artifact_inclusive_wall_seconds includes HTTP requests, decoding, resampling, file writes, fsync, artifact hashing, and advisory screening. It describes harness elapsed time; endpoint throughput is explicitly unmeasured. Per-request latency and RTF continue to use HTTP request-through-EOF timing. The two RTF orientations are explicit:

Evidence field Definition Faster direction
generation_seconds_per_audio_second Request-through-EOF seconds / source audio seconds Lower
audio_seconds_per_generation_second Source audio seconds / request-through-EOF seconds Higher

The declared response format is honored. Raw pcm means signed 16-bit little-endian mono at source_sample_rate; wav uses the validated container's rate and accepts mono PCM16 or IEEE float32. WAV is permitted only in this TTS benchmark context; realtime voice configuration still requires PCM. The original response is retained beside a normalized mono PCM16 WAV at target_sample_rate. Normalization uses whole-response linear interpolation without an anti-aliasing filter, so listening at the native rate remains useful. Nonfinite float samples are replaced with zero for normalization and counted. Responses are bounded to 16 MiB and 120 audio seconds; the entire audio artifact directory is bounded to 512 MiB. Evidence contains only relative audio artifact names and filesystem-path hashes, and never embeds audio byte bodies.

Blank signals, nonfinite samples, repeated 100-ms waveform blocks, and identical audio returned for different text are deterministic advisory flags. They neither prove intelligibility nor automatically fail a quality gate. Independent STT fidelity validation, human listening, and listening preference remain separate and explicitly not_run or not_measured in this evidence. The command does not measure playback or cancellation and never promotes a candidate.

Profiles

Profiles are offline manifest overlays:

anvil-serving voice profiles list
anvil-serving voice profiles list --config ~/.anvil-serving/voice.toml
anvil-serving voice profiles validate --profile dark-audio
anvil-serving voice profiles validate --config ~/.anvil-serving/voice.toml --profile mini-dark-audio-proxy

list validates the base manifest and prints declared names. validate requires one name, merges that overlay, and checks the resolved schema. Neither verb resolves topology, probes an endpoint, starts a service, or changes the manifest.

Speech-to-speech sidecar

The sidecar helpers are offline renderers. Validate first:

anvil-serving voice sidecar validate --config examples/huggingface-speech-to-speech/openclaw-gateway.example.toml
anvil-serving voice sidecar validate --config examples/huggingface-speech-to-speech/openclaw-gateway.example.toml --json

Validation rejects inline secrets, credential-bearing URLs, unsafe container loopback, malformed service values, and endpoints that do not end in /v1 or /v1/realtime as required.

Render host argv or a Compose skeleton without launching anything:

anvil-serving voice sidecar command --config examples/huggingface-speech-to-speech/openclaw-gateway.example.toml
anvil-serving voice sidecar command --config examples/huggingface-speech-to-speech/openclaw-gateway.example.toml --with-auth --json
anvil-serving voice sidecar compose --config examples/huggingface-speech-to-speech/openclaw-gateway.example.toml
anvil-serving voice sidecar compose --config examples/huggingface-speech-to-speech/openclaw-gateway.example.toml --service-name voice-sidecar --with-auth

--with-auth emits only an environment-variable reference. It does not read or print the token. Compose output publishes the realtime port on 127.0.0.1, uses the container-specific router URL, and never invokes Docker or writes a file.