@officexapp/vidfarm-devcli 0.21.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (516) hide show
  1. package/.agents/skills/dollarplatoon-skill/SKILL.md +1227 -0
  2. package/.agents/skills/editor-capabilities/SKILL.md +182 -0
  3. package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
  4. package/.agents/skills/embedded-captions/CATALOG.md +93 -0
  5. package/.agents/skills/embedded-captions/SKILL.md +286 -0
  6. package/.agents/skills/embedded-captions/assets/brand/CDPR-fankit-terms.txt +35 -0
  7. package/.agents/skills/embedded-captions/assets/brand/cyberpunk-widths.json +115 -0
  8. package/.agents/skills/embedded-captions/assets/fonts/char-widths.json +2211 -0
  9. package/.agents/skills/embedded-captions/assets/strokefonts/HersheyScript1.svg +242 -0
  10. package/.agents/skills/embedded-captions/assets/strokefonts/HersheyScriptMed.svg +242 -0
  11. package/.agents/skills/embedded-captions/dna/README.md +148 -0
  12. package/.agents/skills/embedded-captions/dna/chrome.json +68 -0
  13. package/.agents/skills/embedded-captions/dna/cream.json +69 -0
  14. package/.agents/skills/embedded-captions/dna/documentary.json +62 -0
  15. package/.agents/skills/embedded-captions/dna/editorial.json +70 -0
  16. package/.agents/skills/embedded-captions/dna/glitch.json +82 -0
  17. package/.agents/skills/embedded-captions/dna/ink.json +65 -0
  18. package/.agents/skills/embedded-captions/dna/keynote.json +62 -0
  19. package/.agents/skills/embedded-captions/dna/loud.json +75 -0
  20. package/.agents/skills/embedded-captions/dna/neon.json +58 -0
  21. package/.agents/skills/embedded-captions/dna/velocity.json +89 -0
  22. package/.agents/skills/embedded-captions/modes/cinematic/README.md +48 -0
  23. package/.agents/skills/embedded-captions/modes/cinematic/_archive/champion/spec.md +98 -0
  24. package/.agents/skills/embedded-captions/modes/cinematic/_archive/champion/template.html +188 -0
  25. package/.agents/skills/embedded-captions/modes/cinematic/_archive/memory-wall/spec.md +134 -0
  26. package/.agents/skills/embedded-captions/modes/cinematic/_archive/memory-wall/template.html +179 -0
  27. package/.agents/skills/embedded-captions/modes/cinematic/_archive/portrait-header/spec.md +81 -0
  28. package/.agents/skills/embedded-captions/modes/cinematic/_archive/portrait-header/template.html +170 -0
  29. package/.agents/skills/embedded-captions/modes/cinematic/cinematic-cream/spec.md +16 -0
  30. package/.agents/skills/embedded-captions/modes/cinematic/cinematic-cream/template.html +226 -0
  31. package/.agents/skills/embedded-captions/modes/cinematic/engine.html +355 -0
  32. package/.agents/skills/embedded-captions/modes/standard/_anatomy.md +233 -0
  33. package/.agents/skills/embedded-captions/modes/standard/_motion.md +192 -0
  34. package/.agents/skills/embedded-captions/modes/standard/fonts/build-fonts-css.cjs +97 -0
  35. package/.agents/skills/embedded-captions/references/aesthetic-principles.md +168 -0
  36. package/.agents/skills/embedded-captions/references/anti-patterns.md +188 -0
  37. package/.agents/skills/embedded-captions/references/bespoke-vs-presets.md +178 -0
  38. package/.agents/skills/embedded-captions/references/caption-grouping.md +130 -0
  39. package/.agents/skills/embedded-captions/references/composition-craft.md +522 -0
  40. package/.agents/skills/embedded-captions/references/direction-catalog.md +138 -0
  41. package/.agents/skills/embedded-captions/references/example-renders/champion.html +371 -0
  42. package/.agents/skills/embedded-captions/references/example-renders/memory-wall.html +334 -0
  43. package/.agents/skills/embedded-captions/references/failure-modes.md +145 -0
  44. package/.agents/skills/embedded-captions/references/layout-heuristics.md +238 -0
  45. package/.agents/skills/embedded-captions/references/motion-vocabulary.md +148 -0
  46. package/.agents/skills/embedded-captions/references/rail.md +68 -0
  47. package/.agents/skills/embedded-captions/references/reference-bar.md +47 -0
  48. package/.agents/skills/embedded-captions/references/scene-types.md +94 -0
  49. package/.agents/skills/embedded-captions/references/test-set.md +66 -0
  50. package/.agents/skills/embedded-captions/references/typographic-moves.md +266 -0
  51. package/.agents/skills/embedded-captions/references/typography-presets.md +63 -0
  52. package/.agents/skills/embedded-captions/scripts/audio-envelope.cjs +95 -0
  53. package/.agents/skills/embedded-captions/scripts/check-occlusion.cjs +250 -0
  54. package/.agents/skills/embedded-captions/scripts/check-overflow.cjs +194 -0
  55. package/.agents/skills/embedded-captions/scripts/check-rail-climax.cjs +233 -0
  56. package/.agents/skills/embedded-captions/scripts/check-timing.cjs +173 -0
  57. package/.agents/skills/embedded-captions/scripts/fill-timings.cjs +124 -0
  58. package/.agents/skills/embedded-captions/scripts/fit-fonts.cjs +158 -0
  59. package/.agents/skills/embedded-captions/scripts/gen-stroke-path.py +46 -0
  60. package/.agents/skills/embedded-captions/scripts/inject-fonts.cjs +169 -0
  61. package/.agents/skills/embedded-captions/scripts/lib-dna.cjs +201 -0
  62. package/.agents/skills/embedded-captions/scripts/make-cinematic.cjs +1205 -0
  63. package/.agents/skills/embedded-captions/scripts/make-composition.cjs +404 -0
  64. package/.agents/skills/embedded-captions/scripts/make-theme.cjs +8781 -0
  65. package/.agents/skills/embedded-captions/scripts/matte.cjs +294 -0
  66. package/.agents/skills/embedded-captions/scripts/measure-layout.cjs +293 -0
  67. package/.agents/skills/embedded-captions/scripts/prepare.sh +39 -0
  68. package/.agents/skills/embedded-captions/scripts/preview-frames.cjs +268 -0
  69. package/.agents/skills/embedded-captions/scripts/render-and-composite.sh +509 -0
  70. package/.agents/skills/embedded-captions/scripts/render-theme.sh +34 -0
  71. package/.agents/skills/embedded-captions/scripts/safe-zones.cjs +813 -0
  72. package/.agents/skills/embedded-captions/scripts/transcribe.cjs +349 -0
  73. package/.agents/skills/embedded-captions/themes/PORTING.md +49 -0
  74. package/.agents/skills/embedded-captions/themes/README.md +244 -0
  75. package/.agents/skills/embedded-captions/themes/anchor.json +27 -0
  76. package/.agents/skills/embedded-captions/themes/arcade.json +51 -0
  77. package/.agents/skills/embedded-captions/themes/aurora.json +41 -0
  78. package/.agents/skills/embedded-captions/themes/biolume.json +35 -0
  79. package/.agents/skills/embedded-captions/themes/brush.json +74 -0
  80. package/.agents/skills/embedded-captions/themes/chalkboard.json +52 -0
  81. package/.agents/skills/embedded-captions/themes/dossier.json +56 -0
  82. package/.agents/skills/embedded-captions/themes/graffiti.json +62 -0
  83. package/.agents/skills/embedded-captions/themes/hologram.json +41 -0
  84. package/.agents/skills/embedded-captions/themes/inkwater.json +43 -0
  85. package/.agents/skills/embedded-captions/themes/laser.json +46 -0
  86. package/.agents/skills/embedded-captions/themes/lastpage.json +33 -0
  87. package/.agents/skills/embedded-captions/themes/neonsign.json +53 -0
  88. package/.agents/skills/embedded-captions/themes/nightcity.json +56 -0
  89. package/.agents/skills/embedded-captions/themes/ordnance.json +54 -0
  90. package/.agents/skills/embedded-captions/themes/papercut.json +37 -0
  91. package/.agents/skills/embedded-captions/themes/popup.json +46 -0
  92. package/.agents/skills/embedded-captions/themes/ransom.json +62 -0
  93. package/.agents/skills/embedded-captions/themes/scoreboard.json +54 -0
  94. package/.agents/skills/embedded-captions/themes/spectrum.json +46 -0
  95. package/.agents/skills/embedded-captions/themes/stardust.json +29 -0
  96. package/.agents/skills/embedded-captions/themes/stomp.json +43 -0
  97. package/.agents/skills/embedded-captions/themes/terminal.json +49 -0
  98. package/.agents/skills/embedded-captions/themes/thunder.json +44 -0
  99. package/.agents/skills/embedded-captions/themes/transit.json +44 -0
  100. package/.agents/skills/embedded-captions/themes/vhs.json +47 -0
  101. package/.agents/skills/faceless-explainer/SKILL.md +209 -0
  102. package/.agents/skills/faceless-explainer/references/cut-catalog.md +215 -0
  103. package/.agents/skills/faceless-explainer/references/motion-language.md +156 -0
  104. package/.agents/skills/faceless-explainer/references/story-design.md +248 -0
  105. package/.agents/skills/faceless-explainer/references/visual-design.md +146 -0
  106. package/.agents/skills/faceless-explainer/scripts/assemble-index.mjs +570 -0
  107. package/.agents/skills/faceless-explainer/scripts/audio.mjs +260 -0
  108. package/.agents/skills/faceless-explainer/scripts/build-frame.mjs +536 -0
  109. package/.agents/skills/faceless-explainer/scripts/captions.mjs +508 -0
  110. package/.agents/skills/faceless-explainer/scripts/lib/assets.mjs +55 -0
  111. package/.agents/skills/faceless-explainer/scripts/lib/dimensions.mjs +45 -0
  112. package/.agents/skills/faceless-explainer/scripts/lib/storyboard.mjs +249 -0
  113. package/.agents/skills/faceless-explainer/scripts/lib/tokens.mjs +204 -0
  114. package/.agents/skills/faceless-explainer/scripts/lib/transition-registry.mjs +38 -0
  115. package/.agents/skills/faceless-explainer/scripts/lib/transitions.json +71 -0
  116. package/.agents/skills/faceless-explainer/scripts/transitions.mjs +320 -0
  117. package/.agents/skills/faceless-explainer/sub-agents/frame-worker.md +75 -0
  118. package/.agents/skills/farmville-saas-ux/SKILL.md +156 -0
  119. package/.agents/skills/farmville-saas-ux/assets/starter.html +294 -0
  120. package/.agents/skills/farmville-saas-ux/references/components.md +340 -0
  121. package/.agents/skills/farmville-saas-ux/references/porting-guide.md +121 -0
  122. package/.agents/skills/farmville-saas-ux/references/tokens.md +271 -0
  123. package/.agents/skills/flockposter/LICENSE +17 -0
  124. package/.agents/skills/flockposter/README.md +72 -0
  125. package/.agents/skills/flockposter/SKILL.md +562 -0
  126. package/.agents/skills/flockposter/examples/cli-instagram-reel.sh +21 -0
  127. package/.agents/skills/flockposter/examples/cli-multi-platform.json +69 -0
  128. package/.agents/skills/flockposter/examples/cli-tiktok-upload-only.sh +22 -0
  129. package/.agents/skills/flockposter/examples/cli-x-thread.sh +17 -0
  130. package/.agents/skills/flockposter/examples/cli-youtube-short.sh +21 -0
  131. package/.agents/skills/flockposter/examples/public-api-instagram-reel.json +28 -0
  132. package/.agents/skills/general-video/SKILL.md +143 -0
  133. package/.agents/skills/hyperframes/SKILL.md +160 -0
  134. package/.agents/skills/hyperframes-animation/SKILL.md +84 -0
  135. package/.agents/skills/hyperframes-animation/adapters/animate-text.md +64 -0
  136. package/.agents/skills/hyperframes-animation/adapters/animejs.md +114 -0
  137. package/.agents/skills/hyperframes-animation/adapters/css-animations.md +143 -0
  138. package/.agents/skills/hyperframes-animation/adapters/gsap-easing-and-stagger.md +118 -0
  139. package/.agents/skills/hyperframes-animation/adapters/gsap-timeline-and-labels.md +96 -0
  140. package/.agents/skills/hyperframes-animation/adapters/gsap-transforms-and-perf.md +96 -0
  141. package/.agents/skills/hyperframes-animation/adapters/gsap.md +105 -0
  142. package/.agents/skills/hyperframes-animation/adapters/html-in-canvas-patterns.md +507 -0
  143. package/.agents/skills/hyperframes-animation/adapters/lottie.md +117 -0
  144. package/.agents/skills/hyperframes-animation/adapters/three.md +131 -0
  145. package/.agents/skills/hyperframes-animation/adapters/typegpu.md +178 -0
  146. package/.agents/skills/hyperframes-animation/adapters/waapi.md +101 -0
  147. package/.agents/skills/hyperframes-animation/blueprints/comparison-split.md +27 -0
  148. package/.agents/skills/hyperframes-animation/blueprints/constellation-hub.md +44 -0
  149. package/.agents/skills/hyperframes-animation/blueprints/cta-morph-press.md +28 -0
  150. package/.agents/skills/hyperframes-animation/blueprints/cursor-ui-demo.md +57 -0
  151. package/.agents/skills/hyperframes-animation/blueprints/dataviz-countup.md +46 -0
  152. package/.agents/skills/hyperframes-animation/blueprints/device-surface-showcase.md +53 -0
  153. package/.agents/skills/hyperframes-animation/blueprints/grid-card-assemble.md +66 -0
  154. package/.agents/skills/hyperframes-animation/blueprints/kinetic-type-beats.md +78 -0
  155. package/.agents/skills/hyperframes-animation/blueprints/logo-assemble-lockup.md +69 -0
  156. package/.agents/skills/hyperframes-animation/blueprints/overwhelm-surround.md +28 -0
  157. package/.agents/skills/hyperframes-animation/blueprints/spatial-pan-stations.md +37 -0
  158. package/.agents/skills/hyperframes-animation/blueprints/ticker-takeover.md +29 -0
  159. package/.agents/skills/hyperframes-animation/blueprints/titlecard-reveal.md +42 -0
  160. package/.agents/skills/hyperframes-animation/blueprints/typewriter-reveal.md +51 -0
  161. package/.agents/skills/hyperframes-animation/blueprints/video-text-pivot.md +30 -0
  162. package/.agents/skills/hyperframes-animation/blueprints-index.md +146 -0
  163. package/.agents/skills/hyperframes-animation/examples/assets/avatars/02.avif +0 -0
  164. package/.agents/skills/hyperframes-animation/examples/assets/brands/github.avif +0 -0
  165. package/.agents/skills/hyperframes-animation/examples/assets/brands/nvidia.avif +0 -0
  166. package/.agents/skills/hyperframes-animation/examples/assets/brands/visa.avif +0 -0
  167. package/.agents/skills/hyperframes-animation/examples/assets/brands/zoominfo.avif +0 -0
  168. package/.agents/skills/hyperframes-animation/examples/brand-reveal-assemble-zoom.html +382 -0
  169. package/.agents/skills/hyperframes-animation/examples/comparison-split-cards.html +649 -0
  170. package/.agents/skills/hyperframes-animation/examples/concept-demo-decode-pan.html +520 -0
  171. package/.agents/skills/hyperframes-animation/examples/cta-morph-press.html +468 -0
  172. package/.agents/skills/hyperframes-animation/examples/cta-orbit-collapse.html +1298 -0
  173. package/.agents/skills/hyperframes-animation/examples/demo-page-scroll-spotlight.html +759 -0
  174. package/.agents/skills/hyperframes-animation/examples/hook-counter-burst.html +729 -0
  175. package/.agents/skills/hyperframes-animation/examples/messaging-multi-phrase.html +352 -0
  176. package/.agents/skills/hyperframes-animation/examples/metric-video-text-pivot.html +779 -0
  177. package/.agents/skills/hyperframes-animation/examples/problem-mockup-overwhelm.html +1374 -0
  178. package/.agents/skills/hyperframes-animation/examples/proof-logo-chain.html +861 -0
  179. package/.agents/skills/hyperframes-animation/examples/takeover-ticker-displace.html +347 -0
  180. package/.agents/skills/hyperframes-animation/examples/workflow-approve-press.html +606 -0
  181. package/.agents/skills/hyperframes-animation/rules/3d-page-scroll.md +227 -0
  182. package/.agents/skills/hyperframes-animation/rules/3d-text-depth-layers.md +297 -0
  183. package/.agents/skills/hyperframes-animation/rules/ai-tracking-box.md +382 -0
  184. package/.agents/skills/hyperframes-animation/rules/ambient-glow-bloom.md +305 -0
  185. package/.agents/skills/hyperframes-animation/rules/asr-keyword-glow.md +286 -0
  186. package/.agents/skills/hyperframes-animation/rules/avatar-cloud-network.md +371 -0
  187. package/.agents/skills/hyperframes-animation/rules/camera-cursor-tracking.md +246 -0
  188. package/.agents/skills/hyperframes-animation/rules/card-morph-anchor.md +267 -0
  189. package/.agents/skills/hyperframes-animation/rules/center-outward-expansion.md +227 -0
  190. package/.agents/skills/hyperframes-animation/rules/context-sensitive-cursor.md +257 -0
  191. package/.agents/skills/hyperframes-animation/rules/coordinate-target-zoom.md +332 -0
  192. package/.agents/skills/hyperframes-animation/rules/counting-dynamic-scale.md +283 -0
  193. package/.agents/skills/hyperframes-animation/rules/css-marker-patterns.md +373 -0
  194. package/.agents/skills/hyperframes-animation/rules/cursor-click-ripple.md +262 -0
  195. package/.agents/skills/hyperframes-animation/rules/depth-of-field-blur.md +313 -0
  196. package/.agents/skills/hyperframes-animation/rules/depth-scatter-assemble.md +303 -0
  197. package/.agents/skills/hyperframes-animation/rules/discrete-text-sequence.md +273 -0
  198. package/.agents/skills/hyperframes-animation/rules/dynamic-content-sequencing.md +307 -0
  199. package/.agents/skills/hyperframes-animation/rules/gsap-effects.md +299 -0
  200. package/.agents/skills/hyperframes-animation/rules/hacker-flip-3d.md +223 -0
  201. package/.agents/skills/hyperframes-animation/rules/kinetic-beat-slam.md +183 -0
  202. package/.agents/skills/hyperframes-animation/rules/motion-blur-streak.md +328 -0
  203. package/.agents/skills/hyperframes-animation/rules/multi-phase-camera.md +273 -0
  204. package/.agents/skills/hyperframes-animation/rules/orbit-3d-entry.md +301 -0
  205. package/.agents/skills/hyperframes-animation/rules/physics-press-reaction.md +350 -0
  206. package/.agents/skills/hyperframes-animation/rules/press-release-spring.md +296 -0
  207. package/.agents/skills/hyperframes-animation/rules/reactive-displacement.md +277 -0
  208. package/.agents/skills/hyperframes-animation/rules/scale-swap-transition.md +298 -0
  209. package/.agents/skills/hyperframes-animation/rules/sine-wave-loop.md +278 -0
  210. package/.agents/skills/hyperframes-animation/rules/split-tilt-cards.md +277 -0
  211. package/.agents/skills/hyperframes-animation/rules/spring-pop-entrance.md +273 -0
  212. package/.agents/skills/hyperframes-animation/rules/stat-bars-and-fills.md +156 -0
  213. package/.agents/skills/hyperframes-animation/rules/svg-icon-enrichment.md +329 -0
  214. package/.agents/skills/hyperframes-animation/rules/svg-path-draw.md +274 -0
  215. package/.agents/skills/hyperframes-animation/rules/vertical-spring-ticker.md +239 -0
  216. package/.agents/skills/hyperframes-animation/rules/viewport-change.md +349 -0
  217. package/.agents/skills/hyperframes-animation/rules-index.md +86 -0
  218. package/.agents/skills/hyperframes-animation/scripts/animation-map.mjs +606 -0
  219. package/.agents/skills/hyperframes-animation/scripts/package-loader.mjs +288 -0
  220. package/.agents/skills/hyperframes-animation/scripts/package-loader.test.mjs +62 -0
  221. package/.agents/skills/hyperframes-animation/techniques.md +507 -0
  222. package/.agents/skills/hyperframes-animation/transitions/TRANSITION-REGISTRY.md +167 -0
  223. package/.agents/skills/hyperframes-animation/transitions/catalog.md +127 -0
  224. package/.agents/skills/hyperframes-animation/transitions/css-3d.md +12 -0
  225. package/.agents/skills/hyperframes-animation/transitions/css-blur.md +51 -0
  226. package/.agents/skills/hyperframes-animation/transitions/css-cover.md +43 -0
  227. package/.agents/skills/hyperframes-animation/transitions/css-destruction.md +95 -0
  228. package/.agents/skills/hyperframes-animation/transitions/css-dissolve.md +66 -0
  229. package/.agents/skills/hyperframes-animation/transitions/css-distortion.md +45 -0
  230. package/.agents/skills/hyperframes-animation/transitions/css-grid.md +10 -0
  231. package/.agents/skills/hyperframes-animation/transitions/css-light.md +49 -0
  232. package/.agents/skills/hyperframes-animation/transitions/css-mechanical.md +30 -0
  233. package/.agents/skills/hyperframes-animation/transitions/css-other.md +25 -0
  234. package/.agents/skills/hyperframes-animation/transitions/css-push.md +41 -0
  235. package/.agents/skills/hyperframes-animation/transitions/css-radial.md +37 -0
  236. package/.agents/skills/hyperframes-animation/transitions/css-scale.md +24 -0
  237. package/.agents/skills/hyperframes-animation/transitions/overview.md +153 -0
  238. package/.agents/skills/hyperframes-cli/SKILL.md +113 -0
  239. package/.agents/skills/hyperframes-cli/references/doctor-browser.md +45 -0
  240. package/.agents/skills/hyperframes-cli/references/init-and-scaffold.md +55 -0
  241. package/.agents/skills/hyperframes-cli/references/lambda.md +132 -0
  242. package/.agents/skills/hyperframes-cli/references/lint-validate-inspect.md +121 -0
  243. package/.agents/skills/hyperframes-cli/references/preview-render.md +149 -0
  244. package/.agents/skills/hyperframes-cli/references/upgrade-info-misc.md +75 -0
  245. package/.agents/skills/hyperframes-core/SKILL.md +78 -0
  246. package/.agents/skills/hyperframes-core/references/composition-patterns.md +263 -0
  247. package/.agents/skills/hyperframes-core/references/data-attributes.md +70 -0
  248. package/.agents/skills/hyperframes-core/references/determinism-rules.md +68 -0
  249. package/.agents/skills/hyperframes-core/references/full-screen-motion.md +62 -0
  250. package/.agents/skills/hyperframes-core/references/minimal-composition.md +66 -0
  251. package/.agents/skills/hyperframes-core/references/script-format.md +49 -0
  252. package/.agents/skills/hyperframes-core/references/storyboard-format.md +95 -0
  253. package/.agents/skills/hyperframes-core/references/sub-compositions.md +237 -0
  254. package/.agents/skills/hyperframes-core/references/subagent-dispatch.md +41 -0
  255. package/.agents/skills/hyperframes-core/references/tailwind.md +125 -0
  256. package/.agents/skills/hyperframes-core/references/tracks-and-clips.md +76 -0
  257. package/.agents/skills/hyperframes-core/references/variables-and-media.md +90 -0
  258. package/.agents/skills/hyperframes-creative/SKILL.md +70 -0
  259. package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/FRAME.md +289 -0
  260. package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/caption-skin.html +221 -0
  261. package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/frame-showcase.html +1214 -0
  262. package/.agents/skills/hyperframes-creative/frame-presets/blockframe/FRAME.md +275 -0
  263. package/.agents/skills/hyperframes-creative/frame-presets/blockframe/caption-skin.html +224 -0
  264. package/.agents/skills/hyperframes-creative/frame-presets/blockframe/frame-showcase.html +1347 -0
  265. package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/FRAME.md +307 -0
  266. package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/caption-skin.html +223 -0
  267. package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/frame-showcase.html +1372 -0
  268. package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/FRAME.md +275 -0
  269. package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/caption-skin.html +230 -0
  270. package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/frame-showcase.html +1140 -0
  271. package/.agents/skills/hyperframes-creative/frame-presets/broadside/FRAME.md +284 -0
  272. package/.agents/skills/hyperframes-creative/frame-presets/broadside/caption-skin.html +225 -0
  273. package/.agents/skills/hyperframes-creative/frame-presets/broadside/frame-showcase.html +1166 -0
  274. package/.agents/skills/hyperframes-creative/frame-presets/capsule/FRAME.md +284 -0
  275. package/.agents/skills/hyperframes-creative/frame-presets/capsule/caption-skin.html +224 -0
  276. package/.agents/skills/hyperframes-creative/frame-presets/capsule/frame-showcase.html +1465 -0
  277. package/.agents/skills/hyperframes-creative/frame-presets/cartesian/FRAME.md +278 -0
  278. package/.agents/skills/hyperframes-creative/frame-presets/cartesian/caption-skin.html +222 -0
  279. package/.agents/skills/hyperframes-creative/frame-presets/cartesian/frame-showcase.html +1263 -0
  280. package/.agents/skills/hyperframes-creative/frame-presets/claude/FRAME.md +277 -0
  281. package/.agents/skills/hyperframes-creative/frame-presets/claude/caption-skin.html +224 -0
  282. package/.agents/skills/hyperframes-creative/frame-presets/claude/frame-showcase.html +1439 -0
  283. package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/FRAME.md +265 -0
  284. package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/caption-skin.html +238 -0
  285. package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/frame-showcase.html +1468 -0
  286. package/.agents/skills/hyperframes-creative/frame-presets/coral/FRAME.md +314 -0
  287. package/.agents/skills/hyperframes-creative/frame-presets/coral/caption-skin.html +226 -0
  288. package/.agents/skills/hyperframes-creative/frame-presets/coral/frame-showcase.html +1268 -0
  289. package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/FRAME.md +333 -0
  290. package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/caption-skin.html +227 -0
  291. package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/frame-showcase.html +1330 -0
  292. package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/FRAME.md +274 -0
  293. package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/caption-skin.html +230 -0
  294. package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/frame-showcase.html +1444 -0
  295. package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/FRAME.md +265 -0
  296. package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/caption-skin.html +223 -0
  297. package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/frame-showcase.html +1273 -0
  298. package/.agents/skills/hyperframes-creative/palettes/bold-energetic.md +14 -0
  299. package/.agents/skills/hyperframes-creative/palettes/clean-corporate.md +14 -0
  300. package/.agents/skills/hyperframes-creative/palettes/dark-premium.md +14 -0
  301. package/.agents/skills/hyperframes-creative/palettes/jewel-rich.md +14 -0
  302. package/.agents/skills/hyperframes-creative/palettes/monochrome.md +14 -0
  303. package/.agents/skills/hyperframes-creative/palettes/nature-earth.md +14 -0
  304. package/.agents/skills/hyperframes-creative/palettes/neon-electric.md +14 -0
  305. package/.agents/skills/hyperframes-creative/palettes/pastel-soft.md +14 -0
  306. package/.agents/skills/hyperframes-creative/palettes/warm-editorial.md +14 -0
  307. package/.agents/skills/hyperframes-creative/references/audio-reactive.md +76 -0
  308. package/.agents/skills/hyperframes-creative/references/beat-direction.md +164 -0
  309. package/.agents/skills/hyperframes-creative/references/composition-patterns.md +199 -0
  310. package/.agents/skills/hyperframes-creative/references/data-in-motion.md +19 -0
  311. package/.agents/skills/hyperframes-creative/references/design-adherence.md +19 -0
  312. package/.agents/skills/hyperframes-creative/references/design-picker.md +123 -0
  313. package/.agents/skills/hyperframes-creative/references/design-spec.md +55 -0
  314. package/.agents/skills/hyperframes-creative/references/house-style.md +73 -0
  315. package/.agents/skills/hyperframes-creative/references/motion-principles.md +150 -0
  316. package/.agents/skills/hyperframes-creative/references/narration.md +92 -0
  317. package/.agents/skills/hyperframes-creative/references/prompt-expansion.md +68 -0
  318. package/.agents/skills/hyperframes-creative/references/typography.md +218 -0
  319. package/.agents/skills/hyperframes-creative/references/video-composition.md +63 -0
  320. package/.agents/skills/hyperframes-creative/references/visual-styles.md +457 -0
  321. package/.agents/skills/hyperframes-creative/scripts/contrast-report.mjs +353 -0
  322. package/.agents/skills/hyperframes-creative/scripts/extract-audio-data.py +188 -0
  323. package/.agents/skills/hyperframes-creative/scripts/package-loader.mjs +288 -0
  324. package/.agents/skills/hyperframes-creative/scripts/package-loader.test.mjs +62 -0
  325. package/.agents/skills/hyperframes-creative/templates/design-picker.html +1432 -0
  326. package/.agents/skills/hyperframes-keyframes/SKILL.md +237 -0
  327. package/.agents/skills/hyperframes-keyframes/agents/openai.yaml +4 -0
  328. package/.agents/skills/hyperframes-keyframes/references/keyframe-patterns.md +106 -0
  329. package/.agents/skills/motion-graphics/SKILL.md +172 -0
  330. package/.agents/skills/motion-graphics/agents/builder.md +40 -0
  331. package/.agents/skills/motion-graphics/agents/director.md +53 -0
  332. package/.agents/skills/motion-graphics/agents/finalize.md +17 -0
  333. package/.agents/skills/motion-graphics/catalog-map.md +40 -0
  334. package/.agents/skills/motion-graphics/categories/asset-fusion/module.md +37 -0
  335. package/.agents/skills/motion-graphics/categories/charts/module.md +21 -0
  336. package/.agents/skills/motion-graphics/categories/kinetic-type/module.md +19 -0
  337. package/.agents/skills/motion-graphics/categories/logo-reveal/module.md +17 -0
  338. package/.agents/skills/motion-graphics/categories/lower-thirds/module.md +16 -0
  339. package/.agents/skills/motion-graphics/categories/maps/bake-basemap.mjs +273 -0
  340. package/.agents/skills/motion-graphics/categories/maps/module.md +61 -0
  341. package/.agents/skills/motion-graphics/categories/news/module.md +56 -0
  342. package/.agents/skills/motion-graphics/categories/stat/module.md +21 -0
  343. package/.agents/skills/motion-graphics/categories/tweet/module.md +16 -0
  344. package/.agents/skills/motion-graphics/categories/webpage/module.md +21 -0
  345. package/.agents/skills/motion-graphics/grounding/PROTOCOL.md +64 -0
  346. package/.agents/skills/motion-graphics/grounding/locate.mjs +227 -0
  347. package/.agents/skills/motion-graphics/phases/source/guide.md +21 -0
  348. package/.agents/skills/motion-graphics/references/builder-contract.md +38 -0
  349. package/.agents/skills/motion-graphics/references/motion-vocabulary.md +43 -0
  350. package/.agents/skills/motion-graphics/references/shot-plan-ir.md +54 -0
  351. package/.agents/skills/motion-graphics/samples/asset-fusion/_ref-circle-highlight.html +306 -0
  352. package/.agents/skills/music/SKILL.md +416 -0
  353. package/.agents/skills/music/references/api_reference.md +519 -0
  354. package/.agents/skills/music/references/installation.md +65 -0
  355. package/.agents/skills/product-launch-video/SKILL.md +208 -0
  356. package/.agents/skills/product-launch-video/references/cut-catalog.md +220 -0
  357. package/.agents/skills/product-launch-video/references/motion-language.md +156 -0
  358. package/.agents/skills/product-launch-video/references/story-design.md +373 -0
  359. package/.agents/skills/product-launch-video/references/visual-design.md +126 -0
  360. package/.agents/skills/product-launch-video/scripts/assemble-index.mjs +570 -0
  361. package/.agents/skills/product-launch-video/scripts/audio.mjs +260 -0
  362. package/.agents/skills/product-launch-video/scripts/build-frame.mjs +536 -0
  363. package/.agents/skills/product-launch-video/scripts/captions.mjs +508 -0
  364. package/.agents/skills/product-launch-video/scripts/lib/assets.mjs +55 -0
  365. package/.agents/skills/product-launch-video/scripts/lib/dimensions.mjs +45 -0
  366. package/.agents/skills/product-launch-video/scripts/lib/pad-frame-duration.mjs +36 -0
  367. package/.agents/skills/product-launch-video/scripts/lib/pad-frame-duration.test.mjs +76 -0
  368. package/.agents/skills/product-launch-video/scripts/lib/storyboard.mjs +249 -0
  369. package/.agents/skills/product-launch-video/scripts/lib/tokens.mjs +204 -0
  370. package/.agents/skills/product-launch-video/scripts/lib/transition-registry.mjs +38 -0
  371. package/.agents/skills/product-launch-video/scripts/lib/transitions.json +71 -0
  372. package/.agents/skills/product-launch-video/scripts/stage-assets.mjs +39 -0
  373. package/.agents/skills/product-launch-video/scripts/transitions.mjs +327 -0
  374. package/.agents/skills/product-launch-video/sub-agents/frame-worker.md +75 -0
  375. package/.agents/skills/slideshow/SKILL.md +550 -0
  376. package/.agents/skills/slideshow/references/standalone-harness.md +1020 -0
  377. package/.agents/skills/talking-head-recut/NOTICE.md +39 -0
  378. package/.agents/skills/talking-head-recut/SKILL.md +1195 -0
  379. package/.agents/skills/talking-head-recut/assets/vendor/gsap.min.js +10 -0
  380. package/.agents/skills/talking-head-recut/references/DESIGN_INDEX.md +145 -0
  381. package/.agents/skills/talking-head-recut/references/frames/clean.html +92 -0
  382. package/.agents/skills/talking-head-recut/references/frames/hairline.html +171 -0
  383. package/.agents/skills/talking-head-recut/references/frames/polaroid.html +131 -0
  384. package/.agents/skills/talking-head-recut/references/layouts/overlay.html +164 -0
  385. package/.agents/skills/talking-head-recut/references/layouts/pip.html +143 -0
  386. package/.agents/skills/talking-head-recut/references/layouts/split.html +121 -0
  387. package/.agents/skills/talking-head-recut/references/layouts/stack.html +109 -0
  388. package/.agents/skills/talking-head-recut/references/styles/academic.html +172 -0
  389. package/.agents/skills/talking-head-recut/references/styles/audit.html +170 -0
  390. package/.agents/skills/talking-head-recut/references/styles/editorial.html +189 -0
  391. package/.agents/skills/talking-head-recut/references/styles/geom.html +181 -0
  392. package/.agents/skills/talking-head-recut/references/styles/minimal.html +130 -0
  393. package/.agents/skills/talking-head-recut/references/styles/spotlight.html +142 -0
  394. package/.agents/skills/talking-head-recut/references/styles/swiss.html +188 -0
  395. package/.agents/skills/talking-head-recut/references/styles/terminal.html +196 -0
  396. package/.agents/skills/talking-head-recut/references/styles/whiteboard.html +170 -0
  397. package/.agents/skills/talking-head-recut/references/styles/xhs.html +206 -0
  398. package/.agents/skills/text-to-speech/SKILL.md +226 -0
  399. package/.agents/skills/text-to-speech/references/installation.md +90 -0
  400. package/.agents/skills/text-to-speech/references/streaming.md +307 -0
  401. package/.agents/skills/text-to-speech/references/voice-settings.md +115 -0
  402. package/.agents/skills/vidfarm-director/SKILL.md +179 -0
  403. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  404. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  405. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  406. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  407. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +117 -0
  408. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +276 -0
  409. package/.agents/skills/vidfarm-director/references/core-workflows.md +301 -0
  410. package/.agents/skills/vidfarm-director/references/editor-workflows.md +367 -0
  411. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  412. package/.agents/skills/vidfarm-director/references/primitives.md +307 -0
  413. package/.agents/skills/vidfarm-director/references/rest-api.md +84 -0
  414. package/.agents/skills/vidfarm-media/SKILL.md +158 -0
  415. package/.agents/skills/vidfarm-media/references/tts.md +111 -0
  416. package/.agents/skills/vidfarm-media/scripts/audio.mjs +628 -0
  417. package/.agents/skills/vidfarm-media/scripts/wait-bgm.mjs +31 -0
  418. package/.agents/skills/website-to-video/SKILL.md +145 -0
  419. package/.agents/skills/website-to-video/assets/sfx/CREDITS.md +35 -0
  420. package/.agents/skills/website-to-video/assets/sfx/manifest.json +97 -0
  421. package/.agents/skills/website-to-video/references/beat-builder-guide.md +263 -0
  422. package/.agents/skills/website-to-video/references/capabilities.md +713 -0
  423. package/.agents/skills/website-to-video/references/step-0-capture.md +55 -0
  424. package/.agents/skills/website-to-video/references/step-1-design.md +333 -0
  425. package/.agents/skills/website-to-video/references/step-2-brief.md +178 -0
  426. package/.agents/skills/website-to-video/references/step-3-storyboard.md +572 -0
  427. package/.agents/skills/website-to-video/references/step-4-vo.md +225 -0
  428. package/.agents/skills/website-to-video/references/step-5-build.md +483 -0
  429. package/.agents/skills/website-to-video/references/step-6-validate.md +341 -0
  430. package/.agents/skills/website-to-video/scripts/w2h-verify.mjs +759 -0
  431. package/README.md +97 -0
  432. package/SKILL.director.md +1644 -0
  433. package/SKILL.md +83 -0
  434. package/demo/README.md +28 -0
  435. package/demo/dist/app.css +1 -0
  436. package/demo/dist/app.js +968 -0
  437. package/demo/dist/chunks/chunk-3OT2D42H.js +1 -0
  438. package/demo/dist/chunks/chunk-DXB73IDG.js +1 -0
  439. package/demo/dist/chunks/domEditingLayers-AT7G6F4L-ZTHJHB7R.js +1 -0
  440. package/demo/dist/chunks/hyperframes-player-7HKH6NSO.js +459 -0
  441. package/demo/dist/chunks/lib-XAQ37YOE.js +1 -0
  442. package/demo/dist/chunks/src-TJ2QYA4U.js +207 -0
  443. package/demo/dist/favicon.ico +0 -0
  444. package/demo/dist/icons/timeline/audio.svg +7 -0
  445. package/demo/dist/icons/timeline/captions.svg +5 -0
  446. package/demo/dist/icons/timeline/composition.svg +12 -0
  447. package/demo/dist/icons/timeline/image.svg +18 -0
  448. package/demo/dist/icons/timeline/music.svg +10 -0
  449. package/demo/dist/icons/timeline/text.svg +3 -0
  450. package/demo/dist/index.html +16 -0
  451. package/dist/src/cli.js +7258 -0
  452. package/dist/src/devcli/auth-store.js +67 -0
  453. package/dist/src/devcli/captions.js +310 -0
  454. package/dist/src/devcli/clip-store.js +368 -0
  455. package/dist/src/devcli/clips.js +1138 -0
  456. package/dist/src/devcli/composition-edit.js +1292 -0
  457. package/dist/src/devcli/cost-mode.js +149 -0
  458. package/dist/src/devcli/doctor.js +192 -0
  459. package/dist/src/devcli/hyperframes-cli.js +118 -0
  460. package/dist/src/devcli/local-backend.js +167 -0
  461. package/dist/src/devcli/local-frontend-server.js +681 -0
  462. package/dist/src/devcli/local-render.js +136 -0
  463. package/dist/src/devcli/skills.js +476 -0
  464. package/dist/src/devcli/speech.js +178 -0
  465. package/dist/src/devcli/stills.js +249 -0
  466. package/dist/src/devcli/telemetry.js +236 -0
  467. package/dist/src/devcli/timeline-edit.js +490 -0
  468. package/dist/src/devcli/transitions.js +205 -0
  469. package/dist/src/hyperframes/composition.js +780 -0
  470. package/dist/src/lib/crypto.js +45 -0
  471. package/dist/src/lib/dev-log.js +54 -0
  472. package/dist/src/lib/display-name.js +11 -0
  473. package/dist/src/lib/ids.js +24 -0
  474. package/dist/src/lib/images.js +19 -0
  475. package/dist/src/lib/json.js +15 -0
  476. package/dist/src/lib/package-root.js +47 -0
  477. package/dist/src/lib/render-media-prep.js +293 -0
  478. package/dist/src/lib/template-paths.js +28 -0
  479. package/dist/src/lib/time.js +7 -0
  480. package/dist/src/lib/url-clean.js +85 -0
  481. package/dist/src/services/captions.js +123 -0
  482. package/dist/src/services/clip-curation/cost.js +116 -0
  483. package/dist/src/services/clip-curation/ffmpeg.js +352 -0
  484. package/dist/src/services/clip-curation/gemini.js +418 -0
  485. package/dist/src/services/clip-curation/hunt.js +470 -0
  486. package/dist/src/services/clip-curation/index.js +19 -0
  487. package/dist/src/services/clip-curation/local-agent.js +248 -0
  488. package/dist/src/services/clip-curation/media-select.js +152 -0
  489. package/dist/src/services/clip-curation/presets.js +20 -0
  490. package/dist/src/services/clip-curation/presets.v1.json +59 -0
  491. package/dist/src/services/clip-curation/query.js +167 -0
  492. package/dist/src/services/clip-curation/refine.js +165 -0
  493. package/dist/src/services/clip-curation/scan.js +189 -0
  494. package/dist/src/services/clip-curation/source-naming.js +131 -0
  495. package/dist/src/services/clip-curation/taxonomy.js +73 -0
  496. package/dist/src/services/clip-curation/taxonomy.v1.json +102 -0
  497. package/dist/src/services/clip-curation/types.js +7 -0
  498. package/dist/src/services/composition-lint.js +199 -0
  499. package/dist/src/services/provider-errors.js +128 -0
  500. package/dist/src/services/speech.js +629 -0
  501. package/package.json +154 -0
  502. package/public/assets/discover-client-app.js +1 -0
  503. package/public/assets/favicon.ico +0 -0
  504. package/public/assets/file-directory-app.js +89 -0
  505. package/public/assets/homepage-app.js +54 -0
  506. package/public/assets/homepage-client-app.js +80 -0
  507. package/public/assets/landing-page-client-app.js +87 -0
  508. package/public/assets/logo-vidfarm.png +0 -0
  509. package/public/assets/page-runtime-client-app.js +94 -0
  510. package/public/assets/placeholders/scene-placeholder.png +0 -0
  511. package/public/serve-shells/editor.html +3146 -0
  512. package/public/serve-shells/library-files.html +3450 -0
  513. package/public/serve-shells/library-raws.html +4404 -0
  514. package/public/serve-shells/tools-clipper.html +3991 -0
  515. package/public/serve-shells/tools-image.html +5584 -0
  516. package/public/serve-shells/tools-video.html +4404 -0
