@mevdragon/vidfarm-devcli 0.14.0 → 0.16.0

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 (427) hide show
  1. package/.agents/skills/embedded-captions/CATALOG.md +93 -0
  2. package/.agents/skills/embedded-captions/SKILL.md +286 -0
  3. package/.agents/skills/embedded-captions/assets/brand/CDPR-fankit-terms.txt +35 -0
  4. package/.agents/skills/embedded-captions/assets/brand/cyberpunk-widths.json +115 -0
  5. package/.agents/skills/embedded-captions/assets/fonts/char-widths.json +2211 -0
  6. package/.agents/skills/embedded-captions/assets/strokefonts/HersheyScript1.svg +242 -0
  7. package/.agents/skills/embedded-captions/assets/strokefonts/HersheyScriptMed.svg +242 -0
  8. package/.agents/skills/embedded-captions/dna/README.md +148 -0
  9. package/.agents/skills/embedded-captions/dna/chrome.json +68 -0
  10. package/.agents/skills/embedded-captions/dna/cream.json +69 -0
  11. package/.agents/skills/embedded-captions/dna/documentary.json +62 -0
  12. package/.agents/skills/embedded-captions/dna/editorial.json +70 -0
  13. package/.agents/skills/embedded-captions/dna/glitch.json +82 -0
  14. package/.agents/skills/embedded-captions/dna/ink.json +65 -0
  15. package/.agents/skills/embedded-captions/dna/keynote.json +62 -0
  16. package/.agents/skills/embedded-captions/dna/loud.json +75 -0
  17. package/.agents/skills/embedded-captions/dna/neon.json +58 -0
  18. package/.agents/skills/embedded-captions/dna/velocity.json +89 -0
  19. package/.agents/skills/embedded-captions/modes/cinematic/README.md +48 -0
  20. package/.agents/skills/embedded-captions/modes/cinematic/_archive/champion/spec.md +98 -0
  21. package/.agents/skills/embedded-captions/modes/cinematic/_archive/champion/template.html +188 -0
  22. package/.agents/skills/embedded-captions/modes/cinematic/_archive/memory-wall/spec.md +134 -0
  23. package/.agents/skills/embedded-captions/modes/cinematic/_archive/memory-wall/template.html +179 -0
  24. package/.agents/skills/embedded-captions/modes/cinematic/_archive/portrait-header/spec.md +81 -0
  25. package/.agents/skills/embedded-captions/modes/cinematic/_archive/portrait-header/template.html +170 -0
  26. package/.agents/skills/embedded-captions/modes/cinematic/cinematic-cream/spec.md +16 -0
  27. package/.agents/skills/embedded-captions/modes/cinematic/cinematic-cream/template.html +226 -0
  28. package/.agents/skills/embedded-captions/modes/cinematic/engine.html +355 -0
  29. package/.agents/skills/embedded-captions/modes/standard/_anatomy.md +233 -0
  30. package/.agents/skills/embedded-captions/modes/standard/_motion.md +192 -0
  31. package/.agents/skills/embedded-captions/modes/standard/fonts/build-fonts-css.cjs +97 -0
  32. package/.agents/skills/embedded-captions/references/aesthetic-principles.md +168 -0
  33. package/.agents/skills/embedded-captions/references/anti-patterns.md +188 -0
  34. package/.agents/skills/embedded-captions/references/bespoke-vs-presets.md +178 -0
  35. package/.agents/skills/embedded-captions/references/caption-grouping.md +130 -0
  36. package/.agents/skills/embedded-captions/references/composition-craft.md +522 -0
  37. package/.agents/skills/embedded-captions/references/direction-catalog.md +138 -0
  38. package/.agents/skills/embedded-captions/references/example-renders/champion.html +371 -0
  39. package/.agents/skills/embedded-captions/references/example-renders/memory-wall.html +334 -0
  40. package/.agents/skills/embedded-captions/references/failure-modes.md +145 -0
  41. package/.agents/skills/embedded-captions/references/layout-heuristics.md +238 -0
  42. package/.agents/skills/embedded-captions/references/motion-vocabulary.md +148 -0
  43. package/.agents/skills/embedded-captions/references/rail.md +68 -0
  44. package/.agents/skills/embedded-captions/references/reference-bar.md +47 -0
  45. package/.agents/skills/embedded-captions/references/scene-types.md +94 -0
  46. package/.agents/skills/embedded-captions/references/test-set.md +66 -0
  47. package/.agents/skills/embedded-captions/references/typographic-moves.md +266 -0
  48. package/.agents/skills/embedded-captions/references/typography-presets.md +63 -0
  49. package/.agents/skills/embedded-captions/scripts/audio-envelope.cjs +95 -0
  50. package/.agents/skills/embedded-captions/scripts/check-occlusion.cjs +250 -0
  51. package/.agents/skills/embedded-captions/scripts/check-overflow.cjs +194 -0
  52. package/.agents/skills/embedded-captions/scripts/check-rail-climax.cjs +233 -0
  53. package/.agents/skills/embedded-captions/scripts/check-timing.cjs +173 -0
  54. package/.agents/skills/embedded-captions/scripts/fill-timings.cjs +124 -0
  55. package/.agents/skills/embedded-captions/scripts/fit-fonts.cjs +158 -0
  56. package/.agents/skills/embedded-captions/scripts/gen-stroke-path.py +46 -0
  57. package/.agents/skills/embedded-captions/scripts/inject-fonts.cjs +169 -0
  58. package/.agents/skills/embedded-captions/scripts/lib-dna.cjs +201 -0
  59. package/.agents/skills/embedded-captions/scripts/make-cinematic.cjs +1205 -0
  60. package/.agents/skills/embedded-captions/scripts/make-composition.cjs +404 -0
  61. package/.agents/skills/embedded-captions/scripts/make-theme.cjs +8781 -0
  62. package/.agents/skills/embedded-captions/scripts/matte.cjs +294 -0
  63. package/.agents/skills/embedded-captions/scripts/measure-layout.cjs +293 -0
  64. package/.agents/skills/embedded-captions/scripts/prepare.sh +39 -0
  65. package/.agents/skills/embedded-captions/scripts/preview-frames.cjs +268 -0
  66. package/.agents/skills/embedded-captions/scripts/render-and-composite.sh +509 -0
  67. package/.agents/skills/embedded-captions/scripts/render-theme.sh +34 -0
  68. package/.agents/skills/embedded-captions/scripts/safe-zones.cjs +813 -0
  69. package/.agents/skills/embedded-captions/scripts/transcribe.cjs +349 -0
  70. package/.agents/skills/embedded-captions/themes/PORTING.md +49 -0
  71. package/.agents/skills/embedded-captions/themes/README.md +244 -0
  72. package/.agents/skills/embedded-captions/themes/anchor.json +27 -0
  73. package/.agents/skills/embedded-captions/themes/arcade.json +51 -0
  74. package/.agents/skills/embedded-captions/themes/aurora.json +41 -0
  75. package/.agents/skills/embedded-captions/themes/biolume.json +35 -0
  76. package/.agents/skills/embedded-captions/themes/brush.json +74 -0
  77. package/.agents/skills/embedded-captions/themes/chalkboard.json +52 -0
  78. package/.agents/skills/embedded-captions/themes/dossier.json +56 -0
  79. package/.agents/skills/embedded-captions/themes/graffiti.json +62 -0
  80. package/.agents/skills/embedded-captions/themes/hologram.json +41 -0
  81. package/.agents/skills/embedded-captions/themes/inkwater.json +43 -0
  82. package/.agents/skills/embedded-captions/themes/laser.json +46 -0
  83. package/.agents/skills/embedded-captions/themes/lastpage.json +33 -0
  84. package/.agents/skills/embedded-captions/themes/neonsign.json +53 -0
  85. package/.agents/skills/embedded-captions/themes/nightcity.json +56 -0
  86. package/.agents/skills/embedded-captions/themes/ordnance.json +54 -0
  87. package/.agents/skills/embedded-captions/themes/papercut.json +37 -0
  88. package/.agents/skills/embedded-captions/themes/popup.json +46 -0
  89. package/.agents/skills/embedded-captions/themes/ransom.json +62 -0
  90. package/.agents/skills/embedded-captions/themes/scoreboard.json +54 -0
  91. package/.agents/skills/embedded-captions/themes/spectrum.json +46 -0
  92. package/.agents/skills/embedded-captions/themes/stardust.json +29 -0
  93. package/.agents/skills/embedded-captions/themes/stomp.json +43 -0
  94. package/.agents/skills/embedded-captions/themes/terminal.json +49 -0
  95. package/.agents/skills/embedded-captions/themes/thunder.json +44 -0
  96. package/.agents/skills/embedded-captions/themes/transit.json +44 -0
  97. package/.agents/skills/embedded-captions/themes/vhs.json +47 -0
  98. package/.agents/skills/faceless-explainer/SKILL.md +209 -0
  99. package/.agents/skills/faceless-explainer/references/cut-catalog.md +215 -0
  100. package/.agents/skills/faceless-explainer/references/motion-language.md +156 -0
  101. package/.agents/skills/faceless-explainer/references/story-design.md +248 -0
  102. package/.agents/skills/faceless-explainer/references/visual-design.md +146 -0
  103. package/.agents/skills/faceless-explainer/scripts/assemble-index.mjs +570 -0
  104. package/.agents/skills/faceless-explainer/scripts/audio.mjs +260 -0
  105. package/.agents/skills/faceless-explainer/scripts/build-frame.mjs +536 -0
  106. package/.agents/skills/faceless-explainer/scripts/captions.mjs +508 -0
  107. package/.agents/skills/faceless-explainer/scripts/lib/assets.mjs +55 -0
  108. package/.agents/skills/faceless-explainer/scripts/lib/dimensions.mjs +45 -0
  109. package/.agents/skills/faceless-explainer/scripts/lib/storyboard.mjs +249 -0
  110. package/.agents/skills/faceless-explainer/scripts/lib/tokens.mjs +204 -0
  111. package/.agents/skills/faceless-explainer/scripts/lib/transition-registry.mjs +38 -0
  112. package/.agents/skills/faceless-explainer/scripts/lib/transitions.json +71 -0
  113. package/.agents/skills/faceless-explainer/scripts/transitions.mjs +320 -0
  114. package/.agents/skills/faceless-explainer/sub-agents/frame-worker.md +75 -0
  115. package/.agents/skills/flockposter/LICENSE +17 -0
  116. package/.agents/skills/flockposter/README.md +72 -0
  117. package/.agents/skills/flockposter/SKILL.md +562 -0
  118. package/.agents/skills/flockposter/examples/cli-instagram-reel.sh +21 -0
  119. package/.agents/skills/flockposter/examples/cli-multi-platform.json +69 -0
  120. package/.agents/skills/flockposter/examples/cli-tiktok-upload-only.sh +22 -0
  121. package/.agents/skills/flockposter/examples/cli-x-thread.sh +17 -0
  122. package/.agents/skills/flockposter/examples/cli-youtube-short.sh +21 -0
  123. package/.agents/skills/flockposter/examples/public-api-instagram-reel.json +28 -0
  124. package/.agents/skills/general-video/SKILL.md +143 -0
  125. package/.agents/skills/hyperframes/SKILL.md +160 -0
  126. package/.agents/skills/hyperframes-animation/SKILL.md +84 -0
  127. package/.agents/skills/hyperframes-animation/adapters/animate-text.md +64 -0
  128. package/.agents/skills/hyperframes-animation/adapters/animejs.md +114 -0
  129. package/.agents/skills/hyperframes-animation/adapters/css-animations.md +143 -0
  130. package/.agents/skills/hyperframes-animation/adapters/gsap-easing-and-stagger.md +118 -0
  131. package/.agents/skills/hyperframes-animation/adapters/gsap-timeline-and-labels.md +96 -0
  132. package/.agents/skills/hyperframes-animation/adapters/gsap-transforms-and-perf.md +96 -0
  133. package/.agents/skills/hyperframes-animation/adapters/gsap.md +105 -0
  134. package/.agents/skills/hyperframes-animation/adapters/html-in-canvas-patterns.md +507 -0
  135. package/.agents/skills/hyperframes-animation/adapters/lottie.md +117 -0
  136. package/.agents/skills/hyperframes-animation/adapters/three.md +131 -0
  137. package/.agents/skills/hyperframes-animation/adapters/typegpu.md +178 -0
  138. package/.agents/skills/hyperframes-animation/adapters/waapi.md +101 -0
  139. package/.agents/skills/hyperframes-animation/blueprints/comparison-split.md +27 -0
  140. package/.agents/skills/hyperframes-animation/blueprints/constellation-hub.md +44 -0
  141. package/.agents/skills/hyperframes-animation/blueprints/cta-morph-press.md +28 -0
  142. package/.agents/skills/hyperframes-animation/blueprints/cursor-ui-demo.md +57 -0
  143. package/.agents/skills/hyperframes-animation/blueprints/dataviz-countup.md +46 -0
  144. package/.agents/skills/hyperframes-animation/blueprints/device-surface-showcase.md +53 -0
  145. package/.agents/skills/hyperframes-animation/blueprints/grid-card-assemble.md +66 -0
  146. package/.agents/skills/hyperframes-animation/blueprints/kinetic-type-beats.md +78 -0
  147. package/.agents/skills/hyperframes-animation/blueprints/logo-assemble-lockup.md +69 -0
  148. package/.agents/skills/hyperframes-animation/blueprints/overwhelm-surround.md +28 -0
  149. package/.agents/skills/hyperframes-animation/blueprints/spatial-pan-stations.md +37 -0
  150. package/.agents/skills/hyperframes-animation/blueprints/ticker-takeover.md +29 -0
  151. package/.agents/skills/hyperframes-animation/blueprints/titlecard-reveal.md +42 -0
  152. package/.agents/skills/hyperframes-animation/blueprints/typewriter-reveal.md +51 -0
  153. package/.agents/skills/hyperframes-animation/blueprints/video-text-pivot.md +30 -0
  154. package/.agents/skills/hyperframes-animation/blueprints-index.md +146 -0
  155. package/.agents/skills/hyperframes-animation/examples/assets/avatars/02.avif +0 -0
  156. package/.agents/skills/hyperframes-animation/examples/assets/brands/github.avif +0 -0
  157. package/.agents/skills/hyperframes-animation/examples/assets/brands/nvidia.avif +0 -0
  158. package/.agents/skills/hyperframes-animation/examples/assets/brands/visa.avif +0 -0
  159. package/.agents/skills/hyperframes-animation/examples/assets/brands/zoominfo.avif +0 -0
  160. package/.agents/skills/hyperframes-animation/examples/brand-reveal-assemble-zoom.html +382 -0
  161. package/.agents/skills/hyperframes-animation/examples/comparison-split-cards.html +649 -0
  162. package/.agents/skills/hyperframes-animation/examples/concept-demo-decode-pan.html +520 -0
  163. package/.agents/skills/hyperframes-animation/examples/cta-morph-press.html +468 -0
  164. package/.agents/skills/hyperframes-animation/examples/cta-orbit-collapse.html +1298 -0
  165. package/.agents/skills/hyperframes-animation/examples/demo-page-scroll-spotlight.html +759 -0
  166. package/.agents/skills/hyperframes-animation/examples/hook-counter-burst.html +729 -0
  167. package/.agents/skills/hyperframes-animation/examples/messaging-multi-phrase.html +352 -0
  168. package/.agents/skills/hyperframes-animation/examples/metric-video-text-pivot.html +779 -0
  169. package/.agents/skills/hyperframes-animation/examples/problem-mockup-overwhelm.html +1374 -0
  170. package/.agents/skills/hyperframes-animation/examples/proof-logo-chain.html +861 -0
  171. package/.agents/skills/hyperframes-animation/examples/takeover-ticker-displace.html +347 -0
  172. package/.agents/skills/hyperframes-animation/examples/workflow-approve-press.html +606 -0
  173. package/.agents/skills/hyperframes-animation/rules/3d-page-scroll.md +227 -0
  174. package/.agents/skills/hyperframes-animation/rules/3d-text-depth-layers.md +297 -0
  175. package/.agents/skills/hyperframes-animation/rules/ai-tracking-box.md +382 -0
  176. package/.agents/skills/hyperframes-animation/rules/ambient-glow-bloom.md +305 -0
  177. package/.agents/skills/hyperframes-animation/rules/asr-keyword-glow.md +286 -0
  178. package/.agents/skills/hyperframes-animation/rules/avatar-cloud-network.md +371 -0
  179. package/.agents/skills/hyperframes-animation/rules/camera-cursor-tracking.md +246 -0
  180. package/.agents/skills/hyperframes-animation/rules/card-morph-anchor.md +267 -0
  181. package/.agents/skills/hyperframes-animation/rules/center-outward-expansion.md +227 -0
  182. package/.agents/skills/hyperframes-animation/rules/context-sensitive-cursor.md +257 -0
  183. package/.agents/skills/hyperframes-animation/rules/coordinate-target-zoom.md +332 -0
  184. package/.agents/skills/hyperframes-animation/rules/counting-dynamic-scale.md +283 -0
  185. package/.agents/skills/hyperframes-animation/rules/css-marker-patterns.md +373 -0
  186. package/.agents/skills/hyperframes-animation/rules/cursor-click-ripple.md +262 -0
  187. package/.agents/skills/hyperframes-animation/rules/depth-of-field-blur.md +313 -0
  188. package/.agents/skills/hyperframes-animation/rules/depth-scatter-assemble.md +303 -0
  189. package/.agents/skills/hyperframes-animation/rules/discrete-text-sequence.md +273 -0
  190. package/.agents/skills/hyperframes-animation/rules/dynamic-content-sequencing.md +307 -0
  191. package/.agents/skills/hyperframes-animation/rules/gsap-effects.md +299 -0
  192. package/.agents/skills/hyperframes-animation/rules/hacker-flip-3d.md +223 -0
  193. package/.agents/skills/hyperframes-animation/rules/kinetic-beat-slam.md +183 -0
  194. package/.agents/skills/hyperframes-animation/rules/motion-blur-streak.md +328 -0
  195. package/.agents/skills/hyperframes-animation/rules/multi-phase-camera.md +273 -0
  196. package/.agents/skills/hyperframes-animation/rules/orbit-3d-entry.md +301 -0
  197. package/.agents/skills/hyperframes-animation/rules/physics-press-reaction.md +350 -0
  198. package/.agents/skills/hyperframes-animation/rules/press-release-spring.md +296 -0
  199. package/.agents/skills/hyperframes-animation/rules/reactive-displacement.md +277 -0
  200. package/.agents/skills/hyperframes-animation/rules/scale-swap-transition.md +298 -0
  201. package/.agents/skills/hyperframes-animation/rules/sine-wave-loop.md +278 -0
  202. package/.agents/skills/hyperframes-animation/rules/split-tilt-cards.md +277 -0
  203. package/.agents/skills/hyperframes-animation/rules/spring-pop-entrance.md +273 -0
  204. package/.agents/skills/hyperframes-animation/rules/stat-bars-and-fills.md +156 -0
  205. package/.agents/skills/hyperframes-animation/rules/svg-icon-enrichment.md +329 -0
  206. package/.agents/skills/hyperframes-animation/rules/svg-path-draw.md +274 -0
  207. package/.agents/skills/hyperframes-animation/rules/vertical-spring-ticker.md +239 -0
  208. package/.agents/skills/hyperframes-animation/rules/viewport-change.md +349 -0
  209. package/.agents/skills/hyperframes-animation/rules-index.md +86 -0
  210. package/.agents/skills/hyperframes-animation/scripts/animation-map.mjs +606 -0
  211. package/.agents/skills/hyperframes-animation/scripts/package-loader.mjs +288 -0
  212. package/.agents/skills/hyperframes-animation/scripts/package-loader.test.mjs +62 -0
  213. package/.agents/skills/hyperframes-animation/techniques.md +507 -0
  214. package/.agents/skills/hyperframes-animation/transitions/TRANSITION-REGISTRY.md +167 -0
  215. package/.agents/skills/hyperframes-animation/transitions/catalog.md +127 -0
  216. package/.agents/skills/hyperframes-animation/transitions/css-3d.md +12 -0
  217. package/.agents/skills/hyperframes-animation/transitions/css-blur.md +51 -0
  218. package/.agents/skills/hyperframes-animation/transitions/css-cover.md +43 -0
  219. package/.agents/skills/hyperframes-animation/transitions/css-destruction.md +95 -0
  220. package/.agents/skills/hyperframes-animation/transitions/css-dissolve.md +66 -0
  221. package/.agents/skills/hyperframes-animation/transitions/css-distortion.md +45 -0
  222. package/.agents/skills/hyperframes-animation/transitions/css-grid.md +10 -0
  223. package/.agents/skills/hyperframes-animation/transitions/css-light.md +49 -0
  224. package/.agents/skills/hyperframes-animation/transitions/css-mechanical.md +30 -0
  225. package/.agents/skills/hyperframes-animation/transitions/css-other.md +25 -0
  226. package/.agents/skills/hyperframes-animation/transitions/css-push.md +41 -0
  227. package/.agents/skills/hyperframes-animation/transitions/css-radial.md +37 -0
  228. package/.agents/skills/hyperframes-animation/transitions/css-scale.md +24 -0
  229. package/.agents/skills/hyperframes-animation/transitions/overview.md +153 -0
  230. package/.agents/skills/hyperframes-cli/SKILL.md +113 -0
  231. package/.agents/skills/hyperframes-cli/references/doctor-browser.md +45 -0
  232. package/.agents/skills/hyperframes-cli/references/init-and-scaffold.md +55 -0
  233. package/.agents/skills/hyperframes-cli/references/lambda.md +132 -0
  234. package/.agents/skills/hyperframes-cli/references/lint-validate-inspect.md +121 -0
  235. package/.agents/skills/hyperframes-cli/references/preview-render.md +149 -0
  236. package/.agents/skills/hyperframes-cli/references/upgrade-info-misc.md +75 -0
  237. package/.agents/skills/hyperframes-core/SKILL.md +78 -0
  238. package/.agents/skills/hyperframes-core/references/composition-patterns.md +263 -0
  239. package/.agents/skills/hyperframes-core/references/data-attributes.md +70 -0
  240. package/.agents/skills/hyperframes-core/references/determinism-rules.md +68 -0
  241. package/.agents/skills/hyperframes-core/references/full-screen-motion.md +62 -0
  242. package/.agents/skills/hyperframes-core/references/minimal-composition.md +66 -0
  243. package/.agents/skills/hyperframes-core/references/script-format.md +49 -0
  244. package/.agents/skills/hyperframes-core/references/storyboard-format.md +95 -0
  245. package/.agents/skills/hyperframes-core/references/sub-compositions.md +237 -0
  246. package/.agents/skills/hyperframes-core/references/subagent-dispatch.md +41 -0
  247. package/.agents/skills/hyperframes-core/references/tailwind.md +125 -0
  248. package/.agents/skills/hyperframes-core/references/tracks-and-clips.md +76 -0
  249. package/.agents/skills/hyperframes-core/references/variables-and-media.md +90 -0
  250. package/.agents/skills/hyperframes-creative/SKILL.md +70 -0
  251. package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/FRAME.md +289 -0
  252. package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/caption-skin.html +221 -0
  253. package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/frame-showcase.html +1214 -0
  254. package/.agents/skills/hyperframes-creative/frame-presets/blockframe/FRAME.md +275 -0
  255. package/.agents/skills/hyperframes-creative/frame-presets/blockframe/caption-skin.html +224 -0
  256. package/.agents/skills/hyperframes-creative/frame-presets/blockframe/frame-showcase.html +1347 -0
  257. package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/FRAME.md +307 -0
  258. package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/caption-skin.html +223 -0
  259. package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/frame-showcase.html +1372 -0
  260. package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/FRAME.md +275 -0
  261. package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/caption-skin.html +230 -0
  262. package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/frame-showcase.html +1140 -0
  263. package/.agents/skills/hyperframes-creative/frame-presets/broadside/FRAME.md +284 -0
  264. package/.agents/skills/hyperframes-creative/frame-presets/broadside/caption-skin.html +225 -0
  265. package/.agents/skills/hyperframes-creative/frame-presets/broadside/frame-showcase.html +1166 -0
  266. package/.agents/skills/hyperframes-creative/frame-presets/capsule/FRAME.md +284 -0
  267. package/.agents/skills/hyperframes-creative/frame-presets/capsule/caption-skin.html +224 -0
  268. package/.agents/skills/hyperframes-creative/frame-presets/capsule/frame-showcase.html +1465 -0
  269. package/.agents/skills/hyperframes-creative/frame-presets/cartesian/FRAME.md +278 -0
  270. package/.agents/skills/hyperframes-creative/frame-presets/cartesian/caption-skin.html +222 -0
  271. package/.agents/skills/hyperframes-creative/frame-presets/cartesian/frame-showcase.html +1263 -0
  272. package/.agents/skills/hyperframes-creative/frame-presets/claude/FRAME.md +277 -0
  273. package/.agents/skills/hyperframes-creative/frame-presets/claude/caption-skin.html +224 -0
  274. package/.agents/skills/hyperframes-creative/frame-presets/claude/frame-showcase.html +1439 -0
  275. package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/FRAME.md +265 -0
  276. package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/caption-skin.html +238 -0
  277. package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/frame-showcase.html +1468 -0
  278. package/.agents/skills/hyperframes-creative/frame-presets/coral/FRAME.md +314 -0
  279. package/.agents/skills/hyperframes-creative/frame-presets/coral/caption-skin.html +226 -0
  280. package/.agents/skills/hyperframes-creative/frame-presets/coral/frame-showcase.html +1268 -0
  281. package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/FRAME.md +333 -0
  282. package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/caption-skin.html +227 -0
  283. package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/frame-showcase.html +1330 -0
  284. package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/FRAME.md +274 -0
  285. package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/caption-skin.html +230 -0
  286. package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/frame-showcase.html +1444 -0
  287. package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/FRAME.md +265 -0
  288. package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/caption-skin.html +223 -0
  289. package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/frame-showcase.html +1273 -0
  290. package/.agents/skills/hyperframes-creative/palettes/bold-energetic.md +14 -0
  291. package/.agents/skills/hyperframes-creative/palettes/clean-corporate.md +14 -0
  292. package/.agents/skills/hyperframes-creative/palettes/dark-premium.md +14 -0
  293. package/.agents/skills/hyperframes-creative/palettes/jewel-rich.md +14 -0
  294. package/.agents/skills/hyperframes-creative/palettes/monochrome.md +14 -0
  295. package/.agents/skills/hyperframes-creative/palettes/nature-earth.md +14 -0
  296. package/.agents/skills/hyperframes-creative/palettes/neon-electric.md +14 -0
  297. package/.agents/skills/hyperframes-creative/palettes/pastel-soft.md +14 -0
  298. package/.agents/skills/hyperframes-creative/palettes/warm-editorial.md +14 -0
  299. package/.agents/skills/hyperframes-creative/references/audio-reactive.md +76 -0
  300. package/.agents/skills/hyperframes-creative/references/beat-direction.md +164 -0
  301. package/.agents/skills/hyperframes-creative/references/composition-patterns.md +199 -0
  302. package/.agents/skills/hyperframes-creative/references/data-in-motion.md +19 -0
  303. package/.agents/skills/hyperframes-creative/references/design-adherence.md +19 -0
  304. package/.agents/skills/hyperframes-creative/references/design-picker.md +123 -0
  305. package/.agents/skills/hyperframes-creative/references/design-spec.md +55 -0
  306. package/.agents/skills/hyperframes-creative/references/house-style.md +73 -0
  307. package/.agents/skills/hyperframes-creative/references/motion-principles.md +150 -0
  308. package/.agents/skills/hyperframes-creative/references/narration.md +92 -0
  309. package/.agents/skills/hyperframes-creative/references/prompt-expansion.md +68 -0
  310. package/.agents/skills/hyperframes-creative/references/typography.md +218 -0
  311. package/.agents/skills/hyperframes-creative/references/video-composition.md +63 -0
  312. package/.agents/skills/hyperframes-creative/references/visual-styles.md +457 -0
  313. package/.agents/skills/hyperframes-creative/scripts/contrast-report.mjs +353 -0
  314. package/.agents/skills/hyperframes-creative/scripts/extract-audio-data.py +188 -0
  315. package/.agents/skills/hyperframes-creative/scripts/package-loader.mjs +288 -0
  316. package/.agents/skills/hyperframes-creative/scripts/package-loader.test.mjs +62 -0
  317. package/.agents/skills/hyperframes-creative/templates/design-picker.html +1432 -0
  318. package/.agents/skills/hyperframes-keyframes/SKILL.md +237 -0
  319. package/.agents/skills/hyperframes-keyframes/agents/openai.yaml +4 -0
  320. package/.agents/skills/hyperframes-keyframes/references/keyframe-patterns.md +106 -0
  321. package/.agents/skills/motion-graphics/SKILL.md +172 -0
  322. package/.agents/skills/motion-graphics/agents/builder.md +40 -0
  323. package/.agents/skills/motion-graphics/agents/director.md +53 -0
  324. package/.agents/skills/motion-graphics/agents/finalize.md +17 -0
  325. package/.agents/skills/motion-graphics/catalog-map.md +40 -0
  326. package/.agents/skills/motion-graphics/categories/asset-fusion/module.md +37 -0
  327. package/.agents/skills/motion-graphics/categories/charts/module.md +21 -0
  328. package/.agents/skills/motion-graphics/categories/kinetic-type/module.md +19 -0
  329. package/.agents/skills/motion-graphics/categories/logo-reveal/module.md +17 -0
  330. package/.agents/skills/motion-graphics/categories/lower-thirds/module.md +16 -0
  331. package/.agents/skills/motion-graphics/categories/maps/bake-basemap.mjs +273 -0
  332. package/.agents/skills/motion-graphics/categories/maps/module.md +61 -0
  333. package/.agents/skills/motion-graphics/categories/news/module.md +56 -0
  334. package/.agents/skills/motion-graphics/categories/stat/module.md +21 -0
  335. package/.agents/skills/motion-graphics/categories/tweet/module.md +16 -0
  336. package/.agents/skills/motion-graphics/categories/webpage/module.md +21 -0
  337. package/.agents/skills/motion-graphics/grounding/PROTOCOL.md +64 -0
  338. package/.agents/skills/motion-graphics/grounding/locate.mjs +227 -0
  339. package/.agents/skills/motion-graphics/phases/source/guide.md +21 -0
  340. package/.agents/skills/motion-graphics/references/builder-contract.md +38 -0
  341. package/.agents/skills/motion-graphics/references/motion-vocabulary.md +43 -0
  342. package/.agents/skills/motion-graphics/references/shot-plan-ir.md +54 -0
  343. package/.agents/skills/motion-graphics/samples/asset-fusion/_ref-circle-highlight.html +306 -0
  344. package/.agents/skills/product-launch-video/SKILL.md +208 -0
  345. package/.agents/skills/product-launch-video/references/cut-catalog.md +220 -0
  346. package/.agents/skills/product-launch-video/references/motion-language.md +156 -0
  347. package/.agents/skills/product-launch-video/references/story-design.md +373 -0
  348. package/.agents/skills/product-launch-video/references/visual-design.md +126 -0
  349. package/.agents/skills/product-launch-video/scripts/assemble-index.mjs +570 -0
  350. package/.agents/skills/product-launch-video/scripts/audio.mjs +260 -0
  351. package/.agents/skills/product-launch-video/scripts/build-frame.mjs +536 -0
  352. package/.agents/skills/product-launch-video/scripts/captions.mjs +508 -0
  353. package/.agents/skills/product-launch-video/scripts/lib/assets.mjs +55 -0
  354. package/.agents/skills/product-launch-video/scripts/lib/dimensions.mjs +45 -0
  355. package/.agents/skills/product-launch-video/scripts/lib/pad-frame-duration.mjs +36 -0
  356. package/.agents/skills/product-launch-video/scripts/lib/pad-frame-duration.test.mjs +76 -0
  357. package/.agents/skills/product-launch-video/scripts/lib/storyboard.mjs +249 -0
  358. package/.agents/skills/product-launch-video/scripts/lib/tokens.mjs +204 -0
  359. package/.agents/skills/product-launch-video/scripts/lib/transition-registry.mjs +38 -0
  360. package/.agents/skills/product-launch-video/scripts/lib/transitions.json +71 -0
  361. package/.agents/skills/product-launch-video/scripts/stage-assets.mjs +39 -0
  362. package/.agents/skills/product-launch-video/scripts/transitions.mjs +327 -0
  363. package/.agents/skills/product-launch-video/sub-agents/frame-worker.md +75 -0
  364. package/.agents/skills/slideshow/SKILL.md +550 -0
  365. package/.agents/skills/slideshow/references/standalone-harness.md +1020 -0
  366. package/.agents/skills/talking-head-recut/NOTICE.md +39 -0
  367. package/.agents/skills/talking-head-recut/SKILL.md +1195 -0
  368. package/.agents/skills/talking-head-recut/assets/vendor/gsap.min.js +10 -0
  369. package/.agents/skills/talking-head-recut/references/DESIGN_INDEX.md +145 -0
  370. package/.agents/skills/talking-head-recut/references/frames/clean.html +92 -0
  371. package/.agents/skills/talking-head-recut/references/frames/hairline.html +171 -0
  372. package/.agents/skills/talking-head-recut/references/frames/polaroid.html +131 -0
  373. package/.agents/skills/talking-head-recut/references/layouts/overlay.html +164 -0
  374. package/.agents/skills/talking-head-recut/references/layouts/pip.html +143 -0
  375. package/.agents/skills/talking-head-recut/references/layouts/split.html +121 -0
  376. package/.agents/skills/talking-head-recut/references/layouts/stack.html +109 -0
  377. package/.agents/skills/talking-head-recut/references/styles/academic.html +172 -0
  378. package/.agents/skills/talking-head-recut/references/styles/audit.html +170 -0
  379. package/.agents/skills/talking-head-recut/references/styles/editorial.html +189 -0
  380. package/.agents/skills/talking-head-recut/references/styles/geom.html +181 -0
  381. package/.agents/skills/talking-head-recut/references/styles/minimal.html +130 -0
  382. package/.agents/skills/talking-head-recut/references/styles/spotlight.html +142 -0
  383. package/.agents/skills/talking-head-recut/references/styles/swiss.html +188 -0
  384. package/.agents/skills/talking-head-recut/references/styles/terminal.html +196 -0
  385. package/.agents/skills/talking-head-recut/references/styles/whiteboard.html +170 -0
  386. package/.agents/skills/talking-head-recut/references/styles/xhs.html +206 -0
  387. package/.agents/skills/vidfarm-media/SKILL.md +85 -0
  388. package/.agents/skills/vidfarm-media/references/tts.md +54 -0
  389. package/.agents/skills/vidfarm-media/scripts/audio.mjs +628 -0
  390. package/.agents/skills/vidfarm-media/scripts/wait-bgm.mjs +31 -0
  391. package/.agents/skills/website-to-video/SKILL.md +145 -0
  392. package/.agents/skills/website-to-video/assets/sfx/CREDITS.md +35 -0
  393. package/.agents/skills/website-to-video/assets/sfx/manifest.json +97 -0
  394. package/.agents/skills/website-to-video/references/beat-builder-guide.md +263 -0
  395. package/.agents/skills/website-to-video/references/capabilities.md +713 -0
  396. package/.agents/skills/website-to-video/references/step-0-capture.md +55 -0
  397. package/.agents/skills/website-to-video/references/step-1-design.md +333 -0
  398. package/.agents/skills/website-to-video/references/step-2-brief.md +178 -0
  399. package/.agents/skills/website-to-video/references/step-3-storyboard.md +572 -0
  400. package/.agents/skills/website-to-video/references/step-4-vo.md +225 -0
  401. package/.agents/skills/website-to-video/references/step-5-build.md +483 -0
  402. package/.agents/skills/website-to-video/references/step-6-validate.md +341 -0
  403. package/.agents/skills/website-to-video/scripts/w2h-verify.mjs +759 -0
  404. package/SKILL.director.md +37 -0
  405. package/SKILL.platform.md +6 -1
  406. package/demo/dist/app.css +1 -1
  407. package/demo/dist/app.js +369 -85
  408. package/dist/src/app.js +630 -140
  409. package/dist/src/cli.js +529 -12
  410. package/dist/src/composition-runtime.js +262 -3
  411. package/dist/src/config.js +13 -0
  412. package/dist/src/devcli/doctor.js +192 -0
  413. package/dist/src/devcli/hyperframes-cli.js +118 -0
  414. package/dist/src/devcli/skills.js +476 -0
  415. package/dist/src/devcli/stills.js +249 -0
  416. package/dist/src/editor-chat.js +256 -0
  417. package/dist/src/frontend/homepage-client.js +50 -1
  418. package/dist/src/frontend/homepage-shared.js +197 -0
  419. package/dist/src/frontend/homepage-store.js +10 -1
  420. package/dist/src/frontend/homepage-view.js +43 -5
  421. package/dist/src/homepage.js +51 -18
  422. package/dist/src/hyperframes/composition.js +5 -5
  423. package/dist/src/services/composition-lint.js +199 -0
  424. package/dist/src/services/hyperframes.js +67 -13
  425. package/dist/src/services/serverless-records.js +9 -0
  426. package/package.json +2 -1
  427. package/public/assets/homepage-client-app.js +23 -23
