paper2video
GitHub将研究论文或PPT转为带旁白的MP4视频。作为编排器,协调paper2assets、ppt-master和pptx2video技能,确保幻灯片与视频触发器一致,并通过严格QA验证输出。
Trigger Scenarios
Install
npx skills add microsoft/ResearchStudio --skill paper2video -g -y
SKILL.md
Frontmatter
{
"name": "paper2video",
"description": "Turn a research paper, a paper2assets package, or an existing PPT deck into a narrated MP4 video by fully delegating slide authoring to the installed ppt-master skill and fully delegating rendering, subtitles, timeline assembly, and strict media QA to the installed pptx2video skill and its public CLI. Resolves one explicit animation-trigger decision (on-click or after-previous) and hands it to both child skills so the delivered PPTX and the delivered MP4 never disagree, then audits the final PPTX's native p:timing tree to prove the handoff held. Use when the user wants a paper turned into a video, wants \/paper2video, or wants a narrated MP4 plus editable PPTX from a paper2assets bundle.",
"allowed-tools": "Bash(*), Read, Write, Edit, Grep, Glob, AskUserQuestion, WebFetch, WebSearch"
}
paper2video - paper/assets -> ppt-master deck -> pptx2video render -> narrated MP4
paper2video is a thin orchestrator with exactly one supported path. It does
not author slides itself, does not synthesize audio itself, does not
composite video itself, and does not gate QA itself. It resolves inputs and
one shared trigger decision, then fully delegates:
paper.pdf
-> skills/paper2assets/scripts/build_package.py (shared package)
-> assets/meta/paper_spec.md + assets/meta/narration.json
-> ppt_options_contract.py resolve-ppt-trigger (ONE trigger decision)
-> installed ppt-master skill (full workflow, receives the trigger)
-> installed pptx2video skill / CLI `bootstrap` then `render` (full workflow, receives the trigger)
-> ppt_stage_validator.py audit-final-pptx-trigger (proves the handoff held)
-> video.mp4, video_no_subtitles.mp4, video.pptx
There is only one route. An earlier version of this skill had a second,
self-implemented "Route A" that called private narration, duration, and
visual-cue scripts and invoked python -m pptx2video.render_video /
pptx2video.build_timeline / pptx2video.check_video_package as if they were
importable submodules of the installed public package. Those modules do not
exist at that import path in the public pptx2video 0.5.x CLI; the entry
points are the pptx2video render, pptx2video doctor, and pptx2video bootstrap subcommands. Do not recreate that shortcut. Do not hand-assemble a
narration script, a visual-cue file, or a "simplified" render pipeline that
skips the installed pptx2video skill. Do not invoke any private submodule
path under pptx2video.*. Every render goes through /pptx2video (the
installed skill) or the exact pptx2video render CLI invocation in Step 5;
there is no lower-effort fallback for a rushed or low-context session.
Why full delegation is mandatory
pptx2video render is not a smoke-test wrapper. Its own CLI (cli.py
_render()) reads assets/meta/reports/video_qa_report.json after every
render and raises SystemExit unless passed is true with zero errors and
zero warnings; the top-level CLI does not even expose a --no-qa flag, so an
agent invoking pptx2video render directly cannot silently skip strict QA. A
hand-rolled call into an internal module could skip that gate. This is the
concrete mechanism this skill relies on to stop an agent from cutting corners
under time pressure: route every render through the command in Step 5, and
treat a non-zero exit as a blocking failure to fix, never as a reason to fall
back to a simpler local script.
Two child skills, one shared trigger decision
This skill's own job is narrow: resolve inputs, resolve one ppt_trigger
value, pass it identically to both child skills, and verify the delivered
PPTX actually carries it. Everything else belongs to the child skills:
ppt-masterauthors the deck: SVG pages, the Strategist confirmation stage, speaker notes, and PPTX export including native entrance-animation triggers (--animation-trigger).pptx2videorenders the deck: TTS, word-boundary timing, animation scheduling, subtitles, timeline, and strict media QA (assets/meta/reports/video_qa_report.json).
Both read PowerPoint's native p:timing tree, but for different purposes:
ppt-master writes it, and pptx2video's video scheduler reads native row order
and narration timing from it, never the click-group number. If each of these
two skills is allowed to pick its own default trigger, they can produce a
PPTX/MP4 pair whose native badges and playback pacing quietly disagree. This
skill's Step 2 exists specifically to prevent that.
The four concepts the old protocol conflated
The single native p:timing tree has to answer four different questions, and
the previous version of this skill's protocol used only one field
(click-group number) to try to answer all of them, which fails whenever a
deck uses after-previous:
| Concept | What it means | Where it lives | Whose default wins |
|---|---|---|---|
effect_order |
Real entrance-animation sequence, 1..N, independent of trigger type | Native Animation Pane row order (p:cTn order inside mainSeq) |
Fixed by authoring order; never inferred from click-group number |
ppt_trigger |
The one user-facing intent: on-click or after-previous |
Resolved once by this skill's resolve-ppt-trigger command |
This skill, not ppt-master's own CLI default, not pptx2video's own CLI default |
click_group |
PowerPoint's own derived click-group badge, shown in the Animation Pane UI | Computed by PowerPoint/pptx2video's --click-group-policy from trigger topology |
A side effect, never a source of truth |
video_start |
When a phase plays in the rendered MP4 | pptx2video's narration/timeline scheduler, keyed to effect_order plus word-boundary timing |
pptx2video's renderer; click-group number is never consulted |
The failure this fixes: a deck with 38 after-previous effects is fully
valid OOXML, but PowerPoint places every after-previous (automatic) effect
in the same native click group, commonly all showing 0 in the Animation
Pane's click-group column. That is expected PowerPoint behavior for automatic
effects, not data loss and not a rendering bug; effect_order (Pane row
order) still exists and is exactly what pptx2video's scheduler uses to place
each phase in the video's timeline, together with narration/word timing. The
problem in the old protocol was documentation and verification, not the
underlying render: nothing told the agent to pick ppt_trigger on purpose
before authoring, hand it to both children identically, and check the
delivered file. This skill's Step 2 and Step 6 close exactly that gap.
Paths
Run Paper2Video workflow commands from ResearchStudio-Reel/ unless noted.
PAPER2VIDEO=skills/paper2video
PAPER2ASSETS=skills/paper2assets
Install both external skills before starting. Do not clone or vendor either dependency inside ResearchStudio:
npx skills add hugohe3/ppt-master --skill ppt-master
npx skills add ai-nuts/pptx2video --skill pptx2video
Install and verify the compatible public pptx2video 0.5.x CLI runtime. Use a
Python 3.11 or newer environment:
python -m pip install \
'pptx2video[svg] @ git+https://github.com/ai-nuts/pptx2video.git@v0.5.0'
python -m playwright install chromium
python3 -m pptx2video --version
python3 -m pptx2video doctor --svg
Always invoke the runtime as python3 -m pptx2video ..., never as the bare
pptx2video command. A host may have an older pptx2video (for example
0.3.0) earlier on PATH than the intended 0.5.x install; the bare command
then silently resolves to the wrong version with no error. python3 -m pptx2video --version must print pptx2video 0.5.x; a different major/minor
version means fix the environment (pip install target, PATH, virtualenv)
before continuing, not proceed and hope the CLI surface still matches this
document.
Do not hard-code a host-specific skill directory. Users may run this
repository from Codex, Claude Code, a shell, or another agent. This skill
itself never calls a private path under pptx2video.*; it always calls the
installed skill (/pptx2video) or the public pptx2video render /
pptx2video doctor / pptx2video bootstrap CLI subcommands.
Output Contract
Follow the shared paper2assets v2 layout. The bundle top level holds only
deliverable files plus manifest.json; everything else lives under assets/:
<video_outdir>/
video.mp4 # required, burned-in subtitles in an appended black bottom band
video_no_subtitles.mp4 # required, raw/pre-subtitle playback copy for paper2reel
video.pptx # required, the same delivered deck pptx2video rendered from
manifest.json # schema_version "paper2video.v1", layout "v2-assets"
assets/
audio/ # script.json, per-block TTS, word timings
captions/ # video.srt, video.vtt
slides/ # rendered slide frames used by the MP4
clips/ # raw render intermediate
meta/ # timeline.json, QA and authority reports
This layout, and every file inside it, is produced by pptx2video render
itself; Step 5 below is the only place this skill writes into
<video_outdir>. Do not hand-assemble any part of this tree before that step.
Pick <video_outdir> before any file writes. Resolve deterministically:
- An explicit
<video_outdir>argument from the caller wins. Honor it verbatim; the defaults below only fire when no path was passed. - A
paper2assetspackage already exists -> reuse its folder verbatim as<video_outdir>. The canonical detection signal is<dir>/assets/meta/paper_spec.md;<dir>/manifest.jsonwith"layout": "v2-assets"is a confirming hint when present. - Otherwise (a bare PDF is the only input) -> default to
<input_pdf_dir>/<pdf_stem>/, matching thepaper2assetsdefault convention so a laterpaper2assetsrun lands in the same bundle.
pptx2video render refuses to write into a path that already exists, so Step
5 renders into a fresh temporary bundle for exactly this reason and then this
skill promotes the result into <video_outdir> (see Step 5).
Step 1: Build or reuse the shared paper2assets package
python skills/paper2assets/scripts/build_package.py <paper.pdf> --outdir "$VIDEO_OUT"
If paper_spec.md already exists, sync structured claims and narration
instead of re-extracting:
python skills/paper2assets/scripts/build_package.py <paper.pdf> \
--outdir "$VIDEO_OUT" \
--skip-extract \
--paper-spec "$VIDEO_OUT/assets/meta/paper_spec.md"
assets/meta/narration.json is the canonical narration order for this run:
one {"id", "heading", "text"} entry per section, in the same order poster,
blog, and video all use. Do not reorder or rewrite it here; ppt-master reads
the paper2assets package to author slides in this order, and Step 4 passes
narration to pptx2video from the same source.
Step 2: Resolve the one shared ppt_trigger decision
This is the single most important step in this skill. Resolve ppt_trigger
before invoking ppt-master, and pass the exact same resolved value to both
ppt-master and pptx2video. Never let either child skill fall back to its own
independent default.
python3 skills/paper2video/scripts/ppt_options_contract.py resolve-ppt-trigger \
--ppt-trigger {auto,on-click,after-previous}
- Omit
--ppt-trigger, or passauto, when the user has not stated a preference. This resolves toon-click, because a downloaded PPTX with a continuous1..NAnimation Pane badge sequence is the more broadly useful default deliverable, and the rendered MP4 still auto-plays with no manual click either way (see below). - Pass
on-clickexplicitly when the user wants a presenter-controllable, numbered PPTX. - Pass
after-previousonly when the user explicitly asks for a PPTX that also auto-plays natively in PowerPoint (not merely "a video that plays itself" -- the rendered MP4 always plays itself regardless of this choice).
The command prints one JSON object; treat every field as a literal value to carry into the next two steps, not as a suggestion:
{
"ppt_trigger": "on-click",
"ppt_master_animation_trigger": "on-click",
"pptx2video_click_group_policy": "normalize"
}
{
"ppt_trigger": "after-previous",
"ppt_master_animation_trigger": "after-previous",
"pptx2video_click_group_policy": "preserve"
}
ppt_master_animation_trigger is the exact value for ppt-master's
--animation-trigger flag in Step 3. pptx2video_click_group_policy is the
exact value for pptx2video's --click-group-policy flag in Step 5. Both come
from this one command so the two child skills can never disagree about the
user's trigger intent. Record the printed JSON (for example into
$VIDEO_OUT/assets/meta/ppt_trigger_handoff.json) so Step 6's audit and any
retry use the same resolved value.
Why the rendered MP4 auto-plays either way. pptx2video's video scheduler
places every entrance phase in the timeline using native Animation Pane row
order (effect_order) plus narration/word-boundary timing; it never consults
PowerPoint's click-group number. Choosing on-click changes only what the
downloaded PPTX looks like when a human opens it in PowerPoint. It does not
make the MP4 wait for a click.
Step 3: Author the deck with ppt-master, passing the resolved trigger
Invoke the installed ppt-master skill and follow its full workflow: source
conversion, project init/import, the Strategist confirmation stage (fields a
through h, MANDATORY, cannot be skipped), optional image acquisition,
sequential page-by-page SVG authoring, svg_quality_checker.py, then the
three-command export pipeline run one command at a time
(total_md_split.py, finalize_svg.py, svg_to_pptx.py).
- Do not replace ppt-master with handwritten SVG, a local simplified generator, or a copied example deck whose content does not come from the paper.
- If ppt-master reaches a blocking confirmation gate and the user has not already approved defaults, stop and ask the user.
- If a machine dependency is missing, record the concrete missing dependency and stop. Do not silently degrade to a different slide-generation method.
- Pass
$PAPER_ASSETS/assets/meta/narration.jsonsections (or the paper/PDF content directly) as the source content; do not invent claims absent from the paper.
Pass ppt-master's --animation-trigger flag exactly as
resolve_ppt_trigger_handoff returned in Step 2:
python3 ${SKILL_DIR}/scripts/svg_to_pptx.py <project_path> \
--animation-trigger <ppt_master_animation_trigger from Step 2>
If the deck should carry no entrance animation at all (-a none, ppt-master's
own default), the --animation-trigger flag is irrelevant and may be omitted;
the resolved value only matters once -a requests an actual entrance effect.
Leave speaker notes enabled (ppt-master's default; do not pass --no-notes)
so Step 4 can replace its raw Notes with pptx2video's canonical per-element
protocol -- see the note on notes format below.
ppt-master Notes are not pptx2video's Notes protocol. ppt-master's
markdown_to_plain_text() writes plain presenter-note paragraphs, not
pptx2video's canonical ## [handle] semantic block grammar. pptx2video treats
ordinary presenter notes as absent narration and falls back to a generic
per-shape handle. Do not render an animated raw ppt-master PPTX directly.
Step 4 first bootstraps the page-level narration into handle-addressed Notes
and Alt Text.
Before invoking ppt-master, prepare title-slide utility assets when a
paper2assets package exists:
python skills/paper2assets/scripts/fetch_logos.py \
--outdir "$PAPER_ASSETS" --from-spec "$PAPER_ASSETS/assets/meta/paper_spec.md" || true
python skills/paper2assets/scripts/make_qr.py \
--outdir "$PAPER_ASSETS" --from-metadata "$PAPER_ASSETS/assets/meta/metadata.json" || true
Skip fetch_logos.py when paper_spec.md is not available; do not invent
logos. make_qr.py is best-effort and only uses paper_url, code_url, or
the documented arxiv_id fallback from metadata.json. Add a requirement
block to the ppt-master prompt covering restrained placement of these assets
on slide 1 (right-side title area or lower-right safe zone), and instruct
ppt-master to omit unavailable assets cleanly rather than leaving broken
placeholders.
Confirm the export produced a PPTX at <project_path>/exports/<name>.pptx
before continuing to Step 4.
Step 4: Prepare narration and bootstrap the editable protocol
Build a script.json from the shared narration.json so pptx2video's
narration authority is explicit and does not depend on ppt-master's raw Notes
text:
{
"sections": [
{"id": "problem", "heading": "Problem", "text": "..."},
{"id": "method", "heading": "Method", "text": "..."}
]
}
assets/meta/narration.json from paper2assets already has this
{"id", "heading", "text"} shape; it can be used directly as the
bootstrap --script-json input provided its section count matches the PPTX
slide count. The number of sections in this file must equal the number of
slides in the deck exported in Step 3, in the same order; pptx2video rejects a
section-count mismatch. When ppt-master's page-count resolution produced a
different number of slides than narration.json has sections, reconcile the
two before rendering: either regenerate the deck at the corrected page count,
or edit the script.json copy so its section count and order match the
delivered PPTX exactly. Do not truncate or duplicate sections to force a
match.
Bootstrap the ordinary animated deck once. This divides each slide's page-level narration across its animation targets and writes canonical handle-addressed Notes and Alt Text:
PPTX2VIDEO_WORK=$(mktemp -d)
PPTX2VIDEO_SOURCE="$PPTX2VIDEO_WORK/video-protocol-source.pptx"
PPTX2VIDEO_TMP="$PPTX2VIDEO_WORK/video-bundle"
python3 -m pptx2video bootstrap \
"<project_path>/exports/<name>.pptx" \
--script-json "$VIDEO_AUDIO/script.json" \
--output "$PPTX2VIDEO_SOURCE"
Treat a non-zero bootstrap exit as blocking. Do not replace
$PPTX2VIDEO_SOURCE with the raw ppt-master export in Step 5.
Step 5: Render through pptx2video, passing the same resolved trigger
Render the bootstrapped PPTX into the fresh output directory. Pass the
page-level script only as --ids-from-script: this preserves the per-element
narration written in Step 4 while fixing each slide's internal section_id.
Do not use --script-json for this render; it would put the entire slide
transcript on the first animation element while clearing the others.
python3 -m pptx2video render \
"$PPTX2VIDEO_SOURCE" \
"$PPTX2VIDEO_TMP" \
--resolution 1080p \
--ids-from-script "$VIDEO_AUDIO/script.json" \
--animation-order-policy animation-pane \
--click-group-policy <pptx2video_click_group_policy from Step 2>
--click-group-policymust be the exact value Step 2 resolved (normalizeforon-click,preserveforafter-previous). Never omit this flag and rely on pptx2video's own CLI default; the CLI default (normalize) silently overwrites a deliberately authoredafter-previouscascade into a click-driven deck, which is the opposite of what an explicitafter-previousrequest means.--animation-order-policy animation-panemakes the run fully non-interactive and deterministic. pptx2video's ownautodefault only prompts whenstdinis a TTY; in a non-interactive agent session with an actual ordering conflict it exits 3 without rendering. Passinganimation-paneexplicitly confirms "trust the deck's own Animation Pane order" up front so a real conflict never stalls the run silently.- Do not pass
--no-subtitlesunless the user explicitly disabled captions; the default burns captions intovideo.mp4while leavingvideo_no_subtitles.mp4and the VTT/SRT sidecars intact. - Do not pass
--baseline-pptx/--narration-mode regeneratefor a first render; those are for an explicit re-render of a previously delivered deck.
Do not add --no-qa. It does not exist on the top-level pptx2video render CLI; QA is unconditionally enforced by cli.py's own _render(),
which reads assets/meta/reports/video_qa_report.json after rendering and
raises SystemExit unless passed is true with zero errors and zero
warnings. A non-zero exit here is a blocking failure: fix the deck, script, or
flags and rerun this exact command; do not fall back to a private render path
to force a result through.
On success, promote the fresh bundle into <video_outdir> without clobbering
an existing manifest.json from a different paper2* skill sharing the same
bundle root:
rsync -a --exclude=manifest.json "$PPTX2VIDEO_TMP"/ "$VIDEO_OUT"/
If $VIDEO_OUT/manifest.json already exists (written by paper2assets,
paper2poster, or paper2blog sharing the same bundle root), merge
$PPTX2VIDEO_TMP/manifest.json's video-specific fields into it by hand
instead of overwriting; do not let a blind copy erase fields another
paper2* skill wrote to the same manifest.json. Otherwise, copy pptx2video's
manifest.json in as-is.
Step 6: Audit the final PPTX against the resolved trigger (MANDATORY gate)
This closes the loop the old protocol never checked: prove the delivered
video.pptx's native p:timing tree actually carries the trigger this skill
resolved in Step 2, rather than trusting that ppt-master's flag and
pptx2video's flag each did the right thing.
python3 skills/paper2video/scripts/ppt_stage_validator.py audit-final-pptx-trigger \
"$VIDEO_OUT/video.pptx" \
--ppt-trigger <ppt_trigger from Step 2>
on-clickrequires every non-With-Previous native row to beclickEffect; this is what gives a downloaded PPTX a continuous1..NAnimation Pane badge sequence.after-previousrequires every non-With-Previous native row, including every row inside a preserved cascade, to beafterEffect; this is what makes the PPTX auto-play natively in PowerPoint. No numbered Pane is claimed or required for this choice.- The audit raises if the delivered deck has no native
p:timinganimation on any slide at all, so a silently-dropped animation request fails loudly instead of passing by vacuity.
A non-zero exit here is a blocking failure, exactly like a failed
video_qa_report.json in Step 5: it means ppt-master's --animation-trigger
and pptx2video's --click-group-policy disagreed about the trigger, or one
of them fell back to its own default instead of the value resolved in Step 2.
Re-check the flags actually passed in Step 3 and Step 5 against Step 2's
printed JSON, fix whichever command diverged, and re-render before reporting
the video as done.
Step 7: Report
Tell the user the absolute paths of the deliverables:
<video_outdir>/video.mp4(burned-in subtitles)<video_outdir>/video_no_subtitles.mp4(paper2reel source)<video_outdir>/video.pptx(editable deck, the same file the Step 6 audit verified)<video_outdir>/assets/meta/timeline.jsonand<video_outdir>/assets/meta/reports/video_qa_report.json
State explicitly which ppt_trigger was used and confirm Step 6's audit
passed. Do not report the task done without having run and passed both
Step 5's QA gate and Step 6's trigger audit.
Acceptance criteria
- Downloaded PPTX Animation Pane shows a continuous
1..Nbadge sequence whenppt_trigger=on-clickwas resolved and used. - The rendered video auto-plays every phase in an order that matches the
spoken narration, regardless of which
ppt_triggerwas used. - When the user explicitly requested
after-previous, the delivered PPTX still auto-plays natively in PowerPoint, without a claim of continuous native numbering. ppt_stage_validator.py audit-final-pptx-triggerexits 0 against the exactppt_triggerresolved in Step 2.pptx2video render's own QA gate (video_qa_report.json) passed with zero errors and zero warnings.
Key rules
- One route only. Every render goes through
/pptx2videoor the exactpptx2video renderCLI command in Step 5; never a privatepptx2video.<module>import, never a hand-rolled narration/timeline/QA substitute. - One trigger decision. Step 2 resolves
ppt_triggerexactly once per run; Step 3 and Step 5 both consume that same resolved JSON, never their own child-skill default. - QA gate passing is not the finish line by itself. A passing
video_qa_report.jsonproves pptx2video's own render was clean; it does not prove the trigger handoff held. Step 6's audit is the check that proves that, and it is mandatory before reporting completion. - Do not degrade silently. A missing dependency, a blocked ppt-master confirmation gate, a failed QA report, or a failed trigger audit are all stop-and-fix conditions, never a reason to ship a simplified or unverified video.
Tools
scripts/
├── ppt_options_contract.py # resolve-ppt-trigger CLI; also the shared PPT option/appearance contract
└── ppt_stage_validator.py # audit-final-pptx-trigger CLI; also descriptive-option/export-flag audits
The generic render runtime is not owned or vendored by this skill. Install
pptx2video and invoke it only through /pptx2video or
python3 -m pptx2video {doctor,render,bootstrap}.
References
references/pptx2video.md- the standalone renderer's install steps, the exact CLI flags this skill's Step 5 command relies on, and the pitfalls (PATH,--no-qanot existing, click-group-policy defaults) that make an almost-right invocation silently diverge from the protocol.references/script_json_schema.md- thescript.jsonschema Step 4 passes tobootstrapand Step 5 passes only as--ids-from-script, including the handle-levelelementsform and TTS text gotchas.references/ppt_trigger_handoff.md- deep dive on the four-concept model, the native OOXML mechanics behind it, and the failure modes this skill's Step 2/Step 6 close.
Version History
-
8650d6a
Current 2026-08-27 10:30
删除自实现的Route A代码路径,强制执行单一路由委托;拆分触发器传递逻辑为四个独立概念,增加最终PPTX触发器审计以确保一致性。
-
fe1e1d9
2026-08-12 09:56
新增随机化自动PPT外观的功能。
-
88c08f1
2026-07-30 20:40
强化Paper2Video合同与Paper2Reel交付逻辑;重构门户合同至video模块;修复Reel QA布局隔离问题,确保下载隔离幂等;支持按需下载归档;新增视频字幕栏与焦点组合功能。
- 3c120d8 2026-07-19 08:53
Dependencies
-
suggested
hugohe3/ppt-master -
suggested
ai-nuts/pptx2video