@@ -0,0 +1,1292 @@
1
+ // Composition editing for the devcli — parse a composition.html on disk, find
2
+ // its blank timeline gaps and scene layers, and insert / replace a media clip.
3
+ // This is the local-agent counterpart to the browser editor's applyAddLayerAction
4
+ // (demo/src/HyperframesStudioEditor.tsx): it emits clips with the SAME data-*
5
+ // attributes and inline styles so `vidfarm serve` live-morphs them and `publish`
6
+ // pushes them identically to a browser-made edit. Pure DOM (linkedom) — no
7
+ // ffmpeg, no network.
8
+ import { parseHTML } from "linkedom";
9
+ import { CAPTION_ANIMATIONS, CAPTION_STYLE_PRESETS, TRANSITION_OUT_PRESETS, TRANSITION_PRESETS, captionStylePresetById, clampTransitionDuration, estimateCaptionWords, renderHyperframeLayerHtml, transitionOutDelaySeconds } from "../hyperframes/composition.js";
10
+ const KEN_BURNS_PRESETS = new Set([
11
+ "zoom-in", "zoom-out", "pan-left", "pan-right", "pan-up", "pan-down", "zoom-in-left", "zoom-in-right"
12
+ ]);
13
+ // Preset vocabulary comes straight from the canonical source so the devcli can
14
+ // never drift from the stylesheet.
15
+ const TRANSITION_PRESET_SET = new Set(TRANSITION_PRESETS);
16
+ const TRANSITION_OUT_PRESET_SET = new Set(TRANSITION_OUT_PRESETS);
17
+ const round3 = (v) => Number(v.toFixed(3));
18
+ const clampPercent = (v) => Math.min(100, Math.max(0, v));
19
+ function serialize(document) {
20
+ return `<!doctype html>\n${document.documentElement.outerHTML}`;
21
+ }
22
+ function reduceAspectRatio(width, height) {
23
+ if (!width || !height || width <= 0 || height <= 0)
24
+ return null;
25
+ const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
26
+ const w = Math.round(width);
27
+ const h = Math.round(height);
28
+ const g = gcd(w, h) || 1;
29
+ return `${w / g}:${h / g}`;
30
+ }
31
+ function numAttr(node, name) {
32
+ const raw = Number.parseFloat(node?.getAttribute?.(name) ?? "");
33
+ return Number.isFinite(raw) ? raw : NaN;
34
+ }
35
+ function stylePercent(node, prop) {
36
+ const raw = node?.style?.[prop];
37
+ if (!raw)
38
+ return undefined;
39
+ const match = raw.match(/(-?\d+(?:\.\d+)?)\s*%/);
40
+ return match ? Number(match[1]) : undefined;
41
+ }
42
+ // A clip fills the canvas if it's a timeline proxy, an explicit full-canvas
43
+ // marker, or its geometry is ~0,0,100,100. Mirrors the editor's isFullCanvas.
44
+ function isFullCanvasNode(node) {
45
+ if (node?.getAttribute?.("data-vf-timeline-proxy") === "video")
46
+ return true;
47
+ if (node?.getAttribute?.("data-vf-full-canvas") === "true")
48
+ return true;
49
+ const x = stylePercent(node, "left");
50
+ const y = stylePercent(node, "top");
51
+ const w = stylePercent(node, "width");
52
+ const h = stylePercent(node, "height");
53
+ return x !== undefined && y !== undefined && w !== undefined && h !== undefined
54
+ && x <= 0.5 && y <= 0.5 && w >= 99.5 && h >= 99.5;
55
+ }
56
+ function readCompositionDoc(html) {
57
+ const { document } = parseHTML(html);
58
+ const root = document.querySelector("[data-composition-id]");
59
+ if (!root)
60
+ throw new Error("Composition root ([data-composition-id]) not found in composition.html.");
61
+ return { document, root };
62
+ }
63
+ function collectClips(root) {
64
+ return Array.from(root.querySelectorAll("[data-start]"));
65
+ }
66
+ function layerSummary(node) {
67
+ const start = numAttr(node, "data-start");
68
+ const duration = numAttr(node, "data-duration");
69
+ const track = numAttr(node, "data-track-index");
70
+ return {
71
+ key: node.getAttribute("data-hf-id") || node.id || "",
72
+ kind: node.getAttribute("data-layer-kind"),
73
+ label: node.getAttribute("data-label"),
74
+ slug: node.getAttribute("data-hf-slug"),
75
+ start: Number.isFinite(start) ? start : 0,
76
+ duration: Number.isFinite(duration) ? duration : 0,
77
+ track: Number.isFinite(track) ? track : 0,
78
+ is_timeline_proxy: node.getAttribute("data-vf-timeline-proxy") === "video",
79
+ is_full_canvas: isFullCanvasNode(node),
80
+ src: node.getAttribute("src") || node.getAttribute("data-src") || null
81
+ };
82
+ }
83
+ // Blank spans where NO canvas-filling layer is on screen — the "gaps" an agent
84
+ // fills. Overlays (text/caption/audio) don't count. Mirrors the editor's
85
+ // computeTimelineGaps.
86
+ export function computeCompositionGaps(html) {
87
+ const { root } = readCompositionDoc(html);
88
+ const totalDuration = numAttr(root, "data-duration");
89
+ return gapsFromClips(collectClips(root), Number.isFinite(totalDuration) ? totalDuration : 0);
90
+ }
91
+ function gapsFromClips(clips, totalDuration) {
92
+ const MIN_GAP = 0.2;
93
+ if (!Number.isFinite(totalDuration) || totalDuration <= 0)
94
+ return [];
95
+ const intervals = [];
96
+ for (const node of clips) {
97
+ const kind = (node.getAttribute("data-layer-kind") ?? "").toLowerCase();
98
+ const isProxy = node.getAttribute("data-vf-timeline-proxy") === "video";
99
+ const start = numAttr(node, "data-start");
100
+ const duration = numAttr(node, "data-duration");
101
+ if ((isProxy || ["video", "image", "html", "shape"].includes(kind)) && Number.isFinite(start) && Number.isFinite(duration) && duration > 0) {
102
+ intervals.push({ start, end: start + duration });
103
+ }
104
+ }
105
+ intervals.sort((a, b) => a.start - b.start);
106
+ const merged = [];
107
+ for (const interval of intervals) {
108
+ const last = merged[merged.length - 1];
109
+ if (last && interval.start <= last.end + 0.001)
110
+ last.end = Math.max(last.end, interval.end);
111
+ else
112
+ merged.push({ start: Math.max(0, interval.start), end: interval.end });
113
+ }
114
+ const gaps = [];
115
+ let cursor = 0;
116
+ for (const interval of merged) {
117
+ if (interval.start - cursor >= MIN_GAP) {
118
+ gaps.push({ start: round3(cursor), end: round3(interval.start), duration: round3(interval.start - cursor) });
119
+ }
120
+ cursor = Math.max(cursor, interval.end);
121
+ }
122
+ if (totalDuration - cursor >= MIN_GAP) {
123
+ gaps.push({ start: round3(cursor), end: round3(totalDuration), duration: round3(totalDuration - cursor) });
124
+ }
125
+ return gaps;
126
+ }
127
+ // Full read-out for `vidfarm pull` / agent grounding: dimensions, scenes, gaps.
128
+ export function inspectComposition(html) {
129
+ const { root } = readCompositionDoc(html);
130
+ const clips = collectClips(root);
131
+ const totalDuration = numAttr(root, "data-duration");
132
+ const width = numAttr(root, "data-width");
133
+ const height = numAttr(root, "data-height");
134
+ const w = Number.isFinite(width) ? width : null;
135
+ const h = Number.isFinite(height) ? height : null;
136
+ return {
137
+ composition_id: root.getAttribute("data-composition-id"),
138
+ duration_seconds: Number.isFinite(totalDuration) ? totalDuration : 0,
139
+ width: w,
140
+ height: h,
141
+ aspect_ratio: reduceAspectRatio(w, h),
142
+ layers: clips.map(layerSummary),
143
+ gaps: gapsFromClips(clips, Number.isFinite(totalDuration) ? totalDuration : 0)
144
+ };
145
+ }
146
+ function resolveLayerNode(root, key) {
147
+ const trimmed = key.trim();
148
+ if (!trimmed)
149
+ return null;
150
+ const escaped = trimmed.replace(/["\\]/g, "\\$&");
151
+ return root.querySelector(`[data-hf-id="${escaped}"]`)
152
+ ?? root.querySelector(`[data-hf-slug="${escaped}"]`)
153
+ ?? root.querySelector(`#${escaped.replace(/[^\w-]/g, "")}`)
154
+ ?? null;
155
+ }
156
+ function nextAutoTrack(clips) {
157
+ return clips.reduce((max, node) => {
158
+ const track = numAttr(node, "data-track-index");
159
+ return Number.isFinite(track) ? Math.max(max, track + 1) : max;
160
+ }, 0);
161
+ }
162
+ // Enforce "one clip per (track, time)": if the requested track collides with an
163
+ // existing clip's time range, walk upward to the next free lane.
164
+ function findNonCollidingTrack(clips, range, excludeId) {
165
+ const end = range.start + range.duration;
166
+ let track = Math.max(0, Math.floor(range.track));
167
+ const collides = (t) => clips.some((node) => {
168
+ if ((node.getAttribute("data-hf-id") || node.id) === excludeId)
169
+ return false;
170
+ if (numAttr(node, "data-track-index") !== t)
171
+ return false;
172
+ const s = numAttr(node, "data-start");
173
+ const d = numAttr(node, "data-duration");
174
+ if (!Number.isFinite(s) || !Number.isFinite(d))
175
+ return false;
176
+ return s < end && (s + d) > range.start;
177
+ });
178
+ while (collides(track))
179
+ track += 1;
180
+ return track;
181
+ }
182
+ function buildMediaClip(document, opts, id, track, geom) {
183
+ const tagName = opts.kind === "video" ? "video" : opts.kind === "audio" ? "audio" : "img";
184
+ const node = document.createElement(tagName);
185
+ node.id = id;
186
+ node.setAttribute("data-hf-id", id);
187
+ node.setAttribute("data-layer-mode", opts.kind === "audio" ? "both" : "publish");
188
+ node.setAttribute("data-layer-kind", opts.kind);
189
+ node.setAttribute("data-viral-note", `AI generated ${opts.kind} (devcli).`);
190
+ node.setAttribute("data-start", String(round3(geom.start)));
191
+ node.setAttribute("data-duration", String(round3(geom.duration)));
192
+ node.setAttribute("data-end", String(round3(geom.start + geom.duration)));
193
+ node.setAttribute("data-track-index", String(track));
194
+ node.setAttribute("data-label", opts.label || (opts.kind === "video" ? "AI video" : opts.kind === "audio" ? "Audio track" : "AI image"));
195
+ node.className = "clip";
196
+ if (opts.slug)
197
+ node.setAttribute("data-hf-slug", opts.slug);
198
+ // Audio is a geometry-free overlay layer: a bare <audio> mirroring the web
199
+ // editor's renderLayer output (data-timeline-role / data-volume, framework-
200
+ // managed playback, no CSS box). Multiple audio layers on distinct tracks are
201
+ // mixed together at render, each at its own volume — so this is how the
202
+ // devcli overlays narration + a low-volume music bed + SFX as separate tracks.
203
+ if (opts.kind === "audio") {
204
+ node.setAttribute("src", opts.src);
205
+ node.setAttribute("preload", "metadata");
206
+ node.setAttribute("data-timeline-role", "music");
207
+ node.setAttribute("data-volume", String(opts.volume ?? 1));
208
+ if (opts.muted === true || opts.volume === 0)
209
+ node.setAttribute("muted", "");
210
+ const ps = String(round3(opts.playbackStart ?? 0));
211
+ node.setAttribute("data-media-start", ps);
212
+ node.setAttribute("data-playback-start", ps);
213
+ return node;
214
+ }
215
+ const styles = [
216
+ "position:absolute",
217
+ `left:${clampPercent(geom.x)}%`,
218
+ `top:${clampPercent(geom.y)}%`,
219
+ `width:${clampPercent(geom.width)}%`,
220
+ `height:${clampPercent(geom.height)}%`,
221
+ `z-index:${track}`,
222
+ "overflow:hidden",
223
+ `object-fit:${opts.objectFit || "cover"}`
224
+ ];
225
+ if (opts.objectPosition?.trim())
226
+ styles.push(`object-position:${opts.objectPosition.trim()}`);
227
+ node.setAttribute("src", opts.src);
228
+ if (opts.kind === "video") {
229
+ node.setAttribute("playsinline", "");
230
+ node.setAttribute("preload", "auto");
231
+ const wantsAudio = opts.muted === false || (opts.volume !== undefined && opts.volume > 0);
232
+ if (!wantsAudio)
233
+ node.setAttribute("muted", "");
234
+ node.setAttribute("data-volume", String(opts.volume ?? 1));
235
+ if (opts.loop) {
236
+ node.setAttribute("loop", "");
237
+ node.setAttribute("data-loop", "true");
238
+ }
239
+ const ps = String(round3(opts.playbackStart ?? 0));
240
+ node.setAttribute("data-media-start", ps);
241
+ node.setAttribute("data-playback-start", ps);
242
+ // Skip the opaque backdrop for alpha-capable video (webm/mov cut-outs) —
243
+ // it would render the file's transparency as a solid black box.
244
+ if (!/\.(webm|mov)(\?|#|$)/i.test(opts.src))
245
+ styles.push("background:#050604");
246
+ }
247
+ else if (opts.kenBurns && KEN_BURNS_PRESETS.has(opts.kenBurns)) {
248
+ // Ken Burns lives on the <img> itself (see src/hyperframes/composition.ts):
249
+ // data-kenburns picks the keyframes; the inline vars supply span + strength.
250
+ node.setAttribute("data-kenburns", opts.kenBurns);
251
+ styles.push(`--vf-kb-dur:${round3(geom.duration)}s`);
252
+ if (opts.kenBurnsIntensity !== undefined && Number.isFinite(opts.kenBurnsIntensity)) {
253
+ styles.push(`--vf-kb-amount:${Math.max(0.04, Math.min(0.5, opts.kenBurnsIntensity))}`);
254
+ }
255
+ }
256
+ if (opts.transition && TRANSITION_PRESET_SET.has(opts.transition)) {
257
+ // Scene transition (see src/hyperframes/composition.ts): data-transition
258
+ // picks the keyframes; data-transition-duration + --vf-tr-dur set the span.
259
+ // The overlap with the previous clip is granted by the runtime/publish
260
+ // normalizer — the stored timeline stays butt-cut.
261
+ const transitionDuration = clampTransitionDuration(opts.transitionDuration ?? 0.5);
262
+ node.setAttribute("data-transition", opts.transition);
263
+ node.setAttribute("data-transition-duration", String(round3(transitionDuration)));
264
+ styles.push(`--vf-tr-dur:${round3(transitionDuration)}s`);
265
+ }
266
+ if (opts.transitionOut && TRANSITION_OUT_PRESET_SET.has(opts.transitionOut)) {
267
+ // Exit transition: the inline --vf-tr-out-delay (clip duration − out
268
+ // duration) parks the animation window at the clip's tail.
269
+ const outDuration = clampTransitionDuration(opts.transitionOutDuration ?? 0.5);
270
+ node.setAttribute("data-transition-out", opts.transitionOut);
271
+ node.setAttribute("data-transition-out-duration", String(round3(outDuration)));
272
+ styles.push(`--vf-tr-out-dur:${round3(outDuration)}s`, `--vf-tr-out-delay:${transitionOutDelaySeconds(geom.duration, outDuration)}s`);
273
+ }
274
+ node.setAttribute("style", `${styles.join(";")};`);
275
+ return node;
276
+ }
277
+ // Insert a NEW media clip. Returns { html, layerKey } — the layerKey resolves in
278
+ // the editor / later CLI calls. Auto-picks a collision-free track.
279
+ export function insertMediaLayer(html, opts) {
280
+ if (!opts.src?.trim())
281
+ throw new Error("insertMediaLayer requires a src media URL.");
282
+ const { document, root } = readCompositionDoc(html);
283
+ const clips = collectClips(root);
284
+ const totalDuration = numAttr(root, "data-duration");
285
+ const dur = Number.isFinite(totalDuration) ? totalDuration : 0;
286
+ const start = Math.max(0, dur > 0 ? Math.min(dur - 0.1, opts.start ?? 0) : (opts.start ?? 0));
287
+ const remaining = dur > 0 ? Math.max(0.1, dur - start) : 4;
288
+ // Audio beds (narration/music/SFX) default to spanning the rest of the
289
+ // timeline from their start; visual clips default to a 4s slot.
290
+ const defaultDuration = opts.kind === "audio" ? remaining : Math.min(4, remaining);
291
+ const duration = Math.max(0.1, dur > 0 ? Math.min(opts.duration ?? defaultDuration, remaining) : (opts.duration ?? 4));
292
+ const providedKey = opts.layerKey?.trim();
293
+ const genKey = providedKey && /^[A-Za-z][\w-]{0,63}$/.test(providedKey)
294
+ ? providedKey
295
+ : `vf-gen-${Date.now().toString(36)}`;
296
+ const id = genKey.startsWith("element_") ? genKey : `element_${genKey}`;
297
+ const initialTrack = opts.track !== undefined && Number.isFinite(opts.track) ? Math.max(0, Math.floor(opts.track)) : nextAutoTrack(clips);
298
+ const track = findNonCollidingTrack(clips, { start, duration, track: initialTrack }, id);
299
+ const node = buildMediaClip(document, opts, id, track, {
300
+ start,
301
+ duration,
302
+ x: opts.x ?? 0,
303
+ y: opts.y ?? 0,
304
+ width: opts.width ?? 100,
305
+ height: opts.height ?? 100
306
+ });
307
+ root.append(node);
308
+ return { html: serialize(document), layerKey: id };
309
+ }
310
+ const CAPTION_DEFAULT_FRAME = { x: 10, y: 70, width: 80, height: 14 };
311
+ function resolveCaptionLook(opts) {
312
+ const preset = captionStylePresetById(opts.style)
313
+ ?? (opts.animation ? null : CAPTION_STYLE_PRESETS[0]);
314
+ const animation = (opts.animation ?? preset?.animation);
315
+ if (!animation || !CAPTION_ANIMATIONS.includes(animation)) {
316
+ throw new Error(`Unknown caption animation "${opts.animation ?? opts.style}". Use a preset (${CAPTION_STYLE_PRESETS.map((p) => p.id).join(", ")}) or an animation (${CAPTION_ANIMATIONS.join(", ")}).`);
317
+ }
318
+ return {
319
+ animation,
320
+ activeColor: opts.activeColor ?? preset?.activeColor,
321
+ highlightColor: opts.highlightColor ?? preset?.highlightColor,
322
+ uppercase: opts.uppercase ?? preset?.uppercase,
323
+ color: opts.color ?? preset?.color ?? "#ffffff",
324
+ background: opts.background ?? preset?.background ?? "#000000",
325
+ backgroundStyle: (opts.backgroundStyle ?? preset?.textBackgroundStyle ?? "plain"),
326
+ // A 0 (or empty) font size/family renders the caption invisibly — the shared
327
+ // action/flag schema lets callers pass font_size:0 or font_family:"", so treat
328
+ // only a positive size / non-empty family as an intentional override.
329
+ fontFamily: opts.fontFamily?.trim() ? opts.fontFamily.trim() : "Montserrat",
330
+ fontWeight: opts.fontWeight ?? preset?.fontWeight ?? 800,
331
+ fontSize: typeof opts.fontSize === "number" && opts.fontSize > 0 ? opts.fontSize : 38,
332
+ presetId: preset?.id ?? animation
333
+ };
334
+ }
335
+ /** Read the animated-caption cues already in a composition (for restyle/list). */
336
+ export function readCaptionCues(html) {
337
+ const { root } = readCompositionDoc(html);
338
+ return Array.from(root.querySelectorAll("[data-caption-animation]")).map((node) => {
339
+ const words = Array.from(node.querySelectorAll("[data-cap-word]"))
340
+ .map((span) => ({
341
+ text: String(span.textContent || "").trim(),
342
+ start: Number.parseFloat(span.getAttribute("data-word-start") ?? "") || 0,
343
+ end: Number.parseFloat(span.getAttribute("data-word-end") ?? "") || 0
344
+ }))
345
+ .filter((word) => word.text);
346
+ return {
347
+ layerKey: node.getAttribute("data-hf-id") || node.id || "",
348
+ track: Number.isFinite(numAttr(node, "data-track-index")) ? numAttr(node, "data-track-index") : 0,
349
+ text: words.length ? words.map((word) => word.text).join(" ") : String(node.textContent || "").trim(),
350
+ start: Number.isFinite(numAttr(node, "data-start")) ? numAttr(node, "data-start") : 0,
351
+ duration: Number.isFinite(numAttr(node, "data-duration")) ? numAttr(node, "data-duration") : 0,
352
+ words: words.length ? words : undefined
353
+ };
354
+ });
355
+ }
356
+ /** Remove every animated-caption layer. Returns the count removed. */
357
+ export function clearCaptionLayers(html) {
358
+ const { document, root } = readCompositionDoc(html);
359
+ const nodes = Array.from(root.querySelectorAll("[data-caption-animation]"));
360
+ for (const node of nodes)
361
+ node.parentNode?.removeChild(node);
362
+ return { html: serialize(document), removed: nodes.length };
363
+ }
364
+ /**
365
+ * Insert an animated-caption cue run: one caption layer per cue on a shared
366
+ * collision-free track. Replaces any existing animated captions (set
367
+ * semantics — matching the editor's set_captions action).
368
+ */
369
+ export function insertCaptionLayers(html, cues, opts = {}) {
370
+ const usable = cues
371
+ .filter((cue) => cue.text?.trim() && Number.isFinite(cue.start) && Number.isFinite(cue.duration) && cue.duration > 0)
372
+ .sort((a, b) => a.start - b.start);
373
+ if (!usable.length)
374
+ throw new Error("insertCaptionLayers needs at least one cue with text, start, and duration.");
375
+ const look = resolveCaptionLook(opts);
376
+ const { document, root } = readCompositionDoc(html);
377
+ const existing = Array.from(root.querySelectorAll("[data-caption-animation]"));
378
+ for (const node of existing)
379
+ node.parentNode?.removeChild(node);
380
+ const clips = collectClips(root);
381
+ const runStart = usable[0].start;
382
+ const runEnd = usable.reduce((max, cue) => Math.max(max, cue.start + cue.duration), runStart);
383
+ const initialTrack = opts.track !== undefined && Number.isFinite(opts.track)
384
+ ? Math.max(0, Math.floor(opts.track))
385
+ : nextAutoTrack(clips);
386
+ const track = findNonCollidingTrack(clips, { start: runStart, duration: Math.max(0.1, runEnd - runStart), track: initialTrack }, "");
387
+ const stamp = Date.now().toString(36);
388
+ const layerKeys = [];
389
+ usable.forEach((cue, index) => {
390
+ const id = `element_cap_${stamp}_${index}`;
391
+ layerKeys.push(id);
392
+ const layer = {
393
+ id,
394
+ kind: "caption",
395
+ label: cue.text.slice(0, 48),
396
+ start: cue.start,
397
+ duration: cue.duration,
398
+ track,
399
+ z: track,
400
+ x: typeof opts.x === "number" && Number.isFinite(opts.x) ? opts.x : CAPTION_DEFAULT_FRAME.x,
401
+ y: typeof opts.y === "number" && Number.isFinite(opts.y) ? opts.y : CAPTION_DEFAULT_FRAME.y,
402
+ // A 0 (or missing) width/height would render the caption invisibly — treat
403
+ // only a positive number as an intentional override, else use the default.
404
+ width: typeof opts.width === "number" && opts.width > 0 ? opts.width : CAPTION_DEFAULT_FRAME.width,
405
+ height: typeof opts.height === "number" && opts.height > 0 ? opts.height : CAPTION_DEFAULT_FRAME.height,
406
+ text: cue.text,
407
+ color: look.color,
408
+ background: look.background,
409
+ textBackgroundStyle: look.backgroundStyle,
410
+ fontFamily: look.fontFamily,
411
+ fontWeight: look.fontWeight,
412
+ fontSize: look.fontSize,
413
+ captionAnimation: {
414
+ animation: look.animation,
415
+ words: cue.words?.length ? cue.words : estimateCaptionWords(cue.text, cue.duration),
416
+ activeColor: look.activeColor,
417
+ highlightColor: look.highlightColor,
418
+ uppercase: look.uppercase
419
+ }
420
+ };
421
+ const container = document.createElement("div");
422
+ container.innerHTML = renderHyperframeLayerHtml(layer);
423
+ const node = container.firstElementChild;
424
+ if (!node)
425
+ throw new Error(`Failed to render caption cue ${index}.`);
426
+ node.classList?.add?.("clip");
427
+ root.append(node);
428
+ });
429
+ return { html: serialize(document), layerKeys, replaced: existing.length, styleId: look.presetId };
430
+ }
431
+ /** Restyle existing animated captions in place: same cues/timings/track, new look. */
432
+ export function restyleCaptionLayers(html, opts) {
433
+ const cues = readCaptionCues(html);
434
+ if (!cues.length)
435
+ throw new Error("No animated caption layers found. Run `captions generate` (or add captions in the editor) first.");
436
+ const track = opts.track ?? cues[0].track;
437
+ const result = insertCaptionLayers(html, cues, { ...opts, track });
438
+ return { html: result.html, layerKeys: result.layerKeys, styleId: result.styleId };
439
+ }
440
+ // Replace an existing scene/layer with a generated media clip, taking over its
441
+ // timing + geometry (full-canvas when it was a proxy / full-canvas fill).
442
+ export function replaceLayerWithMedia(html, targetKey, opts) {
443
+ if (!opts.src?.trim())
444
+ throw new Error("replaceLayerWithMedia requires a src media URL.");
445
+ const { document, root } = readCompositionDoc(html);
446
+ const target = resolveLayerNode(root, targetKey);
447
+ if (!target)
448
+ throw new Error(`Layer to replace not found: ${targetKey}`);
449
+ const start = opts.start ?? numAttr(target, "data-start");
450
+ const durationRaw = opts.duration ?? numAttr(target, "data-duration");
451
+ const trackRaw = opts.track ?? numAttr(target, "data-track-index");
452
+ const fullCanvas = isFullCanvasNode(target);
453
+ const geom = fullCanvas
454
+ ? { x: 0, y: 0, width: 100, height: 100 }
455
+ : {
456
+ x: opts.x ?? stylePercent(target, "left") ?? 0,
457
+ y: opts.y ?? stylePercent(target, "top") ?? 0,
458
+ width: opts.width ?? stylePercent(target, "width") ?? 100,
459
+ height: opts.height ?? stylePercent(target, "height") ?? 100
460
+ };
461
+ const providedKey = opts.layerKey?.trim();
462
+ const targetId = target.getAttribute("data-hf-id") || target.id || "";
463
+ const genKey = providedKey && providedKey !== targetKey && /^[A-Za-z][\w-]{0,63}$/.test(providedKey)
464
+ ? providedKey
465
+ : `vf-gen-${Date.now().toString(36)}`;
466
+ const id = genKey.startsWith("element_") ? genKey : `element_${genKey}`;
467
+ // Remove the old node, then insert the replacement in its slot.
468
+ if (target.parentNode)
469
+ target.parentNode.removeChild(target);
470
+ const clips = collectClips(root);
471
+ const initialTrack = Number.isFinite(trackRaw) ? Math.max(0, Math.floor(trackRaw)) : nextAutoTrack(clips);
472
+ const track = findNonCollidingTrack(clips, { start: Number.isFinite(start) ? start : 0, duration: Number.isFinite(durationRaw) ? durationRaw : 4, track: initialTrack }, id);
473
+ const node = buildMediaClip(document, opts, id, track, {
474
+ start: Number.isFinite(start) ? start : 0,
475
+ duration: Number.isFinite(durationRaw) && durationRaw > 0 ? durationRaw : 4,
476
+ ...geom
477
+ });
478
+ root.append(node);
479
+ void targetId;
480
+ return { html: serialize(document), layerKey: id };
481
+ }
482
+ function upsertStyleDecl(node, name, value) {
483
+ const styleValue = node.getAttribute("style") || "";
484
+ const escaped = name.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
485
+ const pattern = new RegExp(`(^|;)\\s*${escaped}\\s*:[^;]*`, "i");
486
+ let nextStyle;
487
+ if (value === null) {
488
+ nextStyle = styleValue.replace(pattern, (_match, lead) => lead);
489
+ }
490
+ else if (pattern.test(styleValue)) {
491
+ nextStyle = styleValue.replace(pattern, (_match, lead) => `${lead}${name}:${value}`);
492
+ }
493
+ else {
494
+ nextStyle = styleValue.length === 0 || styleValue.endsWith(";")
495
+ ? `${styleValue}${name}:${value}`
496
+ : `${styleValue};${name}:${value}`;
497
+ }
498
+ if (nextStyle !== styleValue)
499
+ node.setAttribute("style", nextStyle);
500
+ }
501
+ // Scene clips are what junction transitions connect: video/image layers and
502
+ // timeline proxies. Audio, captions, text overlays and the backing source
503
+ // video are excluded (same filter as the editor's timeline chips).
504
+ function isSceneClipNode(node) {
505
+ if (node?.hasAttribute?.("data-vf-playback-source"))
506
+ return false;
507
+ const tag = String(node?.tagName || "").toLowerCase();
508
+ if (tag === "audio")
509
+ return false;
510
+ const kind = node?.getAttribute?.("data-layer-kind");
511
+ return tag === "video" || tag === "img"
512
+ || node?.getAttribute?.("data-vf-timeline-proxy") === "video"
513
+ || kind === "video" || kind === "image";
514
+ }
515
+ function setNodeTransition(node, preset, durationSeconds) {
516
+ if (!preset || preset === "none") {
517
+ node.removeAttribute("data-transition");
518
+ node.removeAttribute("data-transition-duration");
519
+ upsertStyleDecl(node, "--vf-tr-dur", null);
520
+ return;
521
+ }
522
+ if (!TRANSITION_PRESET_SET.has(preset)) {
523
+ throw new Error(`Unknown transition preset "${preset}". Use one of: none, ${TRANSITION_PRESETS.join(", ")}.`);
524
+ }
525
+ const current = Number.parseFloat(node.getAttribute("data-transition-duration") ?? "");
526
+ const duration = clampTransitionDuration(durationSeconds ?? (Number.isFinite(current) ? current : undefined));
527
+ node.setAttribute("data-transition", preset);
528
+ node.setAttribute("data-transition-duration", String(round3(duration)));
529
+ upsertStyleDecl(node, "--vf-tr-dur", `${round3(duration)}s`);
530
+ }
531
+ function setNodeTransitionOut(node, preset, durationSeconds) {
532
+ if (!preset || preset === "none") {
533
+ node.removeAttribute("data-transition-out");
534
+ node.removeAttribute("data-transition-out-duration");
535
+ upsertStyleDecl(node, "--vf-tr-out-dur", null);
536
+ upsertStyleDecl(node, "--vf-tr-out-delay", null);
537
+ return;
538
+ }
539
+ if (!TRANSITION_OUT_PRESET_SET.has(preset)) {
540
+ throw new Error(`Unknown exit transition preset "${preset}". Use one of: none, ${TRANSITION_OUT_PRESETS.join(", ")}.`);
541
+ }
542
+ const current = Number.parseFloat(node.getAttribute("data-transition-out-duration") ?? "");
543
+ const duration = clampTransitionDuration(durationSeconds ?? (Number.isFinite(current) ? current : undefined));
544
+ const layerDuration = numAttr(node, "data-duration");
545
+ node.setAttribute("data-transition-out", preset);
546
+ node.setAttribute("data-transition-out-duration", String(round3(duration)));
547
+ upsertStyleDecl(node, "--vf-tr-out-dur", `${round3(duration)}s`);
548
+ upsertStyleDecl(node, "--vf-tr-out-delay", `${transitionOutDelaySeconds(Number.isFinite(layerDuration) ? layerDuration : 0, duration)}s`);
549
+ }
550
+ // In-place Ken Burns (still-image pan/zoom) toggle on an EXISTING node — mirrors
551
+ // buildMediaClip's ken-burns block (data-kenburns + --vf-kb-dur + --vf-kb-amount)
552
+ // and the web applyKenBurnsToNode. "none"/null clears it.
553
+ function setNodeKenBurns(node, preset, intensity) {
554
+ if (!preset || preset === "none") {
555
+ node.removeAttribute("data-kenburns");
556
+ upsertStyleDecl(node, "--vf-kb-dur", null);
557
+ upsertStyleDecl(node, "--vf-kb-amount", null);
558
+ return;
559
+ }
560
+ if (!KEN_BURNS_PRESETS.has(preset)) {
561
+ throw new Error(`Unknown ken-burns preset "${preset}". Use one of: none, ${Array.from(KEN_BURNS_PRESETS).join(", ")}.`);
562
+ }
563
+ const dur = numAttr(node, "data-duration");
564
+ node.setAttribute("data-kenburns", preset);
565
+ if (Number.isFinite(dur) && dur > 0)
566
+ upsertStyleDecl(node, "--vf-kb-dur", `${round3(dur)}s`);
567
+ if (intensity !== undefined && Number.isFinite(intensity)) {
568
+ upsertStyleDecl(node, "--vf-kb-amount", String(Math.max(0.04, Math.min(0.5, intensity))));
569
+ }
570
+ }
571
+ // Bulk pass over the whole timeline — the devcli twin of the AI's
572
+ // set_transitions action and the editor's junction chips.
573
+ export function applyTransitionsAcross(html, opts) {
574
+ if (opts.junction === undefined && opts.intro === undefined && opts.outro === undefined) {
575
+ throw new Error("applyTransitionsAcross needs a junction preset, an intro, and/or an outro.");
576
+ }
577
+ const { document, root } = readCompositionDoc(html);
578
+ const scenes = collectClips(root).filter(isSceneClipNode).map((node) => ({
579
+ node,
580
+ start: numAttr(node, "data-start") || 0,
581
+ duration: numAttr(node, "data-duration") || 0,
582
+ track: numAttr(node, "data-track-index") || 0
583
+ })).sort((a, b) => (a.start - b.start) || (a.track - b.track));
584
+ if (!scenes.length)
585
+ throw new Error("No scene clips (video/image layers) found in the composition.");
586
+ const firstByTrack = new Map();
587
+ for (const scene of scenes) {
588
+ const first = firstByTrack.get(scene.track);
589
+ if (!first || scene.start < first.start)
590
+ firstByTrack.set(scene.track, scene);
591
+ }
592
+ let junctions = 0;
593
+ if (opts.junction !== undefined) {
594
+ for (const scene of scenes) {
595
+ if (firstByTrack.get(scene.track) === scene)
596
+ continue;
597
+ setNodeTransition(scene.node, opts.junction, opts.duration);
598
+ junctions += 1;
599
+ }
600
+ }
601
+ const firstScene = scenes[0];
602
+ const lastScene = scenes.reduce((latest, scene) => (scene.start + scene.duration > latest.start + latest.duration ? scene : latest), scenes[0]);
603
+ if (opts.intro !== undefined && firstScene) {
604
+ setNodeTransition(firstScene.node, opts.intro, opts.duration);
605
+ }
606
+ if (opts.outro !== undefined && lastScene) {
607
+ setNodeTransitionOut(lastScene.node, opts.outro, opts.outroDuration ?? opts.duration);
608
+ }
609
+ return {
610
+ html: serialize(document),
611
+ junctions,
612
+ intro: opts.intro !== undefined && opts.intro !== "none" && opts.intro !== null,
613
+ outro: opts.outro !== undefined && opts.outro !== "none" && opts.outro !== null
614
+ };
615
+ }
616
+ // Per-layer control — the devcli twin of set_layer_media's transition fields.
617
+ export function setLayerTransitions(html, layerKey, opts) {
618
+ if (opts.transition === undefined && opts.transitionOut === undefined) {
619
+ throw new Error("setLayerTransitions needs --in and/or --out.");
620
+ }
621
+ const { document, root } = readCompositionDoc(html);
622
+ const node = resolveLayerNode(root, layerKey);
623
+ if (!node)
624
+ throw new Error(`Layer not found: ${layerKey}`);
625
+ if (!isSceneClipNode(node) && String(node.tagName || "").toLowerCase() === "audio") {
626
+ throw new Error("Transitions cannot be applied to audio layers.");
627
+ }
628
+ if (opts.transition !== undefined)
629
+ setNodeTransition(node, opts.transition, opts.transitionDuration);
630
+ if (opts.transitionOut !== undefined)
631
+ setNodeTransitionOut(node, opts.transitionOut, opts.transitionOutDuration);
632
+ return { html: serialize(document), layerKey: node.getAttribute("data-hf-id") || node.id || layerKey };
633
+ }
634
+ export function clearAllTransitions(html) {
635
+ const { document, root } = readCompositionDoc(html);
636
+ let cleared = 0;
637
+ for (const node of collectClips(root)) {
638
+ const had = node.hasAttribute("data-transition") || node.hasAttribute("data-transition-out");
639
+ if (!had)
640
+ continue;
641
+ setNodeTransition(node, null);
642
+ setNodeTransitionOut(node, null);
643
+ cleared += 1;
644
+ }
645
+ return { html: serialize(document), cleared };
646
+ }
647
+ export function readTransitions(html) {
648
+ const { root } = readCompositionDoc(html);
649
+ return collectClips(root).filter(isSceneClipNode).map((node) => {
650
+ const duration = Number.parseFloat(node.getAttribute("data-transition-duration") ?? "");
651
+ const outDuration = Number.parseFloat(node.getAttribute("data-transition-out-duration") ?? "");
652
+ return {
653
+ layerKey: node.getAttribute("data-hf-id") || node.id || "",
654
+ label: node.getAttribute("data-label"),
655
+ track: numAttr(node, "data-track-index") || 0,
656
+ start: numAttr(node, "data-start") || 0,
657
+ duration: numAttr(node, "data-duration") || 0,
658
+ transition: node.getAttribute("data-transition"),
659
+ transition_duration: Number.isFinite(duration) ? duration : null,
660
+ transition_out: node.getAttribute("data-transition-out"),
661
+ transition_out_duration: Number.isFinite(outDuration) ? outDuration : null
662
+ };
663
+ }).sort((a, b) => (a.start - b.start) || (a.track - b.track));
664
+ }
665
+ const KEYFRAME_EASINGS = ["linear", "ease", "ease-in", "ease-out", "ease-in-out", "step-start", "step-end"];
666
+ // Whitelist the CSS timing-function so an easing can never smuggle arbitrary
667
+ // declarations into the injected <style>/inline style. Mirrors the editor's
668
+ // normalizeKeyframeEasing exactly.
669
+ function normalizeKeyframeEasing(raw) {
670
+ const value = (raw ?? "").trim().toLowerCase();
671
+ if (KEYFRAME_EASINGS.includes(value))
672
+ return value;
673
+ const cb = /^cubic-bezier\(\s*(-?\d*\.?\d+\s*,\s*){3}-?\d*\.?\d+\s*\)$/;
674
+ if (cb.test(value))
675
+ return value;
676
+ const steps = /^steps\(\s*\d+\s*(,\s*(start|end|jump-(start|end|none|both))\s*)?\)$/;
677
+ if (steps.test(value))
678
+ return value;
679
+ return "linear";
680
+ }
681
+ function nodeTimelineId(node) {
682
+ return node?.getAttribute?.("data-hf-id") || node?.id || "";
683
+ }
684
+ // Couple the timing fields the way the editor's patchElementTiming does: a start
685
+ // or duration change re-derives data-end; a track change re-derives z-index (the
686
+ // runtime/publish normalizer keeps z-index == track); a media in-point (video/
687
+ // audio) rides on data-media-start + data-playback-start together.
688
+ function applyTimingToNode(node, updates) {
689
+ let start = numAttr(node, "data-start");
690
+ let duration = numAttr(node, "data-duration");
691
+ if (updates.start !== undefined) {
692
+ start = updates.start;
693
+ node.setAttribute("data-start", String(round3(start)));
694
+ }
695
+ if (updates.duration !== undefined) {
696
+ duration = updates.duration;
697
+ node.setAttribute("data-duration", String(round3(duration)));
698
+ }
699
+ if (updates.start !== undefined || updates.duration !== undefined) {
700
+ const s = Number.isFinite(start) ? start : 0;
701
+ const d = Number.isFinite(duration) ? duration : 0;
702
+ node.setAttribute("data-end", String(round3(s + d)));
703
+ }
704
+ if (updates.track !== undefined) {
705
+ const t = Math.max(0, Math.trunc(updates.track));
706
+ node.setAttribute("data-track-index", String(t));
707
+ upsertStyleDecl(node, "z-index", String(t));
708
+ }
709
+ if (updates.playbackStart !== undefined) {
710
+ const ps = String(round3(Math.max(0, updates.playbackStart)));
711
+ node.setAttribute("data-media-start", ps);
712
+ node.setAttribute("data-playback-start", ps);
713
+ }
714
+ }
715
+ // set_layer_keyframes twin: author a script-free CSS @keyframes animation on ONE
716
+ // layer. The rule lives in a <style data-vf-kf="hfk-…"> in <head> (any prior
717
+ // block for the same layer is removed first); the layer gets an inline
718
+ // `animation: hfk-… <dur>s <easing> both paused`. The keyframe name is NEVER
719
+ // vf-* (those are reserved for kenburns/transition/caption machinery); paused +
720
+ // fill:both keeps it seek-safe in preview AND render.
721
+ export function setLayerKeyframes(html, layerKey, opts) {
722
+ const frames = (opts.keyframes ?? []).filter((f) => Number.isFinite(f.offset)).sort((a, b) => a.offset - b.offset);
723
+ if (frames.length < 2) {
724
+ throw new Error("keyframes requires at least 2 stops, each with a numeric offset in 0..1.");
725
+ }
726
+ if (frames.every((f) => f.opacity === undefined && f.translate_x === undefined && f.translate_y === undefined && f.scale === undefined && f.rotate === undefined)) {
727
+ throw new Error("each keyframe needs at least one animated property (opacity, translate_x, translate_y, scale, rotate).");
728
+ }
729
+ const { document, root } = readCompositionDoc(html);
730
+ const node = resolveLayerNode(root, layerKey);
731
+ if (!node)
732
+ throw new Error(`Layer not found: ${layerKey}`);
733
+ const baseId = nodeTimelineId(node) || "layer";
734
+ const animName = `hfk-${baseId.replace(/[^A-Za-z0-9_-]/g, "-")}`;
735
+ const clipDuration = numAttr(node, "data-duration");
736
+ const durationSec = opts.durationSec !== undefined && opts.durationSec > 0
737
+ ? opts.durationSec
738
+ : (Number.isFinite(clipDuration) && clipDuration > 0 ? clipDuration : 1);
739
+ const easing = normalizeKeyframeEasing(opts.easing);
740
+ const cssFrames = frames.map((f) => {
741
+ const pct = Math.max(0, Math.min(100, f.offset * 100));
742
+ const pctLabel = Number(pct.toFixed(3)).toString();
743
+ const transforms = [];
744
+ if (f.translate_x !== undefined || f.translate_y !== undefined) {
745
+ transforms.push(`translate(${f.translate_x ?? 0}%, ${f.translate_y ?? 0}%)`);
746
+ }
747
+ if (f.scale !== undefined)
748
+ transforms.push(`scale(${f.scale})`);
749
+ if (f.rotate !== undefined)
750
+ transforms.push(`rotate(${f.rotate}deg)`);
751
+ const decls = [];
752
+ if (transforms.length)
753
+ decls.push(`transform:${transforms.join(" ")}`);
754
+ if (f.opacity !== undefined)
755
+ decls.push(`opacity:${Math.max(0, Math.min(1, f.opacity))}`);
756
+ return `${pctLabel}%{${decls.join(";")}}`;
757
+ }).join("");
758
+ const keyframeCss = `@keyframes ${animName}{${cssFrames}}`;
759
+ // Replace any prior keyframe block authored for THIS layer, then inject fresh.
760
+ // animName is already restricted to [A-Za-z0-9_-] so no selector escaping needed.
761
+ for (const old of Array.from(document.querySelectorAll(`style[data-vf-kf="${animName}"]`))) {
762
+ old.parentNode?.removeChild(old);
763
+ }
764
+ const styleEl = document.createElement("style");
765
+ styleEl.setAttribute("data-vf-kf", animName);
766
+ styleEl.textContent = keyframeCss;
767
+ const head = document.querySelector("head") ?? document.body ?? document.documentElement;
768
+ head.appendChild(styleEl);
769
+ upsertStyleDecl(node, "animation", `${animName} ${round3(durationSec)}s ${easing} both paused`);
770
+ return { html: serialize(document), layerKey: nodeTimelineId(node) || layerKey, stops: frames.length, animName, durationSec: round3(durationSec), easing };
771
+ }
772
+ // nudge_layers twin: relative timeline move of one or more layers, group-aware
773
+ // (a grouped clip carries its whole data-vf-group with it). Start/track clamp to
774
+ // >= 0. Applied in one pass; collisions between the moving clips are NOT
775
+ // auto-resolved (unlike the single-layer restack) — the composition-edit helper
776
+ // only resolves one excluded node at a time.
777
+ export function nudgeLayers(html, keys, opts) {
778
+ const deltaStart = opts.deltaStart ?? 0;
779
+ const deltaTrack = opts.deltaTrack ?? 0;
780
+ if (!deltaStart && !deltaTrack) {
781
+ throw new Error("nudge needs a non-zero --delta-start (seconds) and/or --delta-track (lanes).");
782
+ }
783
+ const cleanKeys = keys.filter((k) => typeof k === "string" && k.trim().length > 0);
784
+ if (!cleanKeys.length)
785
+ throw new Error("nudge requires --layer <key> (repeatable).");
786
+ const { document, root } = readCompositionDoc(html);
787
+ const moveSet = new Map();
788
+ for (const key of cleanKeys) {
789
+ const seed = resolveLayerNode(root, key);
790
+ if (!seed)
791
+ throw new Error(`Layer not found: ${key}`);
792
+ const groupId = seed.getAttribute("data-vf-group");
793
+ if (groupId) {
794
+ const escaped = groupId.replace(/["\\]/g, "\\$&");
795
+ for (const gn of Array.from(root.querySelectorAll(`[data-vf-group="${escaped}"]`))) {
796
+ moveSet.set(nodeTimelineId(gn) || `n${moveSet.size}`, gn);
797
+ }
798
+ }
799
+ else {
800
+ moveSet.set(nodeTimelineId(seed) || `n${moveSet.size}`, seed);
801
+ }
802
+ }
803
+ for (const node of moveSet.values()) {
804
+ const start = numAttr(node, "data-start");
805
+ const track = numAttr(node, "data-track-index");
806
+ applyTimingToNode(node, {
807
+ start: Math.max(0, (Number.isFinite(start) ? start : 0) + deltaStart),
808
+ track: Math.max(0, (Number.isFinite(track) ? track : 0) + Math.trunc(deltaTrack))
809
+ });
810
+ }
811
+ return { html: serialize(document), moved: Array.from(moveSet.values()).map((n) => nodeTimelineId(n)) };
812
+ }
813
+ // ripple_edit twin: insert (delta > 0) or close (delta < 0) time at `at`,
814
+ // shifting every clip whose data-start >= at across ALL tracks so downstream
815
+ // timing stays in sync. Starts clamp to >= 0.
816
+ export function rippleEdit(html, opts) {
817
+ if (!Number.isFinite(opts.at))
818
+ throw new Error("ripple requires --at (seconds on the composition timeline).");
819
+ if (!opts.delta || !Number.isFinite(opts.delta)) {
820
+ throw new Error("ripple requires a non-zero --delta (seconds; positive inserts a gap, negative closes one).");
821
+ }
822
+ const { document, root } = readCompositionDoc(html);
823
+ const affected = collectClips(root).filter((node) => {
824
+ const s = numAttr(node, "data-start");
825
+ return Number.isFinite(s) && s >= opts.at - 0.0001;
826
+ });
827
+ if (!affected.length)
828
+ throw new Error(`No clips start at or after ${opts.at}s to ripple.`);
829
+ for (const node of affected) {
830
+ const s = numAttr(node, "data-start");
831
+ applyTimingToNode(node, { start: Math.max(0, s + opts.delta) });
832
+ }
833
+ return { html: serialize(document), affected: affected.length };
834
+ }
835
+ // trim_layer twin: move ONE edge of a clip to a composition time. edge='start'
836
+ // advances data-start, shrinks data-duration, and (video/audio only) pushes the
837
+ // media in-point forward by the same delta so the visible frame is unchanged;
838
+ // edge='end' just changes data-duration. Guards against non-positive duration.
839
+ export function trimLayer(html, layerKey, opts) {
840
+ if (opts.edge !== "start" && opts.edge !== "end")
841
+ throw new Error("trim requires --edge start|end.");
842
+ if (!Number.isFinite(opts.to))
843
+ throw new Error("trim requires --to (seconds on the composition timeline).");
844
+ const { document, root } = readCompositionDoc(html);
845
+ const node = resolveLayerNode(root, layerKey);
846
+ if (!node)
847
+ throw new Error(`Layer not found: ${layerKey}`);
848
+ const start = numAttr(node, "data-start");
849
+ const duration = numAttr(node, "data-duration");
850
+ const s = Number.isFinite(start) ? start : 0;
851
+ const d = Number.isFinite(duration) ? duration : 0;
852
+ const end = s + d;
853
+ const tag = String(node.tagName || "").toLowerCase();
854
+ const isMedia = tag === "video" || tag === "audio";
855
+ const mediaStart = numAttr(node, "data-media-start");
856
+ const ms = Number.isFinite(mediaStart) ? mediaStart : 0;
857
+ const updates = {};
858
+ if (opts.edge === "start") {
859
+ if (opts.to <= s + 0.0001) {
860
+ throw new Error(`Left trim --to (${opts.to}s) must be after the clip start (${s}s). Use \`move\` to shift the clip earlier.`);
861
+ }
862
+ if (opts.to >= end - 0.05) {
863
+ throw new Error("Left trim would leave no duration — pick a --to before the clip's end.");
864
+ }
865
+ const delta = opts.to - s;
866
+ updates.start = opts.to;
867
+ updates.duration = d - delta;
868
+ if (isMedia)
869
+ updates.playbackStart = ms + delta;
870
+ }
871
+ else {
872
+ if (opts.to <= s + 0.05) {
873
+ throw new Error(`Right trim --to (${opts.to}s) must be after the clip start (${s}s).`);
874
+ }
875
+ updates.duration = opts.to - s;
876
+ }
877
+ applyTimingToNode(node, updates);
878
+ const report = {};
879
+ if (updates.start !== undefined)
880
+ report.start = round3(updates.start);
881
+ if (updates.duration !== undefined)
882
+ report.duration = round3(updates.duration);
883
+ if (updates.playbackStart !== undefined)
884
+ report.playback_start = round3(updates.playbackStart);
885
+ return { html: serialize(document), layerKey: nodeTimelineId(node) || layerKey, updates: report };
886
+ }
887
+ // set_layer_zindex twin: restack a layer. Stacking == data-track-index (higher
888
+ // draws on top). front = maxTrack+1, back = max(0,minTrack-1), forward = track+1,
889
+ // backward = max(0,track-1); an explicit --track wins. The single-node collision
890
+ // resolver (findNonCollidingTrack) re-lanes upward when the target collides, so
891
+ // 'back'/'backward' are best-effort (two time-overlapping clips can't share a lane).
892
+ export function restackLayer(html, layerKey, opts) {
893
+ if (!opts.order && opts.track === undefined) {
894
+ throw new Error("restack needs --order front|back|forward|backward or an explicit --track.");
895
+ }
896
+ const { document, root } = readCompositionDoc(html);
897
+ const node = resolveLayerNode(root, layerKey);
898
+ if (!node)
899
+ throw new Error(`Layer not found: ${layerKey}`);
900
+ const clips = collectClips(root);
901
+ const tracks = clips.map((n) => numAttr(n, "data-track-index")).filter((t) => Number.isFinite(t));
902
+ const currentTrack = numAttr(node, "data-track-index");
903
+ const ct = Number.isFinite(currentTrack) ? currentTrack : 0;
904
+ const maxTrack = tracks.length ? Math.max(...tracks) : ct;
905
+ const minTrack = tracks.length ? Math.min(...tracks) : ct;
906
+ let targetTrack;
907
+ if (opts.track !== undefined)
908
+ targetTrack = Math.max(0, Math.trunc(opts.track));
909
+ else if (opts.order === "front")
910
+ targetTrack = maxTrack + 1;
911
+ else if (opts.order === "back")
912
+ targetTrack = Math.max(0, minTrack - 1);
913
+ else if (opts.order === "forward")
914
+ targetTrack = ct + 1;
915
+ else
916
+ targetTrack = Math.max(0, ct - 1);
917
+ const excludeId = nodeTimelineId(node);
918
+ const start = numAttr(node, "data-start");
919
+ const dur = numAttr(node, "data-duration");
920
+ const resolvedTrack = findNonCollidingTrack(clips, { start: Number.isFinite(start) ? start : 0, duration: Number.isFinite(dur) ? dur : 0, track: targetTrack }, excludeId);
921
+ if (resolvedTrack === ct) {
922
+ throw new Error(`${excludeId || layerKey} is already at track ${ct} — no stacking change.`);
923
+ }
924
+ applyTimingToNode(node, { track: resolvedTrack });
925
+ return { html: serialize(document), layerKey: excludeId || layerKey, track: resolvedTrack, order: opts.order ?? "explicit" };
926
+ }
927
+ // ---------------------------------------------------------------------------
928
+ // Named layer-edit primitives — the devcli twins of the web editor_action verbs
929
+ // set_layer_text / set_layer_style / set_layer_visual / set_layer_identity /
930
+ // duplicate_layer / split_layer / set_layer_timing, plus set_composition. All
931
+ // follow the same seams as the timeline verbs above: readCompositionDoc →
932
+ // resolveLayerNode → mutate via upsertStyleDecl (inline style, seek-safe) +
933
+ // data-* attrs → serialize. Static look = inline style; timing = data-* attrs;
934
+ // caption/text background = data-text-background-*; identity = data-hf-slug/note.
935
+ /** Rewrite a text/caption layer's visible words, preserving the caption
936
+ * word-span/outline wrapper (data-vf-text-inline) when present. */
937
+ export function setLayerText(html, layerKey, text) {
938
+ const { document, root } = readCompositionDoc(html);
939
+ const node = resolveLayerNode(root, layerKey);
940
+ if (!node)
941
+ throw new Error(`Layer not found: ${layerKey}`);
942
+ const inline = node.querySelector?.("[data-vf-text-inline]");
943
+ if (inline)
944
+ inline.textContent = text;
945
+ else
946
+ node.textContent = text;
947
+ node.setAttribute("data-label", text.slice(0, 40));
948
+ return { html: serialize(document), layerKey: nodeTimelineId(node) || layerKey };
949
+ }
950
+ /** Restyle a layer: typography, colours, and static appearance. Mirrors the web
951
+ * set_layer_style (+ the opacity lever from set_layer_visual). Text background
952
+ * is attr-driven (data-text-background-*) like the caption renderer expects;
953
+ * everything else is inline CSS via upsertStyleDecl. */
954
+ export function setLayerStyle(html, layerKey, opts) {
955
+ const { document, root } = readCompositionDoc(html);
956
+ const node = resolveLayerNode(root, layerKey);
957
+ if (!node)
958
+ throw new Error(`Layer not found: ${layerKey}`);
959
+ const changed = [];
960
+ if (opts.color !== undefined) {
961
+ upsertStyleDecl(node, "color", opts.color);
962
+ const inline = node.querySelector?.("[data-vf-text-inline]");
963
+ if (inline)
964
+ upsertStyleDecl(inline, "color", opts.color);
965
+ changed.push("color");
966
+ }
967
+ if (opts.backgroundStyle !== undefined || opts.background !== undefined) {
968
+ const bgStyle = opts.backgroundStyle ?? node.getAttribute("data-text-background-style") ?? "plain";
969
+ const bgColor = opts.background ?? node.getAttribute("data-text-background-color") ?? "#000000";
970
+ node.setAttribute("data-text-background-style", bgStyle);
971
+ node.setAttribute("data-text-background-color", bgColor);
972
+ changed.push("background");
973
+ }
974
+ if (opts.fontFamily !== undefined) {
975
+ node.setAttribute("data-font-family", opts.fontFamily);
976
+ upsertStyleDecl(node, "font-family", `'${opts.fontFamily}', 'TikTok Sans', sans-serif`);
977
+ changed.push("font-family");
978
+ }
979
+ if (opts.fontWeight !== undefined) {
980
+ upsertStyleDecl(node, "font-weight", String(opts.fontWeight));
981
+ changed.push("font-weight");
982
+ }
983
+ if (opts.italic !== undefined) {
984
+ upsertStyleDecl(node, "font-style", opts.italic ? "italic" : "normal");
985
+ changed.push("italic");
986
+ }
987
+ if (opts.underline !== undefined) {
988
+ upsertStyleDecl(node, "text-decoration-line", opts.underline ? "underline" : "none");
989
+ changed.push("underline");
990
+ }
991
+ if (opts.textAlign !== undefined) {
992
+ upsertStyleDecl(node, "text-align", opts.textAlign);
993
+ changed.push("text-align");
994
+ }
995
+ if (opts.borderRadius !== undefined) {
996
+ upsertStyleDecl(node, "border-radius", `${opts.borderRadius}px`);
997
+ changed.push("border-radius");
998
+ }
999
+ if (opts.fontSize !== undefined) {
1000
+ upsertStyleDecl(node, "font-size", `${opts.fontSize}px`);
1001
+ changed.push("font-size");
1002
+ }
1003
+ if (opts.lineHeight !== undefined) {
1004
+ upsertStyleDecl(node, "line-height", String(opts.lineHeight));
1005
+ changed.push("line-height");
1006
+ }
1007
+ if (opts.letterSpacing !== undefined) {
1008
+ upsertStyleDecl(node, "letter-spacing", `${opts.letterSpacing}px`);
1009
+ changed.push("letter-spacing");
1010
+ }
1011
+ if (opts.opacity !== undefined) {
1012
+ upsertStyleDecl(node, "opacity", String(Math.max(0, Math.min(1, opts.opacity))));
1013
+ changed.push("opacity");
1014
+ }
1015
+ if (changed.length === 0)
1016
+ throw new Error("set-style needs at least one style field.");
1017
+ return { html: serialize(document), layerKey: nodeTimelineId(node) || layerKey, changed };
1018
+ }
1019
+ /** Geometry + constant opacity, mirroring the web set_layer_visual. All inline. */
1020
+ export function setLayerVisual(html, layerKey, opts) {
1021
+ const { document, root } = readCompositionDoc(html);
1022
+ const node = resolveLayerNode(root, layerKey);
1023
+ if (!node)
1024
+ throw new Error(`Layer not found: ${layerKey}`);
1025
+ const changed = [];
1026
+ if (opts.x !== undefined) {
1027
+ upsertStyleDecl(node, "left", `${clampPercent(opts.x)}%`);
1028
+ changed.push("x");
1029
+ }
1030
+ if (opts.y !== undefined) {
1031
+ upsertStyleDecl(node, "top", `${clampPercent(opts.y)}%`);
1032
+ changed.push("y");
1033
+ }
1034
+ if (opts.width !== undefined) {
1035
+ upsertStyleDecl(node, "width", `${clampPercent(opts.width)}%`);
1036
+ changed.push("width");
1037
+ }
1038
+ if (opts.height !== undefined) {
1039
+ upsertStyleDecl(node, "height", `${clampPercent(opts.height)}%`);
1040
+ changed.push("height");
1041
+ }
1042
+ if (opts.fontSize !== undefined) {
1043
+ upsertStyleDecl(node, "font-size", `${opts.fontSize}px`);
1044
+ changed.push("font-size");
1045
+ }
1046
+ if (opts.borderRadius !== undefined) {
1047
+ upsertStyleDecl(node, "border-radius", `${opts.borderRadius}px`);
1048
+ changed.push("border-radius");
1049
+ }
1050
+ if (opts.opacity !== undefined) {
1051
+ upsertStyleDecl(node, "opacity", String(Math.max(0, Math.min(1, opts.opacity))));
1052
+ changed.push("opacity");
1053
+ }
1054
+ if (changed.length === 0)
1055
+ throw new Error("set-visual needs at least one of x/y/width/height/font-size/border-radius/opacity.");
1056
+ return { html: serialize(document), layerKey: nodeTimelineId(node) || layerKey, changed };
1057
+ }
1058
+ /**
1059
+ * In-place edit of a MEDIA layer (video / image / audio): the devcli twin of the
1060
+ * web set_layer_media (applySetLayerMediaAction). Writes byte-identical
1061
+ * attributes — critically, a --src swap KEEPS the same node, key, geometry and
1062
+ * timing (unlike `place --replace`, which deletes the node and re-inserts a new
1063
+ * one with a fresh key). Subrange sugar: --source-out sets the timeline duration
1064
+ * to (source_out − in-point), exactly like the web MediaSegmentModal's in/out.
1065
+ */
1066
+ export function setLayerMedia(html, layerKey, opts) {
1067
+ const { document, root } = readCompositionDoc(html);
1068
+ const node = resolveLayerNode(root, layerKey);
1069
+ if (!node)
1070
+ throw new Error(`Layer not found: ${layerKey}`);
1071
+ const tag = String(node.tagName || "").toLowerCase();
1072
+ const kind = node.getAttribute("data-layer-kind") || (tag === "img" ? "image" : tag);
1073
+ if (tag !== "video" && tag !== "audio" && tag !== "img") {
1074
+ throw new Error(`set-media targets a media layer (video/image/audio); "${layerKey}" is a ${kind || tag} layer. Use set-text / set-style / set-visual instead.`);
1075
+ }
1076
+ const changed = [];
1077
+ if (opts.src !== undefined) {
1078
+ const src = opts.src.trim();
1079
+ if (!src)
1080
+ throw new Error("set-media --src cannot be empty.");
1081
+ node.setAttribute("src", src);
1082
+ // Backdrop follows the new src: alpha-capable media must not sit on the
1083
+ // opaque editor backdrop (transparency would render as a black box), while
1084
+ // an opaque video regains it.
1085
+ if (tag === "video" || tag === "img") {
1086
+ const alpha = tag === "img"
1087
+ ? /\.(png|webp|gif|avif)(\?|#|$)/i.test(src)
1088
+ : /\.(webm|mov)(\?|#|$)/i.test(src);
1089
+ if (alpha)
1090
+ upsertStyleDecl(node, "background", "transparent");
1091
+ else if (tag === "video")
1092
+ upsertStyleDecl(node, "background", "#050604");
1093
+ }
1094
+ changed.push("src");
1095
+ }
1096
+ if (opts.volume !== undefined) {
1097
+ node.setAttribute("data-volume", String(opts.volume));
1098
+ changed.push("volume");
1099
+ }
1100
+ if (opts.muted !== undefined) {
1101
+ if (opts.muted)
1102
+ node.setAttribute("muted", "");
1103
+ else
1104
+ node.removeAttribute("muted");
1105
+ changed.push("muted");
1106
+ }
1107
+ if (opts.loop !== undefined && tag === "video") {
1108
+ if (opts.loop) {
1109
+ node.setAttribute("loop", "");
1110
+ node.setAttribute("data-loop", "true");
1111
+ }
1112
+ else {
1113
+ node.removeAttribute("loop");
1114
+ node.removeAttribute("data-loop");
1115
+ }
1116
+ changed.push("loop");
1117
+ }
1118
+ if (opts.objectFit !== undefined) {
1119
+ upsertStyleDecl(node, "object-fit", opts.objectFit);
1120
+ changed.push("object-fit");
1121
+ }
1122
+ if (opts.objectPosition !== undefined) {
1123
+ upsertStyleDecl(node, "object-position", opts.objectPosition);
1124
+ changed.push("object-position");
1125
+ }
1126
+ if (opts.kenBurns !== undefined) {
1127
+ setNodeKenBurns(node, opts.kenBurns, opts.kenBurnsIntensity);
1128
+ changed.push("ken-burns");
1129
+ }
1130
+ if (opts.transition !== undefined) {
1131
+ setNodeTransition(node, opts.transition, opts.transitionDuration);
1132
+ changed.push("transition");
1133
+ }
1134
+ if (opts.transitionOut !== undefined) {
1135
+ setNodeTransitionOut(node, opts.transitionOut, opts.transitionOutDuration);
1136
+ changed.push("transition-out");
1137
+ }
1138
+ // Subrange: in-point (media-start) and optional out-point → timeline duration.
1139
+ let nextDuration = opts.duration;
1140
+ if (opts.sourceOut !== undefined) {
1141
+ const currentIn = numAttr(node, "data-media-start");
1142
+ const inPoint = opts.playbackStart !== undefined ? opts.playbackStart : (Number.isFinite(currentIn) ? currentIn : 0);
1143
+ const span = opts.sourceOut - inPoint;
1144
+ if (!(span > 0))
1145
+ throw new Error(`--source-out (${opts.sourceOut}s) must be greater than the in-point (${inPoint}s).`);
1146
+ nextDuration = span;
1147
+ }
1148
+ if (opts.playbackStart !== undefined || nextDuration !== undefined) {
1149
+ applyTimingToNode(node, {
1150
+ playbackStart: opts.playbackStart,
1151
+ duration: nextDuration !== undefined ? Math.max(0.1, nextDuration) : undefined
1152
+ });
1153
+ if (opts.playbackStart !== undefined)
1154
+ changed.push("playback-start");
1155
+ if (nextDuration !== undefined)
1156
+ changed.push("duration");
1157
+ }
1158
+ if (changed.length === 0) {
1159
+ throw new Error("set-media needs at least one of --src / --volume / --muted / --loop / --playback-start / --source-out / --duration / --object-fit / --object-position / --ken-burns / --transition / --transition-out.");
1160
+ }
1161
+ return { html: serialize(document), layerKey: nodeTimelineId(node) || layerKey, changed, kind: kind || tag };
1162
+ }
1163
+ /** Set a layer's stable slug + human note (the AI/viral-DNA handle). Pass an
1164
+ * empty string to clear a note. Mirrors the web set_layer_identity. */
1165
+ export function setLayerIdentity(html, layerKey, opts) {
1166
+ if (opts.slug === undefined && opts.note === undefined)
1167
+ throw new Error("set-identity needs --slug and/or --note.");
1168
+ const { document, root } = readCompositionDoc(html);
1169
+ const node = resolveLayerNode(root, layerKey);
1170
+ if (!node)
1171
+ throw new Error(`Layer not found: ${layerKey}`);
1172
+ if (opts.slug !== undefined) {
1173
+ const slug = opts.slug.trim();
1174
+ if (slug)
1175
+ node.setAttribute("data-hf-slug", slug);
1176
+ else
1177
+ node.removeAttribute("data-hf-slug");
1178
+ }
1179
+ if (opts.note !== undefined) {
1180
+ if (opts.note.length)
1181
+ node.setAttribute("data-hf-note", opts.note);
1182
+ else
1183
+ node.removeAttribute("data-hf-note");
1184
+ }
1185
+ return { html: serialize(document), layerKey: nodeTimelineId(node) || layerKey };
1186
+ }
1187
+ /** Clone a layer, offsetting its start so the copy doesn't sit exactly on top,
1188
+ * and resolving to a collision-free track. Mirrors the web duplicate_layer. */
1189
+ export function duplicateLayer(html, layerKey, opts = {}) {
1190
+ const { document, root } = readCompositionDoc(html);
1191
+ const node = resolveLayerNode(root, layerKey);
1192
+ if (!node)
1193
+ throw new Error(`Layer not found: ${layerKey}`);
1194
+ const clips = collectClips(root);
1195
+ const start = numAttr(node, "data-start");
1196
+ const duration = numAttr(node, "data-duration");
1197
+ const track = numAttr(node, "data-track-index");
1198
+ const s = Number.isFinite(start) ? start : 0;
1199
+ const d = Number.isFinite(duration) ? duration : 0.1;
1200
+ const clone = node.cloneNode(true);
1201
+ const providedKey = opts.newKey?.trim();
1202
+ const genKey = providedKey && /^[A-Za-z][\w-]{0,63}$/.test(providedKey) ? providedKey : `vf-dup-${Date.now().toString(36)}`;
1203
+ const newId = genKey.startsWith("element_") ? genKey : `element_${genKey}`;
1204
+ clone.id = newId;
1205
+ clone.setAttribute("data-hf-id", newId);
1206
+ const newStart = opts.start !== undefined && Number.isFinite(opts.start) ? Math.max(0, opts.start) : s + d;
1207
+ const initialTrack = opts.track !== undefined && Number.isFinite(opts.track) ? Math.max(0, Math.floor(opts.track)) : (Number.isFinite(track) ? track : 0);
1208
+ const resolvedTrack = findNonCollidingTrack(clips, { start: newStart, duration: d, track: initialTrack }, newId);
1209
+ applyTimingToNode(clone, { start: newStart, duration: d, track: resolvedTrack });
1210
+ root.append(clone);
1211
+ return { html: serialize(document), layerKey: newId };
1212
+ }
1213
+ /** Cut a clip in two at splitTime (composition seconds). The tail inherits the
1214
+ * media in-point offset so video/audio stays continuous. Mirrors split_layer. */
1215
+ export function splitLayer(html, layerKey, splitTime) {
1216
+ const { document, root } = readCompositionDoc(html);
1217
+ const node = resolveLayerNode(root, layerKey);
1218
+ if (!node)
1219
+ throw new Error(`Layer not found: ${layerKey}`);
1220
+ const start = numAttr(node, "data-start");
1221
+ const duration = numAttr(node, "data-duration");
1222
+ const s = Number.isFinite(start) ? start : 0;
1223
+ const d = Number.isFinite(duration) ? duration : 0;
1224
+ if (!(splitTime > s + 0.001 && splitTime < s + d - 0.001)) {
1225
+ throw new Error(`--at ${splitTime}s must fall strictly inside the clip (${round3(s)}–${round3(s + d)}s).`);
1226
+ }
1227
+ const tail = node.cloneNode(true);
1228
+ const tailId = `element_vf-split-${Date.now().toString(36)}`;
1229
+ tail.id = tailId;
1230
+ tail.setAttribute("data-hf-id", tailId);
1231
+ // First half: trim end to the split point.
1232
+ applyTimingToNode(node, { duration: splitTime - s });
1233
+ // Tail: starts at split, remaining duration, media in-point advanced by the cut.
1234
+ const priorMediaStart = numAttr(node, "data-media-start");
1235
+ const tailMediaStart = (Number.isFinite(priorMediaStart) ? priorMediaStart : 0) + (splitTime - s);
1236
+ applyTimingToNode(tail, { start: splitTime, duration: s + d - splitTime, playbackStart: tailMediaStart });
1237
+ root.append(tail);
1238
+ return { html: serialize(document), firstKey: nodeTimelineId(node) || layerKey, secondKey: tailId };
1239
+ }
1240
+ /** Absolute set of a layer's timing (start/duration/track/playback-start),
1241
+ * resolving track collisions. The devcli twin of the web set_layer_timing —
1242
+ * unlike trim (edges) or nudge (relative), this sets values directly. */
1243
+ export function setLayerTiming(html, layerKey, opts) {
1244
+ if (opts.start === undefined && opts.duration === undefined && opts.track === undefined && opts.playbackStart === undefined) {
1245
+ throw new Error("retime needs at least one of --start/--duration/--track/--playback-start.");
1246
+ }
1247
+ const { document, root } = readCompositionDoc(html);
1248
+ const node = resolveLayerNode(root, layerKey);
1249
+ if (!node)
1250
+ throw new Error(`Layer not found: ${layerKey}`);
1251
+ const clips = collectClips(root);
1252
+ const excludeId = nodeTimelineId(node);
1253
+ const nextStart = opts.start !== undefined ? Math.max(0, opts.start) : numAttr(node, "data-start");
1254
+ const nextDur = opts.duration !== undefined ? Math.max(0.1, opts.duration) : numAttr(node, "data-duration");
1255
+ const wantTrack = opts.track !== undefined ? Math.max(0, Math.floor(opts.track)) : numAttr(node, "data-track-index");
1256
+ const resolvedTrack = findNonCollidingTrack(clips, { start: Number.isFinite(nextStart) ? nextStart : 0, duration: Number.isFinite(nextDur) ? nextDur : 0.1, track: Number.isFinite(wantTrack) ? wantTrack : 0 }, excludeId);
1257
+ applyTimingToNode(node, {
1258
+ start: opts.start !== undefined ? Math.max(0, opts.start) : undefined,
1259
+ duration: opts.duration !== undefined ? Math.max(0.1, opts.duration) : undefined,
1260
+ track: resolvedTrack,
1261
+ playbackStart: opts.playbackStart
1262
+ });
1263
+ return { html: serialize(document), layerKey: excludeId || layerKey, track: resolvedTrack };
1264
+ }
1265
+ /** Composition-level (canvas / theme) edit: resize the frame, retarget total
1266
+ * render length, or recolour the canvas background. Written on the
1267
+ * [data-composition-id] root per the hyperframes contract (data-width/height/
1268
+ * duration + a background colour). The devcli twin of the web set_composition. */
1269
+ export function setComposition(html, opts) {
1270
+ const { document, root } = readCompositionDoc(html);
1271
+ const changed = [];
1272
+ if (opts.width !== undefined && Number.isFinite(opts.width) && opts.width > 0) {
1273
+ root.setAttribute("data-width", String(Math.round(opts.width)));
1274
+ changed.push(`width=${Math.round(opts.width)}`);
1275
+ }
1276
+ if (opts.height !== undefined && Number.isFinite(opts.height) && opts.height > 0) {
1277
+ root.setAttribute("data-height", String(Math.round(opts.height)));
1278
+ changed.push(`height=${Math.round(opts.height)}`);
1279
+ }
1280
+ if (opts.duration !== undefined && Number.isFinite(opts.duration) && opts.duration > 0) {
1281
+ root.setAttribute("data-duration", String(round3(opts.duration)));
1282
+ changed.push(`duration=${round3(opts.duration)}`);
1283
+ }
1284
+ if (opts.backgroundColor !== undefined && opts.backgroundColor.trim()) {
1285
+ upsertStyleDecl(root, "background", opts.backgroundColor.trim());
1286
+ changed.push(`background=${opts.backgroundColor.trim()}`);
1287
+ }
1288
+ if (changed.length === 0)
1289
+ throw new Error("set-composition needs at least one of --width/--height/--duration/--background.");
1290
+ return { html: serialize(document), changed };
1291
+ }
1292
+ //# sourceMappingURL=composition-edit.js.map