Disclosure up front: this is our product. I'm Yusuf, founder of Homingo, and this is a paid course from Homingo Learn: ₹1,499 one-time (about US$17; international cards are accepted).
Why voice agents are their own problem
A text agent can take two seconds to answer and nobody minds. On a call, a pause that long feels broken. A voice agent has to decide when the caller has finished talking, start speaking before the model has finished writing, and stop at once when the caller cuts in. Then comes the bug most demos never meet: after an interruption, the model's history still says it spoke the whole reply, so it carries on as if the caller had heard words they never heard.
The course builds an AI receptionist that handles all of this, one tested part at a time, in your own repo.
One example: remember only what the caller heard
When the caller barges in, the agent measures how much of the reply was actually played, then rewrites that turn of its history. This is the function from the course repo:
def commit_heard(self, heard: str) -> None:
"""The caller cut us off: keep the facts (tool calls ran, a booking is real), and replace this
turn's spoken text with only what they heard, marked as cut off."""
turn = self.history[self._turn_start:]
keep = []
for m in turn:
if m["role"] == "assistant" and not m.get("tool_calls"):
continue # unsaid text: dropped
if m["role"] == "assistant":
m = {**m, "content": None} # spoken parts move into the heard line
keep.append(m)
answered = {m.get("tool_call_id") for m in keep if m["role"] == "tool"}
fixed = []
for m in keep: # every tool call needs a result message, or the next request is rejected
fixed.append(m)
for c in m.get("tool_calls") or []:
if c["id"] not in answered:
fixed.append({"role": "tool", "tool_call_id": c["id"],
"content": json.dumps({"error": "interrupted before the result came back"})})
self.history[self._turn_start:] = fixed
if heard.strip():
self.history.append({"role": "assistant", "content": heard.strip() + " —"})
Tool calls that ran stay in the history, because a booking that was made is real. Text the caller never heard is dropped. A tool call cut off before its result came back gets an explicit error result, so the next model request is still valid.
What you build, module by module
- The real-time voice loop and its latency budget: the repo running end to end offline, and turn metrics that split every turn into endpointing, speech to text, model, text to speech and playout time.
- Audio in: 20 ms frames, loudness in dBFS, voice activity with hysteresis, a minimum speech length and a silence timeout that ends the turn.
- Streaming speech: a chunker that turns the model's token stream into speakable pieces, so the first clause is spoken while the model is still writing.
- Turn-taking and interruptions: barge-in that ignores clicks and short noises, stops the agent about half a second after the caller starts talking, and cancels the model and speech work.
- Tool calls mid-conversation: free-slot search over opening hours, a hold-then-book flow that cannot double-book, idempotent retries, and a booking guard that needs the caller's own yes.
- Call handling: a G.711 mu-law codec you write for phone audio, a silence ladder that re-prompts and then hangs up, and the repo adds an optional Twilio Media Streams line.
- Safety, evaluation and shipping: masked numbers and emails in stored transcripts, bookings only for the caller's own number, emergencies answered without the model, and an eval gate over scripted calls.
Every module is a video lesson, an MCQ check and a coding round. The capstone is your own public repo with a live demo anyone can call; certificate on completion. Level: intermediate. You need Python and async basics.
Watch the first lesson first
The first lesson, "The real-time voice loop: where every millisecond goes", is open to everyone in full, no sign-in needed. The whole syllabus is on the same page:
If you have built a voice agent, I'd like to hear where your latency went, or what interruptions broke.
Top comments (0)