5 Commits
Author SHA1 Message Date
zeekay 5ef263e73b fix(session): a reply spoken over ends its turn, not the socket; it writes on the session's context and leaves the next reply's floor alone
Claude-Session: https://claude.ai/code/session_01AGuBEqCjuman4E9FdKEgeV
2026-10-03 03:14:26 -07:00
zeekayandHanzo Dev 30c22bcbd9 voice is a library, not a second service
I gave it a Dockerfile and an image, which mints another thing to build, deploy,
route and operate for one package. api.hanzo.ai is the AI surface and already
serves /v1/audio/*; the realtime socket belongs beside them, reached the one way
everything else is.

The gate stays. This repo had none, its suite opens no database, and a finished
service that nothing compiles on push is how it came to be complete and
unreachable at the same time.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-15 09:30:30 -07:00
zeekayandHanzo Dev ea0da10976 Fetch the modules the way they are published
Both dependencies are public and on the proxy. The private-module apparatus I
copied from a sibling — GOPRIVATE, a git rewrite, a build secret — was for a
problem this tree does not have, and it is the half of the build that cannot
explain itself when it fails.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-15 09:29:10 -07:00
zeekayandHanzo Dev fdb98ab60f Give voice a way to be built
The service was finished and measured — answer latency, chunking, the three
seams — and had no Dockerfile, no gate and no image. So it ran nowhere:
/v1/voice answers 404 in production while the code sits complete in main, and
nothing that wants a spoken conversation can have one.

The gate here runs the tests, not just a compile. Nothing in this tree opens a
database; the seams are interfaces and talk_test.go drives them with stand-ins,
so the suite is hermetic and belongs on every push rather than in a lane of its
own.

Everything it reaches is already ours and needs no argument: speech at
speech.hanzo.svc, the model through api.hanzo.ai, JWKS from hanzo.id. Audio is
billed to whoever spoke, never to a service account.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-15 09:24:59 -07:00
zeekayandHanzo Dev 03a9f42e10 Serve a spoken conversation over one socket
Audio in, audio out, on an IAM bearer, billed to whoever spoke. speech holds
the models; this holds the conversation.

The wire is the OpenAI realtime protocol so existing clients work unchanged.
hanzoai/go-openai-realtime already carries the event types but is a client, so
it can only unmarshal the server's half; Read supplies the other direction.

A browser cannot put a header on a WebSocket, so the bearer is spent on a POST
and the URL carries a single-use thirty-second ticket naming a principal
already verified here. It has no claims and cannot be verified elsewhere, so a
copy lifted from a log buys nothing.

Replies are synthesised a sentence at a time: 452 ms to first sound against
2267 ms for a whole reply, and every sentence after the first is free because
synthesis outruns speech. Measured end to end at 1.15-1.55 s from the speaker
stopping, of which 500 ms is the silence detector that Turn exists to replace.

Turn, Mind and Meter are the seams. Hush ends a turn on silence alone, which
cuts people off mid-thought and waits through gaps it should have taken; it is
there so the surface works while a detector that reads intent is built.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 17:37:04 -07:00