@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,1138 @@
1
+ // devcli `vidfarm clips …` — the local clip-curation surface. LOCAL-FIRST:
2
+ // local ffmpeg for compute and, by default, the user's local agent CLI
3
+ // subscription (claude/codex) for scene evaluation — no API key needed. BYOK
4
+ // provider keys are the first fallback, and `--cloud` is the explicit backup
5
+ // that runs the hunt on the deployed pipeline instead (never the default).
6
+ // Persists to a SQLite clip library (~/.vidfarm). Every keyed scan shows an
7
+ // estimated cost first; scans over $1 require --yes or an interactive confirm.
8
+ // Mirrors the cloud REST surface 1:1.
9
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
10
+ import path from "node:path";
11
+ import { homedir } from "node:os";
12
+ import { parseArgs } from "node:util";
13
+ import { createInterface } from "node:readline/promises";
14
+ import { createIdV7 } from "../lib/ids.js";
15
+ import { buildEffectiveGuidance, buildPrivateUserNotesGuidance, ClipModelClient, DEFAULT_CLIP_ASPECT, DEFAULT_CLIP_DURATION_BAND, detectLocalAgent, detectScenes, effectiveHuntDurationSec, estimateScanCostFromScenes, formatCostEstimate, hasFfmpeg, LocalAgentClipClient, normalizeAspect, normalizeCropFocus, normalizeWindows, parseClipHuntPrompt, parseTimeRanges, probeVideo, resolveDurationBand, resolveTargetClipCount, scanVideo, searchClips } from "../services/clip-curation/index.js";
16
+ import { ClipStore } from "./clip-store.js";
17
+ export const CLIPS_HELP = `vidfarm raws — build & search a local raws library (local ffmpeg + local agent by default)
18
+
19
+ raws scan <video-path> Mine a long-form video into tagged, searchable raws
20
+ --provider <p> agent | gemini | openai | openrouter | nvidia
21
+ Default: agent when a local claude/codex CLI is installed
22
+ (your subscription, no API key), else gemini.
23
+ nvidia = lite, clipping-first key (minimax-m3); no audio/embeddings.
24
+ --agent claude|codex Which local agent CLI to use for --provider agent (default: auto)
25
+ --tier flash|flash-lite Gemini tier (default: flash-lite; ignored for openai/openrouter/agent)
26
+ --prompt "<guidance>" Reshape clipping toward your goal: an LLM pass
27
+ keeps/drops/merges scenes, and tags are biased to it.
28
+ Inline hints are honored too: "between 12:30 and 15:45",
29
+ "30 sec clips", "vertical", "no captions".
30
+ --notes "<private notes>" Store private owner notes as user_notes on derived raws
31
+ and feed them into decomposition/categorization. Explicit
32
+ private shot-kind hints like "content_type: demo" or
33
+ "category: talking_head" are honored and stay owner-only.
34
+ --range "MM:SS-MM:SS" Only hunt inside this source range (repeatable / comma-separated)
35
+ --duration <sec> Target raw length as a SOFT band (10→5-20s, 30→20-40s, …)
36
+ --aspect <a> Crop raws to 9:16 | 16:9 | 4:3 | 1:1 (or vertical/horizontal/square)
37
+ --crop-focus <f> center | top | bottom | left | right (default: center)
38
+ --no-text Prefer scenes WITHOUT burned-in captions/on-screen text
39
+ (scene selection — NEVER GhostCut on the long-form source)
40
+ --no-refine Keep prompt tag-bias but skip the keep/drop/merge pass
41
+ --cloud BACKUP: run the hunt on the deployed pipeline instead of this
42
+ machine (uploads to your temp folder — auto-deletes in 30 days —
43
+ or pass --url; needs VIDFARM_API_KEY; bills AWS compute only)
44
+ --url <video-url> With --cloud: hunt a YouTube/TikTok/IG/X URL without uploading
45
+ --tracer <id> With --cloud: tag the hunt's billing/observability rollup
46
+ --api-url <base> With --cloud: deployment base URL (or VIDFARM_API_URL)
47
+ --api-key <key> With --cloud: API key (or VIDFARM_API_KEY)
48
+ --dry-run Print the estimated cost and exit
49
+ --yes Skip the >$1 confirmation prompt
50
+ --frames <n> Keyframes per scene for tagging (1-3, default 2)
51
+ --no-audio Skip per-scene audio (Gemini only; no transcripts; cheaper)
52
+ --audio-only Save each mined clip as an audio-only .m4a (no video); waveform thumb
53
+ --concurrency <n> Parallel scenes (default 3; agent default 2)
54
+ --threshold <0..1> Scene-cut sensitivity (default 0.3; lower = more cuts)
55
+ --min-scene <sec> Merge scenes shorter than this (default 0.6)
56
+ --max-scene <sec> Split scenes longer than this (default 8; --duration overrides)
57
+ --gemini-key / --openai-key / --openrouter-key / --nvidia-key <key> Provider key (or *_API_KEY env)
58
+ --embed-provider gemini|openai --embed-key <key> Embeddings for agent/openrouter/nvidia scans
59
+ --home <dir> Library dir (default: ~/.vidfarm or VIDFARM_HOME)
60
+
61
+ raws list List raws in the library
62
+ --source <id> Only raws from one source video
63
+ --content-type <a,b> Only raws whose shot-KIND tag matches (comma-separated,
64
+ exact): talking_head, b_roll, product_shot, screen_recording,
65
+ demo, reaction, interview, establishing, lifestyle, text_graphic
66
+ --offset <n> Skip the first N (paginate with --limit)
67
+ --limit <n> | --json
68
+
69
+ raws search "<natural language>" Hybrid structured + semantic search
70
+ --content-type <a,b> Post-filter hits by shot-KIND tag (see \`raws list\`)
71
+ --limit <n> | --json | --gemini-key <key>
72
+
73
+ raws match "<text>" Top library raws to REPLACE a scene (vector search)
74
+ --criteria '<json>' Match a structured SplicingCriteria instead of text
75
+ --scenes <annotations.json> Batch: match every scene of a decomposed video
76
+ (array of scene annotations, or {annotations|scenes:[…]})
77
+ --limit <n> | --json
78
+
79
+ raws preset list List built-in + saved presets
80
+ raws preset run <name> Run a preset query [--limit <n>] [--json]
81
+ raws preset save <name> --from-query '<json | natural language>'
82
+
83
+ raws sources List scanned source videos
84
+ raws export <raw-ids...> --to <dir> Copy raw mp4s (+ metadata.json) out
85
+ `;
86
+ export async function runClipsCommand(argv) {
87
+ const sub = argv[0];
88
+ const rest = argv.slice(1);
89
+ switch (sub) {
90
+ case "scan":
91
+ return runScan(rest);
92
+ case "list":
93
+ return runList(rest);
94
+ case "search":
95
+ return runSearch(rest);
96
+ case "match":
97
+ return runMatch(rest);
98
+ case "preset":
99
+ return runPreset(rest);
100
+ case "sources":
101
+ return runSources(rest);
102
+ case "export":
103
+ return runExport(rest);
104
+ case undefined:
105
+ case "help":
106
+ case "--help":
107
+ case "-h":
108
+ return runOverview();
109
+ default:
110
+ console.error(`Unknown raws subcommand: ${sub}\n`);
111
+ console.log(CLIPS_HELP);
112
+ process.exitCode = 1;
113
+ }
114
+ }
115
+ // ── clips (overview) ────────────────────────────────────────────────────────
116
+ function runOverview() {
117
+ const store = new ClipStore();
118
+ const stats = store.stats();
119
+ store.close();
120
+ console.log(`vidfarm raws — library at ${resolveHome()}`);
121
+ console.log(` ${stats.clips} raws · ${stats.sources} source videos · ${stats.presets} presets\n`);
122
+ console.log(CLIPS_HELP);
123
+ }
124
+ // ── clips scan ──────────────────────────────────────────────────────────────
125
+ async function runScan(argv) {
126
+ const { values, positionals } = parseArgs({
127
+ args: argv,
128
+ allowPositionals: true,
129
+ options: {
130
+ provider: { type: "string" },
131
+ agent: { type: "string" },
132
+ tier: { type: "string" },
133
+ prompt: { type: "string" },
134
+ range: { type: "string", multiple: true },
135
+ duration: { type: "string" },
136
+ aspect: { type: "string" },
137
+ "crop-focus": { type: "string" },
138
+ "no-text": { type: "boolean" },
139
+ "allow-text": { type: "boolean" },
140
+ "max-clips": { type: "string" },
141
+ "no-refine": { type: "boolean" },
142
+ "audio-only": { type: "boolean" },
143
+ cloud: { type: "boolean" },
144
+ url: { type: "string" },
145
+ tracer: { type: "string" },
146
+ folder: { type: "string" },
147
+ "api-url": { type: "string" },
148
+ "api-key": { type: "string" },
149
+ "dry-run": { type: "boolean" },
150
+ yes: { type: "boolean" },
151
+ frames: { type: "string" },
152
+ "no-audio": { type: "boolean" },
153
+ concurrency: { type: "string" },
154
+ threshold: { type: "string" },
155
+ "min-scene": { type: "string" },
156
+ "max-scene": { type: "string" },
157
+ "gemini-key": { type: "string" },
158
+ "openai-key": { type: "string" },
159
+ "openrouter-key": { type: "string" },
160
+ "nvidia-key": { type: "string" },
161
+ "embed-provider": { type: "string" },
162
+ "embed-key": { type: "string" },
163
+ notes: { type: "string" },
164
+ home: { type: "string" }
165
+ }
166
+ });
167
+ // ── Hunt spec: explicit flags win; inline prompt hints fill the gaps ──────
168
+ const userNotes = values.notes?.trim() || undefined;
169
+ const guidancePromptRaw = values.prompt?.trim() || undefined;
170
+ const parsedPrompt = parseClipHuntPrompt(guidancePromptRaw);
171
+ const rawWindows = values.range?.length
172
+ ? parseTimeRanges(values.range)
173
+ : parsedPrompt.windows;
174
+ // Explicit flags / prompt hints win; otherwise fall back to the same
175
+ // unspecified-prompt product defaults (5–15s clips, vertical 9:16, avoid text,
176
+ // ~10 clips / 10 min). Opt out with `--aspect original` / `--allow-text`.
177
+ const explicitBand = values.duration
178
+ ? resolveDurationBand(Number(values.duration))
179
+ : parsedPrompt.target_duration_sec != null
180
+ ? resolveDurationBand(parsedPrompt.target_duration_sec)
181
+ : null;
182
+ const durationBand = explicitBand ?? { ...DEFAULT_CLIP_DURATION_BAND };
183
+ const explicitAspect = values.aspect
184
+ ? (normalizeAspect(values.aspect) ?? (() => { throw new Error(`--aspect must be 9:16 | 16:9 | 4:3 | 1:1 | original (or vertical/horizontal/square), got "${values.aspect}"`); })())
185
+ : parsedPrompt.aspect;
186
+ const aspect = explicitAspect ?? DEFAULT_CLIP_ASPECT;
187
+ const cropFocus = normalizeCropFocus(values["crop-focus"]);
188
+ const avoidText = values["allow-text"] ? false : (Boolean(values["no-text"]) || parsedPrompt.avoid_text || true);
189
+ const maxClips = values["max-clips"] ? Math.max(1, Math.round(Number(values["max-clips"]))) : parsedPrompt.max_clips ?? undefined;
190
+ const guidance = buildEffectiveGuidance({
191
+ contentPrompt: [guidancePromptRaw, buildPrivateUserNotesGuidance(userNotes)].filter(Boolean).join("\n"),
192
+ avoidText
193
+ }) || undefined;
194
+ // ── BACKUP path: run the hunt on the deployed pipeline (--cloud) ──────────
195
+ if (values.cloud) {
196
+ const apiProvider = values.provider && !["agent", "local", "local-agent"].includes(values.provider)
197
+ ? normalizeProvider(values.provider)
198
+ : undefined; // unset → the cloud auto-picks from the account's saved keys
199
+ return runScanCloud({
200
+ videoArg: positionals[0],
201
+ sourceUrl: values.url?.trim() || undefined,
202
+ apiUrl: values["api-url"],
203
+ apiKey: values["api-key"],
204
+ tracer: values.tracer,
205
+ folder: values.folder,
206
+ prompt: guidancePromptRaw,
207
+ userNotes,
208
+ provider: apiProvider,
209
+ windows: rawWindows,
210
+ durationBand,
211
+ aspect,
212
+ cropFocus,
213
+ avoidText,
214
+ maxClips,
215
+ audioOnly: Boolean(values["audio-only"])
216
+ });
217
+ }
218
+ const videoArg = positionals[0];
219
+ if (!videoArg)
220
+ throw new Error("Usage: vidfarm raws scan <video-path> [--range MM:SS-MM:SS] [--duration 30] [--aspect 9:16] [--dry-run]");
221
+ const videoPath = path.resolve(process.cwd(), videoArg);
222
+ if (!existsSync(videoPath))
223
+ throw new Error(`No such video: ${videoPath}`);
224
+ if (!(await hasFfmpeg())) {
225
+ throw new Error("ffmpeg is required for `raws scan` but was not found (bundled ffmpeg-static missing and none on PATH).");
226
+ }
227
+ // ── Model resolution: LOCAL-FIRST ─────────────────────────────────────────
228
+ // 1. --provider agent (or no --provider + a local claude/codex CLI installed)
229
+ // → the user's local agent subscription evaluates scenes; no API key.
230
+ // 2. --provider gemini|openai|openrouter (or a key in env/config) → BYOK API.
231
+ // 3. --cloud (handled above) → the deployed pipeline, as an explicit backup.
232
+ const provider = resolveScanProvider(values);
233
+ const tier = normalizeTier(values.tier);
234
+ const framesPerScene = clampInt(values.frames, 2, 1, 3);
235
+ // Audio is Gemini-only (chat models on openai/openrouter — and CLI agents — don't take the clip).
236
+ const includeAudio = !values["no-audio"] && provider === "gemini";
237
+ // Local agents run one process per call — keep parallelism modest by default.
238
+ const concurrency = clampInt(values.concurrency, provider === "agent" ? 2 : 3, 1, 12);
239
+ const sceneOptions = {
240
+ threshold: values.threshold ? Number(values.threshold) : undefined,
241
+ minSceneSec: values["min-scene"] ? Number(values["min-scene"]) : undefined,
242
+ // A duration band lifts the split ceiling; an explicit --max-scene still wins.
243
+ maxSceneSec: values["max-scene"] ? Number(values["max-scene"]) : durationBand?.max_sec
244
+ };
245
+ // Resolve the tagging key + optional embedding config. Optional here so
246
+ // --dry-run can show the cost without a key; required below to scan (API providers).
247
+ const apiKey = provider === "agent" ? "" : resolveProviderKey(provider, values, { optional: true });
248
+ const embedding = provider === "agent"
249
+ ? resolveAgentEmbeddingConfig(values)
250
+ : apiKey ? resolveEmbeddingConfig(provider, apiKey, values) : null;
251
+ console.log(`[raws] probing ${path.basename(videoPath)} …`);
252
+ const probe = await probeVideo(videoPath);
253
+ console.log(`[raws] ${fmtDuration(probe.duration_sec)} · ${probe.width ?? "?"}x${probe.height ?? "?"} · ${probe.codec ?? "?"}`);
254
+ const windows = normalizeWindows(rawWindows, probe.duration_sec);
255
+ if (rawWindows.length && !windows.length) {
256
+ throw new Error("--range produced no usable window inside the video's duration.");
257
+ }
258
+ // Density/uniqueness target: ~10 clips / 10 min unless --max-clips / "N clips".
259
+ const targetClipCount = resolveTargetClipCount(effectiveHuntDurationSec(probe.duration_sec, windows), { max_clips: maxClips });
260
+ if (windows.length) {
261
+ console.log(`[raws] hunting only ${windows.map((w) => `${fmtClock(w.start_sec)}–${fmtClock(w.end_sec)}`).join(", ")} (${fmtDuration(windows.reduce((a, w) => a + w.end_sec - w.start_sec, 0))} of ${fmtDuration(probe.duration_sec)}).`);
262
+ }
263
+ if (durationBand) {
264
+ console.log(`[raws] target clip length ~${durationBand.target_sec}s (soft range ${durationBand.min_sec}–${durationBand.max_sec}s).`);
265
+ }
266
+ if (aspect && aspect !== "original")
267
+ console.log(`[raws] cropping clips to ${aspect} (${cropFocus}).`);
268
+ if (avoidText)
269
+ console.log(`[raws] preferring scenes without on-screen text/captions (selection only — GhostCut is never run on the source).`);
270
+ console.log(`[raws] aiming for ~${targetClipCount} distinct raw${targetClipCount === 1 ? "" : "s"}${maxClips ? " (--max-clips)" : " (~10 / 10 min)"}.`);
271
+ console.log(`[raws] detecting scenes …`);
272
+ const scenes = await detectScenes(videoPath, { durationSec: probe.duration_sec, windows, ...sceneOptions });
273
+ if (scenes.length === 0)
274
+ throw new Error("No scenes detected (is the video readable / non-empty?).");
275
+ const store = new ClipStore(values.home);
276
+ let client = null;
277
+ if (provider === "agent") {
278
+ client = new LocalAgentClipClient({
279
+ agent: values.agent === "claude" || values.agent === "codex" ? values.agent : "auto",
280
+ embedding,
281
+ tier
282
+ });
283
+ console.log(`[raws] evaluating scenes with your local ${client.tagModelId} subscription (no API cost).`);
284
+ }
285
+ else if (apiKey) {
286
+ client = new ClipModelClient({ provider, apiKey, tier, embedding });
287
+ }
288
+ if (client && !client.canEmbed) {
289
+ console.warn(`[raws] no embedding provider for "${provider}" — raws will be structured-searchable only.`);
290
+ console.warn(` Add --embed-provider gemini|openai --embed-key <key> for semantic search.`);
291
+ }
292
+ // Cost estimate: local-agent scans ride the user's subscription, so only the
293
+ // optional embedding spend applies; API scans estimate the full BYOK cost.
294
+ const estimate = provider === "agent"
295
+ ? null
296
+ : estimateScanCostFromScenes(scenes, {
297
+ tier,
298
+ provider,
299
+ tagModelId: client?.tagModelId,
300
+ embedModelId: client ? client.embeddingModelId : defaultEmbedModelForEstimate(provider),
301
+ framesPerScene,
302
+ includeAudio
303
+ });
304
+ if (estimate) {
305
+ console.log(`\n Cost estimate: ${formatCostEstimate(estimate)}`);
306
+ console.log(` Target: $0.05–0.10 per source hour (${withinTarget(estimate.usd_per_source_hour) ? "on target" : "note"}).`);
307
+ }
308
+ else {
309
+ console.log(`\n Cost: local agent subscription (${scenes.length} scenes; embeddings ${embedding ? `via ${embedding.provider}` : "off"}).`);
310
+ }
311
+ if (guidance)
312
+ console.log(` Guidance: "${guidance.replace(/\n/g, " · ")}"`);
313
+ console.log("");
314
+ if (values["dry-run"]) {
315
+ console.log("[raws] --dry-run: not scanning. Re-run without --dry-run to build the library.");
316
+ return;
317
+ }
318
+ // Actually scanning now — an evaluator is required.
319
+ if (!client) {
320
+ if (provider !== "agent")
321
+ resolveProviderKey(provider, values); // throws the "No <provider> API key" error
322
+ throw new Error("No scene evaluator available. Install Claude Code (`claude`) or Codex (`codex`), pass a provider key, or run with --cloud.");
323
+ }
324
+ const activeClient = client;
325
+ // $1 confirmation gate (handoff Target 1) — keyed scans only.
326
+ if (estimate && estimate.usd > 1 && !values.yes) {
327
+ const ok = await confirm(`This scan is estimated at $${estimate.usd.toFixed(2)}. Proceed? [y/N] `);
328
+ if (!ok) {
329
+ console.log("[raws] aborted.");
330
+ return;
331
+ }
332
+ }
333
+ // Warn if this scan's embedding model differs from the library's (vectors won't compare).
334
+ if (activeClient.embeddingModelId) {
335
+ const existing = store.getLibraryEmbeddingModel();
336
+ if (existing && existing !== activeClient.embeddingModelId) {
337
+ console.warn(`[raws] WARNING: library was built with "${existing}" embeddings but this scan uses "${activeClient.embeddingModelId}". Semantic search across them will be unreliable.`);
338
+ }
339
+ else {
340
+ store.rememberEmbeddingModel(activeClient.embeddingModelId);
341
+ }
342
+ }
343
+ const ctx = {
344
+ client: activeClient,
345
+ workDir: path.join(store.paths.workDir, createIdV7("scan")),
346
+ clipsDir: store.paths.clipsDir,
347
+ thumbsDir: store.paths.thumbsDir,
348
+ framesPerScene,
349
+ includeAudio,
350
+ guidancePrompt: guidance,
351
+ userNotes,
352
+ refineSegmentation: !values["no-refine"],
353
+ reencodeClips: true,
354
+ audioOnly: Boolean(values["audio-only"]),
355
+ crop: aspect && aspect !== "original" ? { aspect, focus: cropFocus } : undefined
356
+ };
357
+ console.log(`[raws] scanning ${scenes.length} scenes (${provider}/${activeClient.tagModelId}, ${concurrency}x) …`);
358
+ let errors = 0;
359
+ const started = timestamp();
360
+ const result = await scanVideo({
361
+ ctx,
362
+ videoPath,
363
+ scenes,
364
+ probe,
365
+ windows,
366
+ durationBand,
367
+ targetClipCount,
368
+ sourceFilename: path.basename(videoPath),
369
+ concurrency,
370
+ onRefine: (r) => {
371
+ if (r.refined) {
372
+ console.log(`[raws] guided segmentation: ${scenes.length} → ${r.kept} raws (dropped ${r.dropped}, merged ${r.merged}).`);
373
+ }
374
+ else if (r.note) {
375
+ console.log(`[raws] guided segmentation skipped: ${r.note}.`);
376
+ }
377
+ },
378
+ onClip: (clip, i, total) => {
379
+ store.insertClip(clip);
380
+ const tagline = summarizeTags(clip);
381
+ console.log(` [${i}/${total}] ${clip.clip_id.slice(0, 14)} ${fmtRange(clip)} ${tagline}`);
382
+ },
383
+ onError: (scene, error) => {
384
+ errors++;
385
+ console.warn(` ✗ scene @${scene.start_time_sec.toFixed(1)}s: ${error instanceof Error ? error.message : String(error)}`);
386
+ }
387
+ });
388
+ store.recordSourceVideo({
389
+ source_video_id: result.source_video_id,
390
+ filename: path.basename(videoPath),
391
+ source_path: videoPath,
392
+ duration_sec: probe.duration_sec,
393
+ clip_count: result.clips.length,
394
+ tier
395
+ });
396
+ // Scratch frames/audio are disposable.
397
+ try {
398
+ rmSync(ctx.workDir, { recursive: true, force: true });
399
+ }
400
+ catch {
401
+ /* ignore */
402
+ }
403
+ store.close();
404
+ console.log(`\n[raws] done in ${elapsed(started)}: ${result.clips.length} raws${errors ? `, ${errors} scene(s) failed` : ""}.`);
405
+ console.log(` raws → ${store.paths.clipsDir}`);
406
+ console.log(` db → ${store.paths.db}`);
407
+ console.log(` try: vidfarm raws search "…" | vidfarm raws preset run "funny reaction gifs"`);
408
+ }
409
+ // ── clips scan --cloud (explicit backup: the deployed pipeline) ─────────────
410
+ // Uploads the local file into the user's temp folder (30-day TTL, auto-removed)
411
+ // — or passes --url straight through — then starts POST /clips/scan and polls.
412
+ // The hunt bills AWS compute only; AI runs on the account's saved BYOK keys.
413
+ async function runScanCloud(input) {
414
+ const baseUrl = (input.apiUrl || process.env.VIDFARM_API_URL || process.env.VIDFARM_UPSTREAM_HOST || "https://vidfarm.cc").replace(/\/$/, "");
415
+ const apiKey = input.apiKey || process.env.VIDFARM_API_KEY || "";
416
+ if (!apiKey)
417
+ throw new Error("--cloud needs an API key: pass --api-key or set VIDFARM_API_KEY.");
418
+ const headers = { "vidfarm-api-key": apiKey, "content-type": "application/json", accept: "application/json" };
419
+ let tempFileId;
420
+ let fileName;
421
+ if (!input.sourceUrl) {
422
+ if (!input.videoArg)
423
+ throw new Error("Usage: vidfarm raws scan --cloud <video-path> | --cloud --url <video-url>");
424
+ const videoPath = path.resolve(process.cwd(), input.videoArg);
425
+ if (!existsSync(videoPath))
426
+ throw new Error(`No such video: ${videoPath}`);
427
+ fileName = path.basename(videoPath);
428
+ const sizeBytes = statSync(videoPath).size;
429
+ console.log(`[raws] uploading ${fileName} (${(sizeBytes / (1024 * 1024)).toFixed(1)} MB) to your temp folder (auto-deletes in 30 days) …`);
430
+ const presignRes = await fetch(`${baseUrl}/api/v1/user/me/temporary-files/presign`, {
431
+ method: "POST",
432
+ headers,
433
+ body: JSON.stringify({ file_name: fileName, content_type: "video/mp4", size_bytes: sizeBytes, folder_path: "clip-sources" })
434
+ });
435
+ const presign = await presignRes.json();
436
+ if (!presignRes.ok || !presign.upload || !presign.file_id) {
437
+ throw new Error(`Temp upload presign failed (${presignRes.status}): ${presign.error ?? "unexpected response"}`);
438
+ }
439
+ if (presign.transport !== "presigned") {
440
+ throw new Error("The deployment does not support presigned uploads — upload via the web UI, then re-run with --url or temp_file_id.");
441
+ }
442
+ const putRes = await fetch(presign.upload.url, {
443
+ method: presign.upload.method,
444
+ headers: presign.upload.headers,
445
+ body: readFileSync(path.resolve(process.cwd(), input.videoArg))
446
+ });
447
+ if (!putRes.ok)
448
+ throw new Error(`Upload failed (${putRes.status}).`);
449
+ const finalizeRes = await fetch(`${baseUrl}/api/v1/user/me/temporary-files`, {
450
+ method: "POST",
451
+ headers,
452
+ body: JSON.stringify({
453
+ file_id: presign.file_id,
454
+ file_name: fileName,
455
+ content_type: "video/mp4",
456
+ size_bytes: sizeBytes,
457
+ storage_key: presign.storage_key,
458
+ folder_path: "clip-sources"
459
+ })
460
+ });
461
+ if (!finalizeRes.ok) {
462
+ const body = await finalizeRes.text().catch(() => "");
463
+ throw new Error(`Temp upload finalize failed (${finalizeRes.status}): ${body.slice(0, 300)}`);
464
+ }
465
+ tempFileId = presign.file_id;
466
+ }
467
+ console.log(`[raws] starting cloud hunt …`);
468
+ const scanRes = await fetch(`${baseUrl}/raws/scan`, {
469
+ method: "POST",
470
+ headers,
471
+ body: JSON.stringify({
472
+ ...(input.sourceUrl ? { source_url: input.sourceUrl } : { temp_file_id: tempFileId, filename: fileName }),
473
+ prompt: input.prompt ?? "",
474
+ ...(input.userNotes ? { user_notes: input.userNotes } : {}),
475
+ ...(input.provider ? { provider: input.provider } : {}),
476
+ ...(input.tracer ? { tracer: input.tracer } : {}),
477
+ ...(input.folder ? { folder_path: input.folder } : {}),
478
+ hunt_spec: {
479
+ ...(input.windows.length ? { windows: input.windows } : {}),
480
+ ...(input.durationBand ? { duration_band: input.durationBand } : {}),
481
+ ...(input.aspect ? { aspect: input.aspect } : {}),
482
+ crop_focus: input.cropFocus,
483
+ // Send explicitly (incl. false) so --allow-text can override the
484
+ // server-side avoid-text default.
485
+ avoid_text: input.avoidText,
486
+ ...(input.maxClips ? { max_clips: input.maxClips } : {}),
487
+ ...(input.audioOnly ? { audio_only: true } : {})
488
+ }
489
+ })
490
+ });
491
+ const scan = await scanRes.json();
492
+ if (!scanRes.ok || !scan.scan_id) {
493
+ throw new Error(`Cloud scan failed to start (${scanRes.status}): ${scan.error ?? "unexpected response"}`);
494
+ }
495
+ console.log(`[raws] scan ${scan.scan_id} ${scan.status ?? "queued"}${scan.tracer ? ` (tracer ${scan.tracer})` : ""}.`);
496
+ if (scan.compute_estimate?.estimated_charge_usd != null) {
497
+ console.log(`[raws] estimated AWS compute charge ~$${scan.compute_estimate.estimated_charge_usd.toFixed(4)} (AI runs on your saved provider keys).`);
498
+ }
499
+ // Async by design — poll until the pipeline finishes.
500
+ const started = timestamp();
501
+ for (;;) {
502
+ await new Promise((r) => setTimeout(r, 5000));
503
+ const pollRes = await fetch(`${baseUrl}/raws/scan/${encodeURIComponent(scan.scan_id)}`, { headers });
504
+ if (!pollRes.ok)
505
+ continue;
506
+ const poll = await pollRes.json();
507
+ const status = poll.source?.status ?? poll.scan?.status ?? "running";
508
+ if (status === "complete") {
509
+ console.log(`\n[raws] cloud hunt done in ${elapsed(started)}: ${poll.source?.clip_count ?? 0} raws.`);
510
+ console.log(` browse → ${baseUrl}/library/raws`);
511
+ return;
512
+ }
513
+ if (status === "failed") {
514
+ throw new Error(`Cloud hunt failed: ${poll.scan?.error ?? "unknown error"}`);
515
+ }
516
+ process.stdout.write(`\r[raws] ${status} … ${elapsed(started)} `);
517
+ }
518
+ }
519
+ /**
520
+ * LOCAL-FIRST provider pick: an explicit --provider always wins; otherwise use
521
+ * the local agent CLI when one is installed (subscription-powered, keyless),
522
+ * else fall back to gemini BYOK. The cloud pipeline is never a silent default
523
+ * — it's the explicit `--cloud` backup.
524
+ */
525
+ function resolveScanProvider(values) {
526
+ if (values.provider === "agent" || values.provider === "local" || values.provider === "local-agent")
527
+ return "agent";
528
+ if (values.provider)
529
+ return normalizeProvider(values.provider);
530
+ if (values.agent)
531
+ return "agent";
532
+ if (detectLocalAgent())
533
+ return "agent";
534
+ return "gemini";
535
+ }
536
+ /** Embeddings for a local-agent scan: any gemini/openai key found makes the library semantically searchable. */
537
+ function resolveAgentEmbeddingConfig(flags) {
538
+ const explicit = flags["embed-provider"]?.trim();
539
+ const tryProvider = (p) => {
540
+ const key = flags["embed-key"]?.trim() || resolveProviderKey(p, flags, { optional: true });
541
+ return key ? { provider: p, apiKey: key } : null;
542
+ };
543
+ if (explicit === "gemini")
544
+ return tryProvider("gemini");
545
+ if (explicit === "openai")
546
+ return tryProvider("openai");
547
+ return tryProvider("gemini") ?? tryProvider("openai");
548
+ }
549
+ // ── clips list ──────────────────────────────────────────────────────────────
550
+ async function runList(argv) {
551
+ const { values } = parseArgs({
552
+ args: argv,
553
+ options: {
554
+ source: { type: "string" },
555
+ "content-type": { type: "string" },
556
+ offset: { type: "string" },
557
+ limit: { type: "string" },
558
+ json: { type: "boolean" },
559
+ home: { type: "string" }
560
+ }
561
+ });
562
+ const store = new ClipStore(values.home);
563
+ const clips = store.listClips({
564
+ sourceVideoId: values.source,
565
+ contentTypes: parseContentTypes(values["content-type"]),
566
+ offset: values.offset ? clampInt(values.offset, 0, 0, 1_000_000) : undefined,
567
+ limit: clampInt(values.limit, 50, 1, 5000)
568
+ });
569
+ store.close();
570
+ if (values.json) {
571
+ console.log(JSON.stringify(clips, null, 2));
572
+ return;
573
+ }
574
+ if (clips.length === 0) {
575
+ console.log("No raws yet. Run: vidfarm raws scan <video-path>");
576
+ return;
577
+ }
578
+ for (const clip of clips) {
579
+ console.log(`${clip.clip_id.slice(0, 14)} ${fmtRange(clip)} ${clip.source_filename} ${summarizeTags(clip)}`);
580
+ }
581
+ console.log(`\n${clips.length} raws.`);
582
+ }
583
+ // ── clips search ────────────────────────────────────────────────────────────
584
+ async function runSearch(argv) {
585
+ const { values, positionals } = parseArgs({
586
+ args: argv,
587
+ allowPositionals: true,
588
+ options: {
589
+ provider: { type: "string" },
590
+ "content-type": { type: "string" },
591
+ limit: { type: "string" },
592
+ json: { type: "boolean" },
593
+ "gemini-key": { type: "string" },
594
+ "openai-key": { type: "string" },
595
+ "openrouter-key": { type: "string" },
596
+ home: { type: "string" }
597
+ }
598
+ });
599
+ const query = positionals.join(" ").trim();
600
+ if (!query)
601
+ throw new Error('Usage: vidfarm raws search "<natural language>"');
602
+ const contentTypes = parseContentTypes(values["content-type"]);
603
+ const limit = clampInt(values.limit, 10, 1, 200);
604
+ const store = new ClipStore(values.home);
605
+ if (store.stats().clips === 0) {
606
+ store.close();
607
+ console.log("Library is empty — scan a video first: vidfarm raws scan <video-path>");
608
+ return;
609
+ }
610
+ const provider = normalizeProvider(values.provider);
611
+ const apiKey = resolveProviderKey(provider, values, { optional: true });
612
+ let criteria = { semantic_text: query };
613
+ let queryEmbedding;
614
+ if (apiKey) {
615
+ const client = new ClipModelClient({ provider, apiKey });
616
+ criteria = await client.naturalLanguageToCriteria(query);
617
+ if (client.canEmbed) {
618
+ const libModel = store.getLibraryEmbeddingModel();
619
+ if (libModel && client.embeddingModelId && libModel !== client.embeddingModelId) {
620
+ console.warn(`[raws] note: library uses "${libModel}" embeddings but you're searching with "${client.embeddingModelId}" — semantic re-rank may be off; pass --provider to match.`);
621
+ }
622
+ queryEmbedding = await client.embedQuery(criteria.semantic_text ?? query);
623
+ }
624
+ }
625
+ else if (detectLocalAgent()) {
626
+ // Local-first: no key, but a local agent CLI can still turn the natural-
627
+ // language query into a structured filter (vector re-rank needs a key).
628
+ const agent = new LocalAgentClipClient({});
629
+ console.log(`[raws] structuring query with your local ${agent.tagModelId} …`);
630
+ criteria = await agent.naturalLanguageToCriteria(query);
631
+ }
632
+ else {
633
+ console.warn(`[raws] no ${provider} key or local agent CLI — structured/keyword search only.`);
634
+ }
635
+ const t0 = timestamp();
636
+ // Over-fetch when a content-type filter is on so the post-filter still fills
637
+ // the requested limit (the tag lives in tags_json, not the search index).
638
+ const searchLimit = contentTypes.length ? Math.min(1000, limit * 5) : limit;
639
+ let hits = store.search({ criteria, queryEmbedding, limit: searchLimit });
640
+ if (contentTypes.length) {
641
+ hits = hits.filter((h) => (h.clip.tags.content_type ?? []).some((t) => contentTypes.includes(String(t).toLowerCase()))).slice(0, limit);
642
+ }
643
+ const ms = elapsedMs(t0);
644
+ store.close();
645
+ if (values.json) {
646
+ console.log(JSON.stringify(hits, null, 2));
647
+ return;
648
+ }
649
+ printHits(hits, { query, ms, criteria });
650
+ }
651
+ function normalizeMatchScene(s, i) {
652
+ const mc = (s.match_criteria ?? s.criteria);
653
+ const embText = (s.embedding_text ?? s.text);
654
+ const criteria = mc ?? (embText ? { semantic_text: embText } : {});
655
+ const text = embText || criteria.semantic_text || "";
656
+ return {
657
+ id: s.scene_slug || s.id || `scene_${i}`,
658
+ criteria,
659
+ text,
660
+ meta: {
661
+ replaceability: typeof s.replaceability === "string" ? s.replaceability : undefined,
662
+ duration: typeof s.duration === "number" ? s.duration : undefined,
663
+ start: typeof s.start === "number" ? s.start : undefined,
664
+ scene_role: typeof s.scene_role === "string" ? s.scene_role : undefined
665
+ }
666
+ };
667
+ }
668
+ async function runMatch(argv) {
669
+ const { values, positionals } = parseArgs({
670
+ args: argv,
671
+ allowPositionals: true,
672
+ options: {
673
+ criteria: { type: "string" },
674
+ scenes: { type: "string" },
675
+ provider: { type: "string" },
676
+ limit: { type: "string" },
677
+ json: { type: "boolean" },
678
+ "gemini-key": { type: "string" },
679
+ "openai-key": { type: "string" },
680
+ "openrouter-key": { type: "string" },
681
+ home: { type: "string" }
682
+ }
683
+ });
684
+ const store = new ClipStore(values.home);
685
+ if (store.stats().clips === 0) {
686
+ store.close();
687
+ console.log("Library is empty — scan a video first: vidfarm raws scan <video-path>");
688
+ return;
689
+ }
690
+ // Embed the match query in the SAME space the library was built with.
691
+ const libModel = store.getLibraryEmbeddingModel();
692
+ const embedProvider = libModel && libModel.startsWith("text-embedding") ? "openai" : "gemini";
693
+ const embedKey = resolveProviderKey(embedProvider, values, { optional: true });
694
+ const client = embedKey ? new ClipModelClient({ provider: embedProvider, apiKey: embedKey }) : null;
695
+ if (!client)
696
+ console.warn(`[raws] no ${embedProvider} key — structured match only (set the key for vector re-rank).`);
697
+ if (values.scenes) {
698
+ const filePath = path.resolve(process.cwd(), values.scenes);
699
+ if (!existsSync(filePath))
700
+ throw new Error(`No such file: ${filePath}`);
701
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"));
702
+ const rawScenes = (Array.isArray(parsed)
703
+ ? parsed
704
+ : (parsed.annotations ??
705
+ parsed.scenes ??
706
+ []));
707
+ if (rawScenes.length === 0)
708
+ throw new Error("No scene annotations found in file (expected an array, or {annotations|scenes:[…]}).");
709
+ const scenes = rawScenes.map(normalizeMatchScene);
710
+ const perScene = clampInt(values.limit, 5, 1, 50);
711
+ const embeddings = client ? await client.embedQueries(scenes.map((s) => s.text || " ")) : [];
712
+ // Load the library once for the whole batch.
713
+ const all = store.listClips();
714
+ store.attachEmbeddings(all);
715
+ store.close();
716
+ const output = scenes.map((s, i) => ({
717
+ scene_slug: s.id,
718
+ ...s.meta,
719
+ matches: searchClips(all, {
720
+ criteria: s.criteria,
721
+ queryEmbedding: client ? embeddings[i] : undefined,
722
+ limit: perScene,
723
+ relaxWhenEmpty: true
724
+ })
725
+ }));
726
+ if (values.json) {
727
+ console.log(JSON.stringify(output, null, 2));
728
+ return;
729
+ }
730
+ console.log(`Matched ${scenes.length} scenes against ${all.length} raws:`);
731
+ for (const o of output) {
732
+ const rep = o.replaceability ? ` [replaceability: ${o.replaceability}]` : "";
733
+ const dur = typeof o.duration === "number" ? ` (${o.duration.toFixed(1)}s)` : "";
734
+ console.log(`\n▶ ${o.scene_slug}${rep}${dur}`);
735
+ if (o.matches.length === 0) {
736
+ console.log(" (no library match)");
737
+ continue;
738
+ }
739
+ o.matches.forEach((h, j) => {
740
+ const sim = h.similarity != null ? ` sim=${h.similarity.toFixed(2)}` : "";
741
+ console.log(` ${j + 1}. ${h.clip.clip_id.slice(0, 14)} ${fmtRange(h.clip)} score=${h.score.toFixed(2)}${sim} ${summarizeTags(h.clip)}`);
742
+ });
743
+ }
744
+ return;
745
+ }
746
+ // Single scene: --criteria '<json>' or a free-text query.
747
+ const limit = clampInt(values.limit, 8, 1, 50);
748
+ let criteria;
749
+ let text;
750
+ const asJson = values.criteria ? tryParseJson(values.criteria) : null;
751
+ if (asJson && typeof asJson === "object") {
752
+ criteria = asJson;
753
+ text = criteria.semantic_text ?? positionals.join(" ").trim();
754
+ }
755
+ else {
756
+ text = positionals.join(" ").trim();
757
+ if (!text) {
758
+ store.close();
759
+ throw new Error('Usage: vidfarm raws match "<text>" | --criteria \'<json>\' | --scenes <annotations.json>');
760
+ }
761
+ criteria = { semantic_text: text };
762
+ }
763
+ const queryEmbedding = client && text ? await client.embedQuery(text) : undefined;
764
+ const t0 = timestamp();
765
+ const hits = store.search({ criteria, queryEmbedding, limit });
766
+ const ms = elapsedMs(t0);
767
+ store.close();
768
+ if (values.json) {
769
+ console.log(JSON.stringify(hits, null, 2));
770
+ return;
771
+ }
772
+ printHits(hits, { query: text || "criteria", ms, criteria });
773
+ }
774
+ // ── clips preset ────────────────────────────────────────────────────────────
775
+ async function runPreset(argv) {
776
+ const action = argv[0];
777
+ const rest = argv.slice(1);
778
+ if (action === "list")
779
+ return runPresetList(rest);
780
+ if (action === "run")
781
+ return runPresetRun(rest);
782
+ if (action === "save")
783
+ return runPresetSave(rest);
784
+ throw new Error("Usage: vidfarm raws preset <list|run <name>|save <name> --from-query '<...>'>");
785
+ }
786
+ async function runPresetList(argv) {
787
+ const { values } = parseArgs({ args: argv, options: { json: { type: "boolean" }, home: { type: "string" } } });
788
+ const store = new ClipStore(values.home);
789
+ const presets = store.listPresets();
790
+ store.close();
791
+ if (values.json) {
792
+ console.log(JSON.stringify(presets, null, 2));
793
+ return;
794
+ }
795
+ for (const p of presets) {
796
+ const tag = p.builtin ? "(built-in)" : "(saved)";
797
+ console.log(`${p.name} ${tag}\n ${p.description ?? ""}\n ${describeCriteria(p.criteria)}`);
798
+ }
799
+ }
800
+ async function runPresetRun(argv) {
801
+ const { values, positionals } = parseArgs({
802
+ args: argv,
803
+ allowPositionals: true,
804
+ options: {
805
+ provider: { type: "string" },
806
+ limit: { type: "string" },
807
+ json: { type: "boolean" },
808
+ "gemini-key": { type: "string" },
809
+ "openai-key": { type: "string" },
810
+ "openrouter-key": { type: "string" },
811
+ home: { type: "string" }
812
+ }
813
+ });
814
+ const name = positionals.join(" ").trim();
815
+ if (!name)
816
+ throw new Error("Usage: vidfarm raws preset run <name>");
817
+ const store = new ClipStore(values.home);
818
+ const preset = store.findPreset(name);
819
+ if (!preset) {
820
+ store.close();
821
+ throw new Error(`No preset "${name}". Try: vidfarm raws preset list`);
822
+ }
823
+ const limit = clampInt(values.limit, 10, 1, 200);
824
+ // Presets carry semantic_text; embed it if we have a key for the re-rank.
825
+ let queryEmbedding;
826
+ const provider = normalizeProvider(values.provider);
827
+ const apiKey = resolveProviderKey(provider, values, { optional: true });
828
+ if (apiKey && preset.criteria.semantic_text) {
829
+ const client = new ClipModelClient({ provider, apiKey });
830
+ if (client.canEmbed)
831
+ queryEmbedding = await client.embedQuery(preset.criteria.semantic_text);
832
+ }
833
+ const t0 = timestamp();
834
+ const hits = store.search({ criteria: preset.criteria, queryEmbedding, limit });
835
+ const ms = elapsedMs(t0);
836
+ store.close();
837
+ if (values.json) {
838
+ console.log(JSON.stringify(hits, null, 2));
839
+ return;
840
+ }
841
+ printHits(hits, { query: preset.name, ms, criteria: preset.criteria });
842
+ }
843
+ async function runPresetSave(argv) {
844
+ const { values, positionals } = parseArgs({
845
+ args: argv,
846
+ allowPositionals: true,
847
+ options: {
848
+ "from-query": { type: "string" },
849
+ description: { type: "string" },
850
+ provider: { type: "string" },
851
+ "gemini-key": { type: "string" },
852
+ "openai-key": { type: "string" },
853
+ "openrouter-key": { type: "string" },
854
+ home: { type: "string" }
855
+ }
856
+ });
857
+ const name = positionals.join(" ").trim();
858
+ const fromQuery = values["from-query"];
859
+ if (!name || !fromQuery) {
860
+ throw new Error("Usage: vidfarm raws preset save <name> --from-query '<json | natural language>'");
861
+ }
862
+ let criteria;
863
+ const asJson = tryParseJson(fromQuery);
864
+ if (asJson && typeof asJson === "object") {
865
+ criteria = asJson;
866
+ }
867
+ else {
868
+ // Natural language → structured: provider key when present, else the
869
+ // local agent CLI (local-first), else the usual missing-key error.
870
+ const provider = normalizeProvider(values.provider);
871
+ const apiKey = resolveProviderKey(provider, values, { optional: true });
872
+ if (apiKey) {
873
+ criteria = await new ClipModelClient({ provider, apiKey }).naturalLanguageToCriteria(fromQuery);
874
+ }
875
+ else if (detectLocalAgent()) {
876
+ criteria = await new LocalAgentClipClient({}).naturalLanguageToCriteria(fromQuery);
877
+ }
878
+ else {
879
+ resolveProviderKey(provider, values); // throws the "No <provider> API key" error
880
+ throw new Error("unreachable");
881
+ }
882
+ }
883
+ const store = new ClipStore(values.home);
884
+ const preset = store.savePreset({ name, criteria, description: values.description });
885
+ store.close();
886
+ console.log(`Saved preset "${preset.name}" (${preset.preset_id}).`);
887
+ console.log(` ${describeCriteria(preset.criteria)}`);
888
+ }
889
+ // ── clips sources ───────────────────────────────────────────────────────────
890
+ async function runSources(argv) {
891
+ const { values } = parseArgs({ args: argv, options: { json: { type: "boolean" }, home: { type: "string" } } });
892
+ const store = new ClipStore(values.home);
893
+ const sources = store.listSourceVideos();
894
+ store.close();
895
+ if (values.json) {
896
+ console.log(JSON.stringify(sources, null, 2));
897
+ return;
898
+ }
899
+ if (sources.length === 0) {
900
+ console.log("No sources scanned yet.");
901
+ return;
902
+ }
903
+ for (const s of sources) {
904
+ console.log(`${s.source_video_id.slice(0, 14)} ${s.filename} ${s.clip_count} clips ${s.duration_sec ? fmtDuration(s.duration_sec) : ""} ${s.scanned_at}`);
905
+ }
906
+ }
907
+ // ── clips export ────────────────────────────────────────────────────────────
908
+ async function runExport(argv) {
909
+ const { values, positionals } = parseArgs({
910
+ args: argv,
911
+ allowPositionals: true,
912
+ options: { to: { type: "string" }, home: { type: "string" } }
913
+ });
914
+ const ids = positionals;
915
+ const to = values.to;
916
+ if (ids.length === 0 || !to) {
917
+ throw new Error("Usage: vidfarm raws export <raw-ids...> --to <dir>");
918
+ }
919
+ const outDir = path.resolve(process.cwd(), to);
920
+ mkdirSync(outDir, { recursive: true });
921
+ const store = new ClipStore(values.home);
922
+ const exported = [];
923
+ for (const id of ids) {
924
+ const clip = store.getClip(id);
925
+ if (!clip) {
926
+ console.warn(` ✗ no clip ${id}`);
927
+ continue;
928
+ }
929
+ const src = path.join(store.paths.clipsDir, `${clip.clip_id}.mp4`);
930
+ if (!existsSync(src)) {
931
+ console.warn(` ✗ missing file for ${id} (${src})`);
932
+ continue;
933
+ }
934
+ const dest = path.join(outDir, `${clip.clip_id}.mp4`);
935
+ copyFileSync(src, dest);
936
+ const thumb = path.join(store.paths.thumbsDir, `${clip.clip_id}.jpg`);
937
+ if (existsSync(thumb))
938
+ copyFileSync(thumb, path.join(outDir, `${clip.clip_id}.jpg`));
939
+ exported.push(clip);
940
+ console.log(` ✓ ${clip.clip_id} → ${dest}`);
941
+ }
942
+ store.close();
943
+ const { writeFileSync } = await import("node:fs");
944
+ writeFileSync(path.join(outDir, "clips.metadata.json"), JSON.stringify(exported, null, 2));
945
+ console.log(`\nExported ${exported.length} raws + clips.metadata.json → ${outDir}`);
946
+ }
947
+ // ── helpers ─────────────────────────────────────────────────────────────────
948
+ function normalizeProvider(value) {
949
+ if (value === undefined || value === "gemini")
950
+ return "gemini";
951
+ if (value === "openai" || value === "openrouter" || value === "nvidia")
952
+ return value;
953
+ throw new Error(`--provider must be gemini | openai | openrouter | nvidia (got "${value}")`);
954
+ }
955
+ // Parse a comma-separated --content-type flag into lowercase shot-KIND tags for
956
+ // the tags.content_type filter (talking_head, b_roll, product_shot, …).
957
+ function parseContentTypes(value) {
958
+ if (!value)
959
+ return [];
960
+ return value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
961
+ }
962
+ function normalizeTier(value) {
963
+ if (value === "flash")
964
+ return "flash";
965
+ if (value === "flash-lite" || value === undefined)
966
+ return "flash-lite";
967
+ throw new Error(`--tier must be "flash" or "flash-lite" (got "${value}")`);
968
+ }
969
+ // Best-guess embedding model for the --dry-run estimate when no key is present
970
+ // (openrouter falls back to gemini embeddings by default).
971
+ function defaultEmbedModelForEstimate(provider) {
972
+ return provider === "openai" ? "text-embedding-3-small" : "gemini-embedding-001";
973
+ }
974
+ const ENV_KEYS = {
975
+ gemini: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
976
+ openai: ["OPENAI_API_KEY"],
977
+ openrouter: ["OPENROUTER_API_KEY"],
978
+ nvidia: ["NVIDIA_API_KEY"]
979
+ };
980
+ const CONFIG_KEYS = {
981
+ gemini: "gemini_api_key",
982
+ openai: "openai_api_key",
983
+ openrouter: "openrouter_api_key",
984
+ nvidia: "nvidia_api_key"
985
+ };
986
+ /** Resolve a provider key: --<provider>-key flag → *_API_KEY env → ~/.vidfarm/config.json. */
987
+ function resolveProviderKey(provider, flags, opts = {}) {
988
+ const flagVal = flags[`${provider}-key`]?.trim();
989
+ if (flagVal)
990
+ return flagVal;
991
+ for (const env of ENV_KEYS[provider]) {
992
+ const v = (process.env[env] || "").trim();
993
+ if (v)
994
+ return v;
995
+ }
996
+ const cfgPath = path.join(resolveHome(), "config.json");
997
+ if (existsSync(cfgPath)) {
998
+ try {
999
+ const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
1000
+ const v = cfg[CONFIG_KEYS[provider]]?.trim();
1001
+ if (v)
1002
+ return v;
1003
+ }
1004
+ catch {
1005
+ /* ignore malformed config */
1006
+ }
1007
+ }
1008
+ if (opts.optional)
1009
+ return "";
1010
+ const envHint = ENV_KEYS[provider][0];
1011
+ throw new Error(`No ${provider} API key. Pass --${provider}-key <key> or set ${envHint}.`);
1012
+ }
1013
+ /**
1014
+ * Embedding config for a scan. gemini/openai embed with their own key; openrouter
1015
+ * and nvidia can't embed, so pair them with a gemini/openai key via
1016
+ * --embed-provider/--embed-key (or GEMINI_API_KEY/OPENAI_API_KEY env). Returns
1017
+ * null → semantic search disabled (structured-only library).
1018
+ */
1019
+ function resolveEmbeddingConfig(provider, apiKey, flags) {
1020
+ if (provider === "gemini")
1021
+ return { provider: "gemini", apiKey };
1022
+ if (provider === "openai")
1023
+ return { provider: "openai", apiKey };
1024
+ // openrouter / nvidia → look for an explicit embed provider, else any gemini/openai key.
1025
+ const explicit = flags["embed-provider"]?.trim();
1026
+ const tryProvider = (p) => {
1027
+ const key = flags["embed-key"]?.trim() || resolveProviderKey(p, flags, { optional: true });
1028
+ return key ? { provider: p, apiKey: key } : null;
1029
+ };
1030
+ if (explicit === "gemini")
1031
+ return tryProvider("gemini");
1032
+ if (explicit === "openai")
1033
+ return tryProvider("openai");
1034
+ return tryProvider("gemini") ?? tryProvider("openai");
1035
+ }
1036
+ function clampInt(value, fallback, min, max) {
1037
+ if (value === undefined)
1038
+ return fallback;
1039
+ const n = Math.round(Number(value));
1040
+ if (!Number.isFinite(n))
1041
+ return fallback;
1042
+ return Math.max(min, Math.min(max, n));
1043
+ }
1044
+ function resolveHome() {
1045
+ return process.env.VIDFARM_HOME ? path.resolve(process.env.VIDFARM_HOME) : path.join(homedir(), ".vidfarm");
1046
+ }
1047
+ async function confirm(question) {
1048
+ if (!process.stdin.isTTY)
1049
+ return false;
1050
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1051
+ try {
1052
+ const answer = (await rl.question(question)).trim().toLowerCase();
1053
+ return answer === "y" || answer === "yes";
1054
+ }
1055
+ finally {
1056
+ rl.close();
1057
+ }
1058
+ }
1059
+ function tryParseJson(text) {
1060
+ const trimmed = text.trim();
1061
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
1062
+ return null;
1063
+ try {
1064
+ return JSON.parse(trimmed);
1065
+ }
1066
+ catch {
1067
+ return null;
1068
+ }
1069
+ }
1070
+ function summarizeTags(clip) {
1071
+ const t = clip.tags;
1072
+ const bits = [...t.subject.slice(0, 1), ...t.action.slice(0, 1), ...t.emotion.slice(0, 1)].filter(Boolean);
1073
+ const energy = `⚡${t.energy}`;
1074
+ const desc = clip.description ? `— ${clip.description.slice(0, 48)}` : "";
1075
+ return `${bits.join("/") || "untagged"} ${energy} ${desc}`.trim();
1076
+ }
1077
+ function printHits(hits, ctx) {
1078
+ console.log(`\n"${ctx.query}" → ${hits.length} results in ${ctx.ms}ms`);
1079
+ console.log(` filter: ${describeCriteria(ctx.criteria)}\n`);
1080
+ if (hits.length === 0) {
1081
+ console.log(" (no matches — try a broader query or scan more footage)");
1082
+ return;
1083
+ }
1084
+ hits.forEach((hit, i) => {
1085
+ const c = hit.clip;
1086
+ const sim = hit.similarity != null ? ` sim=${hit.similarity.toFixed(2)}` : "";
1087
+ console.log(` ${String(i + 1).padStart(2)}. ${c.clip_id.slice(0, 14)} ${fmtRange(c)} score=${hit.score.toFixed(2)}${sim}`);
1088
+ console.log(` ${summarizeTags(c)}`);
1089
+ console.log(` ${c.thumbnail_path}`);
1090
+ });
1091
+ }
1092
+ function describeCriteria(criteria) {
1093
+ const parts = [];
1094
+ for (const key of ["subject", "action", "emotion", "setting", "composition", "motion", "audio_type", "energy"]) {
1095
+ const v = criteria[key];
1096
+ if (Array.isArray(v) && v.length)
1097
+ parts.push(`${key} ∈ {${v.join(",")}}`);
1098
+ }
1099
+ if (criteria.min_duration_sec != null)
1100
+ parts.push(`≥${criteria.min_duration_sec}s`);
1101
+ if (criteria.max_duration_sec != null)
1102
+ parts.push(`≤${criteria.max_duration_sec}s`);
1103
+ if (criteria.has_dialogue != null)
1104
+ parts.push(criteria.has_dialogue ? "has dialogue" : "no dialogue");
1105
+ if (criteria.has_subject != null)
1106
+ parts.push(criteria.has_subject ? "has subject" : "no subject");
1107
+ if (criteria.semantic_text)
1108
+ parts.push(`~"${criteria.semantic_text}"`);
1109
+ return parts.length ? parts.join(" · ") : "(no constraints)";
1110
+ }
1111
+ function withinTarget(usdPerHour) {
1112
+ return usdPerHour <= 0.12;
1113
+ }
1114
+ function fmtRange(clip) {
1115
+ return `${fmtClock(clip.start_time_sec)}–${fmtClock(clip.end_time_sec)} (${clip.duration_sec.toFixed(1)}s)`;
1116
+ }
1117
+ function fmtClock(sec) {
1118
+ const m = Math.floor(sec / 60);
1119
+ const s = Math.floor(sec % 60);
1120
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
1121
+ }
1122
+ function fmtDuration(sec) {
1123
+ if (sec >= 60)
1124
+ return `${Math.floor(sec / 60)}m${Math.round(sec % 60)}s`;
1125
+ return `${sec.toFixed(1)}s`;
1126
+ }
1127
+ function timestamp() {
1128
+ return process.hrtime();
1129
+ }
1130
+ function elapsedMs(start) {
1131
+ const [s, ns] = process.hrtime(start);
1132
+ return Math.round(s * 1000 + ns / 1e6);
1133
+ }
1134
+ function elapsed(start) {
1135
+ const ms = elapsedMs(start);
1136
+ return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
1137
+ }
1138
+ //# sourceMappingURL=clips.js.map