Build a voice-first AI rehearsal coach with Box and the ElevenLabs Agents Platform

|
Share

The night before a conference talk, most of us do the same lonely thing: we stand in a hotel room and rehearse looking at the mirror. The mirror is a generous audience. It never interrupts, never asks uncomfortable questions, and never tells you that you missed an important context information that actually matters to your audience.

But maybe that’s a bit too generous? I decided to create a better rehearsal partner, one that had actually read my deck, knows my domain, would push back on my weak claims, and could write rehearsal notes and suggestions, so I don't have to type them myself. 

As it turned out, I already had everything I needed: my slides live in Box, Box AI can read them, and ElevenLabs, a powerful Voice AI provider, can give a warm and professional coach a voice. The same day I started the project, I had a POC Rehearsal Room app pointing at a Box folder with my slide deck and it started coaching me out loud, and creating speaking notes and suggestions for me. This quick turnaround was possible with Claude Code and Box Agent Skills, which have steered the coding assistant with best practices and spun the application in minutes.


The app itself includes my slide deck displayed with Box Content Preview, so I can have this handy while rehearsing. There's a presentation summary, a list of challenging questions to my talk track generated by Box AI, and the ElevenLabs voice agent. Finally, I gave the voice agent a little bit of a personality by naming him Henry, so we can focus together on improving my talk.

Let’s dive into how the pieces fit in this project and how you can use it for your next speaking opportunity.

Image of ElevenLabs Agents Platform

Connecting Box and ElevenLabs

My content is a .pptx that lives in Box, where colleagues from the brand and marketing teams can collaborate on it while I’m rehearsing. Box AI reads the file server-side, so it can summarize the talk, pull out its key claims, and generate the questions a skeptical audience would ask, all without downloading a single byte to my app to keep content secure. The interaction, obviously, is voice: the ElevenLabs Agents Platform gives me a low-latency conversational agent that can listen, respond in real time, and be genuinely helpful to elevate my presentation from content and enunciation angle. I can also tell Henry, as this is how I named my agent, what’s the goal of the presentation, what I mostly worry about, and who’s my audience to get tailored coaching.

The app is a loop that starts and ends in Box. First, Box AI reads the deck and does the prep work. It writes a short summary, extracts the key claims, drafts the toughest questions an audience might ask, and produces a per-slide Talking Points {slide deck name}.md document, all saved back into the same folder. Then the ElevenLabs voice agent takes over: it introduces itself, interviews me about the audience and goals for the talk, and role-plays a sharp attendee while I rehearse out loud. When I finish, the conversation is transcribed, and Box AI reads the deck and the transcript together to write a Rehearsal Notes file: what landed well, what was thin, and what to improve.

Reading the deck with Box AI

If you’d like to recreate this experience yourself, create a Box free developer account, to get access to free Box AI Units (1000 per month) and your ElevenLabs free account (10,000 free credits). You’ll need to create a Box app in Developer Console with CCG authentication with a Manage AI scope enabled, and generate ElevenLabs API key.

There’s also a ready to use open-source repository for you to clone and set up in just a few minutes. Here’s the .env.example file you’ll need to update:

# Box: Primary auth: Client Credentials Grant (service account). 
BOX_CLIENT_ID=
BOX_CLIENT_SECRET=
BOX_ENTERPRISE_ID=
# Optional fast path for local runs: a 60-min developer token.  
BOX_DEVELOPER_TOKEN=
# The Box folder that holds your .pptx deck(s). Speaker and rehearsal notes are written back here. 
BOX_COACH_FOLDER_ID=
# ElevenLabs
ELEVENLABS_API_KEY=
# Produced by `python scripts/setup_agent.py` — paste the printed id back here.
ELEVENLABS_AGENT_ID=
ALLOW_PPTX_DOWNLOAD=false
HOST=127.0.0.1
PORT=8000

The whole experience powered by Box Python SDK and the starting point is the .ai.create_ai_ask method which takes a slide deck file ID and a prompt, and answers questions about the content, with no download, no chunking, no vector store to manage:

