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:
- 16 pads in a 4×4 grid. Each pad holds one sound, which is either a
Tone.js synth (
MembraneSynthkick,MetalSynthhat,NoiseSynthsnare/clap,FM/AMSynthleads,PluckSynth,Synth) or a bytebeat expression. - Tap a pad to play it; tapping also selects it for the keyboard. The ✎ corner opens the editor: kind (synth / bytebeat), synth type or expression, ADSR envelope, volume, and base note.
- Pads persist to
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):
- Sounds —
define_pad(one pad) anddefine_pads(a whole kit at once),trigger_pad(audition),clear_pad. - Arrangement —
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 deployedPERSONA_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- Start the Set — the backend mints a conversation; no keys to paste.
- Tap a pad once to enable audio, then talk to Charlie: “give me a trap kit,” “make the lead warmer,” “write a bassline and loop it,” “add a snare on the off-beats.”
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.
- “make pad 0 a punchy kick” → defines one pad's instrument
- “give me a full drum kit / a set of sounds” → composes a whole bank of pads at once
- “make pad 5 a bright lead / warm bass / plucky” → picks a synth + envelope to match
- “play pad 3” · “clear pad 7” → auditions / empties a pad
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.
- “write a 4-bar bassline at 100 BPM” → sets tempo + drops notes
- “four-on-the-floor kick, off-beat hats” → lays a drum pattern across pads
- “play / loop it” · “stop” → runs / stops the arrangement
- “move / shorten / delete that note” · “clear the hats” → edits notes by id, or clears a pad
- Tap a pad to play its sound (and select it for the keyboard).
- Keyboard plays the selected pad;
− 4 +shifts octave. - Timeline: click to add a note, drag to move/resize, double-click to delete.
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 deployThe 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.