SUPERNOVA.md: short animated lessons
Supernova is openvidstudio's second kind of production: a STEM concept explained in up to two minutes, vertical
(1080x1920), narrated and captioned, with a script first and pictures that illustrate it. It is a production
mode (production.mode: "supernova"), not a fourth creative level. The process is the supernova skill; the
script rules are in SCRIPT.md; the primitives are in API.md.
Make a lesson project with init_project and mode: "supernova". It stores the mode, defaults the frame to
1080x1920, and installs @openvidstudio/stem, its display font and the maths renderer.
One concept in up to two minutes, a series for a big topic
A lesson teaches one thing and takes at most two minutes. A big topic is a short series ("Part 2 of 4") and each part ends on the question the next one answers. Every lesson has the same seven-beat shape (hook, picture, set up, derive, use it, aha, close), written out with times and rules in SCRIPT.md, so a viewer, and a series, can rely on it.
The concept card
src/videos/<name>/concept.json, next to beats.json. check_script reads it, and validate_scenes lets the
numbers in its worked example appear on screen.
{
"goal": "tell why a derivative is a slope",
"terms": ["slope", "derivative"],
"example": "a ball rolling down a hill, followed for one second",
"reference": "Calculus 5.1",
"referenceSpoken": "calculus, topic five point one",
"maxSeconds": 120
}
goal is what the viewer can do afterwards, terms the two or three that need defining, example the one worked
example the lesson keeps coming back to, with its real numbers. reference is where the lesson sits (a syllabus
chapter, "Part 2 of 4"), referenceSpoken is how it is said, and the opening beat has to say it. Both are
optional. maxSeconds is the length budget (default 120). A worked example, projectile motion:
{
"goal": "work out how far and how high a launched ball goes, from its speed and angle",
"terms": ["projectile", "range"],
"example": "a ball launched at 20 m/s at 30 degrees, with g = 9.81 m/s squared: it flies for 2.04 s, lands 35.3 m away and peaks at 5.1 m",
"reference": "Kinematics, part 1 of 2",
"referenceSpoken": "kinematics, part one of two"
}
Its script, in the shape SCRIPT.md asks for (each beat's duration comes from the measured voice, see NARRATION.md):
| Beat | Narration |
|---|---|
| hook | How far does a thrown ball go? This is kinematics, part one of two. Throw it harder and it goes farther. But how much farther? |
| picture | Picture a ball leaving your hand at an angle. A ball in flight like this is called a projectile. It rises, slows and falls. |
| set-up | Say it leaves at a speed v and an angle theta. Ignore the air. Gravity pulls it down at g, and nothing pulls it sideways. |
| derive | Split the launch in two. Sideways, x equals v cosine theta times t. Upward, y equals v sine theta times t, minus a half g t squared. It lands when y is zero again, at t equals two v sine theta over g. |
| use-it | Use twenty metres a second at thirty degrees. That is two point oh four seconds in the air. The range is how far it lands: thirty-five point three metres. |
| aha | Now change the angle. Forty-five degrees gives the longest range, and two angles that add to ninety land in the same spot. |
| close | So: two separate motions, one steady, one pulled by gravity. At the very top of the path, what is the ball's acceleration? |
This is a worked example of the format, not a lesson that has been built and listened to.
A card can also set "arc": "story" for the story-first lesson, with situation, character, formulas,
practiceQuestion and answer; see SCRIPT.md.
Design: Void + Spark
The idea: light is meaning. Everything is dark and flat except the one thing the viewer must look at.
| Token | Value |
|---|---|
| Void | #07060B, panels #0E0C16, hairline grid at 5% white |
| Text | warm white #F3EFE6 (never pure white), dim #8C8799 |
| Math | blue giant #5CC8FF |
| Physics | solar amber #FFB347, ember #FF7A3D |
| CS | nebula violet #B58CFF |
| Chemistry | red dwarf #FF4D5E |
| Biology | aurora #7CFFB2 |
One accent per subject, so a profile grid of lessons reads as a spectrum. supernovaDesignSystem(subject) builds it:
export const design = supernovaDesignSystem("physics"). The display face is Space Grotesk, the body face Inter, both from
local files. Equations are in sans-serif maths (MathJax with the Fira font; the comparison with KaTeX is in API.md).
Glow budget: one glowing element at a time. The Spark is one; a Nova's flash is one. validate_scenes (render
mode) counts them: more than one is a warning, more than two an error, except in a beat that holds a Nova.
The primitives
All in @openvidstudio/stem, documented with stills in API.md:
Spark: the pen, the pointer and the particle. Traces curves, carries a value along a function, orbits, splits, falls under gravity, walks pointers through arrays.Nova: the payoff, once per video, on the aha sentence. Silence, a three-frame flash, a shockwave ring, the result condenses from particles.Axes,Plot,NumberLine: graphs, tangents, shaded area, intervals.Equation: sans-serif maths revealed term by term, with a requiredspokenform that must match the narration.Derivation: the derive beat. A formula built line by line, each step with itsspokenform and an optionalbecause; the line before moves up and dims (a stack of three), terms that carry over stay put, new ones arrive in the accent, gone ones fade. Each step is revealed at the word it is spoken at, socheck_scriptalso reads itsspokenstrings and checks they are in the narration in order.LessonCaptions: word-timed captions that break at sentence ends, in the Reels safe zone.FreeBody: forces as arrows whose length is exactly magnitude times the scale shown.ArrayViz: cells, named pointers, a lit range and a dimmed one, a Spark-driven pointer.useNarrationWords(beatId)(in core): when each word of the narration is said, so the Spark can point at it.
One idea on screen at a time, at most two things moving.
The voice
One fixed voice for the handle, so a viewer knows it by ear and lessons can be made in any order.
| Engine | edge-tts (fast, and it gives word timings). It sends the script text to Microsoft, which is fine for public lesson scripts. preflight speaks one word to check it works: Microsoft refused 7.2.8 with a 403 in October 2026, and pip install -U edge-tts fixes it |
| Voice | en-US-AndrewNeural |
| Rate | the engine's default, +0%. Pace comes from the script (2.3 to 2.9 words per second); the stretch is clamped to 0.9 to 1.12 so it never sounds processed |
| Final pass | omnivoice gives better control but runs about 40 times slower than real time on a CPU: use it for a last pass, never to iterate |
This is a proposal, the default voice of the engine, chosen so there is one. Changing it is a decision for the handle's
owner, made once and for every lesson: pass the same voice to generate_narration each time.
Fit the beats to the voice
The voice reads plain prose at about 3 words per second and number-heavy lines at about 2.2, so a word count
alone mis-sizes a beat. Write the beats with provisional lengths, run generate_narration, then run fit_beats.
It sets each beat to the measured spoken length plus half a second (padSeconds), clamps it into the 2.3 to 2.9
words per second window validate_beats wants, rounds to whole frames, moves the later beats' starts, and never
touches vo. A very short line ("Now the honest part.") ends up near 1.7 seconds. A beat it cannot fit, because the
voice needs more than the window allows, is listed in unfittable with the words to cut or the frames to lengthen it to.
Pass dryRun: true to see the lengths first, then run generate_narration again with overwrite: true.
NARRATION.md has the clamp.
Captions
Burn captions in for silent autoplay with LessonCaptions, one line in a scene:
<LessonCaptions beatId="hook" text={vo} /> // vo is the beat's narration text
The word timings carry bare words with no punctuation, so the component takes the text from the beat's vo (split on
spaces, it matches the timings one to one) and breaks a caption at a sentence end, at a comma after at least three
words, and at maxWords (default 6), so a caption never runs one sentence into the next. The word being said is
picked out in the subject's accent. If the text and the timings differ in length it falls back to the timings' bare
words, and with no timings at all (an engine that gives none) it spreads the words evenly over the scene and says so
in data-ovs-captions-timing="estimated". It stays out of the Reels safe zones by default (above the bottom 22
percent of a tall frame, below the top 14), works at 1920x1080, and its box is tagged data-ovs-captions, which
validate_scenes ignores when it looks for a diagram or a static stretch. See API.md for the props.
Canvas explainers
A lesson can also be an HTML canvas explainer (hand-drawn chalkboard, whiteboard, flat vector, a pixel-character look) turned
into a video by render_canvas_explainer, with the same script gates: beats.json after fit_beats drives the beat times,
the narration clips are mixed over the score, and the text is captioned. validate_canvas_explainer is its validate_scenes.
See "Canvas explainers" in PIPELINE.md.
Canvas explainers in a pixel style have a ready kit: @openvidstudio/styles has the recurring pixel narrator (idle, talk,
point, think, celebrate), a small prop set, parallax, a perspective floor grid, fog and shadows, the accent per subject, a
chip sound palette, and vertical 720x1280 layout helpers (safeArea, fitText with the role minimums, a caption band).
It is plain 2D canvas and a pure function of t. Cropping a landscape layout to vertical is not offered: lay the lesson
out vertically. See "Pixel narrator and depth kit" in API.md.
Reels safe zones
Instagram draws its own interface over a reel, so keep key content (text, the part of a diagram being explained) out of the
top about 14% (270 px) and the bottom about 22% (420 px) of the 1080x1920 frame, and about 10% (108 px) off the right
edge. supernovaDesignSystem sets its layout grid to exactly that (108 / 270 / 420).
Checked against current guidance in October 2026: the top figure agrees with the one most sources cite (14%, about 269 px, from Meta's percentages for Reels ads). The bottom does not: sources give anywhere from about 340 px to 35% (672 px). The 35% is for ads; 22% is a design choice for an organic reel and a minimum. If a lesson is also run as an ad, keep key content out of the bottom 35%. Meta publishes percentages, not pixels, and the interface changes, so look at a real upload before trusting any of these. Backgrounds can run to the edge; key content stays inside.
The description
write_description writes output/<name>-DESCRIPTION.md for a lesson as for any video (chapters from the beats,
credits, and one line saying the video is a constructed explanation, not footage). Two things come from the lesson's
own files. If concept.json has a practiceQuestion and an answer, the description gets a "Practice question"
section with the question and the line "Drop your answer in the comments.", and the answer is written to
output/<name>-ANSWER.md instead, never into the description: the answer, and one line of working if the card has an
optional answerWorking. Paste that file as the pinned comment. And the disclaimers array in
openvidstudio.config.json (for example ["Constructed animation, not footage.", "Not affiliated with or endorsed by any exam board."]) is appended, each line once, at the end of every lesson description. A project with none of these
gets the same description as before. See SCRIPT.md.
Checks
validate_beats (pace), check_script (the script, the two-minute budget and the reference), validate_scenes with
render: true (framing, numbers, glow, and the arc: diagram-late and static-stretch), a contact sheet, then listen. finalize_video must report no audio-tick. See
the skill for the order. render_video skips its brand-lock gate on a Supernova project, because a lesson has no brand to
extract, and says so in brandLockSkipReason.
In a lesson validate_scenes leaves out the camera checks (keyframes, a camera that never moves): lesson scenes are flat on purpose.