Skip to main content

topic-to-video

Turn ANY topic — or a slide deck — into a narrated explainer short video, automatically: researches the topic, writes and voices the script, designs and builds the visuals (cinematic Remotion tier or HTML pages; a deck's slides full-frame with gentle motion and timed callouts) and burns in captions. Any language; can voice a deck in a different language than its slides. Use whenever the user wants a short video about a subject or from slides — e.g. "make a video explaining how MRI works", "make a launch video for this product", "做一个讲解视频介绍X", "turn these slides into a video", "narrate my pptx", "add a voiceover to my slides", "把幻灯片做成视频", "把英文幻灯片配成中文解说视频". Accepts a topic, .pptx, .pdf, an image folder, or a hand-off from slide-maker. Free by default (edge-tts + ffmpeg); optional cloud TTS. Editable storyboard, a sample for sign-off, then the full video. Trigger even without the word "skill".

Quellinformationen

Repository
addsumtech/topic-to-video
Letzte Quellaktivität
5. November 2026 um 10:29
Erkannte Sprache von SKILL.md
Englisch
Sterne
2
Forks
2

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
topic-to-video
slug
topic-to-video
displayName
Topic to Video
version
2.0.0
description
Turn ANY topic — or a slide deck — into a narrated explainer short video, automatically: researches the topic, writes and voices the script, designs and builds the visuals (cinematic Remotion tier or HTML pages; a deck's slides full-frame with gentle motion and timed callouts) and burns in captions. Any language; can voice a deck in a different language than its slides. Use whenever the user wants a short video about a subject or from slides — e.g. "make a video explaining how MRI works", "make a launch video for this product", "做一个讲解视频介绍X", "turn these slides into a video", "narrate my pptx", "add a voiceover to my slides", "把幻灯片做成视频", "把英文幻灯片配成中文解说视频". Accepts a topic, .pptx, .pdf, an image folder, or a hand-off from slide-maker. Free by default (edge-tts + ffmpeg); optional cloud TTS. Editable storyboard, a sample for sign-off, then the full video. Trigger even without the word "skill".
# Topic to Video You turn a topic — or a slide deck — into a narrated short video. Source of truth is `storyboard.json` — you edit text/timing; the scripts render the media. ## Tools (all free; verify with `scripts/_common.have_tool`) `soffice` (pptx→pdf), PyMuPDF `fitz`, `python-pptx`, **edge-tts** (preferred neural voice, free, cross-platform — needs network), macOS `say` (offline fallback), `ffmpeg`, PIL. Captions burn via PIL + a bundled font (no libass needed). Optional: OpenAI/ElevenLabs TTS if the user gives an API key. ## Pipeline scripts This is an index. Each line gives what a script is for, when to run it, and the rules that apply at the moment you run it. The full entry for each script is in **`references/pipeline-scripts.md`**: every flag, what each `meta.*`/`slide.*` field means in detail, the measured incident behind each rule, and the library-maintenance tooling. **Open a script's entry there before you** set a field this index does not explain, pass a flag not shown here, act on a gate finding you do not understand, or edit the library (a register, `runtime/`, a gate). **Ingest, voice, palette** - `scripts/doctor.py [--json]`: environment preflight, run once at the START (step 1). It exits non-zero only when a REQUIRED dependency (ffmpeg/ffprobe/PIL) is missing, and prints the stage each missing optional tool gates. - `scripts/ingest.py <input> <out_dir>`: pptx/pdf/image folder → `frames/` + skeleton `storyboard.json` (pulls pptx speaker notes). - `scripts/tts.py <out_dir> [--only N]`: narration → `audio/` + measured durations. edge-tts writes `.mp3` + `audio/NN.words.json` (per-word times, which karaoke, cues and the sync gate need); macOS `say` writes `.wav` and no word times. **`tts.py` REWRITES `storyboard.json`**, so run it (and any duration-editing pass of your own, e.g. tail-padding) **BEFORE you hand-set per-scene fields like `transition`**, or a later re-synth silently clobbers them. Order: finalize audio & durations → THEN set transitions/motion → validate → render (render only READS the board). A scored remotion film's clips also get their tails snapped to the beat grid (see **Sound**; words.json untouched). Every clip is mastered in place (EBU R128 I=-14, trailing dead air trimmed, word times stay valid; `meta.audio_master: false` opts out). **`meta.scene_pause` (default 0.55 s) is the pause the viewer HEARS between scenes, not the pad.** `tts.py` converts it per tier (remotion adds the 0.40 s cross-dissolve overlap, so both tiers sound the same) and targets the film's own 75th-percentile sentence pause PLUS a 0.45 s breath, floored at `scene_pause` and capped at 1.5 s; it prints the number. `meta.audio_breath` sets a RAW pad, only for a board that deliberately wants one. **Keep `scene_pause` ≤ ~0.9 s**: it is a pause, not a reading buffer (a long tail reads as the video hanging); reading time goes into a slower `meta.tts_speed`. - `scripts/theme.py <out_dir> [--mode light|dark] [--accent #hex] [--accent2 #hex] [--bg #hex] [--style <key>] [--brand-logo <png>]`: topic palette → `<out_dir>/theme.css`, which HTML pages link and the programmatic renderers parse (one colour world). **Run it before authoring pages OR programmatic scenes.** Contrast is gamma-correct WCAG: fg/bg ≥7:1, a SAMPLED accent nudged toward ≥3:1, an explicit `--accent` never touched. `--brand-logo`, or `meta.brand.logo` when no accent flag is passed, makes the logo's hue the accent. Re-run it on an out_dir themed before the contrast fix. Printed/filmic styles get fixed-seed grain; a data-heavy page opts out with `<body class="no-grain">`. - `scripts/dna_from_deck.py <out_dir> [--pptx deck.pptx] [--mode light|dark] [--dry-run] [--force]`: remotion tier. Writes `meta.design_dna` (with a `provenance` block) from the deck's OWN pptx theme (`scripts/pptx_theme.py`) and frames. Run it after ingest and BEFORE the shot-director. It never overwrites an authored DNA without `--force` and fails loudly when there is nothing to derive from. The html tier's `theme.py` is untouched. **Imagery** - `scripts/imagegen.py <out_dir> [--key KEY]`: OPTIONAL **paid** step. Generates AI atmosphere art for scenes carrying an `image_prompt`, cached under `.assets/`. It acts only when `meta.image_source == "openai"` (`meta.image_provider` `auto`/`codex`/`openai`; the Codex CLI path needs no key). The images are backplanes BEHIND HTML pages: never a slideshow of generated stills, never text or data, never a stand-in for a real named subject (that is `documentary`). `validate.py` enforces all three. - `scripts/imagefetch.py <query> <out_path> [--prefer W] [--min-width PX] [--require TERM...] [--duotone SHADOW HIGHLIGHT]`: the FREE `documentary` tier. Fetches a license-clean Wikimedia Commons image of the ACTUAL named subject and writes a `<out_path>.json` sidecar with licence + attribution + which field of the file's record named each query term. A candidate is taken only when its title, object name or categories attest EVERY key term of the query (or its description holds the whole phrase); otherwise it is refused, and when all are refused the reasons and a hint print to stderr — rephrase, or pass `--require` with the terms that are the subject when the rest only qualify it. Helpers: `fetch()` returns None when nothing relevant and license-clean exists, `attribution_line()` gives the on-screen `.credit`, `duotone()` tones the image into the palette. Reach for it on CONCRETE topics; the design-director picks `media.treatment: none|atmosphere|documentary` per beat. **Take the credit FROM the sidecar, never from memory**: `validate.py` ERRORS when a beat shows a `media/*` file without crediting its sidecar's author. **Render: html / image tier** (`scripts/render.py`) - `scripts/render.py <out_dir> [--sample N] [--only N] [--vertical] [--reduced-motion] [--preview]`: storyboard → `out.mp4` + `out.srt`. `--preview` is the cheap iteration render (12 fps, its own cache, writes `preview.mp4`, never touches `out.mp4`). Use it while experimenting, and DELIVER the full render. The fields it reads: - **Transitions are SEMANTIC.** `meta.transition` (`dip`|`none`) + `meta.transition_seconds`; per scene `slide.transition` (`dip`|`in`|`out`|`none`) + `slide.transition_seconds`. Map the beat's `relation`: continuation → hard cut; new-topic/payoff/close → an in-edge on the opener PLUS an out-edge on the predecessor (a dip needs both sides; validate lints an unpaired edge). `meta.transition_style` (`dip`|`fadethrough`|`drift`|`push`|`lift`|`wipe`|`blur`) is chosen ONCE per deck from the register. A transition is a claim about how two beats relate, never decoration: budget ~2-3 marked seams per video, never random, no spins, `blur` once per film at most. Both tiers share one seam vocabulary, and **`validate.py` ERRORS on a seam word the renderer cannot resolve** (`references/motion-grammar.md` "Transition rhythm" / "Transition STYLE"). - **Captions.** `meta.caption_mode` defaults to `overlay`: the subtitle sits directly on the frame's own ground, with no strip and no white/black halo. Its ink is the deck's own content colour for that ground, and its outline is the sampled ground. **Authors MUST keep content out of the bottom caption band (`caption_band_px`: 200 px ≈ 18.5% at 16:9, 257 px on 9:16/1:1/4:5 — keyframe_qa's finding prints the exact edge)**; keyframe_qa gates it. `band` is the opt-in footer strip (canvas grows; `meta.caption_band` `flush`|`contrast`). `meta.caption_scale` defaults to 1.0. Karaoke `meta.captions_karaoke` (`auto`|`off`) needs edge word times (CJK matched by char-span) and falls back to line chunks without them. The remotion tier follows the same rule with no scrim (`references/captions.md`). **A 9:16 film uses the platform-safe profile on this tier too** (`meta.safe_zone`, default `"social"`; `"none"` opts out; `band` mode is left as it was): each page is drawn in the scaled frame of `runtime/safeZone.ts`, its edges dissolving into a margin of its own ground (or, for a photo/gradient edge, its own edges extended and blurred), the caption is a size larger, ends short of the button column and sits above the platform's description, the brand bug sits below the tabs, and the full render writes `out.cover.png`. Pages are authored and gated exactly as before (keyframe_qa measures the page; its caption band lands under the caption). - **Timed callouts.** A callout with a `cue` anchor phrase (or `at` 0..1) appears as its region is spoken and holds to the clip end. Untimed callouts show for the whole slide. - **Motion.** `meta.intensity` / `slide.intensity` (`subtle`|`balanced`|`bold` or a non-negative multiplier) scale the Ken Burns move, clamped under the zoom/pan caps. **`shot` is authoring shorthand: translate it into `motion.from/to/zoom`, because render reads `motion`, not `shot`.** - **`meta.audacity`** (`conservative`|`balanced`|`bold`|`experimental`) is the register's RISK BUDGET and the register-contest width (skip / 3 / 5 / 7 candidates). It **NEVER relaxes the floor**: clarity, faithfulness, WCAG and every gate are the same at every level. A register move that breaks one is redone in-bounds. - `--reduced-motion` / `meta.reduced_motion`: the accessibility variant (static frames, opacity-only reveals). It busts the clip cache. - `scripts/scenes.py`: animated programmatic scenes (`type` `title`/`keypoints`/`chart`/`timeline`/`compare`/`html` + a typed `props`; `type` defaults to `slide`). They render ONLY their `props`, so they are faithful by construction, and they read `theme.css`. `props.climax:true` lands a counter at ~68% and holds it. `props.cue` (one anchor phrase per element, in spoken order) lands each bar/node/row as it is spoken. Cues change only WHEN an element appears, never the final frame. - HTML pages tier: `scenes.py:render_html` captures pages authored on `assets/html/base.css` (`references/html-pages.md`) in headless Chrome. For audio-synced reveals, tag each point `data-cue-index="N"` (spoken order) + `data-cue="<spoken anchor>"`. `scripts/cues.py` resolves the anchors from edge word times, or estimates them from text position when there are none. Capture runs over CDP (`scripts/_cdp.py`) and falls back to per-frame screenshots on its own; tune it with `meta.capture` (`cdp`|`screenshot`), `meta.chrome_workers`, `meta.jpeg_quality`. **Render: remotion cinematic tier** (`meta.renderer:"remotion"`) - `scripts/tier_advice.py`: decides from purpose + topic whether a film warrants this tier (interview ③). Remotion is for launch / promo / story films, NOT every video. - `scripts/render_remotion.py <out_dir>`: renders the bespoke-shot library (`remotion/LIBRARY.md`) one beat at a time and keeps unchanged beats in `.remotion_segments/`. `python3 scripts/remotion_segments.py <out_dir>` lists which beats the next render would redo. A visual edit re-renders that beat and the next; a narration edit re-renders every later beat. A render that dies resumes; `--whole`, `meta.segment_cache: false` or `REMOTION_SEGMENTS=0` render in one pass. `"remotion-legacy"` errors (that tier was removed). Run `cd remotion && npm install` once. Rules on this tier: - **The cut has to be authored.** No seam reaches a film unless something writes `transition_in`, so run `python3 scripts/seam_plan.py <out_dir>`. From each beat's `relation` it derives ~2-3 MARKED seams (≥5 beats apart, luminance-safe) and ~2-4 motivated `slide.camera` moves, and cuts the UNMARKED seams by the film's tempo: a launch or promo (`meta.purpose`) cuts every one hard (`cut`), a scored film half, a calm explainer keeps the dissolve (`meta.cuts` names it). An authored `transition_in` always wins. validate WARNS on a ≥12-beat film with no mark, or more than ~1-in-8 marked. Only `carry`, `focus`, `through-dark`, `drift`, `blur`, `container` and `cut` may sit on a dark↔light seam; any other mark there (`portal` included) is silently forced to a fade. The ANCHORED seams — `container` (an element carried across the cut), `grow`, `through-window`, `punch` (once, the payoff) — and the camera hand-off (`camera.handoff`) are in `references/motion-grammar.md`; an anchor is `transition_in.carry`, and `python3 scripts/carry_anchors.py` lists what each register draws. A bespoke direction must declare `meta.design_dna.seam` (e.g. `{type:'wipe',direction:'from-bottom'}` or `{type:'fade'}`), or its unmarked seams fall through to an anonymous cross-fade. - **Camera** is per beat and motivated: `slide.camera:{move:'push'|'pull'|'hold'}` (payoff → push, close/opener → pull, else hold; `handoff: true` carries a push across a continuation cut). A subject with a geography — scales, distances, layers — is a `WorldStage` run instead (Continuous stage, below). `meta.camera_drift` stays off. The camera bed is OFF by default (`meta.camera_bed: true` opts in) and never coexists with `camera_drift`. On a beat of 10 s or more an authored push/pull arrives in steps on the voice's clauses instead of an invisible creep, and on a beat that shows a LIST the item the voice names leads while its peers recede (`render_remotion.attach_phrases`; `meta.phrase_camera: false` or a beat's `"phrase_camera": false` turns both off). Neither adds a camera to a beat nobody gave one: that was measured to make type tremble (pipeline-scripts.md, `phrase_steps.py`). - **The cut carries.** On a film that cuts hard by tempo (kinetic or scored; `meta.cut_carry` says so outright) every hard cut carries one move through the cut frame (runtime/cutCarry): the leaving beat drifts and leans in, the arriving one comes out of the same drift already mid-entrance. `scripts/seam_ledger.py <out_dir>` measures each cut on out.mp4 and fails a kinetic film whose cuts join still frames. - **Footage is shown as itself.** `FilmScreen` plays a delivered film or a recording at its own pixels (a close-up region, the film's beats running beneath, a counted reel); a film's own pictures go in with `workshop.py <out_dir> media add <file> [--at s]`, and `workshop.py <out_dir> motion <Name>` says whether a built shot keeps moving after its reveal. - **The hidden cut.** `transition_in: {type: "sweep"}` (alias `hidden-cut`): a panel passes the lens and the beat changes behind it — two stops of one journey, "and on we go" (motion-grammar.md). - **Time** is grammar too, per beat and named by the words: `slide.time: {hits: [{cue}], hold: 2}` freezes the drawing on an impact (hit-stop), `freeze: {at: {cue}, hold: s}` holds a moment for a look, `sakuga: {until: {cue}, every: 2|3}` moves on held drawings until the climax and on ones from it (runtime/timeGrammar; every pause is repaid within a few frames, so the beat stays on its voice; validate and Film refuse a hold past the bounds). Use it where the beat has an impact, a moment to look at, or a climax — not as texture on every beat. A hit lands on a word where something ARRIVES: on a still frame it shows nothing. - **One structure, many beats.** When the argument IS one structure (a pipeline, a system), name it once in `meta.maps` and build it with `LivingMap` beats (`shot_props: {map, add, focus}`): laid out once, added to beat by beat, the camera moving through it, every map cut invisible (remotion/cards/LivingMap.md). - **Bookends.** A launch/promo film of 6+ beats may open COLD (`ColdOpen`: 2-4 of its own later beats flash past, then the title) and any film that long may end on a `Callback` (its earlier beats gather as cards round the closing line); `shot_director.py` proposes both by position, `shot_props.beats` names the beats (1-based). - **Places** (styled films, `style.surface.world: place`): each beat may name the `world` its subject is in — `orbit`, `sky`, `field`, `room` (`remotion/src/runtime/places.json`) — and is drawn inside it; a beat about an idea names none. Named per beat from the subject, never one place for the film. - **Reach.** After the forms and ideals, each beat's argument is read against the table of shapes the library has a
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen