| name | present |
| description | Turn a research exploration (or a /study dossier) into a served visual presentation: choose a medium per result (Manim animation, marimo app, static figure, or served notebook), render on the right compute via ncn, assemble one landing page, and serve it over Tailscale. Composes manim, marimo, ncn. Invoked by /present. Use when the user wants to present/show/demo what an exploration found, build a talk or interactive demo from notebooks, or animate a result. |
present: exploration to served visual presentation
Take a finished (or in-progress) exploration (notebooks, scripts, data, results
sitting in a repo or directory) and turn it into a presentation: the
conclusion, told with a medium chosen per result, rendered on the right compute,
and served at a URL the user (or collaborators) can open over Tailscale.
You are a curator: decide what to show and how, then build and serve it. Do not
redo the science.
This plugin composes three others:
manim: render animations (process, structure, change over time).
marimo: reactive Python notebooks that run as web apps (interactive).
ncn: connect to a node to run heavy compute and to serve the result.
Input and home: the seam with /study
$ARGUMENTS is usually a directory. The highest-value input is a conclusion
handed over from /study: a paper already situated and understood. In that case
this is the demonstration twin of the dossier. /study gives the reading
artifact; you give the showing artifact.
- Read
~/.anu/atlas/papers/<id>/study.json for the claim, method, result, and
the gap. That IS the exploration. You are showing the paper's mechanism (or
the opening it implies), not re-deriving the science.
- Write the presentation into the shared atlas folder
~/.anu/atlas/papers/<id>/present/, so the dossier (../index.html) and this
demo sit side by side.
- Frame it with one fixed landing page, like map/study. Write a
present.json,
then:
python3 ~/.local/share/anu/plugins/present/skills/present/render.py present.json index.html.
The template never varies per demo; all per-demo content lives in present.json.
present.json schema
template.html reads exactly these fields. Fill every one you have data for; an
omitted field renders that section empty. medium MUST be one of the four enum
tokens below or the demo stage renders nothing.
{
"plan": {
"title": "short demo title",
"substrate": "local | <mesh-device> | cluster",
"heavy": false,
"takeaway": "the one-sentence conclusion",
"medium": "animation | interactive | static | notebook",
"knobs": ["control label", "..."]
},
"demo": {
"entry_filename": "scene.mp4 | app.html | figure.png",
"render_cmd": "command that produced or serves the artifact",
"port": 8000
},
"companion": {
"headline": "headline if plan.title is absent",
"takeaway": "the conclusion (overrides plan.takeaway in the banner)",
"what_it_shows": "one paragraph: what the viewer is looking at",
"how_to_read": ["bullet", "bullet"],
"caveats": "honesty note, e.g. an illustration is not the paper's own data"
},
"paper": { "title": "source paper title", "id": "arXiv id, DOI, or URL" },
"served_url": "http://<node-tailscale-ip>:8000",
"generated_at": "2026-06-15 14:00"
}
medium enum
template.html branches on the exact string in plan.medium. Anything else
falls through to a "not rendered" stage. Map each medium-table row to its token:
| Token | Table row | How it renders | Demo field consumed |
|---|
animation | Manim animation | mp4 in a <video> | demo.entry_filename (the mp4) |
interactive | marimo app, interactive HTML | entry_filename in an <iframe> | demo.entry_filename |
static | static figure | image | demo.entry_filename (png/svg) |
notebook | served notebook | served panel | served_url + demo.render_cmd |
Moves
1. Distill the exploration
Read the repo/dir. Establish, in one paragraph each:
- The question the exploration was chasing.
- What was tried (methods, the path).
- The result(s): the concrete findings, with their artifacts (plots, numbers,
notebooks, checkpoints).
- The one conclusion: if a viewer remembers a single sentence, what is it?
If the exploration is thin or the conclusion is unclear, say so and ask. Do not
manufacture a result.
2. Curate
Pick the 3-6 results that carry the conclusion. Everything you keep must earn its
place by moving the viewer toward the conclusion. Drop the rest (note what you
dropped).
3. Choose the medium per result
For each kept result, pick the medium. The right column is the plan.medium enum
token to write; see the medium-enum table above.
| The result is | Best medium (enum token) | Why |
|---|
| a process, transformation, derivation, dynamical system, geometry, an algorithm unfolding | Manim animation (animation) | motion and structure over time: show the thing happening |
| a parameter space ("vary X, watch Y") | marimo app (interactive) | reactive widgets let the viewer drive it live |
| a single comparison / one number / a ranking | static figure (static), matplotlib/pgfplots | an animation or widget would be noise |
| a dataset/result others should poke at | served notebook (notebook), marimo or Jupyter | hand them the live artifact |
| context, framing, the conclusion | prose (a few lines) | not everything needs a visual |
Heuristics: animate change and structure; interact for spaces; static for a
single fact; serve for explorables. When unsure between animation and a static
figure, ask whether time or transformation carries the meaning. If yes, animate.
Check what's installed first (manim, marimo, matplotlib). When the rich
tools are absent, the always-available medium is a self-contained interactive
HTML page (SVG plus vanilla JS, zero dependencies). It opens in any browser and is
a single static file, so bound 0.0.0.0 on an ncn node it is a Tailscale URL
with nothing to install on the node. Use the interactive enum token for it.
Prefer the richer media where their tools live (e.g. the GPU node); fall back to
interactive HTML otherwise. For a geometric mechanism (a projection, a phase
portrait, a network relaxing) the HTML interactive is often the best medium.
State your medium plan (result, medium, why) and check it with the user before
rendering anything heavy.
4. Decide where compute runs
Match each artifact's cost to a node:
- Light (CPU plots, short/low-res Manim, a marimo app): a local box,
ncn --local. Instant, free, contained.
- Heavy (3D / high-res / many-scene Manim renders, GPU sims, a large model
for inference): a GPU node,
ncn <gpu-device> (a GPU mesh box; substitute a
real mesh device name) or ncn --cluster (Slurm plus Apptainer). Render there,
pull the artifacts (mp4/png) back to the host.
- Renders are embarrassingly parallel: fan scenes across a node, or across
several with
mesh spawn, when there are many.
5. Produce the artifacts
Invoke the composed plugins:
- Manim scenes: follow the manim skill; render at preview quality first
(
/manim-preview), then final; /manim-review watches the frames for pacing,
out-of-frame, or dead-air before you ship a clip.
- Interactive panels: follow the marimo skill; one
app.py, mo.ui widgets,
PEP 723 deps so it runs anywhere.
- Static figures: matplotlib/pgfplots; save vector where it will be projected.
6. Assemble ONE presentation
Two shapes, pick by whether the talk is interactive:
- Static deck: a single self-contained HTML page. The conclusion up top, then
one section per kept result (embedded mp4 for animations, png/svg for figures,
prose for context). Self-contained, so it travels.
- Interactive app: a marimo notebook that embeds the Manim clips
(
mo.video(src=...)) and the live widgets, so the viewer can both watch and
poke. Use this when interaction is central; it is also what you serve live.
7. Serve it over Tailscale
The presentation is itself an ncn endpoint. Serve it on a node, bound to
0.0.0.0, and share the node's address.
In an anu window, prefer delve serve <dir>: it splits a pane and runs the right
server there (zero-dep static for an HTML demo, marimo run when app.py is
present), prints the URL, and delve down stops it. That is the watch-it-served
path /delve uses. The bullets below are where it serves to.
Fallback (non-anu / non-tmux contexts), serve by hand:
python3 -m http.server 8080 --bind 0.0.0.0
marimo run deck.py --host 0.0.0.0 --port 8080
- On a mesh device,
0.0.0.0 means the service is reachable at the device's
Tailscale IP, so http://<device-tailscale-ip>:8080 works from any device
on the tailnet (you, a collaborator, your phone). That is the served-over-
Tailscale path.
- On a local box, it is at the box's container IP (
http://<box-ip>:8080,
see ncn-box, not localhost).
- On the cluster, forward it home with
ncn tunnel <node> 8080 to localhost.
Hand over the final URL plainly. Keep the serving pane alive; ncn down or Ctrl-C
when done.
Rules
- Don't fabricate results. You present what the exploration found; if it found
little, the honest presentation is short.
- One conclusion, few results. Resist the urge to show everything.
- Cheap before expensive. Preview-quality renders and a local box first; only
send to the cluster/GPU once the medium plan is settled.
- The artifact is the deliverable. Deliver a served URL or a self-contained
deck. Report the URL, the medium choices, and what you dropped.