Chapter 3 · Part 2
Brain: Claude, tuned for talking
The transcript is just text, so sending it to Claude is an ordinary API call. The interesting part is that a good spoken reply is different from a good written one, so we have to tell Claude that.
Write for the ear, not the eye
Ask Claude a question in a chat window and you might get a heading, three bullet points, a bold phrase and a follow-up question. That's great to read. Read aloud, it's a disaster: the voice says "asterisk asterisk", or ploughs through a list with no pauses, for forty seconds.
The fix is a system prompt, standing instructions that apply to every reply:
from anthropic import Anthropic
client = Anthropic()
SYSTEM = """You are a friendly voice assistant. Everything you write is read aloud by a
text-to-speech voice, so:
- Answer in one to three short sentences unless the user asks for more.
- Plain spoken sentences only: no markdown, lists, headings, code or emoji.
- Write numbers and symbols the way you would say them."""Each line earns its place:
- Short. Listening is slower than reading, and you can't skim speech. A short answer is also a fast answer, which matters a lot here.
- No formatting. A voice can't show bold or bullets. Anything markdown-shaped either gets read out as symbols or lost.
- Numbers as spoken. "3,776 m" might come out as "three comma seven seven six m". Asking for "about three thousand seven hundred seventy-six meters" gets it said properly.
Remember the conversation
The Claude API doesn't remember anything between calls. Each request has to carry the whole
conversation so far. So we keep a messages list, add what the user said, and add Claude's reply
after each turn:
messages = []
def think(text):
messages.append({"role": "user", "content": text})
reply = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
system=SYSTEM,
output_config={"effort": "low"}, # quick answers for small talk
messages=messages,
)
messages.append({"role": "assistant", "content": reply.content})
return "".join(b.text for b in reply.content if b.type == "text")That's what lets you say "how tall is it?" after asking about Mount Fuji, and have "it" mean the mountain.
Two lines here need explaining:
output_config={"effort": "low"}. Effort controls how hard Claude thinks before it answers. For a quick spoken question, deep reasoning mostly adds waiting, so we turn it down. If you ask it something tricky and the answers feel shallow, try"medium".if b.type == "text". Claude can think before it answers, and that thinking comes back as its own block inreply.content, ahead of the text. Soreply.content[0]isn't always the answer. Filtering fortextblocks is the safe way to get only the words to speak.
We store the full reply.content in the history, not just the text string. That keeps any
thinking blocks in the conversation, which is what the API expects when you send the history
back on the next turn.
Try the brain on its own
Wire the ears and the brain together and print the reply:
if __name__ == "__main__":
heard = listen()
print("You:", heard)
print("Claude:", think(heard))You: What's the tallest mountain in Japan, and how tall is it?
Claude: The tallest mountain in Japan is Mount Fuji, at about three thousand seven hundred
seventy-six meters. It's also an active volcano.(Claude's exact wording will differ.) It's short, plain and speakable, which is just what the mouth needs.