package/dist/src/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
- import { createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
2
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { Readable } from "node:stream";
5
5
  import { pipeline as streamPipeline } from "node:stream/promises";
@@ -22,6 +22,7 @@ import { assertForkAccess, resolveForkAccess, ForkAccessDeniedError, ForkNotFoun
22
22
  import { writeForkManifest } from "./services/fork-manifest.js";
23
23
  import { renderHelpPage } from "./help-page.js";
24
24
  import { renderHomepage } from "./homepage.js";
25
+ import { POPULAR_INSPIRATION_GROUPS } from "./frontend/homepage-shared.js";
25
26
  import { COMPOSITION_RUNTIME_SCRIPT_BODY } from "./composition-runtime.js";
26
27
  import { CAPTION_ANIM_CSS, CAPTION_STYLE_MARKER, KEN_BURNS_CSS, KEN_BURNS_STYLE_MARKER, TRANSITION_CSS, TRANSITION_STYLE_MARKER, upgradeLegacyTransitionCss } from "./hyperframes/composition.js";
27
28
  import { normalizeCompositionZIndexHtml, sanitizeCompositionHtml } from "./services/composition-sanitize.js";
@@ -35,7 +36,7 @@ import { resolveRootFileCandidates } from "./lib/package-root.js";
35
36
  import { stripTrackingParams } from "./lib/url-clean.js";
36
37
  import { addSeconds, nowIso } from "./lib/time.js";
37
38
  import { templateRegistry } from "./registry.js";
38
- import { buildEditorChatSystemInstruction, compactEditorChatModelMessages, defaultEditorChatModel, EDITOR_CHAT_PROVIDER_PRIORITY, extractEditorChatCompositionBrief, formatEditorChatCurrentDateTime, toEditorChatModelMessages } from "./editor-chat.js";
39
+ import { buildAsyncJobHandoffText, buildEditorChatGuardrailNotice, buildEditorChatSystemInstruction, collectVerifiedHttpJobIds, compactEditorChatModelMessages, createLoadSkillTool, createStreamingJobIdRedactor, defaultEditorChatModel, EDITOR_CHAT_PROVIDER_PRIORITY, extractEditorChatCompositionBrief, formatEditorChatCurrentDateTime, lintReplaceCompositionAction, readAsyncJobHandoff, toEditorChatModelMessages } from "./editor-chat.js";
39
40
  import { apiCallHistoryStore } from "./services/api-call-history.js";
40
41
  import { AuthService } from "./services/auth.js";
41
42
  import { BillingService } from "./services/billing.js";
@@ -54,12 +55,12 @@ import { loadSpeechAudioLocally, MAX_LOCAL_TRANSCRIBE_AUDIO_BYTES } from "./devc
54
55
  import { DescribeExecutionCommand, SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
55
56
  import { clipRecords, makeUserPreset } from "./services/clip-records.js";
56
57
  import { matchScenesToClips, searchUserClips, vectorIndex } from "./services/clip-search.js";
57
- import { BUILTIN_PRESETS, buildEffectiveGuidance, ClipModelClient, cosineSimilarity, detectLocalAgent, effectiveHuntDurationSec, estimateScanCostFromDuration, hasFfmpeg, LocalAgentClipClient, normalizeCropFocus, normalizeHuntSpec, parseClipHuntPrompt, probeVideo, resolveDurationBand, scanVideo } from "./services/clip-curation/index.js";
58
+ import { BUILTIN_PRESETS, buildClipEmbeddingText, buildEffectiveGuidance, ClipModelClient, cosineSimilarity, detectLocalAgent, effectiveHuntDurationSec, estimateScanCostFromDuration, hasFfmpeg, LocalAgentClipClient, normalizeCropFocus, normalizeHuntSpec, parseClipHuntPrompt, probeVideo, resolveDurationBand, scanVideo } from "./services/clip-curation/index.js";
58
59
  import { analyzeCastMembers, annotateScenesFromSource, buildSmartDecomposedHtml, HyperframesService, smartDecomposeVideo, stampCompositionIdentity } from "./services/hyperframes.js";
59
60
  import { buildPendingSceneAnnotationsState } from "./services/scene-annotations.js";
60
61
  import { createPrimitiveJobContext } from "./primitive-context.js";
61
62
  import { buildPendingCastState, castMemberFromRaw, extractCastStill, isolateCastMember } from "./services/cast.js";
62
- import { MediaDurationExceededError, probeMediaAsset } from "./services/media-processing.js";
63
+ import { MediaDurationExceededError, probeMediaAsset, trimVideoAsset } from "./services/media-processing.js";
63
64
  import { estimateGhostcutCostUsd, isGhostcutConfigured, mirrorGhostcutVideoToStorage, pollStatus as pollGhostcutStatus, submitSubtitleRemoval } from "./services/ghostcut.js";
64
65
  import { hasUpstreamKey, isUpstreamEnabled, proxyRequestToUpstream, seedFromUpstream, upstreamHost, upstreamJson } from "./services/upstream.js";
65
66
  import { TemplateSourceService } from "./services/template-sources.js";
@@ -3609,7 +3610,12 @@ function normalizeEditorChatPath(input) {
3609
3610
  // and NEVER runs GhostCut on the long-form source — "no text" is a
3610
3611
  // scene-selection filter; caption removal stays a per-finished-clip
3611
3612
  // primitive (/api/v1/primitives/videos/remove-captions).
3612
- CLIPS_PREFIX
3613
+ CLIPS_PREFIX,
3614
+ // Public skill knowledge: the vendored skill packs (load_skill fetches
3615
+ // these too) and per-template SKILL.md manifests. Read-only markdown.
3616
+ "/skill/",
3617
+ "/skill-pack/",
3618
+ "/discover/skills/"
3613
3619
  ];
3614
3620
  const parsed = trimmed.startsWith("http://") || trimmed.startsWith("https://")
3615
3621
  ? new URL(trimmed)
@@ -4463,95 +4469,14 @@ function createEditorActionTool() {
4463
4469
  explanation: z.string().describe("One-sentence reason shown in the UI.")
4464
4470
  }),
4465
4471
  execute: async (args) => {
4472
+ const linted = lintReplaceCompositionAction(args);
4473
+ if (linted) {
4474
+ return linted;
4475
+ }
4466
4476
  return { ...args };
4467
4477
  }
4468
4478
  });