def ask(client: BoxClient, file_id: str, prompt: str) -> str:
    """Ask Box AI about a single file. Returns the answer text."""
    if not file_id:
        raise ValueError("ask() requires a file id")
    _pace()
    response = client.ai.create_ai_ask(
        mode=CreateAiAskMode.SINGLE_ITEM_QA,
        prompt=prompt,
        items=[AiItemAsk(id=file_id, type=AiItemAskTypeField.FILE)],
    )
    return response.answer or ""

The API is called a handful of times during the prep step. In return it gives a comprehensive overview and context for the voice agent. Box AI generates: 

  • presentation summary,
  • key claims,
  • main talking points to generate Talking Points.md,
  • and a few challenging questions a critical attendee would ask.
TALKING_POINTS_JSON_PROMPT = (
    "This file is a conference presentation. For EACH slide, in order, produce "
    "concise talking points a presenter would say, based on the slide content "
    "(and speaker notes if available). Return a JSON array where each item is "
    '{"slide": <int>, "title": "<slide headline>", "points": ["...", "..."]}. '
    "One array item per slide, in slide order. No preamble, JSON only."
)

SUMMARY_PROMPT = (
    "Summarize this conference talk deck in 4-6 sentences: what it argues, who "
    "it's for, and the through-line a listener should remember. "
    "Return only the summary — no preamble, and do not ask any follow-up questions."
)

KEY_CLAIMS_PROMPT = (
    "List the 5-8 most important claims or takeaways this talk makes. Return a "
    'JSON array of short strings, e.g. ["claim one", "claim two"].'
)

TOUGH_QUESTIONS_PROMPT = (
    "You are a sharp, skeptical conference audience. Based on this deck, write "
    "8-10 tough questions a critical attendee would ask — challenge assumptions, "
    "probe weak evidence, ask for specifics and trade-offs. Return a JSON array "
    "of short question strings."
)

That last prompt turns a passive slide reader into an antagonist. On my own Box developer experience deck, it came back with questions I'd like to formulate my answer before the presentation rather than answer on the spot. A great additional feature of this app would be to pass additional context to Box AI, as it can accept up to 25 files within one API call, or a Box Hub to give the agent extensive knowledge base.

Creating a session brief

Once Box AI returns all generated information based on my presentation deck. It is later used in dynamic variables both from Box AI responses, as well data from the user input related to audience size, presentation duration, and things the speaker finds challenging. Once Box AI finishes prep, the talk brief is stored in server memory as _STATE["brief"] during Prepare talk (POST /api/prep). When you click Start rehearsal, POST /api/session merges that saved brief with user intake form (audience, duration, goals, worries) and passes the result to ElevenLabs as dynamic variables.

def _dynamic_variables(brief: dict, intake: dict) -> dict:
    """Everything the agent's {{vars}} need, as strings."""
    def bullets(items):
        return "\n".join(f"- {x}" for x in items) if items else ""
    return {
        "talk_title": brief.get("talk_title", ""),
        "summary": brief.get("summary", ""),
        "key_points": bullets(brief.get("key_points")),
        "speaker_notes": brief.get("speaker_notes", "")[:6000],
        "tough_questions": bullets(brief.get("tough_questions")),
        "audience": intake.get("audience", ""),
        "venue": intake.get("venue", ""),
        "duration": intake.get("duration", ""),
        "goals": intake.get("goals", ""),
        "worries": intake.get("worries", ""),
    }

So, now let’s see how it looks from the voice agent perspective.

Giving the coach a voice with ElevenLabs Agent Platform

What I really liked about ElevenLabs developer experience was that I could create the agent programmatically, instead of clicking through the UI. So, here’s my one run script to create the instance and copy its ID to my .env file.

from app import elevenlabs_agent

def main() -> None:
    agent_id = elevenlabs_agent.create_agent()
    print("\n✅ Created ElevenLabs coach agent.")
    print(f"ELEVENLABS_AGENT_ID={agent_id}")

In the elevenlabs_agent.py I defined the agent by passing the name and the conversation_config with the coach system prompt and the coach first message.

