Skip to content

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):

BeatNarration
hookHow far does a thrown ball go? This is kinematics, part one of two. Throw it harder and it goes farther. But how much farther?
picturePicture a ball leaving your hand at an angle. A ball in flight like this is called a projectile. It rises, slows and falls.
set-upSay it leaves at a speed v and an angle theta. Ignore the air. Gravity pulls it down at g, and nothing pulls it sideways.
deriveSplit 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-itUse 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.
ahaNow change the angle. Forty-five degrees gives the longest range, and two angles that add to ninety land in the same spot.
closeSo: 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.

TokenValue
Void#07060B, panels #0E0C16, hairline grid at 5% white
Textwarm white #F3EFE6 (never pure white), dim #8C8799
Mathblue giant #5CC8FF
Physicssolar amber #FFB347, ember #FF7A3D
CSnebula violet #B58CFF
Chemistryred dwarf #FF4D5E
Biologyaurora #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 required spoken form that must match the narration.
  • Derivation: the derive beat. A formula built line by line, each step with its spoken form and an optional because; 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, so check_script also reads its spoken strings 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.

Engineedge-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
Voiceen-US-AndrewNeural
Ratethe 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 passomnivoice 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.