A Tavus replica that DJs in a Studio — 16 playable pads, an on-screen keyboard, and a piano-roll timeline, all powered by Tone.js. Talk to Charlie and he composes the sounds, plays them, and arranges a beat in real time; you can also play and edit everything by hand.
It deploys on Cloudflare Pages — a static front end plus one tiny Pages Function that holds the keys server-side:
| Path | Role |
|---|---|
public/index.html | The booth — Daily call + DJ Charlie video, the full-screen Studio, and the tool-call bridge that lets Charlie drive it. |
functions/api/conversation.js | Pages Function. POST /api/conversation creates the Tavus conversation using a server-held TAVUS_API_KEY + PERSONA_ID — no secret ever reaches the browser. |
persona.json | The DJ Charlie persona config (system prompt + Studio tool definitions). The api_key is a <CEREBRAS_API_KEY> placeholder, injected at mint time. |
scripts/mint-persona.mjs | One-shot, run locally: mints/refreshes the persona on Tavus from persona.json and prints the persona_id to set as PERSONA_ID. |
wrangler.toml | Pages project config (output dir public/, functions in functions/). |
The Studio fills the screen during a set. Three sections, top to bottom:
MembraneSynth kick, MetalSynth hat, NoiseSynth snare/clap,
FM/AMSynth leads, PluckSynth, Synth) or a bytebeat expression.localStorage (djc-pads); a small starter kit is seeded on
first run. The first tap also unlocks the Studio audio (browser autoplay rule).Bytebeat pads are a per-sample expression in terms of
t(seconds since note-on),f(the note's frequency in Hz),sr(sample rate), andi(sample index). Write pitched sounds in terms off, e.g.sin(2*PI*f*t)*0.6; drums ignoref, e.g. a kicksin(2*PI*60*t)*exp(-t*8). ATone.AmplitudeEnvelopegives every note its attack/release.
A chromatic on-screen keyboard plays the selected pad across pitches; the
− 4 + control shifts octave. Great for auditioning a pad melodically.
Set a BPM and a bars loop length, then click in the grid to drop notes
(drag to move/re-pitch, drag the right edge to resize, double-click to delete).
▶ Play loops the arrangement. Each note references a pad + pitch +
start/length (in beats) and persists to localStorage (djc-roll).
Charlie drives the Studio with tool calls (every call shows in the Tool Calls console):
define_pad (one pad) and define_pads (a whole kit at once),
trigger_pad (audition), clear_pad.set_bpm, set_bars (loop length, 1–16), create_note /
create_notes, update_note, delete_note, clear_notes,
play_timeline / stop_timeline.create_note returns a stable id (e.g. n7) so Charlie can edit or delete that
exact note later. Try: “make pad 0 a punchy kick and pad 1 a bright lead, then
write a 4-bar bassline at 100 BPM and loop it,” then “move that last note later”
or “delete the third note.”
⚠️ Charlie needs the Studio tools. Tool definitions are attached to a persona at creation time, so the deployed
PERSONA_IDmust point at a persona minted from this repo'spersona.json(runnpm run mint-persona). (Playing the pads, keyboard, and timeline by hand works regardless.)
The page needs the backend (/api/conversation) and HTTPS-equivalent context
(AudioWorklet + mic) — so run it through Wrangler, which serves public/ and the
Pages Function together:
cd ~/repos/dj_charlie
npm install # wrangler
cat > .dev.vars <<'VARS' # gitignored
TAVUS_API_KEY="<your tavus key>"
PERSONA_ID="p235b2543f75"
DEV_BYPASS_ACCESS="1"
VARS
npm run dev # wrangler pages dev → http://localhost:8788
.dev.vars feeds the local Function its secrets. DEV_BYPASS_ACCESS=1 skips the
Cloudflare Access gate (there's no Access JWT on localhost) — it lives only
in this local file and is never set on the deployed project, which stays gated.
Then open the printed URL in Chrome/Edge/Firefox.
The persona (DJ Charlie + the Studio tools, on the Cerebras LLM) is created
out-of-band, not from the browser. With TAVUS_API_KEY + CEREBRAS_API_KEY in
.env:
npm run mint-persona # prints a persona_id → set it as PERSONA_ID
LLM: DJ Charlie runs on Cerebras-hosted Kimi K2 as a custom OpenAI-compatible LLM —
model: moonshotai-kimi-k2.6,base_url: https://api.cerebras.ai/v1. The persona stores the Cerebras API key server-side (set via.env→CEREBRAS_API_KEYwhen creating). Note: Cerebras (fast-inference API) is not Cerebrium (serverless-GPU platform).
Charlie doesn't need exact words — these phrases just map cleanly onto what the Studio can do. Mix and match.
Each pad is a Tone.js synth or a pitched bytebeat expression:
| Synth | Good for |
|---|---|
MembraneSynth | kicks, toms |
MetalSynth | hats, cymbals |
NoiseSynth | snares, claps |
FMSynth / AMSynth | rich leads, basses, stabs |
PluckSynth | plucky strings |
Synth | basic tone |
bytebeat f(t) | glitchy / 8-bit timbres, written in t, f |
Descriptive words steer the envelope + synth choice: punchy, boomy, clicky, metallic, warm, bright, plucky, glitchy/8-bit.
− 4 + shifts octave.Tip: comparative tweaks work best once a pad is playing — “punchier,” “brighter,” “more swing,” “now add a snare.”
Hosted on Cloudflare Pages at https://dj.tavus-preview.io
(project dj-charlie). No build step — Pages serves public/ and bundles the
Function in functions/.
# one-time
wrangler pages project create dj-charlie --production-branch main
# secrets the Function reads at runtime
wrangler pages secret put TAVUS_API_KEY --project-name dj-charlie
wrangler pages secret put PERSONA_ID --project-name dj-charlie # from `npm run mint-persona`
# deploy
npm run deploy # wrangler pages deploy
The custom domain dj.tavus-preview.io is a proxied CNAME →
dj-charlie.pages.dev in the tavus-preview.io Cloudflare zone; Cloudflare provisions TLS.
Access is gated by Cloudflare Access (Zero Trust) on dj.tavus-preview.io,
restricted to @tavus.io. The Function also verifies the Access JWT, so the
dj-charlie.pages.dev hostname (which Access does not front) stays locked too.
Set on the Pages project: ACCESS_AUD, ACCESS_TEAM_DOMAIN, ALLOWED_EMAIL_DOMAIN.
✅ No secret reaches the browser — the Tavus key and persona id live only in the Function's environment. The Cerebras key lives only in the persona (server-side on Tavus) and in the local
.envused bymint-persona; it's never shipped.
user speech ─▶ DJ Charlie (Tavus LLM) ─▶ conversation.tool_call (Daily app-message)
│
index.html onAppMessage ◀───────┘
│ define_pad(s) / trigger_pad / set_bpm / set_bars /
│ create_note(s) / update_note / play_timeline …
▼
Tone.js engine (synths + pitched-bytebeat worklet) ─▶ 🔊
The booth feeds Charlie his tool instructions via
conversation.append_llm_context and narrates state changes back to him, so he
always knows the kit and the arrangement he's working with.
A Tavus replica that DJs in a Studio — 16 playable pads, an on-screen keyboard, and a piano-roll timeline, all powered by Tone.js. Talk to Charlie and he composes the sounds, plays them, and arranges a beat in real time; you can also play and edit everything by hand.
It deploys on Cloudflare Pages — a static front end plus one tiny Pages Function that holds the keys server-side:
| Path | Role |
|---|---|
public/index.html | The booth — Daily call + DJ Charlie video, the full-screen Studio, and the tool-call bridge that lets Charlie drive it. |
functions/api/conversation.js | Pages Function. POST /api/conversation creates the Tavus conversation using a server-held TAVUS_API_KEY + PERSONA_ID — no secret ever reaches the browser. |
persona.json | The DJ Charlie persona config (system prompt + Studio tool definitions). The api_key is a <CEREBRAS_API_KEY> placeholder, injected at mint time. |
scripts/mint-persona.mjs | One-shot, run locally: mints/refreshes the persona on Tavus from persona.json and prints the persona_id to set as PERSONA_ID. |
wrangler.toml | Pages project config (output dir public/, functions in functions/). |
The Studio fills the screen during a set. Three sections, top to bottom:
MembraneSynth kick, MetalSynth hat, NoiseSynth snare/clap,
FM/AMSynth leads, PluckSynth, Synth) or a bytebeat expression.localStorage (djc-pads); a small starter kit is seeded on
first run. The first tap also unlocks the Studio audio (browser autoplay rule).Bytebeat pads are a per-sample expression in terms of
t(seconds since note-on),f(the note's frequency in Hz),sr(sample rate), andi(sample index). Write pitched sounds in terms off, e.g.sin(2*PI*f*t)*0.6; drums ignoref, e.g. a kicksin(2*PI*60*t)*exp(-t*8). ATone.AmplitudeEnvelopegives every note its attack/release.
A chromatic on-screen keyboard plays the selected pad across pitches; the
− 4 + control shifts octave. Great for auditioning a pad melodically.
Set a BPM and a bars loop length, then click in the grid to drop notes
(drag to move/re-pitch, drag the right edge to resize, double-click to delete).
▶ Play loops the arrangement. Each note references a pad + pitch +
start/length (in beats) and persists to localStorage (djc-roll).
Charlie drives the Studio with tool calls (every call shows in the Tool Calls console):
define_pad (one pad) and define_pads (a whole kit at once),
trigger_pad (audition), clear_pad.set_bpm, set_bars (loop length, 1–16), create_note /
create_notes, update_note, delete_note, clear_notes,
play_timeline / stop_timeline.create_note returns a stable id (e.g. n7) so Charlie can edit or delete that
exact note later. Try: “make pad 0 a punchy kick and pad 1 a bright lead, then
write a 4-bar bassline at 100 BPM and loop it,” then “move that last note later”
or “delete the third note.”
⚠️ Charlie needs the Studio tools. Tool definitions are attached to a persona at creation time, so the deployed
PERSONA_IDmust point at a persona minted from this repo'spersona.json(runnpm run mint-persona). (Playing the pads, keyboard, and timeline by hand works regardless.)
The page needs the backend (/api/conversation) and HTTPS-equivalent context
(AudioWorklet + mic) — so run it through Wrangler, which serves public/ and the
Pages Function together:
cd ~/repos/dj_charlie
npm install # wrangler
cat > .dev.vars <<'VARS' # gitignored
TAVUS_API_KEY="<your tavus key>"
PERSONA_ID="p235b2543f75"
DEV_BYPASS_ACCESS="1"
VARS
npm run dev # wrangler pages dev → http://localhost:8788
.dev.vars feeds the local Function its secrets. DEV_BYPASS_ACCESS=1 skips the
Cloudflare Access gate (there's no Access JWT on localhost) — it lives only
in this local file and is never set on the deployed project, which stays gated.
Then open the printed URL in Chrome/Edge/Firefox.
The persona (DJ Charlie + the Studio tools, on the Cerebras LLM) is created
out-of-band, not from the browser. With TAVUS_API_KEY + CEREBRAS_API_KEY in
.env:
npm run mint-persona # prints a persona_id → set it as PERSONA_ID
LLM: DJ Charlie runs on Cerebras-hosted Kimi K2 as a custom OpenAI-compatible LLM —
model: moonshotai-kimi-k2.6,base_url: https://api.cerebras.ai/v1. The persona stores the Cerebras API key server-side (set via.env→CEREBRAS_API_KEYwhen creating). Note: Cerebras (fast-inference API) is not Cerebrium (serverless-GPU platform).
Charlie doesn't need exact words — these phrases just map cleanly onto what the Studio can do. Mix and match.
Each pad is a Tone.js synth or a pitched bytebeat expression:
| Synth | Good for |
|---|---|
MembraneSynth | kicks, toms |
MetalSynth | hats, cymbals |
NoiseSynth | snares, claps |
FMSynth / AMSynth | rich leads, basses, stabs |
PluckSynth | plucky strings |
Synth | basic tone |
bytebeat f(t) | glitchy / 8-bit timbres, written in t, f |
Descriptive words steer the envelope + synth choice: punchy, boomy, clicky, metallic, warm, bright, plucky, glitchy/8-bit.
− 4 + shifts octave.Tip: comparative tweaks work best once a pad is playing — “punchier,” “brighter,” “more swing,” “now add a snare.”
Hosted on Cloudflare Pages at https://dj.tavus-preview.io
(project dj-charlie). No build step — Pages serves public/ and bundles the
Function in functions/.
# one-time
wrangler pages project create dj-charlie --production-branch main
# secrets the Function reads at runtime
wrangler pages secret put TAVUS_API_KEY --project-name dj-charlie
wrangler pages secret put PERSONA_ID --project-name dj-charlie # from `npm run mint-persona`
# deploy
npm run deploy # wrangler pages deploy
The custom domain dj.tavus-preview.io is a proxied CNAME →
dj-charlie.pages.dev in the tavus-preview.io Cloudflare zone; Cloudflare provisions TLS.
Access is gated by Cloudflare Access (Zero Trust) on dj.tavus-preview.io,
restricted to @tavus.io. The Function also verifies the Access JWT, so the
dj-charlie.pages.dev hostname (which Access does not front) stays locked too.
Set on the Pages project: ACCESS_AUD, ACCESS_TEAM_DOMAIN, ALLOWED_EMAIL_DOMAIN.
✅ No secret reaches the browser — the Tavus key and persona id live only in the Function's environment. The Cerebras key lives only in the persona (server-side on Tavus) and in the local
.envused bymint-persona; it's never shipped.
user speech ─▶ DJ Charlie (Tavus LLM) ─▶ conversation.tool_call (Daily app-message)
│
index.html onAppMessage ◀───────┘
│ define_pad(s) / trigger_pad / set_bpm / set_bars /
│ create_note(s) / update_note / play_timeline …
▼
Tone.js engine (synths + pitched-bytebeat worklet) ─▶ 🔊
The booth feeds Charlie his tool instructions via
conversation.append_llm_context and narrates state changes back to him, so he
always knows the kit and the arrangement he's working with.