def create_agent(name: str = "Rehearsal Room Coach") -> str:
    """Create the coach agent. Returns the new agent id."""
    payload = {
        "name": name,
        "conversation_config": {
            "agent": {
                "prompt": {"prompt": COACH_SYSTEM_PROMPT},
                "first_message": COACH_FIRST_MESSAGE,
                "language": "en",
            }
        },
    }
    with httpx.Client(base_url=config.ELEVENLABS_BASE_URL, timeout=_TIMEOUT) as c:
        r = c.post("/v1/convai/agents/create", headers=_headers(), json=payload)
        r.raise_for_status()
        data = r.json()
    agent_id = data.get("agent_id") or data.get("agentId")
    if not agent_id:
        raise RuntimeError(f"Agent create returned no id: {data}")
    return agent_id

The agent's system prompt is written once, but with dynamic variables which allow for tailored conversation each time. In the end, this is where the output form Box AI will land into:

COACH_SYSTEM_PROMPT = """You are "Rehearsal Room", a warm but sharp public-speaking coach who also role-plays a skeptical conference audience.

The presenter is about to rehearse this talk:
- Title: {{talk_title}}
- Summary: {{summary}}
- Key points they intend to make: {{key_points}}
- Their own speaker notes / talking points: {{speaker_notes}}
- Tough questions a critical audience might ask: {{tough_questions}}

Presenter-provided context (may be blank until you ask):
- Audience: {{audience}}
- Time limit: {{duration}}
- Goals: {{goals}}
- Worried about: {{worries}}

RUN THE SESSION IN THREE PHASES:

1) INTRO & INTAKE (short). Introduce yourself in one or two sentences as their rehearsal audience and coach. Then ask 3-4 quick questions to fill any blanks in the context above (who's the audience, the setting/venue, the time limit, and what they're most worried about). If a value is already provided, confirm it briefly instead of re-asking. Keep this under a minute.

2) REHEARSAL. Invite them to start delivering the talk. Let them speak. Interject like a real, engaged-but-critical audience member: press on weak evidence, ask for specifics and trade-offs, and pull from the tough questions above. Notice when they drift from their own speaker notes or key points and gently flag it. Do not lecture — keep your turns short so they do most of the talking.

3) WRAP. When they say they're done (or time is up), give 2-3 sentences of immediate spoken feedback and tell them you'll leave detailed written notes in their Box folder.

Style: conversational, encouraging, concrete. Never read long lists aloud. One question at a time."""

COACH_FIRST_MESSAGE = (
    "Hi! I'm Henry, your speaking coach — think of me as one attentive, slightly "
    "skeptical person in your audience, but also as a friendly and supportive colleague. Let's kick this off, I'm ready to hear your talk."
)

And finally, I needed the ElevenLabs conversational widget to display the interface to the user. For simplicity purposes I just used CDN.

<!-- ElevenLabs conversational widget -->
<script
  src="https://unpkg.com/@elevenlabs/convai-widget-embed"
  async
  type="text/javascript"
></script>

The web/app.js mounts the ElevenLabs widget and accepts the agent ID and dynamic variables:

