- name
- meeting-presenter
- description
- Join a Google Meet, Zoom, or Teams meeting as an AI presenter and DELIVER a presentation — slides on the bot's camera, narrated aloud, auto-advancing. Hand it a DOCUMENT (PDF, PowerPoint, or Word) or a TOPIC. A PowerPoint (already a deck) is shown as-is; a Word/PDF document is turned into a meaningful deck the agent authors from the source's text AND figures. Cross-platform and self-contained. Use when the user wants their agent to present something in a live meeting.
- argument-hint
- <meeting-url> --deck <file.pdf|.pptx|.docx|deck.json> [--mode auto|show|generate] [--name <bot>] [--voice af_heart] [--pace 1.0] [--avatar-mode]
- user-invocable
- true
- license
- MIT
# Presenter — present a document (or a topic) in a meeting
An AI bot **joins a meeting and presents**: by default the deck goes **big on the meeting's main stage
as a screenshare**, narrated in the bot's voice and advancing by itself, with the bot's **face in a small
camera tile**. (Pass `--avatar-mode` — or if the user says "join in avatar mode" — to instead show the
deck *on* the camera tile, the original single-tile look.) No clicker either way.
**Display modes:** *screenshare* is the default (readable full-stage slides + a presenter face);
*avatar mode* (`--avatar-mode`) puts the deck on the camera tile only. Everything else — voice control,
the companion remote, narration, Q&A — works identically in both.
The headline use: **the user gives a meeting link and a document — PDF, PowerPoint, or
Word — and the bot presents it.** You can also start from just a topic.
## Do this in order (the whole flow — same every time, no detours)
**Before anything — you need something to present. Never join a meeting empty.** If the user named
no document, no topic, and no existing deck, do NOT launch. First **offer what's already in `decks/`**
(list those decks by title), **or ask what topic to build one from** (you can author a deck from just a
topic — see B). Only move on once you have material in hand. A quick "what should I present?" beats a
bot that joins with nothing to say. *(Suggesting the user's existing `decks/` and offering to build from
a topic is the standard no-material flow — do it for every user, not just when you happen to know their
decks.)*
**0 · Preflight (once).**
- **API key — check in this order, and DON'T ask if it's already there** (one config, the SAME file
AgentCall uses; this is the exact order `load_api_key` uses):
1. `~/.agentcall/config.json` exists with an `api_key`? → ready.
2. `AGENTCALL_API_KEY` env var set? → ready.
3. Neither? → ask the user for their key (free at agentcall.dev/api-keys) and **write it to
`~/.agentcall/config.json`** (see Setup) — creating that file exactly like AgentCall would — then continue.
- **Dependencies** — `pip install -r requirements.txt` (fast; pure-Python wheels, no system tools).
**1 · Turn the input into a deck. You (the agent) always own the narration — never let the bot read
the slide off the screen.** Run `doc_to_deck.py` first, then look at what it produced:
- **A `.pptx`/`.ppt`, or a slide-shaped PDF** → *show the real slides*, but the narration is yours:
- If the file has **speaker notes on every slide** (`.pptx` only — legacy `.ppt` notes can't be read;
ask for a `.pptx`), they become the narration — you're done, just present. Notes on only SOME
slides → the deck is flagged; author the missing ones.
- If it has **no notes**, the deck comes back flagged **`needs_narration`** with every `notes` empty
(and a `decks/<name>/source.json`). **Author a spoken `notes` line per slide** (read `source.json`
+ glance at the slide images) before presenting — `present.py` **refuses to run** an un-authored
converted deck, so there is no shortcut around this. Authoring IS the presenting.
- **A `.docx`/`.doc`, a report-style PDF, or a topic** → *author a deck*: run `doc_to_deck.py --mode generate`,
read **`decks/<name>/source.json`** (its text + extracted figures) and write `decks/<name>/deck.json`.
**Do NOT try to open or render the file yourself** (no `pdftoppm`, no image tools) — `source.json` is your
input. Keep authoring tight: one idea per slide, short bullets, natural narration — don't over-polish.
- **Authored work is protected — but only in the DEFAULT deck folder.** Re-running the conversion (or
`present.py --deck <same document>`) with an unchanged source **reuses the authored `deck.json`** in
`decks/<name>/` — instant, nothing re-rendered, your narration survives. It only re-converts when the
document itself changed (then re-author — the flag comes back). **Do NOT pass `--out <a different
folder>` for a document you'll author and present** — a fresh folder has no deck to reuse, so you get an
empty-notes draft (and the real authored deck goes stale). `--out` is only for throwaway/inspection
conversions. Author, present, and re-present all from the default `decks/<name>/deck.json`.
- Full detail + deck schema: sections **A** and **B** below.
**Hard rules (each of these has burned a real session — the code enforces most of them, don't fight it):**
1. **Never one-shot a raw document into `present.py` and walk away.** Convert → check `needs_narration`
→ author `notes` → present. The refusal exit (code 2) is your signal to author, not an error to bypass.
2. **Never overwrite an authored `deck.json` by re-converting.** Unchanged source → it's reused
automatically; only delete the deck folder if the user explicitly wants a fresh conversion.
3. **Never invent a placeholder deck or apologize on camera.** If conversion fails, present.py exits —
fix the input (or install LibreOffice) and relaunch. A bot that never joins beats one that looks broken.
4. **Never leave a call running — and never hard-kill it to rejoin.** present.py stops billing on every
exit path itself — but if YOU spawned it, confirm the process actually exited when the presentation
ends. **To stop, restart, or rejoin, ALWAYS send `{"cmd":"leave"}` and WAIT for the process to fully
exit** (you'll see `Billing stopped.` and the process ends) **before relaunching.** Killing the process
(TaskStop / taskkill) to rejoin can skip the call's server-side teardown, so the old bot lingers and
your fresh join lands *beside* it — two bots in the meeting (this burned a real session). A clean
`{"cmd":"leave"}` is enough and never does that.
5. **The deck does NOT start by itself — YOU must reply `{"cmd":"present"}`.** The moment you launch,
start tailing `link/heard.jsonl`. When anyone says "go ahead" / "you can start" / "begin", your ONLY
correct action is to append `{"id":<that line's id>,"cmd":"present"}` to `link/commands.jsonl`. Nothing
else starts the narration. If you don't, the bot just sits there greeting an audience that's waiting —
this has happened in a real meeting. Do not get distracted mid-call, do not stop watching the file
until the bot has left. (A human can rescue it by tapping **Start** on the phone remote, but that is
their fallback, not your excuse.)
**1b · Honor the user's brief — their instructions shape the deck AND how you drive it.** When the user
says anything beyond "present this", capture it and apply it. It lands in one of two places:
- **Authoring-time (bake into `deck.json`)** — anything about *content or delivery*:
- *"keep it short / ~5 min / one-liner per slide"* → write shorter `notes`; the slide hold is
length-based, so shorter notes = a faster talk. *"go deep"* → richer `notes`.
- *"say ‹X› on the pricing slide"*, *"open with a joke"*, *"emphasise ‹Y›"* → put that line into that
specific slide's `notes` so it's spoken exactly where intended.
- *"skip the roadmap slide"* → drop/merge it while authoring.
- **Live-driving policy (you enforce while running)** — anything about *reacting in the room*:
- *"don't answer questions about ‹Z› / pricing / headcount"* → when such a question comes through
`heard.jsonl`, DON'T answer it: reply with a brief deflection (`{"cmd":"say","text":"Good question —
let's take that offline."}`) instead of a real answer. *"defer all Q&A to the end"* → acknowledge and
keep presenting rather than answering mid-deck.
- *"pause on slide 3 for discussion"*, *"only advance when I say so"* → adjust how eagerly you send
`next`/`present`.
Keep a short mental note of these rules for the whole session — they persist across the call, not just
the first slide. If a request is ambiguous, ask the user before launching.
**2 · (optional) Preview** — `present.py --local --deck <deck>` prints a localhost URL to watch it before going live.
**3 · Launch live, in the background** — `python scripts/present.py "<url>" --deck <deck> &`
(name/voice come from `~/.agentcall/config.json`; `--name`/`--voice` override per-run).
The bot takes 30–90s to appear, then greets and waits.
**4 · Drive it — start watching immediately.** Tail `link/heard.jsonl` and reply in `link/commands.jsonl`
(full protocol under **Voice control**). It won't *start* until you reply `present` to a "go ahead".
**5 · Finish** — it auto-leaves and stops billing when everyone leaves, on the time cap, or on a `leave`
command. If you spawned it, make sure the process exits.
## Two modes (this is the key idea)
| Mode | What it does | Default for |
|---|---|---|
| **show** | Renders the document's REAL pages/slides to images and shows them unchanged | `.pptx`/`.ppt`, and slide-shaped PDFs |
| **generate** | Parses the document, then YOU author a meaningful deck (titles, bullets, spoken narration) and pull in the source's own figures | `.docx`/`.doc`, dense/report PDFs, and topics |
Rule of thumb: **a file that's already a slide deck → show it. A document (prose) → generate a deck from it.** `auto` (the default) decides by file type, and for PDFs by page shape (landscape + sparse text = a deck → show; portrait + dense = a document → generate). Override with `--mode show|generate`. The user's intent always wins ("present my slides exactly" → show; "summarize this into a deck" → generate).
## A) Show a deck as-is (PowerPoint, or a slide-style PDF)
Show mode keeps every slide's real design (a full-bleed image of the actual slide). What it **says**
depends on whether the file carries speaker notes:
```bash
python scripts/doc_to_deck.py "/path/deck.pptx" # render slides + detect notes
```
- **Has speaker notes on every slide** (`.pptx`) → they're the narration. Just present:
```bash
python scripts/present.py "<MEETING_URL>" --deck decks/deck/deck.json
```
- **No notes — or notes on only SOME slides** → the deck is flagged `needs_narration` (the un-noted
slides would otherwise show in dead silence). **Author a spoken `notes` line for every slide that
lacks one** (from `decks/<name>/source.json` + the slide images), then present the authored
`deck.json`. This is what makes it a *presenter* and not a screen-reader.
**`present.py` REFUSES to present a `needs_narration` deck until every slide has `notes`** (it exits
with an error pointing you at `source.json`). There is **no** "read the slide text aloud" fallback —
by design. So never hand a raw notes-less file to `present.py` expecting it to talk: convert, author
the `notes`, then present the authored `deck.json`. The refusal is the safety net; authoring is the job.
## B) Generate a meaningful deck (Word, dense PDF, or a topic)
A document isn't slides — showing its pages makes a poor talk. So **you build the deck.**
1. **Convert to get the source content + figures:**
```bash
python scripts/doc_to_deck.py "/path/file.docx" --mode generate
# writes decks/<name>/deck.json — a mechanical draft with EMPTY notes, flagged needs_narration;
# present.py REFUSES it until you author the notes (that's the next step, not optional)
# and decks/<name>/source.json ← the parsed sections + extracted images, for you to author from
# and decks/<name>/img/ ← the document's own figures, already pulled out
```
2. **Author `deck.json` from `source.json`** — this is the important part. Do it well:
- **Outline first, then fill.** Make a slide per main idea. Build a mental checklist of the
document's sections/headings and make sure **every key point lands on a slide** — don't drop things.
- Each slide: a short `title`, ≤6 short `bullets`, and `notes` = natural **spoken** narration
(1–4 sentences; acronyms spelled phonetically like "A.P.I." so TTS says them right).
- **Reuse the source's figures.** `source.json` lists images extracted from the document (in
`sections[].images` and `all_images`). Put the relevant one in a slide's `image` field — the
slide then shows your bullets beside that real figure (a split layout). Don't invent images.
- Open with a one-line intro slide; close with a wrap-up (the bot leaves after the last slide).
3. **Preview and present** (sections C & D). For a topic with no file, skip the conversion and just
author `decks/<name>.json` directly.
Deck JSON schema (text slide, image slide, or both together):
```json
{
"title": "My Talk",
"slides": [
{ "title": "Intro", "notes": "Spoken intro." },
{ "title": "A point", "bullets": ["short phrase", "another"], "notes": "Narration." },
{ "title": "With a figure", "bullets": ["what it shows"], "image": "fig1.png", "notes": "Narration." },
{ "image": "slide3.png", "notes": "A full-bleed real slide (show mode)." }
]
}
```
(`bullets`/`notes`/`image` all optional; aliases `points`→bullets, `say`→notes; a title-only slide is a cover.)
## C) Preview (recommended)
```bash
python scripts/present.py --local --deck decks/<name>/deck.json # or decks/<name>.json, or a raw file
```
Prints a `http://localhost:PORT/?ws=local` URL — open it to watch the slides render and
auto-advance, with no meeting.
## D) Present live
```bash
python scripts/present.py "<MEETING_URL>" --deck <file-or-deck> [--mode show|generate]
```
**Live meetings are interactive by default (consent-driven):** the bot joins, introduces itself, and
**waits** until someone tells it to begin. Two ways to steer it:
- **By voice → routed to YOU (the brain).** There is **no keyword matching in the code.** Whatever a
participant says — once they address the bot by name, or during a short follow-up window — is handed to
you to interpret, and you reply with one command. Protocol below.
- **By the companion control page (direct).** A phone/browser page (Prev / Pause / Next / tap-a-slide /
Restart / Dismiss) whose link the bot drops in the meeting chat, and ALSO prints to the console
(`▸ Controls page: …`) — **relay the console URL to the user directly**; chat delivery on Meet can be
unreliable. Buttons are unambiguous and act **in-process, instantly** (they don't go through you), so
they're the reliable fallback and the way to drive it with no agent attached.
The bot **auto-leaves and stops billing** when everyone else leaves, on a hard cap, or on a clean exit —
an orphaned bot can't run up cost. Add **`--auto`** to present immediately without asking; `--pace <s>`
sets the gap between slides.
## Voice control — YOU are the brain (the important part)
In interactive mode the bot forwards what it hears to a file link and runs the command you write back.
No hardcoded phrases: *you* understand the request and decide the action. Run it in the **background**
and loop:
1. Start it (interactive is the default for live):
`python scripts/present.py "<url>" --deck <file> &`
It prints two paths: **`link/heard.jsonl`** (bot → you) and **`link/commands.jsonl`** (you → bot).
**Start watching immediately** — the greeting plays on its own, but the presentation won't *start*
until you reply to the first "go ahead". Read only NEW lines (track a byte/line offset, or block on
the file — don't re-process old lines), and reply within **~45s** (after that the deck resumes on its
own). The efficient, event-like way to watch, no busy-polling:
```bash
tail -n +1 -f link/heard.jsonl # streams each new utterance as a line; handle it, then append your reply to link/commands.jsonl
```
(`&`/`tail` are POSIX — on Windows use your background-run facility and
`Get-Content link/heard.jsonl -Wait`.)
present.py runs whatever you append to `commands.jsonl` within ~0.3s.
2. **When the bot is addressed by name** (*"Presenter, …"*) — or during the **~20s follow-up window**
after any exchange — a line is appended to **`link/heard.jsonl`**:
Ver no GitHub