---
title: Expressive audio passthrough with Director Notes cues
description: "Drive an Anam avatar's voice and face from one annotated line of text — Cartesia Sonic 3.5 for the voice, Director Notes cues over the data channel for the face, kept in sync with word timestamps."
tags: [python, audio-passthrough, director-notes, tts]
difficulty: intermediate
sdk: python
date: 2026-07-11
authors: [sr-anam]
---
In [audio passthrough](/python-audio-passthrough-tts) mode Anam runs face generation only — it never sees the persona's text, so it can't infer facial expression on its own, and there's no text to carry [Director Notes **inline cue tags**](https://docs.anam.ai/personas/director-notes) like `[warm]` or `[surprised]`. Instead, you send those same cues as `director_note_cue` messages over the WebRTC data channel to steer the avatar's face during a turn.
This recipe combines three things into one Python script:
- **Cartesia Sonic 3.5** synthesises the speech for the **voice** — Sonic reads the emotional subtext of the text, and `[laughter]` stays inline so it laughs.
- **Audio passthrough** streams that PCM into Anam for lip-sync.
- **Director Notes cues** steer the **face**, timed to the spoken word using Cartesia's word-level timestamps.
The neat part: **one annotated line drives the whole performance.** You write `[warm] Come closer. [surprised] Wait — what was that? [laughter] Oh, it's nothing.` — the tags steer the face, `[laughter]` makes the voice laugh too, and Sonic reads the rest from the words.
The complete code is at [examples/python-director-notes-audio-passthrough](https://github.com/anam-org/anam-cookbook/tree/main/examples/python-director-notes-audio-passthrough).
Director Notes require a **cara-4 avatar** (`avatar_model="cara-4"`) — older models ignore the cues. `send_director_note_cue` also only exists in **`anam` ≥ 0.7.0a1**, so this example installs a prerelease. Audio passthrough and Director Notes are both **Beta**.
## What you'll build
A Python script that:
- Accepts a typed line (optionally annotated with `[tags]`) and splits it into expression segments
- Streams it from Cartesia Sonic 3.5 as 24 kHz PCM, with word timestamps
- Forwards the audio to the avatar as it arrives (audio passthrough)
- Fires Director Notes cues over the data channel as each word streams in, aligned to the spoken word
## Prerequisites
- Python 3.10+ and [uv](https://docs.astral.sh/uv/)
- An Anam API key from [lab.anam.ai](https://lab.anam.ai) and a **cara-4** avatar
- A Cartesia API key from [cartesia.ai](https://cartesia.ai)
## Project setup
```bash
git clone https://github.com/anam-org/anam-cookbook.git
cd anam-cookbook/examples/python-director-notes-audio-passthrough
uv sync --prerelease=allow
cp .env.example .env
```
Edit `.env`:
```bash
ANAM_API_KEY=your_anam_api_key
ANAM_AVATAR_ID=your_cara4_avatar_id
ANAM_AVATAR_MODEL=cara-4
CARTESIA_API_KEY=your_cartesia_api_key
CARTESIA_VOICE_ID=6ccbfb76-1fc6-48f7-b71d-91ac6298247b
```
## How the tags drive expression
You author one line with the **Anam cue tags** — `[happy] [warm] [playful] [curious] [supportive] [concerned] [sad] [surprised] [angry] [distressed] [laughter]` — and they steer the **face**, sent verbatim via `send_director_note_cue`.
The **voice** needs no separate tag vocabulary: Sonic 3.5 reads the emotional subtext of the words on its own, so you just synthesise the plain text. The one exception is `[laughter]` — Cartesia's documented non-verbal — which is kept inline in the transcript so the voice laughs while the face does.
## Configure the persona
Enable passthrough and set a baseline performance style. `avatar_model="cara-4"` is what makes Director Notes work.
```python
from anam.types import PersonaConfig, DirectorNotes
persona_config = PersonaConfig(
avatar_id=avatar_id,
avatar_model="cara-4",
enable_audio_passthrough=True,
director_notes=DirectorNotes(preset_style="warm", expressivity=0.7),
)
```
The baseline `director_notes` style sets the avatar's default presence for the whole session; the cues below shift it during a specific turn on top of that. `expressivity` (0–1) dials how strongly cues are followed.
## Parse the line into segments
Split the typed line at each known `[tag]`. Each segment carries the tag (for the face) and clean spoken text. `[laughter]` is kept inline so Cartesia's voice actually laughs.
```python
segments = parse_tagged_line("[warm] Hi there. [curious] What's that noise? [laughter]")
# -> [Segment("warm", "Hi there."), Segment("curious", "What's that noise?"), Segment("laughter", "")]
```
## Open the Cartesia stream
Open one websocket context with `add_timestamps=True` and push each segment's text. Request raw 24 kHz PCM for a good latency/quality balance; Anam suggests 24000 Hz for best performance.
```python
with cartesia.tts.websocket_connect() as ws:
ctx = ws.context(
model_id="sonic-3.5",
voice={"mode": "id", "id": voice_id},
output_format={"container": "raw", "encoding": "pcm_s16le", "sample_rate": 24000},
language="en",
add_timestamps=True,
)
for seg in segments:
text = seg.cartesia_text() # "[laughter]" kept inline so the voice laughs
if text:
ctx.push(text)
ctx.no_more_inputs()
```
## Turn streaming timestamps into cues
Audio chunks and word timestamps stream back interleaved (`resp.type` is `"chunk"` or `"timestamps"`). We don't wait for the whole line — a small `CueTimer` consumes the timestamps as they arrive and emits each face cue the moment its segment's first word appears. It matches *forward* through the word stream, so a stray non-verbal token (Cartesia emitting `laughs` for your inline `[laughter]`) never desyncs the cues. `feed()` returns any cues that just started; `flush()` emits the rest at the end. (Full implementation in [`cues.py`](https://github.com/anam-org/anam-cookbook/tree/main/examples/python-director-notes-audio-passthrough).)
```python
timer = CueTimer(segments)
new_cues = timer.feed(wt.words, wt.start, wt.end) # -> [(tag, at_seconds), ...]
```
## Stream into the avatar
Drain Cartesia: forward each audio chunk to the passthrough stream as it arrives, and fire each cue the moment `CueTimer` produces it. The avatar starts speaking almost immediately instead of waiting for the whole line to synthesise.
```python
from anam.types import AgentAudioInputConfig
agent = session.create_agent_audio_input_stream(
AgentAudioInputConfig(encoding="pcm_s16le", sample_rate=24000, channels=1)
)
timer = CueTimer(segments)
for resp in ctx.receive():
if resp.type == "chunk" and resp.audio:
await agent.send_audio_chunk(resp.audio) # forward as it arrives
elif resp.type == "timestamps" and resp.word_timestamps:
wt = resp.word_timestamps
for tag, at in timer.feed(wt.words, wt.start, wt.end):
await session.send_director_note_cue(tag, at_seconds=at)
for tag, at in timer.flush():
await session.send_director_note_cue(tag, at_seconds=at)
await agent.end_sequence()
```
Cues can be sent early — Anam latches each onto the persona speech turn that begins within ~1 second, and `at_seconds` sequences them across the turn so each lands on its word. Streaming audio faster than realtime is fine: Anam buffers it and paces playback, and because Cartesia streams faster than realtime each cue reaches Anam before its word renders. `end_sequence()` returns the avatar to a neutral listening pose.
Cartesia's Python client is synchronous, so in the example these `await`s run in a worker thread that bridges each send back to the event loop with `asyncio.run_coroutine_threadsafe(...)`. See [`main.py`](https://github.com/anam-org/anam-cookbook/tree/main/examples/python-director-notes-audio-passthrough) for the wiring.
## Run it
```bash
# Interactive: type lines, the avatar speaks each one
uv run python main.py
# One-shot
uv run python main.py --text "[warm] Come closer. [surprised] Wait — what was that? [laughter] Oh, it's nothing."
```
The avatar appears in an OpenCV window, lip-syncing to the Cartesia audio, and its face shifts on each cue word — brightening on `[happy]`, widening on `[surprised]`, laughing on `[laughter]`.
## Terminology
- **Audio passthrough** – you supply the speech audio (your own TTS); Anam only renders the face.
- **Director Notes** – a baseline *style* for the session plus *cues* that steer emotion or delivery during a turn.
In this recipe the voice comes from Cartesia and the facial performance from Director Notes cues — both driven by the same tags, kept in sync by Cartesia's word timestamps.