zeekayandHanzo Dev 6f516b97e5 speech: return the duration it measured, and the container it produced
Two values were computed here and thrown away, and both were the answer to a
question the caller had to ask.

DURATION. `segments, _ = load(model).transcribe(...)` discarded faster-whisper's
info, which carries the audio's duration. Transcription is billed per MINUTE by
everyone who sells it, so a response without a duration cannot be billed at all
— the ai plane metered every transcription at zero seconds no matter how much
audio was sent. transcribe returns it now, and verbose_json carries it exactly
as OpenAI's does (plain json stays {"text"}, so a client reading the standard
shape is untouched). It is `duration`, the audio SUBMITTED, never
`duration_after_vad`: a caller is charged for what they asked us to listen to,
not for how much of it turned out to be talking.

CONTAINER. `if response_format == "wav" ... else mp3` answered opus, aac, flac
and pcm with an MP3. The gateway then labelled the response from the REQUEST, so
a caller asking for opus received `Content-Type: audio/opus` wrapping an MP3 —
the API lying to whatever had to play it. ffmpeg in this image has libopus,
libmp3lame, aac, flac and pcm_s16le with the matching muxers, verified against
`ffmpeg -encoders` in the running pod, so every OpenAI format is really encoded
and anything else is a 400 that names what IS available. Substitution is gone,
not relabelled.

A suite arrives with them, and gates the build before the registry login so a
red run cannot publish. It stubs the two model calls, needing no weights and no
GPU, and answers in seconds.

The mutation that matters: stubbing `stt.transcribe` to test the ROUTE let the
duration fix be reverted with the suite still green. The tests that catch it
stub the MODEL and exercise the real function — testing the seam you fixed, not
the one above it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 16:00:10 -07:00

speech

CPU speech for the Hanzo platform: STT (faster-whisper, CTranslate2 int8) and TTS (kokoro-onnx, 82M) behind one OpenAI-shaped surface.

Routes

  • POST /v1/audio/transcriptions — multipart file + model (+language, response_format) → {"text": …}
  • POST /v1/audio/speech — {model, input, voice, response_format} → audio bytes (mp3 default, wav native)
  • GET /v1/models, GET /healthz

Trust model

No auth of its own, in-cluster only. api.hanzo.ai fronts it through the ai plane, which authenticates and meters every caller — a provider row pointing at http://speech.hanzo.svc/v1 is the whole integration. Weights bake into the image at build; the container boots without the network.

Run

uv sync && uv run uvicorn main:app --port 8000

Phase 2 is /v1/realtime: websocket full-duplex (silero-vad segmentation, partial transcripts, kokoro frames, barge-in) plus camera-frame relay into the multimodal models.

S
Description
CPU speech service: STT (faster-whisper) + TTS (kokoro) behind api.hanzo.ai /v1/audio
Readme Apache-2.0
361 KiB
Languages
Go 98.8%
Dockerfile 1.2%