refactor(skills): move product-launch / pr-to-video / faceless-explainer onto the script-driven architecture#1635
refactor(skills): move product-launch / pr-to-video / faceless-explainer onto the script-driven architecture#1635WaterrrForever wants to merge 11 commits into
Conversation
…ecture Move product-launch-video onto the shared script-driven authoring flow: build-frame remixes a hyperframes-creative preset onto brand tokens, audio routes through the shared hyperframes-media engine, per-preset caption skins, and every frame is authored as a directed shot. Removes the old bespoke scripts (captions/validate/prep/hoist/…) in favour of the shared lib. assemble-index.mjs keeps upstream #1629's blank/partial scene-file guard (reject an empty or markup-less scene file at assembly, before emitting data-composition-src, and re-dispatch) carried onto the restructured reader. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move pr-to-video onto the shared script-driven authoring flow: ingest.mjs folds the gh PR artifacts into the synthetic capture package the shared backend (build-frame / captions / assemble-index) reads, add the mechanism beat, route audio through hyperframes-media, and remix a hyperframes-creative preset onto brand tokens via the shared lib. - Fix skill name: pr-to-video-refactor -> pr-to-video (match directory). - Drop a stale faceless-explainer-refactor reference in an ingest.mjs comment. - assemble-index.mjs keeps upstream #1629's blank/partial scene-file guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ture Move faceless-explainer onto the shared script-driven authoring flow: every visual is invented (typography / abstract graphics / diagram / data-viz) and authored through the shared backend (build-frame remixes a hyperframes-creative preset onto tokens, audio via hyperframes-media, assemble-index builds the standalone index.html) using the shared lib. - Fix skill name: faceless-explainer-refactor -> faceless-explainer (match directory). - assemble-index.mjs keeps upstream #1629's blank/partial scene-file guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the install-and-verify harness to the current surface: 10 workflows (adds website-to-video, embedded-captions, graphic-overlays, slideshow; drops the removed footage-recut) and refreshed example prompts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run oxfmt over lib/storyboard.mjs — formatting only, no logic change. Fixes the Format / Preflight CI check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The function was split out into gsapDragPositionCommit.ts in #1605, but the test kept importing it from ./gsapDragCommit, which no longer exports it — yielding 'is not a function' at runtime. Import from the correct module. Inherited main breakage (same fix as #1631); fixes the Test CI check on this branch independently of merge order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the entry router's metadata tags (video / animation / router focus); oxfmt collapses the now-shorter metadata to a single line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jrusso1020
left a comment
There was a problem hiding this comment.
Scope-down review per REVIEW_DISCIPLINE rule #4 (631 files / +8330/-87294 — net deletion is the per-skill-script removal claimed in the body). Posting as COMMENT — CodeQL is failing with 12 new high-severity alerts, which per REVIEW_DISCIPLINE rule #1 disqualifies an APPROVE on its own.
CodeQL — required check failing
CodeQL is FAILURE: "12 new alerts including 12 high severity security vulnerabilities." Pulling the alerts for refs/pull/1635/merge returns 263 entries (40 error-severity, 199 warning); the 12 new in this PR are a subset.
Two categories I traced:
1. In-scope (workflow-skill files) — 3 alerts, all js/incomplete-multi-character-sanitization:
skills/product-launch-video/scripts/captions.mjs:227skills/pr-to-video/scripts/captions.mjs:227skills/faceless-explainer/scripts/captions.mjs:227
All three sites are out = out.replace(/<!--[\s\S]*?-->/g, "") — stripping HTML comments from a skin file. The CodeQL concern is the classic "incomplete comment sanitization" — /<!--[\s\S]*?-->/g doesn't handle nested or partial comment markers, so a payload like <!--<!---->--> leaves a dangling start-marker.
Read the actual code: skin comes from a caption-skin.html file shipped with each preset in hyperframes-creative/frame-presets/<preset>/caption-skin.html (PR #1632). So the input is internal content from the preset library, not attacker-controlled at runtime. The vulnerability is theoretical — a malicious preset could exploit it, but presets are codebase-shipped and gated by code review.
Still worth either:
- (a) Replacing the regex with a tighter loop (
while (out.includes("<!--")) { ... }) so CodeQL stops flagging it, OR - (b) Adding a one-line comment at the call site noting "input is preset-library content, not user-controlled — sanitization is comment-strip for lint-cleanliness, not XSS defense" so the suppress-CodeQL reasoning is visible.
The cleanest fix is the tighter loop — kills the alert + matches the actual defensive intent.
2. Out-of-scope — carryover from the upstream-squash-merge stack:
The branch contains commits with PR-suffix titles ((#1605), (#1553), (#1628)) that landed to main via squash-merge but with different SHAs. The branch carries the same content under different SHAs, and CodeQL's analysis of the PR ref includes alerts on those carried files (e.g. packages/core/src/runtime/bridge.ts:76,82, packages/studio/src/hooks/use*.ts, packages/core/src/generators/hyperframes.ts). These are NOT bugs introduced by this PR — they're alerts from upstream code that CodeQL is re-reporting via the PR ref.
Same shape as #1631 — git merge-base says clean-merge per git's recursive heuristic, but CodeQL doesn't dedupe by content equivalence.
Practical implication: of the "12 new" CodeQL counts, the 3 captions.mjs ones are this PR's; the other ~9 are upstream-carryover. The team's CodeQL gate may or may not let the merge through with upstream-carryover alerts — that's a team-discipline call, not a code-review call.
Body claims verified against the diff (per REVIEW_DISCIPLINE rule #3)
- "4 commits, 629 files" — confirmed:
gh pr viewreturns 631 files (paginated; thegh ... --json filesdefault truncates at 100 — I went throughgh api ... pulls/1635/files --paginateto get the full count). - "
+8322 / −87291" — confirmed:+8330 / −87294, near-exact match. Net deletion is per-skill-script removal in favour of the shared backend, per the body. - "Depends on #1632" — confirmed: paths in workflow skills (
../../hyperframes-creative/frame-presets/../../hyperframes-media/scripts/audio.mjs) resolve into #1632's content. Merge ordering MUST be #1632 → #1635. - "#1629 guard ported to each
assemble-index.mjs" — sampledskills/pr-to-video/scripts/assemble-index.mjs; the!html.trim() || !/<\w/.test(html)blank-scene guard is present. ✓ - Skill-name fixes (
name: pr-to-video-refactor→pr-to-video, etc.) — sampled SKILL.md, confirmed names match directories. ✓
Foundation scope respected
The three workflow skills each now resolve hyperframes-creative + hyperframes-media by path. Verified skills/pr-to-video/scripts/audio.mjs shells out to the foundation engine; build-frame.mjs consumes presets via ../../hyperframes-creative/frame-presets/. No duplicated copies of the engine or preset library — good. ✓
Concerns
- CodeQL alerts above — at minimum, the 3
captions.mjs:227regex sanitizations are worth tightening before merge to clear the in-scope portion of the failure. - Stack ordering with #1632 — both PRs are open. They MUST land in order (#1632 → #1635) since #1635 references foundation paths. If a stamp lands on #1635 first, the merge will fail (foundation files don't yet exist).
- Massive content I didn't read — the 631-file scope means I sampled rather than read in full. SKILL.md changes per workflow,
ingest.mjsforpr-to-video,assemble-index.mjsper workflow, andtest-skills-fresh.share the auditable surface I read. The other ~600 file changes (mostly deletions of per-skill scripts) I trusted to match the body's "removal in favour of shared lib" claim. If specific per-script content matters for correctness, a second reviewer focused on the deletion sweep would close that gap.
Stamp posture
Same as #1631 / #1632 — per the team's customer-partner-PR discipline (Miao is allowlisted but not a trusted-stamper), stamp eligibility routes through the team. From my read: needs CodeQL alerts addressed (or explicitly accepted on team-discipline grounds) + #1632 to merge first. Not stamping unilaterally.
Review by Jerrai
miga-heygen
left a comment
There was a problem hiding this comment.
Review: refactor(skills) — script-driven architecture migration
What this PR does
This is a major refactor, not a fix for the #1632 findings. It moves three narrated workflow skills (product-launch-video, pr-to-video, faceless-explainer) from bespoke per-skill scripts onto a shared "script-driven" architecture where TTS/BGM/SFX route through the shared hyperframes-media engine and frame authoring uses hyperframes-creative presets. Additionally: test-skills-fresh.sh is updated (6→10 workflows), a gsapDragCommit test import is fixed, and the router skill metadata is refined.
631 files changed, +8330/−87291. The net deletion is massive because each skill's old self-contained TTS/BGM/transcribe/provider logic (800+ lines each) is replaced by a thin ~215-line adapter that writes an audio_request.json and spawns the shared engine.
Relationship to #1632 bugs
The two bugs flagged on #1632 are indirectly resolved by elimination:
-
wait-bgm.mjsfield mismatch (audioMeta.bgm_path/bgm_enabledvs.bgm.pathnested /bgm_pending):wait-bgm.mjsis deleted from all three skills. The new adapter uses BGMmode: "retrieve"(synchronous retrieval, no detached background process), so there is no longer a wait step. The field mismatch ceases to exist because the old flatbgm_path/bgm_pendingshape is replaced by a nestedbgm: { path, volume, query, duration_s }object. This is a valid architectural fix — the mismatch was a symptom of the bespoke-scripts duplication. -
heygenCredential()unguardedJSON.parse: The function is removed entirely from all three adapter scripts. Credential resolution is now the shared engine's responsibility. Whether the shared engine inhyperframes-mediaguards its ownJSON.parseis outside this PR's scope (it's in #1632), but the per-skill copies of the vulnerable code are gone.
Both bugs are resolved in the sense that the buggy code no longer exists in these skills. The correctness now depends on the shared engine in #1632 being sound.
Code quality observations
Strengths:
- Clean separation of concerns: each skill's audio.mjs is now a thin adapter (~215 lines) that maps its domain model to the engine's neutral format and back. Much easier to reason about.
- The #1629 blank/partial scene-file guard is properly ported to the new
assemble-index.mjsreader (!html.trim() || !/<\w/.test(html)). - Skill name fixes (
pr-to-video-refactor → pr-to-video,faceless-explainer-refactor → faceless-explainer) are good housekeeping. - The gsapDragCommit test fix is a sensible inclusion (inherited
mainbreakage). - CI is green except CodeQL (which flags TOCTOU races and HTML sanitization — pre-existing patterns, not new to this PR).
Observations / questions:
-
Near-identical adapter scripts across three skills.
audio.mjs(213-214 lines),assemble-index.mjs(181-183 lines),lib/storyboard.mjs(249 lines each), andlib/assets.mjs(55 lines each) are virtually identical across all three skills. The PR description says the refactor moves them "onto the shared backend" — but the adapter layer itself is copy-pasted. If a bug surfaces inparseStoryboard()orstageAssets(), it needs fixing in three places. Is there a plan to hoist these into a shared location (e.g. ahyperframes-skills-common/package), or is per-skill duplication intentional for skill isolation/portability? -
audio_engine_meta.jsonas a stable sidecar. TheneutralPath()function writes the engine's output next to the skill'saudio_meta.json. The comment says "so--onlymerges (generate then fetch-sfx) accumulate" — but I don't see merge logic in the adapter.runFetchSfxoverwritesaudio_meta.jsonentirely withtoProductLaunchMeta(neutral), which would lose the voices from the earlierrunGeneratestep unless the engine itself merges into the neutral file. Is the engine responsible for that merge? If so, a comment clarifying "the engine merges intoneutralacross--onlypasses" would help future readers. -
parseScriptregex fragility. The(Frame N)pattern (/^#{2,3}\s+.*?\(frame\s+(\d+)\)/i) requires the exact parenthesized token in the heading. A SCRIPT.md heading like## Introduction (frame 1) — hookworks, but## Frame 1: Introductionwouldn't. Presumably the upstream skill orchestrator controls the SCRIPT.md format, so this is fine — but a comment noting the contract would help. -
CodeQL findings. The 9 CodeQL alerts (TOCTOU file-system races + incomplete HTML sanitization) are pre-existing patterns that this PR inherits, not introduces. Not blocking, but worth tracking.
Verdict
Well-scoped refactor that achieves a significant reduction in per-skill complexity. The deletion of wait-bgm.mjs and heygenCredential() from the skill layer resolves the two #1632 bugs by elimination. The main concern is the three-way duplication of adapter code — not a blocker, but worth a follow-up plan.
No blocking issues. I'd recommend approval once the audio_engine_meta.json merge-accumulation question (point 2) is clarified.
— Review by Miga
Review follow-ups (#1635): - captions.mjs (x3): the HTML-comment strip used a single global replace, which CodeQL flags as incomplete multi-character sanitization (a nested/partial pair can re-form a marker the single pass misses). Strip in a fixpoint loop instead. Input is preset-library content, not user-controlled, so this is lint- cleanliness, not XSS defense. - audio.mjs (x3): document that fetch-sfx (--only sfx) MERGES into the neutral audio_engine_meta.json sidecar — the engine reads prev and recomputes only the sfx section, so voices/bgm from the generate pass are preserved (review Q). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clears the 9 js/file-system-race CodeQL alerts (captions/audio/transitions x3).
Each was an existsSync precheck followed by a later write of the same path:
- captions.mjs: caption-overrides shim -> atomic writeFileSync({ flag: 'wx' }).
- audio.mjs (sync-durations) + transitions.mjs (inject): drop the existsSync
precheck and read directly, surfacing the same friendly error from a try/catch
on readFileSync — no check->write gap.
Behavior is unchanged (same error messages); these are local single-process
deterministic scripts so the race was never a real risk, but this clears the gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jrusso1020
left a comment
There was a problem hiding this comment.
Round-2 — verified the fixes.
fix(skills): tighten caption comment-strip + document audio --only merge (cfdc21312):
- Captions sanitization —
buildFromSkinin all three workflows now strips comments in a fixpoint loop instead of a single global pass. Verified atskills/pr-to-video/scripts/captions.mjs:230(and the matching sites in the other two workflows). Matches the suggested shape in my round-1 review and is the correct defensive intent — removing one comment can re-form a marker from a nested/partial pair (<!--<!---->-->), which a single pass misses. ✓ - Clears the 3 in-scope
js/incomplete-multi-character-sanitizationalerts.
fix(skills): remove existsSync->write TOCTOU in workflow scripts (cb6de92ee):
- 9
js/file-system-racealerts cleared via the right shape:captions.mjs: caption-overrides shim → atomicwriteFileSync({ flag: 'wx' })(fail-on-exists is the actual race fix, not a recheck).audio.mjs(sync-durations) +transitions.mjs(inject): dropped theexistsSyncprecheck entirely, read directly, surface the same friendly error from atry/catchonreadFileSync. No check→write gap.
- This is the right fix for TOCTOU — eliminate the check, not paper over it. ✓
CI: CodeQL PASS now (was 12 high-sev failures last round). The load-bearing blocker from round-1 is resolved.
Other concerns from round-1 still open:
- Stack ordering with #1632 — still strict; merge ordering must be #1632 → #1635.
- Massive content I didn't read — unchanged; same scope-down posture.
Stamp posture unchanged — routes through the team.
Review by Jerrai
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-review after cfdc213 / cb6de92.
The CodeQL blockers from the earlier review are addressed: the three workflow captions.mjs files now strip comments in a fixpoint loop, the audio_engine_meta.json --only sfx merge behavior is documented at the adapter call sites, and the existsSync→write/read TOCTOU patterns were replaced with atomic create or direct read + catch. gh pr checks 1635 is now green at current head.
Remaining gate: stack order. #1635 references the foundation introduced by #1632, so #1632 must land first; do not approve/merge #1635 ahead of it.
Verdict: COMMENT (ready after #1632 lands; approval/stamp withheld until stack order is satisfied and authorized)
Reasoning: The technical blockers I checked are resolved and CI/CodeQL are green, but approving/merging before #1632 would violate the dependency order.
Per-frame roots carry data-start/data-duration and get clip-gated against the global timeline at render, so only the first frame's window overlaps global 0 — a frame's own full-bleed background can't serve as the video ground, and every frame after the first renders on the bare body color (black). Paint the ground on the always-present root composition using the project's frame.md canvas color (the same role the caption skin maps to --cap-canvas); fall back to the body letterbox color when frame.md is absent or has no resolvable ground. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The function was split out into gsapDragPositionCommit.ts in #1605, but the test kept importing it from ./gsapDragCommit, which no longer exports it — yielding 'is not a function' at runtime. Import from the correct module. Inherited main breakage (same fix as #1631/#1635); fixes the Test CI check on this branch independently of merge order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The entry SKILL.md is rewritten wholesale by the frame-presets/media foundation PR (#1632); editing it here too guaranteed a merge conflict. Restore this file to main and let the router-tag tweak live with the rewrite in #1632, so the two PRs no longer both touch it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
miga-heygen
left a comment
There was a problem hiding this comment.
Re-review (round 3) — post cfdc213 / cb6de92 / e882ce9 / 6192b5e
Four new commits since my last review. All address review feedback or fix rendering correctness. Verified each:
1. cfdc2131 — caption comment-strip + audio --only merge documentation
Captions: fixpoint loop (for (let prev = ""; prev !== out; )) is the correct shape — iterates until stable, handles the nested-marker corner case (<!--<!---->-->). Applied consistently across all three workflows. Clears the 3 in-scope CodeQL js/incomplete-multi-character-sanitization alerts.
Audio --only merge: my round-1 question about audio_engine_meta.json accumulation across --only passes is now answered directly in code comments: "the engine reads the existing neutral sidecar and recomputes only the sfx section, so voices/bgm from the earlier generate pass are preserved." The merge responsibility lives in the engine (in #1632), not in the adapter — the adapter correctly reads back the merged result via toProductLaunchMeta(JSON.parse(...)). This is the right split: adapter writes request, engine owns merge semantics, adapter reads back the unified output. Question resolved.
2. cb6de92e — existsSync→write TOCTOU elimination
Three patterns fixed across 9 files:
- captions.mjs:
existsSync+writeFileSync→ atomicwriteFileSync({ flag: 'wx' })with swallowed EEXIST. Correct —wxis exclusive-create, fails atomically if the file exists. - audio.mjs (sync-durations):
existsSyncprecheck on storyboard → directreadFileSyncin try/catch. Same error message preserved. - transitions.mjs (inject): same pattern for
indexPath.
All three are the idiomatic fix — eliminate the check, let the actual operation surface the error. Behavior-preserving. Clears the 9 js/file-system-race CodeQL alerts.
3. e882ce97 — root composition ground color
Substantive rendering fix: per-frame roots are clip-gated by data-start/data-duration, so only the first frame's window overlaps global time 0. Frames after the first were rendering on bare body background (black). Fix: read frame.md canvas color via parseColors + semanticColors, paint it on #root as background. Falls back gracefully (null → no background rule, body letterbox shows through).
Note: this commit does use existsSync(framePath) — but this is NOT a TOCTOU issue. It guards an optional read path with no subsequent write to the same file, and the actual read is wrapped in try/catch anyway. The existsSync here is "skip optional enrichment if file is absent," not "check-then-write."
Good addition — this is a genuine rendering bug that would have surfaced in multi-frame videos with non-black backgrounds.
4. 6192b5e0 — drop router-tag edit
Restores skills/hyperframes/SKILL.md to avoid a guaranteed merge conflict with #1632 (which rewrites it wholesale). Correct housekeeping — the tag edit belongs in the foundation PR.
CI status
All checks green, including CodeQL (was 12 high-severity failures in round 1). Build, test, lint, format, typecheck, CLI smoke, SDK contract, studio load smoke, render on Windows, preview parity — all pass.
Open items from earlier rounds
| Concern | Status |
|---|---|
| CodeQL (3 sanitization + 9 TOCTOU) | Resolved — fixpoint loop + atomic/direct-read |
audio_engine_meta.json merge-accumulation |
Resolved — documented; engine owns merge |
| Stack ordering #1632 → #1635 | Still applies — #1632 must land first |
| Three-way adapter duplication | Unchanged — not blocking, follow-up item |
Verdict
All technical blockers from rounds 1-2 are resolved. CI is fully green. The only remaining gate is merge ordering: #1632 must land before #1635.
Ready for approval once #1632 merges.
— Review by Miga
What
Restructure the three narrated workflow skills — product-launch-video, pr-to-video, faceless-explainer — onto the shared script-driven authoring architecture. Each now authors through the common backend (build-frame remixes a
hyperframes-creativepreset onto brand tokens, audio routes through thehyperframes-mediaengine,assemble-indexbuilds the standaloneindex.html) instead of its own bespoke scripts.4 commits, 629 files (
+8322 / −87291— the large deletion count is the removal of each skill's old per-skill scripts in favour of the shared lib):refactor(product-launch-video)refactor(pr-to-video)refactor(faceless-explainer)chore(skills)test-skills-fresh.shworkflow roster (6 → 10)Why
The three workflows had each grown their own copies of capture/validate/prep/assembly logic. Moving them onto one script-driven backend means the authoring flow (storyboard → frames → audio → assemble) is shared, and each workflow keeps only what's genuinely specific to it (e.g.
pr-to-video'singest.mjsthat foldsghPR artifacts into the synthetic capture package the shared backend reads).How
ingest.mjsfolds the twoghartifacts into the synthetic capture package; adds the mechanism beat (show behavior, not just the diff).Reconciled with upstream
name:in bothSKILL.mds to match their directories:pr-to-video-refactor → pr-to-video,faceless-explainer-refactor → faceless-explainer(and dropped a stalefaceless-explainer-refactormention in apr-to-video/ingest.mjscomment).assemble-index.mjscarries upstream fix(skills): reject empty or partial scene files at assembly time #1629's blank/partial scene-file guard, ported onto the restructured reader: before emittingdata-composition-src, reject a scene file that exists but is empty / markup-less (!html.trim() || !/<\w/.test(html)) and tell the caller to re-dispatch that worker — so an interrupted worker fails loudly at assembly instead of cryptically at render.Test plan
Skill content is
.md/.html/.mjs/.json.largefiles/oxfmt --check/commitlintclean.*-refactorresidue anywhere in the three skills.scripts/test-skills-fresh.shis the full install-and-verify harness; it asserts the complete workflow roster, so it lives here (the last PR of the series).Branches off current
main. Merge after #1632.