4469
4479
  }
4470
- const JOB_ID_PATTERN = /\bjob_[a-z0-9]+\b/gi;
4471
- const HTTP_TOOL_CLAIM_PATTERN = /\b(?:http_request|REST API call|API call|GET\s+https?:\/\/|POST\s+https?:\/\/|GET\s+\/api\/|POST\s+\/api\/)/i;
4472
- function collectJobIdsFromUnknown(value, output = new Set()) {
4473
- if (typeof value === "string") {
4474
- for (const match of value.matchAll(JOB_ID_PATTERN)) {
4475
- output.add(match[0]);
4476
- }
4477
- return output;
4478
- }
4479
- if (Array.isArray(value)) {
4480
- for (const entry of value) {
4481
- collectJobIdsFromUnknown(entry, output);
4482
- }
4483
- return output;
4484
- }
4485
- if (value && typeof value === "object") {
4486
- for (const entry of Object.values(value)) {
4487
- collectJobIdsFromUnknown(entry, output);
4488
- }
4489
- }
4490
- return output;
4491
- }
4492
- function collectVerifiedHttpJobIds(toolResult) {
4493
- if (!toolResult || typeof toolResult !== "object") {
4494
- return new Set();
4495
- }
4496
- const record = toolResult;
4497
- return collectJobIdsFromUnknown(record.response?.body ?? record.result?.body);
4498
- }
4499
- function readAsyncJobHandoff(output) {
4500
- if (!output || typeof output !== "object") {
4501
- return null;
4502
- }
4503
- const record = output;
4504
- const body = record.response?.body;
4505
- if (!body || typeof body !== "object") {
4506
- return null;
4507
- }
4508
- const job = body;
4509
- if (typeof job.job_id !== "string" || typeof job.status !== "string") {
4510
- return null;
4511
- }
4512
- if (job.status === "succeeded" || job.status === "failed" || job.status === "cancelled") {
4513
- return null;
4514
- }
4515
- return {
4516
- jobId: job.job_id,
4517
- tracer: typeof job.tracer === "string" ? job.tracer : null,
4518
- status: job.status,
4519
- isNewQueuedJob: record.request?.method === "POST" && record.response?.status === 202
4520
- };
4521
- }
4522
- function buildAsyncJobHandoffText(input) {
4523
- if (!input.isNewQueuedJob) {
4524
- return `Render \`${input.jobId}\` is ${input.status}.`;
4525
- }
4526
- return [
4527
- `Started render \`${input.jobId}\`${input.tracer ? ` with tracer \`${input.tracer}\`` : ""}.`,
4528
- "If it takes a while, ask for an update later and I can check it for you."
4529
- ].join("\n\n");
4530
- }
4531
- function buildEditorChatGuardrailNotice(input) {
4532
- const mentionedJobIds = collectJobIdsFromUnknown(input.assistantText);
4533
- const unverifiedJobIds = [...mentionedJobIds].filter((jobId) => !input.verifiedJobIds.has(jobId));
4534
- const notices = [];
4535
- if (input.redactedJobIds.size) {
4536
- notices.push("Guardrail: I withheld unverified job ID text because it did not come from a visible HTTP response card.");
4537
- }
4538
- else if (unverifiedJobIds.length) {
4539
- notices.push("Guardrail: I mentioned an unverified job ID. Ignore it unless it appears in a visible HTTP response card.");
4540
- }
4541
- if (!input.hasHttpRequestResult && HTTP_TOOL_CLAIM_PATTERN.test(input.assistantText)) {
4542
- notices.push("Guardrail: no `http_request` tool result was recorded for this response, so any claimed REST/API call should be treated as not executed.");
4543
- }
4544
- return notices.length ? `\n\n${notices.join("\n\n")}` : "";
4545
- }
4546
- function redactUnverifiedJobIds(input) {
4547
- return input.text.replace(JOB_ID_PATTERN, (jobId) => {
4548
- if (input.verifiedJobIds.has(jobId)) {
4549
- return jobId;
4550
- }
4551
- input.redactedJobIds.add(jobId);
4552
- return "[unverified job id]";
4553
- });
4554
- }
4555
4480
  function normalizeHistoryAttachments(attachments) {
4556
4481
  return Array.isArray(attachments)
4557
4482
  ? attachments.map((attachment) => ({
@@ -4595,6 +4520,10 @@ async function streamEditorChatAgent(input, send) {
4595
4520
  vidfarmApiKey: input.vidfarmApiKey,
4596
4521
  abortSignal: input.abortSignal
4597
4522
  }),
4523
+ load_skill: createLoadSkillTool({
4524
+ publicBaseUrl: config.PUBLIC_BASE_URL,
4525
+ abortSignal: input.abortSignal
4526
+ }),
4598
4527
  frontend_action: createEditorFrontendActionTool({
4599
4528
  tracerRegistry
4600
4529
  }),
@@ -4621,6 +4550,7 @@ async function streamEditorChatAgent(input, send) {
4621
4550
  let hasHttpRequestResult = false;
4622
4551
  const verifiedJobIds = new Set();
4623
4552
  const redactedJobIds = new Set();
4553
+ const jobIdRedactor = createStreamingJobIdRedactor({ verifiedJobIds, redactedJobIds });
4624
4554
  const toolExchangeOrder = [];
4625
4555
  const toolExchangeMap = new Map();
4626
4556
  const httpExchanges = [];
@@ -4635,16 +4565,14 @@ async function streamEditorChatAgent(input, send) {
4635
4565
  case "text-delta":
4636
4566
  assistantTextForAudit += part.text;
4637
4567
  {
4638
- const safeText = redactUnverifiedJobIds({
4639
- text: part.text,
4640
- verifiedJobIds,
4641
- redactedJobIds
4642
- });
4643
- assistantTextForUser += safeText;
4644
- send({
4645
- type: "text-delta",
4646
- delta: safeText
4647
- });
4568
+ const safeText = jobIdRedactor.push(part.text);
4569
+ if (safeText) {
4570
+ assistantTextForUser += safeText;
4571
+ send({
4572
+ type: "text-delta",
4573
+ delta: safeText
4574
+ });
4575
+ }
4648
4576
  }
4649
4577
  break;
4650
4578
  case "tool-call":
@@ -4738,6 +4666,14 @@ async function streamEditorChatAgent(input, send) {
4738
4666
  break;
4739
4667
  case "finish":
4740
4668
  {
4669
+ const heldText = jobIdRedactor.flush();
4670
+ if (heldText) {
4671
+ assistantTextForUser += heldText;
4672
+ send({
4673
+ type: "text-delta",
4674
+ delta: heldText
4675
+ });
4676
+ }
4741
4677
  const guardrailNotice = buildEditorChatGuardrailNotice({
4742
4678
  assistantText: assistantTextForAudit,
4743
4679
  verifiedJobIds,
@@ -5002,6 +4938,28 @@ app.get("/editor/:templateId", async (c) => {
5002
4938
  if (!forkRecord || forkRecord.deletedAt) {
5003
4939
  effectiveForkParam = null;
5004
4940
  }
4941
+ else if (customer) {
4942
+ // A caller who can VIEW but not EDIT this fork (e.g. a public template's
4943
+ // default fork opened via a shared deep link) must not be pinned onto it:
4944
+ // the editor would happily let them "edit", but every composition.html PUT
4945
+ // is 403'd and the change silently reverts on reload — the exact "the AI
4946
+ // said it changed the text but it didn't" failure. Drop the fork and fall
4947
+ // through to per-user resolution so they get (or mint) their own editable
4948
+ // copy instead.
4949
+ try {
4950
+ const access = await resolveForkAccess({
4951
+ forkId: effectiveForkParam,
4952
+ customerId: customer.id,
4953
+ shareToken: readShareToken(c) ?? undefined
4954
+ });
4955
+ if (!access.capabilities.canEdit) {
4956
+ effectiveForkParam = null;
4957
+ }
4958
+ }
4959
+ catch {
4960
+ effectiveForkParam = null;
4961
+ }
4962
+ }
5005
4963
  }
5006
4964
  if (!effectiveForkParam) {
5007
4965
  // Per-user default fork resolution: prefer caller's most-recently-updated
@@ -5018,8 +4976,14 @@ app.get("/editor/:templateId", async (c) => {
5018
4976
  if (mostRecent)
5019
4977
  targetForkId = mostRecent.id;
5020
4978
  }
5021
- if (!targetForkId)
4979
+ // Only fall back to the template's canonical default fork when the caller can
4980
+ // actually edit it — i.e. they own the template. A non-owner sent to the
4981
+ // default fork would land back on a read-only fork (and, since it's also the
4982
+ // page we just dropped, loop). Leaving targetForkId null routes them to the
4983
+ // bare editor, where the client mints their own editable fork on first save.
4984
+ if (!targetForkId && template.customerId === customer?.id) {
5022
4985
  targetForkId = template.defaultForkId;
4986
+ }
5023
4987
  if (targetForkId) {
5024
4988
  // Do NOT auto-attach a thread_id here: the caller's browser is the
5025
4989
  // source of truth for "which chat is active" via localStorage. Injecting
@@ -5127,6 +5091,11 @@ app.get("/discover/feed", async (c) => {
5127
5091
  }
5128
5092
  const templates = await serverlessRecords.listTemplatesFeed({ limit, query });
5129
5093
  for (const template of templates) {
5094
+ // Curated "Popular" picks live only under the Popular toggle
5095
+ // (GET /discover/popular) — keep them out of the continuously-grown Feed
5096
+ // so the two views stay distinct.
5097
+ if (typeof template.popularGroup === "string" && template.popularGroup.trim().length > 0)
5098
+ continue;
5130
5099
  const entry = buildTemplateHomepageEntry(template);
5131
5100
  if (entry)
5132
5101
  templateEntries.push(entry);
@@ -5143,7 +5112,7 @@ app.get("/discover/feed", async (c) => {
5143
5112
  const upstreamFeed = (await upstreamJson(`/discover/feed?${search.toString()}`)).json?.templates ?? [];
5144
5113
  const seen = new Set(templateEntries.map((entry) => entry.templateId));
5145
5114
  for (const entry of upstreamFeed) {
5146
- if (entry && typeof entry.templateId === "string" && !seen.has(entry.templateId)) {
5115
+ if (entry && typeof entry.templateId === "string" && !seen.has(entry.templateId) && !entry.popularGroup) {
5147
5116
  templateEntries.push(entry);
5148
5117
  seen.add(entry.templateId);
5149
5118
  }
@@ -5158,16 +5127,60 @@ app.get("/discover/feed", async (c) => {
5158
5127
  next_cursor: null
5159
5128
  });
5160
5129
  });
5130
+ // Curated "Popular" feed: the editorial list surfaced under the /discover
5131
+ // Popular toggle. Same template records as Feed, just the ones flagged with a
5132
+ // popularGroup — rendered client-side as autoplay TemplateCards grouped by that
5133
+ // label. Public + identical for everyone, so it caches shared. Mirrors upstream
5134
+ // (vidfarm serve) exactly like /discover/feed so a locally-empty box still sees
5135
+ // the cloud-seeded picks. Seed with POST /api/v1/admin/popular/seed.
5136
+ app.get("/discover/popular", async (c) => {
5137
+ const templateEntries = [];
5138
+ const seen = new Set();
5139
+ const popular = await serverlessRecords.listPopularTemplates();
5140
+ for (const template of popular) {
5141
+ const entry = buildTemplateHomepageEntry(template);
5142
+ if (entry) {
5143
+ templateEntries.push(entry);
5144
+ seen.add(entry.templateId);
5145
+ }
5146
+ }
5147
+ if (isUpstreamEnabled()) {
5148
+ const upstream = (await upstreamJson(`/discover/popular`)).json?.templates ?? [];
5149
+ for (const entry of upstream) {
5150
+ if (entry && typeof entry.templateId === "string" && !seen.has(entry.templateId) && entry.popularGroup) {
5151
+ templateEntries.push(entry);
5152
+ seen.add(entry.templateId);
5153
+ }
5154
+ }
5155
+ }
5156
+ // Stable editorial order: popularRank is a global sequence assigned by the
5157
+ // seed (group index * 1000 + item index), so a plain rank sort preserves both
5158
+ // group order and within-group order. Group label is only a tiebreak.
5159
+ templateEntries.sort((a, b) => {
5160
+ const rankCompare = (a.popularRank ?? 0) - (b.popularRank ?? 0);
5161
+ if (rankCompare !== 0)
5162
+ return rankCompare;
5163
+ return String(a.popularGroup ?? "").localeCompare(String(b.popularGroup ?? ""));
5164
+ });
5165
+ c.header("cache-control", "public, max-age=60");
5166
+ return c.json({
5167
+ templates: templateEntries,
5168
+ next_cursor: null
5169
+ });
5170
+ });
5161
5171
  function buildTemplateHomepageEntry(template) {
5162
5172
  // Undecomposed templates (no defaultForkId yet) still appear on /discover so
5163
5173
  // users can open them and mint the first fork via the Decompose modal.
5164
5174
  if (!template.videoUrl)
5165
5175
  return null;
5166
5176
  const isPrivate = template.visibility === "private";
5177
+ const isPopular = typeof template.popularGroup === "string" && template.popularGroup.trim().length > 0;
5167
5178
  // Older records stored the source host ("youtube.com") as a title fallback —
5168
5179
  // never show that as a card title; the template id is the canonical default.
5169
5180
  const storedTitle = template.title && template.title !== template.sourceHost ? template.title : null;
5170
- const title = isPrivate ? (storedTitle || template.id) : template.id;
5181
+ // Public feed cards default to the template id, but curated Popular picks show
5182
+ // their editorial title (the whole point of the Popular list).
5183
+ const title = (isPrivate || isPopular) ? (storedTitle || template.id) : template.id;
5171
5184
  const previewUrl = template.thumbnailUrl || template.videoUrl || null;
5172
5185
  return {
5173
5186
  title,
@@ -5187,6 +5200,8 @@ function buildTemplateHomepageEntry(template) {
5187
5200
  summary: template.searchSummary ?? null,
5188
5201
  visibility: template.visibility,
5189
5202
  origin: template.origin ?? "inspiration",
5203
+ popularGroup: template.popularGroup ?? null,
5204
+ popularRank: template.popularRank ?? null,
5190
5205
  notes: template.notes,
5191
5206
  status: "ready"
5192
5207
  };
@@ -5554,6 +5569,8 @@ function buildPendingInspirationHomepageEntry(inspiration) {
5554
5569
  durationSeconds: inspiration.durationSeconds ?? null,
5555
5570
  visibility: "private",
5556
5571
  origin: inspiration.origin ?? "inspiration",
5572
+ popularGroup: inspiration.popularGroup ?? null,
5573
+ popularRank: inspiration.popularRank ?? null,
5557
5574
  notes: inspiration.notes,
5558
5575
  status: inspiration.status === "failed" ? "failed" : "processing"
5559
5576
  };
@@ -5677,21 +5694,193 @@ const PUBLIC_SKILL_FILES = {
5677
5694
  "vidfarm-director": "SKILL.director.md",
5678
5695
  "vidfarm-platform": "SKILL.platform.md"
5679
5696
  };
5697
+ // Vendored skill packs (the heygen-scrubbed hyperframes pack + vidfarm-media)
5698
+ // mirrored from .agents/skills. Vidfarm is the distribution origin: the devcli
5699
+ // (`vidfarm skills add`) and the chat copilot's load_skill tool both pull from
5700
+ // these routes, so customers never fetch skills from an upstream vendor org.
5701
+ const SKILL_PACK_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,64}$/;
5702
+ const SKILL_PACK_IGNORED_ENTRIES = new Set([".DS_Store", "node_modules", ".git"]);
5703
+ function resolveSkillPackDir(name) {
5704
+ if (!SKILL_PACK_NAME_PATTERN.test(name)) {
5705
+ return null;
5706
+ }
5707
+ for (const candidate of resolveRootFileCandidates(path.join(".agents", "skills", name))) {
5708
+ try {
5709
+ if (statSync(candidate).isDirectory()) {
5710
+ return candidate;
5711
+ }
5712
+ }
5713
+ catch {
5714
+ continue;
5715
+ }
5716
+ }
5717
+ return null;
5718
+ }
5719
+ function listSkillPackFiles(dir, prefix = "") {
5720
+ const files = [];
5721
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
5722
+ if (SKILL_PACK_IGNORED_ENTRIES.has(entry.name)) {
5723
+ continue;
5724
+ }
5725
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
5726
+ const absolute = path.join(dir, entry.name);
5727
+ if (entry.isDirectory()) {
5728
+ files.push(...listSkillPackFiles(absolute, relative));
5729
+ }
5730
+ else if (entry.isFile()) {
5731
+ files.push(relative);
5732
+ }
5733
+ }
5734
+ return files.sort();
5735
+ }
5736
+ function readSkillPackDescription(dir) {
5737
+ try {
5738
+ const skillMd = readFileSync(path.join(dir, "SKILL.md"), "utf8");
5739
+ const match = skillMd.match(/^description:\s*(.+)$/m);
5740
+ return match ? match[1].trim().slice(0, 300) : "";
5741
+ }
5742
+ catch {
5743
+ return "";
5744
+ }
5745
+ }
5680
5746
  app.get("/skill/:name", (c) => {
5681
5747
  const name = c.req.param("name").replace(/\.md$/i, "");
5682
5748
  const filename = PUBLIC_SKILL_FILES[name];
5683
- if (!filename) {
5684
- return c.text("Unknown skill", 404);
5685
- }
5686
- const contents = readRootSkillFile(filename);
5749
+ const contents = filename
5750
+ ? readRootSkillFile(filename)
5751
+ : (() => {
5752
+ const packDir = resolveSkillPackDir(name);
5753
+ if (!packDir) {
5754
+ return "";
5755
+ }
5756
+ try {
5757
+ return readFileSync(path.join(packDir, "SKILL.md"), "utf8");
5758
+ }
5759
+ catch {
5760
+ return "";
5761
+ }
5762
+ })();
5687
5763
  if (!contents) {
5688
- return c.text("Skill file unavailable", 404);
5764
+ return c.text(filename ? "Skill file unavailable" : "Unknown skill", 404);
5689
5765
  }
5690
5766
  return c.body(contents, 200, {
5691
5767
  "content-type": "text/markdown; charset=utf-8",
5692
5768
  "cache-control": "no-cache"
5693
5769
  });
5694
5770
  });
5771
+ app.get("/skill-pack/index.json", (c) => {
5772
+ const skills = [];
5773
+ for (const candidate of resolveRootFileCandidates(path.join(".agents", "skills"))) {
5774
+ let entries;
5775
+ try {
5776
+ entries = readdirSync(candidate);
5777
+ }
5778
+ catch {
5779
+ continue;
5780
+ }
5781
+ for (const entry of entries.sort()) {
5782
+ if (!SKILL_PACK_NAME_PATTERN.test(entry) || skills.some((skill) => skill.name === entry)) {
5783
+ continue;
5784
+ }
5785
+ const dir = path.join(candidate, entry);
5786
+ try {
5787
+ if (!statSync(dir).isDirectory()) {
5788
+ continue;
5789
+ }
5790
+ }
5791
+ catch {
5792
+ continue;
5793
+ }
5794
+ skills.push({ name: entry, description: readSkillPackDescription(dir) });
5795
+ }
5796
+ break;
5797
+ }
5798
+ return c.json({ skills });
5799
+ });
5800
+ // Manifest hashing reads every file in a pack (hyperframes-animation alone is
5801
+ // ~100 files) — memoize per pack dir, invalidated by a cheap stat signature,
5802
+ // so `vidfarm skills update` sweeps don't re-hash 5MB per request.
5803
+ const skillPackManifestCache = new Map();
5804
+ app.get("/skill-pack/:name/manifest.json", (c) => {
5805
+ const packDir = resolveSkillPackDir(c.req.param("name"));
5806
+ if (!packDir) {
5807
+ return c.json({ error: "Unknown skill pack." }, 404);
5808
+ }
5809
+ const relativePaths = listSkillPackFiles(packDir);
5810
+ const signature = relativePaths
5811
+ .map((relative) => {
5812
+ try {
5813
+ const stats = statSync(path.join(packDir, relative));
5814
+ return `${relative}:${stats.size}:${stats.mtimeMs}`;
5815
+ }
5816
+ catch {
5817
+ return `${relative}:missing`;
5818
+ }
5819
+ })
5820
+ .join("|");
5821
+ const cached = skillPackManifestCache.get(packDir);
5822
+ if (cached && cached.signature === signature) {
5823
+ return c.json(cached.manifest);
5824
+ }
5825
+ const files = relativePaths.map((relative) => {
5826
+ const bytes = readFileSync(path.join(packDir, relative));
5827
+ return {
5828
+ path: relative,
5829
+ size: bytes.length,
5830
+ sha256: createHash("sha256").update(bytes).digest("hex")
5831
+ };
5832
+ });
5833
+ const manifest = { name: path.basename(packDir), files };
5834
+ skillPackManifestCache.set(packDir, { signature, manifest });
5835
+ return c.json(manifest);
5836
+ });
5837
+ app.get("/skill-pack/:name/files/*", (c) => {
5838
+ const packDir = resolveSkillPackDir(c.req.param("name"));
5839
+ if (!packDir) {
5840
+ return c.text("Unknown skill pack.", 404);
5841
+ }
5842
+ const marker = "/files/";
5843
+ const rawPath = c.req.path.slice(c.req.path.indexOf(marker) + marker.length);
5844
+ let relative;
5845
+ try {
5846
+ relative = rawPath
5847
+ .split("/")
5848
+ .map((segment) => decodeURIComponent(segment))
5849
+ .join("/");
5850
+ }
5851
+ catch {
5852
+ return c.text("Bad file path.", 400);
5853
+ }
5854
+ const resolved = path.resolve(packDir, relative);
5855
+ if (resolved !== packDir && !resolved.startsWith(packDir + path.sep)) {
5856
+ return c.text("Bad file path.", 400);
5857
+ }
5858
+ let bytes;
5859
+ try {
5860
+ bytes = readFileSync(resolved);
5861
+ }
5862
+ catch {
5863
+ return c.text("File not found.", 404);
5864
+ }
5865
+ const ext = path.extname(resolved).toLowerCase();
5866
+ const contentType = ext === ".md" || ext === ".markdown" ? "text/markdown; charset=utf-8"
5867
+ : ext === ".json" ? "application/json; charset=utf-8"
5868
+ : ext === ".mjs" || ext === ".cjs" || ext === ".js" ? "text/javascript; charset=utf-8"
5869
+ : ext === ".html" ? "text/html; charset=utf-8"
5870
+ : ext === ".css" ? "text/css; charset=utf-8"
5871
+ : ext === ".txt" || ext === ".srt" || ext === ".vtt" || ext === ".sh" || ext === ".yaml" || ext === ".yml" ? "text/plain; charset=utf-8"
5872
+ : ext === ".png" ? "image/png"
5873
+ : ext === ".avif" ? "image/avif"
5874
+ : ext === ".webp" ? "image/webp"
5875
+ : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg"
5876
+ : ext === ".mp3" ? "audio/mpeg"
5877
+ : ext === ".woff2" ? "font/woff2"
5878
+ : "application/octet-stream";
5879
+ return c.body(new Uint8Array(bytes), 200, {
5880
+ "content-type": contentType,
5881
+ "cache-control": "no-cache"
5882
+ });
5883
+ });
5695
5884
  if (config.SENTRY_DEBUG_ROUTE === "true") {
5696
5885
  app.get("/debug-sentry", () => {
5697
5886
  Sentry.logger.info("User triggered test error", {
@@ -6601,7 +6790,13 @@ app.post(`${COMPOSITIONS_PREFIX}/:forkId/captions`, async (c) => {
6601
6790
  const text = str("text");
6602
6791
  if (text) {
6603
6792
  const windowStart = Math.max(0, num("start") ?? 0);
6604
- const windowDuration = num("duration") ?? Math.max(0.4, compositionDuration - windowStart);
6793
+ const availableWindow = compositionDuration - windowStart;
6794
+ // When the composition declares no usable window (missing data-duration,
6795
+ // or windowStart at/after the end), fall back to a word-count estimate
6796
+ // (~0.32s/word, same as the /audio/captions primitive) instead of
6797
+ // cramming the whole script into a 0.4s window of overlapping cues.
6798
+ const estimatedWindow = Math.max(0.4, text.trim().split(/\s+/).filter(Boolean).length * 0.32);
6799
+ const windowDuration = num("duration") ?? (availableWindow > 0.4 ? availableWindow : estimatedWindow);
6605
6800
  cues = cuesFromText(text, windowStart + offsetSec, windowDuration, { maxWordsPerCue: maxWords });
6606
6801
  sourceNote = "text";
6607
6802
  }
@@ -7018,6 +7213,13 @@ app.post(`${INSPIRATIONS_PREFIX}/:inspirationId/decompose`, async (c) => {
7018
7213
  // so a forced re-run may start. Must exceed the longest real decompose but stay
7019
7214
  // under the API lambda's 15-min timeout.
7020
7215
  const DECOMPOSE_PROCESSING_STALE_MS = 10 * 60 * 1000;
7216
+ // Inspiration ingest + auto-decompose no longer hard-cap the source at 120s.
7217
+ // Users can bring longer inspiration; an operator can still re-impose a ceiling
7218
+ // via INSPIRATION_MAX_DURATION_SEC (>0). 0 → unlimited, translated to Infinity so
7219
+ // probeMediaAsset never rejects a source purely on length.
7220
+ const INSPIRATION_PROBE_MAX_DURATION_SECONDS = config.INSPIRATION_MAX_DURATION_SEC > 0
7221
+ ? config.INSPIRATION_MAX_DURATION_SEC
7222
+ : Number.POSITIVE_INFINITY;
7021
7223
  async function readDecomposeJobState(forkId) {
7022
7224
  const text = await storage.readText(storage.compositionForkWorkingKey(forkId, "decompose.json"));
7023
7225
  if (!text)
@@ -7137,7 +7339,10 @@ app.post(`${COMPOSITIONS_PREFIX}/:forkId/auto-decompose`, async (c) => {
7137
7339
  let probedDurationSeconds = null;
7138
7340
  if (sourceUrl) {
7139
7341
  try {
7140
- const probe = await probeMediaAsset({ sourceUrl });
7342
+ const probe = await probeMediaAsset({
7343
+ sourceUrl,
7344
+ maxDurationSeconds: INSPIRATION_PROBE_MAX_DURATION_SECONDS
7345
+ });
7141
7346
  const probed = probe.metadata.durationSeconds;
7142
7347
  if (Number.isFinite(probed) && probed && probed > 0) {
7143
7348
  probedDurationSeconds = probed;
@@ -7185,7 +7390,17 @@ app.post(`${COMPOSITIONS_PREFIX}/:forkId/auto-decompose`, async (c) => {
7185
7390
  ghostcutSkipReason = "ghostcut_not_configured";
7186
7391
  console.warn(`ghostcut: skipping subtitle removal for fork ${fork.id} — GHOSTCUT_KEY/GHOSTCUT_SECRET not configured. Composition will keep the original video with baked-in text.`);
7187
7392
  }
7188
- if (sourceUrl && sourceIsVideo && isGhostcutConfigured()) {
7393
+ else if (config.GHOSTCUT_DECOMPOSE_MAX_DURATION_SEC > 0 &&
7394
+ probedDurationSeconds !== null &&
7395
+ probedDurationSeconds > config.GHOSTCUT_DECOMPOSE_MAX_DURATION_SEC) {
7396
+ // GhostCut is per-30s-chunk and meant for short clips. Long inspiration is
7397
+ // now allowed through ingest/decompose, but laundering a multi-minute video
7398
+ // through GhostCut is disproportionately expensive — skip it and keep the
7399
+ // original burned-in captions instead.
7400
+ ghostcutSkipReason = `source_too_long:${probedDurationSeconds.toFixed(1)}s>${config.GHOSTCUT_DECOMPOSE_MAX_DURATION_SEC}s`;
7401
+ console.warn(`ghostcut: skipping subtitle removal for fork ${fork.id} — source is ${probedDurationSeconds.toFixed(1)}s, over the ${config.GHOSTCUT_DECOMPOSE_MAX_DURATION_SEC}s decompose ceiling. Composition keeps the original burned-in captions.`);
7402
+ }
7403
+ if (sourceUrl && sourceIsVideo && isGhostcutConfigured() && !ghostcutSkipReason) {
7189
7404
  try {
7190
7405
  const submission = await submitSubtitleRemoval(sourceUrl);
7191
7406
  ghostcutTaskId = submission.taskId;
@@ -9005,6 +9220,8 @@ function serializeInspiration(inspiration) {
9005
9220
  summary: inspiration.searchSummary ?? null,
9006
9221
  visibility: inspiration.visibility ?? "public",
9007
9222
  origin: inspiration.origin ?? "inspiration",
9223
+ popular_group: inspiration.popularGroup ?? null,
9224
+ popular_rank: inspiration.popularRank ?? null,
9008
9225
  notes: inspiration.notes ?? null,
9009
9226
  ingester_customer_id: inspiration.customerId,
9010
9227
  error: inspiration.errorMessage,
@@ -11109,7 +11326,10 @@ async function ensureInspirationReadyThenFork(input) {
11109
11326
  // the true length instead of a placeholder.
11110
11327
  if (!inspiration.durationSeconds) {
11111
11328
  try {
11112
- const probe = await probeMediaAsset({ sourceUrl: inspiration.videoUrl });
11329
+ const probe = await probeMediaAsset({
11330
+ sourceUrl: inspiration.videoUrl,
11331
+ maxDurationSeconds: INSPIRATION_PROBE_MAX_DURATION_SECONDS
11332
+ });
11113
11333
  const probedDuration = probe.metadata.durationSeconds;
11114
11334
  if (Number.isFinite(probedDuration) && probedDuration && probedDuration > 0) {
11115
11335
  inspiration = (await serverlessRecords.updateInspiration({
@@ -11188,6 +11408,205 @@ function buildTemplateEditorUrl(publicBaseUrl, templateId) {
11188
11408
  const path = `/editor/${encodeURIComponent(templateId)}`;
11189
11409
  return publicBaseUrl ? `${publicBaseUrl.replace(/\/$/, "")}${path}` : path;
11190
11410
  }
11411
+ // Curated Popular picks are short-form: a source longer than the 120s ingest
11412
+ // limit is clipped to this many seconds (kept safely under 120 for the probe).
11413
+ const POPULAR_MAX_CLIP_SEC = 115;
11414
+ // Seed the curated "Popular" catalog from POPULAR_INSPIRATION_GROUPS: ingest
11415
+ // each URL once into a real PUBLIC template (same path as admin TikTok
11416
+ // submissions) and stamp it with the editorial title + popularGroup +
11417
+ // popularRank so it always shows under the /discover Popular toggle and
11418
+ // autoplays inline exactly like a Feed card. Over-length sources are clipped to
11419
+ // POPULAR_MAX_CLIP_SEC. Idempotent — find-or-create by URL, then (re)stamp.
11420
+ // Requires RAPIDAPI_KEY (the download primitive), so run this on staging/prod.
11421
+ app.post(`${API_PREFIX}/admin/popular/seed`, async (c) => {
11422
+ try {
11423
+ requireSuperagency(c);
11424
+ }
11425
+ catch (error) {
11426
+ return c.json({ error: error instanceof Error ? error.message : "Forbidden" }, 403);
11427
+ }
11428
+ const body = asRecord(await c.req.json().catch(() => ({}))) ?? {};
11429
+ const requestedOwnerEmail = readNonEmptyString(body.owner_email)?.trim().toLowerCase();
11430
+ const ownerEmail = requestedOwnerEmail ?? config.adminEmails[0]?.trim().toLowerCase();
11431
+ if (!ownerEmail)
11432
+ return c.json({ error: "No owner_email supplied and no admin email configured." }, 400);
11433
+ const ownerCustomer = await serverlessRecords.getCustomerByEmail(ownerEmail);
11434
+ if (!ownerCustomer)
11435
+ return c.json({ error: `Owner not found: ${ownerEmail}` }, 404);
11436
+ const primitive = primitiveRegistry.get("primitive:video_download");
11437
+ if (!primitive)
11438
+ return c.json({ error: "video_download primitive missing" }, 500);
11439
+ const operation = primitive.operations["run"];
11440
+ if (!operation)
11441
+ return c.json({ error: "video_download.run missing" }, 500);
11442
+ const publicBaseUrl = config.PUBLIC_BASE_URL?.trim() || "";
11443
+ try {
11444
+ await assertWalletCanQueueJobs(ownerCustomer.id);
11445
+ }
11446
+ catch (error) {
11447
+ if (error instanceof InsufficientWalletFundsError)
11448
+ return insufficientFundsResponse(c, error);
11449
+ throw error;
11450
+ }
11451
+ // Flatten the manifest into one ranked list. popularRank is a global sequence
11452
+ // (group index * 1000 + item index) so a plain rank sort restores both the
11453
+ // group order and the order within each group.
11454
+ const allItems = POPULAR_INSPIRATION_GROUPS.flatMap((group, gi) => group.items.map((item, ri) => ({ title: item.title, url: item.url, group: group.label, rank: gi * 1000 + ri })));
11455
+ // Optional { groups: [...] } filter seeds one category at a time so a large
11456
+ // catalog stays well under the Lambda timeout (each URL downloads inline).
11457
+ // Reconcile below still runs against the FULL manifest, so a partial seed
11458
+ // never retires the groups it skipped.
11459
+ const onlyGroups = Array.isArray(body.groups) ? new Set(body.groups.map((g) => String(g))) : null;
11460
+ const items = onlyGroups ? allItems.filter((it) => onlyGroups.has(it.group)) : allItems;
11461
+ const results = [];
11462
+ for (const item of items) {
11463
+ const originalUrl = stripTrackingParams(item.url);
11464
+ const sourceHost = safeUrlHostname(originalUrl);
11465
+ if (!sourceHost) {
11466
+ results.push({ source_url: item.url, title: item.title, error: "invalid URL" });
11467
+ continue;
11468
+ }
11469
+ let inspiration = await serverlessRecords.findInspirationByOriginalUrl(originalUrl);
11470
+ // (Re)start the download only when there's no usable video yet — a genuine
11471
+ // download failure (no videoUrl) retries, but a pick that DID download and
11472
+ // only tripped the duration limit keeps its bytes and gets clipped below.
11473
+ const needsIngest = !inspiration
11474
+ || (!inspiration.videoUrl && inspiration.status !== "downloading");
11475
+ if (needsIngest) {
11476
+ const payload = operation.inputSchema.parse({ source_url: originalUrl, save_manifest: true });
11477
+ let job;
11478
+ try {
11479
+ job = await jobs.createRootJob({
11480
+ templateId: primitive.id,
11481
+ operationName: "run",
11482
+ workflowName: operation.workflow,
11483
+ tracer: `popular_seed_ingest:${ownerCustomer.id}`,
11484
+ payload,
11485
+ webhookUrl: null,
11486
+ customer: ownerCustomer,
11487
+ providerHint: undefined
11488
+ });
11489
+ }
11490
+ catch (error) {
11491
+ results.push({ source_url: item.url, title: item.title, error: error instanceof Error ? error.message : String(error) });
11492
+ continue;
11493
+ }
11494
+ inspiration = inspiration
11495
+ ? ((await serverlessRecords.updateInspiration({
11496
+ inspirationId: inspiration.id,
11497
+ patch: { status: "downloading", downloadJobId: job.id, errorMessage: null }
11498
+ })) ?? inspiration)
11499
+ : await serverlessRecords.createInspiration({
11500
+ customerId: ownerCustomer.id,
11501
+ sourceUrl: item.url,
11502
+ originalUrl,
11503
+ sourceHost,
11504
+ downloadJobId: job.id,
11505
+ trendTagline: item.title
11506
+ });
11507
+ }
11508
+ // Stamp the editorial marker + curated title up front so a still-downloading
11509
+ // pick already surfaces under Popular with the right label (pending cards
11510
+ // read trendTagline, which finalize never clobbers).
11511
+ inspiration = (await serverlessRecords.updateInspiration({
11512
+ inspirationId: inspiration.id,
11513
+ patch: { title: item.title, trendTagline: item.title, popularGroup: item.group, popularRank: item.rank, visibility: "public" }
11514
+ })) ?? inspiration;
11515
+ // On a local box (no Step Functions) the download job would queue forever —
11516
+ // run it in-process so ensureInspirationReadyThenFork can finalize.
11517
+ if (!config.VIDFARM_JOB_STATE_MACHINE_ARN && inspiration.status === "downloading" && inspiration.downloadJobId) {
11518
+ const pendingJobId = inspiration.downloadJobId;
11519
+ void runPrimitiveJobInProcess(pendingJobId).catch((error) => {
11520
+ console.error("popular seed download failed", pendingJobId, error instanceof Error ? error.message : error);
11521
+ });
11522
+ }
11523
+ const finalized = await ensureInspirationReadyThenFork({ inspiration, ownerCustomer, publicBaseUrl, note: null });
11524
+ let templateId = typeof finalized.template_id === "string" ? finalized.template_id : null;
11525
+ let clipped = false;
11526
+ let sourceDurationSec = null;
11527
+ // Curated Popular picks are short-form. If the source downloaded fine but was
11528
+ // rejected ONLY for exceeding the 120s ingest limit, clip it to <=115s and
11529
+ // finalize the clip — so a long reference video still becomes a Popular card.
11530
+ if (!templateId) {
11531
+ const rejected = await serverlessRecords.getInspiration(inspiration.id);
11532
+ const tooLong = Boolean(rejected?.videoUrl)
11533
+ && ((Number.isFinite(rejected?.durationSeconds) && Number(rejected?.durationSeconds) > POPULAR_MAX_CLIP_SEC)
11534
+ || /exceeds/i.test(rejected?.errorMessage ?? ""));
11535
+ if (Number.isFinite(rejected?.durationSeconds))
11536
+ sourceDurationSec = Number(rejected?.durationSeconds);
11537
+ else {
11538
+ const m = /([0-9.]+)s exceeds/.exec(rejected?.errorMessage ?? "");
11539
+ if (m)
11540
+ sourceDurationSec = Number(m[1]);
11541
+ }
11542
+ if (rejected?.videoUrl && tooLong) {
11543
+ try {
11544
+ const trimmed = await trimVideoAsset({ sourceUrl: rejected.videoUrl, startMs: 0, durationMs: POPULAR_MAX_CLIP_SEC * 1000 });
11545
+ // Store under the public "primitives/" prefix so getPublicUrl can hand
11546
+ // back a directly-fetchable S3 URL (other prefixes resolve to null).
11547
+ const clipKey = joinStorageKey("primitives", "popular-clips", `${rejected.id}.mp4`);
11548
+ const stored = await storage.putBuffer(clipKey, trimmed.bytes, "video/mp4");
11549
+ const clipUrl = stored.url ?? storage.getPublicUrl(clipKey);
11550
+ if (!clipUrl)
11551
+ console.error("popular seed clip: no public URL for", clipKey);
11552
+ const clipDurRaw = trimmed.metadata.trimmed?.durationSeconds;
11553
+ const clipDur = Number.isFinite(clipDurRaw) && clipDurRaw ? Number(clipDurRaw) : POPULAR_MAX_CLIP_SEC;
11554
+ if (clipUrl) {
11555
+ const readyInsp = await serverlessRecords.updateInspiration({
11556
+ inspirationId: rejected.id,
11557
+ patch: {
11558
+ videoUrl: clipUrl,
11559
+ videoStorageKey: clipKey,
11560
+ thumbnailUrl: null,
11561
+ durationSeconds: clipDur,
11562
+ status: "ready",
11563
+ errorMessage: null
11564
+ }
11565
+ });
11566
+ if (readyInsp) {
11567
+ const reFinalized = await ensureInspirationReadyThenFork({ inspiration: readyInsp, ownerCustomer, publicBaseUrl, note: null });
11568
+ templateId = typeof reFinalized.template_id === "string" ? reFinalized.template_id : null;
11569
+ clipped = Boolean(templateId);
11570
+ }
11571
+ }
11572
+ }
11573
+ catch (error) {
11574
+ console.error("popular seed clip failed", rejected.id, error instanceof Error ? error.message : error);
11575
+ }
11576
+ }
11577
+ }
11578
+ if (templateId) {
11579
+ await serverlessRecords.updateTemplate({
11580
+ templateId,
11581
+ patch: { title: item.title, popularGroup: item.group, popularRank: item.rank, visibility: "public" }
11582
+ });
11583
+ }
11584
+ results.push({
11585
+ source_url: item.url,
11586
+ title: item.title,
11587
+ group: item.group,
11588
+ rank: item.rank,
11589
+ template_id: templateId,
11590
+ status: templateId ? "ready" : (finalized.status ?? null),
11591
+ clipped,
11592
+ source_duration_sec: sourceDurationSec,
11593
+ pending: finalized.pending ?? false
11594
+ });
11595
+ }
11596
+ // Reconcile: any previously-seeded pick whose URL is no longer in the manifest
11597
+ // (a replaced or removed entry) gets un-flagged so it drops out of the Popular
11598
+ // view — the manifest is the single source of truth. Compared after the same
11599
+ // tracking-param stripping the stored originalUrl went through.
11600
+ const manifestUrls = new Set(allItems.map((it) => stripTrackingParams(it.url)));
11601
+ const retired = [];
11602
+ for (const tmpl of await serverlessRecords.listPopularTemplates()) {
11603
+ if (!manifestUrls.has(tmpl.originalUrl)) {
11604
+ await serverlessRecords.updateTemplate({ templateId: tmpl.id, patch: { popularGroup: null, popularRank: null } });
11605
+ retired.push({ template_id: tmpl.id, title: tmpl.title, original_url: tmpl.originalUrl });
11606
+ }
11607
+ }
11608
+ return c.json({ ok: true, owner_customer_id: ownerCustomer.id, count: results.length, items: results, retired }, 202);
11609
+ });
11191
11610
  function buildRawTikTokCompositionHtml(input) {
11192
11611
  const duration = Number.isFinite(input.durationSeconds) && input.durationSeconds > 0
11193
11612
  ? Number(input.durationSeconds.toFixed(3))
@@ -14938,6 +15357,31 @@ app.get(`${USER_PREFIX}/me/generated-media`, async (c) => {
14938
15357
  next_cursor: page.nextCursor
14939
15358
  });
14940
15359
  });
15360
+ // OpenAI's embedding models are named text-embedding-3-* / text-embedding-ada-*;
15361
+ // Gemini's family INCLUDES text-embedding-004 (plus gemini-embedding-*), so a
15362
+ // bare startsWith("text-embedding") misroutes a Gemini-built library to OpenAI
15363
+ // and returns cross-space (garbage) cosine scores. Match the OpenAI naming
15364
+ // precisely; everything else is gemini.
15365
+ function inferEmbeddingProviderFromModel(model) {
15366
+ return model && /^text-embedding-(3|ada)/i.test(model) ? "openai" : "gemini";
15367
+ }
15368
+ // Embed-capable client for populating/searching a clip library. Matches the
15369
+ // library's existing embedding space when one exists (so vectors stay
15370
+ // cosine-comparable); otherwise takes the first embed-capable saved key
15371
+ // (gemini → openai; openrouter can't embed). Null → no key, keyword-only.
15372
+ async function buildClipLibraryEmbedder(customerId) {
15373
+ const existing = await clipRecords.getLibraryEmbeddingModel(customerId);
15374
+ const providers = existing ? [inferEmbeddingProviderFromModel(existing)] : ["gemini", "openai"];
15375
+ for (const provider of providers) {
15376
+ const apiKey = await resolveCustomerProviderKey(customerId, provider);
15377
+ if (!apiKey)
15378
+ continue;
15379
+ const client = new ClipModelClient({ provider, apiKey });
15380
+ if (client.canEmbed)
15381
+ return client;
15382
+ }
15383
+ return null;
15384
+ }
14941
15385
  // Embedder for My Files notes — the same BYOK seam clip search uses. Prefer the
14942
15386
  // provider matching the embedding model the file library was built with (from
14943
15387
  // the newest annotated attachment) so query vectors stay cosine-comparable; a
@@ -14946,7 +15390,7 @@ app.get(`${USER_PREFIX}/me/generated-media`, async (c) => {
14946
15390
  async function buildFileNotesEmbedder(customerId) {
14947
15391
  const attachments = await serverlessRecords.listUserAttachments(customerId);
14948
15392
  const libModel = attachments.find((a) => a.notesEmbeddingModel && Array.isArray(a.notesEmbedding) && a.notesEmbedding.length > 0)?.notesEmbeddingModel ?? null;
14949
- const providers = libModel ? [libModel.startsWith("text-embedding") ? "openai" : "gemini"] : ["gemini", "openai"];
15393
+ const providers = libModel ? [inferEmbeddingProviderFromModel(libModel)] : ["gemini", "openai"];
14950
15394
  for (const provider of providers) {
14951
15395
  const apiKey = await resolveCustomerProviderKey(customerId, provider);
14952
15396
  if (!apiKey)
@@ -16162,7 +16606,7 @@ async function pickClipProviderFromKeys(customerId) {
16162
16606
  */
16163
16607
  async function buildMatchEmbedder(customerId) {
16164
16608
  const libModel = await clipRecords.getLibraryEmbeddingModel(customerId);
16165
- const provider = libModel && libModel.startsWith("text-embedding") ? "openai" : "gemini";
16609
+ const provider = inferEmbeddingProviderFromModel(libModel);
16166
16610
  const key = await resolveCustomerProviderKey(customerId, provider);
16167
16611
  if (!key)
16168
16612
  return null;
@@ -16532,29 +16976,54 @@ app.post(`${CLIPS_PREFIX}/scan`, async (c) => {
16532
16976
  const computeEstimate = effectiveDurationSec
16533
16977
  ? estimateClipScanComputeUsd(effectiveDurationSec, huntSpec.duration_band?.target_sec)
16534
16978
  : null;
16535
- const res = await clipScanSfn.send(new StartExecutionCommand({
16536
- stateMachineArn: config.VIDFARM_CLIP_SCAN_STATE_MACHINE_ARN,
16537
- name: scanId.replace(/[^A-Za-z0-9-_]/g, "-").slice(0, 80),
16538
- input: JSON.stringify({
16539
- scan_id: scanId,
16540
- owner_id: customer.id,
16541
- source_video_id: sourceVideoId,
16542
- source_filename: filename,
16543
- raw_s3_uri: rawS3Uri ?? "",
16544
- tier,
16545
- provider,
16546
- tag_api_key: tagApiKey,
16547
- embedding_provider: client.embeddingProvider ?? "",
16548
- embedding_api_key: embeddingKey ?? "",
16549
- guidance_prompt: guidancePrompt,
16550
- frames_per_scene: framesPerScene,
16551
- include_audio: includeAudio,
16552
- hunt_spec: huntSpec,
16553
- tracer
16554
- })
16555
- }));
16556
- await putScanRecords({ scanStatus: "running", executionArn: res.executionArn });
16557
- return scanResponse({ status: "running", executionArn: res.executionArn, pipeline: "cloud", estimate, computeEstimate });
16979
+ // Records first: a Dynamo failure here aborts submission before anything
16980
+ // is billed, and the warm probe lambda's later status:"scanning" write is
16981
+ // never clobbered back to "pending" by a post-start putScanRecords.
16982
+ await putScanRecords({ scanStatus: "running" });
16983
+ let executionArn;
16984
+ try {
16985
+ const res = await clipScanSfn.send(new StartExecutionCommand({
16986
+ stateMachineArn: config.VIDFARM_CLIP_SCAN_STATE_MACHINE_ARN,
16987
+ name: scanId.replace(/[^A-Za-z0-9-_]/g, "-").slice(0, 80),
16988
+ input: JSON.stringify({
16989
+ scan_id: scanId,
16990
+ owner_id: customer.id,
16991
+ source_video_id: sourceVideoId,
16992
+ source_filename: filename,
16993
+ raw_s3_uri: rawS3Uri ?? "",
16994
+ tier,
16995
+ provider,
16996
+ tag_api_key: tagApiKey,
16997
+ embedding_provider: client.embeddingProvider ?? "",
16998
+ embedding_api_key: embeddingKey ?? "",
16999
+ guidance_prompt: guidancePrompt,
17000
+ frames_per_scene: framesPerScene,
17001
+ include_audio: includeAudio,
17002
+ hunt_spec: huntSpec,
17003
+ tracer
17004
+ })
17005
+ }));
17006
+ executionArn = res.executionArn;
17007
+ }
17008
+ catch (error) {
17009
+ // The execution never started — roll the scan (and its source) to failed
17010
+ // so pollers do not spin on a "running" record that will never progress,
17011
+ // mirroring the lazy-reconcile failure path.
17012
+ const orphan = await clipRecords.getScan(customer.id, scanId);
17013
+ if (orphan)
17014
+ await clipRecords.putScan({ ...orphan, status: "failed", error: "Failed to start the scan pipeline.", updated_at: nowIso() });
17015
+ const orphanSource = await clipRecords.getSource(customer.id, sourceVideoId);
17016
+ if (orphanSource && orphanSource.status !== "complete")
17017
+ await clipRecords.putSource({ ...orphanSource, status: "failed", updated_at: nowIso() });
17018
+ throw error;
17019
+ }
17020
+ // Attach the execution ARN for lazy SFN reconciliation (best-effort — the
17021
+ // record is already pollable; spread the current record so a runner that
17022
+ // has already advanced the status is not regressed).
17023
+ const startedScan = await clipRecords.getScan(customer.id, scanId);
17024
+ if (startedScan)
17025
+ await clipRecords.putScan({ ...startedScan, execution_arn: executionArn, updated_at: nowIso() });
17026
+ return scanResponse({ status: "running", executionArn, pipeline: "cloud", estimate, computeEstimate });
16558
17027
  }
16559
17028
  // ── vidfarm serve (no deployed pipeline): LOCAL-FIRST, cloud as backup ──
16560
17029
  // Same policy as `vidfarm clips scan`: the user's local claude/codex CLI
@@ -16856,6 +17325,11 @@ async function mirrorUpstreamClipsToLocal(input) {
16856
17325
  if (!feed.ok || !feed.json)
16857
17326
  throw new Error(`Upstream clip feed unavailable (HTTP ${feed.status}).`);
16858
17327
  let stored = 0;
17328
+ // Upstream /clips/feed strips the embedding vector, so re-embed each mirrored
17329
+ // clip on the owner's BYOK key — otherwise it lands with no embedding and is
17330
+ // permanently invisible to semantic clip search (keyword-only). Fail-soft:
17331
+ // no embed-capable key → store without embedding (keyword-only), as before.
17332
+ const embedder = await buildClipLibraryEmbedder(ownerId);
16859
17333
  for (const raw of feed.json.clips ?? []) {
16860
17334
  const clipId = typeof raw.clip_id === "string" ? raw.clip_id : null;
16861
17335
  const viewUrl = typeof raw.view_url === "string" ? raw.view_url : null;
@@ -16875,16 +17349,32 @@ async function mirrorUpstreamClipsToLocal(input) {
16875
17349
  await storage.putBuffer(thumbKey, new Uint8Array(await thumbRes.arrayBuffer()), "image/jpeg");
16876
17350
  }
16877
17351
  }
16878
- const { view_url: _v, thumbnail_url: _t, ...rest } = raw;
17352
+ const { view_url: _v, thumbnail_url: _t, embedding: _e, embedding_model: _em, ...rest } = raw;
16879
17353
  void _v;
16880
17354
  void _t;
16881
- await clipRecords.putClip({
17355
+ void _e;
17356
+ void _em;
17357
+ const clip = {
16882
17358
  ...rest,
16883
17359
  owner_id: ownerId,
16884
17360
  source_video_id: scan.source_video_id,
16885
17361
  file_path: clipKey,
16886
17362
  thumbnail_path: thumbKey
16887
- });
17363
+ };
17364
+ if (embedder?.embeddingModelId) {
17365
+ try {
17366
+ const [vector] = await embedder.embedDocuments([buildClipEmbeddingText(clip.tags, clip.description)]);
17367
+ if (vector?.length) {
17368
+ clip.embedding = vector;
17369
+ clip.embedding_model = embedder.embeddingModelId;
17370
+ }
17371
+ }
17372
+ catch (error) {
17373
+ devLog("clip_scan.upstream_mirror_embed_failed", { scan_id: scan.scan_id, clip_id: clipId, ...devErrorFields(error) }, "warn");
17374
+ }
17375
+ }
17376
+ await clipRecords.putClip(clip);
17377
+ await vectorIndex.upsert(ownerId, clip);
16888
17378
  stored++;
16889
17379
  }
16890
17380
  const source = await clipRecords.getSource(ownerId, scan.source_video_id);