function mountWidget(session) {
  const holder = $("widget-holder");
  holder.innerHTML = "";
  const el = document.createElement("elevenlabs-convai");
  el.setAttribute("agent-id", session.agent_id);
  el.setAttribute("dynamic-variables", JSON.stringify(session.dynamic_variables));

So now all prework based on your slide deck is in the AI agent system prompt. But that’s not it.

Box Content Preview

The Rehearsal Room needed one more element, and that’s a rendered slide deck, so I can have that handy while speaking to my coach. Again, for simplicity of this app, I leveraged CDN for pulling the scripts, but both platforms have dedicated React packages for embedding those experiences in more complex applications. 

<!-- Box Content Preview -->
<link
  rel="stylesheet"
  href="https://cdn01.boxcdn.net/platform/preview/2.106.0/en-US/preview.css"
/>
<script src="https://cdn01.boxcdn.net/platform/preview/2.106.0/en-US/preview.js"></script>

In the app.js file there’s a script invoking Box Content Preview. It reaches the /api/preview endpoint to get the file ID and the downscoped token. It renders the slide deck in the preview container.

async function loadPreview() {
  const { file_id, access_token } = await fetch("/api/preview").then((r) => r.json());
  const preview = new window.Box.Preview();
  preview.show(file_id, access_token, {
    container: "#preview",
    showDownload: false,
  });
}

With this, the agent is able to coach you, but the clue is actually to write Henry’s feedback back to Box.

Creating actionable notes from rehearsal

ElevenLabs gives possibilities like retrieving the conversation transcript, which is later used to feed Box AI to create notes from the coaching session. Next, the slide deck ID and transcript is passed to a final Box AI API again to generate concise rehearsal notes. As a result, feedback is captured and written back to Box as a markdown file, and I can review both the transcript and the rehearsal notes. Files are versioned, so each time I chat with my agent, the application uploads a new version of the rehearsal notes.

Image of ElevenLabs Agents Platform

Here’s a function definition that builds the prompt that is passed to Box AI to build the conversation brief.

def build_debrief_prompt(
    talk_title: str,
    intake: dict,
    transcript_text: str,
    summary: str = "",
    key_points: list[str] | None = None,
) -> str:
    """Prompt for coaching notes.

    The rehearsal transcript is inlined verbatim (we already hold it in memory,
    so we never depend on Box AI reading a just-uploaded file whose text
    representation may not be ready yet). The call is grounded on the deck file
    so the model can compare what was *said* against what's on the *slides*.
    """
    intake_lines = "\n".join(f"- {k}: {v}" for k, v in intake.items() if v)
    key_lines = "\n".join(f"- {p}" for p in (key_points or []))
    return f"""You are an expert public-speaking coach reviewing a rehearsal of the talk "{talk_title}".

You have two sources:
1. The attached file is the presentation DECK — use it to judge what was on the slides.
2. Below is the VERBATIM TRANSCRIPT of the rehearsal (what the presenter and coach actually said).

Critical: base every observation ONLY on what actually appears in the transcript below. Do NOT invent questions, answers, numbers, or moments that are not in it. If the presenter barely started, skipped Q&A, or only did the intake interview, say that plainly instead of fabricating feedback. Quote short phrases from the transcript to support your points.

Talk summary (reference): {summary or "(not provided)"}
Key points the presenter intended to make:
{key_lines or "- (not provided)"}

Presenter's context:
{intake_lines or "- (none provided)"}

=== REHEARSAL TRANSCRIPT (verbatim) ===
{transcript_text.strip()}
=== END TRANSCRIPT ===

Write concise, actionable coaching notes in Markdown with these sections:

## Overall
2-3 sentences on how this specific rehearsal went and the single biggest thing to fix.

## Content gaps
Where the delivered talk was thin, unclear, or missing evidence — compared against the deck.

## Q&A performance
Only if the transcript contains questions and answers. Quote a weak answer, then give a stronger rewrite. If there was no Q&A, say so.

## Delivery & pacing
Filler words, hedging, rushing, and structure — cite examples from the transcript.

## Do these 3 things next
A short, prioritized checklist.

Be specific, kind, and honest, and stay grounded in the transcript above."""

And that’s it a high-level overview of the Rehearsal Room app. There’s definitely more code to it, so you can explore the full repo with an end to end solution. Bear in mind that this demo was meant to be a simple POC app. For production, add user/session auth on the FastAPI routes (and ideally per-user Box OAuth instead of a shared service account), to keep your content secure.

Closing remarks

Box and ElevenLabs give a powerful interface to AI agents. In this example we leveraged the default AI agent, but in reality, you can also pick and adjust parameters of the AI model that powers the application. ElevenLabs also supports multiple languages, multiple voices, and allows for additional agent widget customizations, so this experience can be adjusted to your specific needs or use case. This pattern is universal and can be also used as a voice-first support chat bot, onboarding buddy that answers questions, or a legal agent. The possibilities are unlimited here.

So the next time you're rehearsing before a big conference or a local community meetup, consider giving your slides a voice. They've been listening to you the whole time, but they may as well talk back and give you the best possible rehearsal in your career. Good luck with your next talk!