@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.
- package/.agents/skills/dollarplatoon-skill/SKILL.md +1227 -0
- package/.agents/skills/editor-capabilities/SKILL.md +182 -0
- package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
- package/.agents/skills/embedded-captions/CATALOG.md +93 -0
- package/.agents/skills/embedded-captions/SKILL.md +286 -0
- package/.agents/skills/embedded-captions/assets/brand/CDPR-fankit-terms.txt +35 -0
- package/.agents/skills/embedded-captions/assets/brand/cyberpunk-widths.json +115 -0
- package/.agents/skills/embedded-captions/assets/fonts/char-widths.json +2211 -0
- package/.agents/skills/embedded-captions/assets/strokefonts/HersheyScript1.svg +242 -0
- package/.agents/skills/embedded-captions/assets/strokefonts/HersheyScriptMed.svg +242 -0
- package/.agents/skills/embedded-captions/dna/README.md +148 -0
- package/.agents/skills/embedded-captions/dna/chrome.json +68 -0
- package/.agents/skills/embedded-captions/dna/cream.json +69 -0
- package/.agents/skills/embedded-captions/dna/documentary.json +62 -0
- package/.agents/skills/embedded-captions/dna/editorial.json +70 -0
- package/.agents/skills/embedded-captions/dna/glitch.json +82 -0
- package/.agents/skills/embedded-captions/dna/ink.json +65 -0
- package/.agents/skills/embedded-captions/dna/keynote.json +62 -0
- package/.agents/skills/embedded-captions/dna/loud.json +75 -0
- package/.agents/skills/embedded-captions/dna/neon.json +58 -0
- package/.agents/skills/embedded-captions/dna/velocity.json +89 -0
- package/.agents/skills/embedded-captions/modes/cinematic/README.md +48 -0
- package/.agents/skills/embedded-captions/modes/cinematic/_archive/champion/spec.md +98 -0
- package/.agents/skills/embedded-captions/modes/cinematic/_archive/champion/template.html +188 -0
- package/.agents/skills/embedded-captions/modes/cinematic/_archive/memory-wall/spec.md +134 -0
- package/.agents/skills/embedded-captions/modes/cinematic/_archive/memory-wall/template.html +179 -0
- package/.agents/skills/embedded-captions/modes/cinematic/_archive/portrait-header/spec.md +81 -0
- package/.agents/skills/embedded-captions/modes/cinematic/_archive/portrait-header/template.html +170 -0
- package/.agents/skills/embedded-captions/modes/cinematic/cinematic-cream/spec.md +16 -0
- package/.agents/skills/embedded-captions/modes/cinematic/cinematic-cream/template.html +226 -0
- package/.agents/skills/embedded-captions/modes/cinematic/engine.html +355 -0
- package/.agents/skills/embedded-captions/modes/standard/_anatomy.md +233 -0
- package/.agents/skills/embedded-captions/modes/standard/_motion.md +192 -0
- package/.agents/skills/embedded-captions/modes/standard/fonts/build-fonts-css.cjs +97 -0
- package/.agents/skills/embedded-captions/references/aesthetic-principles.md +168 -0
- package/.agents/skills/embedded-captions/references/anti-patterns.md +188 -0
- package/.agents/skills/embedded-captions/references/bespoke-vs-presets.md +178 -0
- package/.agents/skills/embedded-captions/references/caption-grouping.md +130 -0
- package/.agents/skills/embedded-captions/references/composition-craft.md +522 -0
- package/.agents/skills/embedded-captions/references/direction-catalog.md +138 -0
- package/.agents/skills/embedded-captions/references/example-renders/champion.html +371 -0
- package/.agents/skills/embedded-captions/references/example-renders/memory-wall.html +334 -0
- package/.agents/skills/embedded-captions/references/failure-modes.md +145 -0
- package/.agents/skills/embedded-captions/references/layout-heuristics.md +238 -0
- package/.agents/skills/embedded-captions/references/motion-vocabulary.md +148 -0
- package/.agents/skills/embedded-captions/references/rail.md +68 -0
- package/.agents/skills/embedded-captions/references/reference-bar.md +47 -0
- package/.agents/skills/embedded-captions/references/scene-types.md +94 -0
- package/.agents/skills/embedded-captions/references/test-set.md +66 -0
- package/.agents/skills/embedded-captions/references/typographic-moves.md +266 -0
- package/.agents/skills/embedded-captions/references/typography-presets.md +63 -0
- package/.agents/skills/embedded-captions/scripts/audio-envelope.cjs +95 -0
- package/.agents/skills/embedded-captions/scripts/check-occlusion.cjs +250 -0
- package/.agents/skills/embedded-captions/scripts/check-overflow.cjs +194 -0
- package/.agents/skills/embedded-captions/scripts/check-rail-climax.cjs +233 -0
- package/.agents/skills/embedded-captions/scripts/check-timing.cjs +173 -0
- package/.agents/skills/embedded-captions/scripts/fill-timings.cjs +124 -0
- package/.agents/skills/embedded-captions/scripts/fit-fonts.cjs +158 -0
- package/.agents/skills/embedded-captions/scripts/gen-stroke-path.py +46 -0
- package/.agents/skills/embedded-captions/scripts/inject-fonts.cjs +169 -0
- package/.agents/skills/embedded-captions/scripts/lib-dna.cjs +201 -0
- package/.agents/skills/embedded-captions/scripts/make-cinematic.cjs +1205 -0
- package/.agents/skills/embedded-captions/scripts/make-composition.cjs +404 -0
- package/.agents/skills/embedded-captions/scripts/make-theme.cjs +8781 -0
- package/.agents/skills/embedded-captions/scripts/matte.cjs +294 -0
- package/.agents/skills/embedded-captions/scripts/measure-layout.cjs +293 -0
- package/.agents/skills/embedded-captions/scripts/prepare.sh +39 -0
- package/.agents/skills/embedded-captions/scripts/preview-frames.cjs +268 -0
- package/.agents/skills/embedded-captions/scripts/render-and-composite.sh +509 -0
- package/.agents/skills/embedded-captions/scripts/render-theme.sh +34 -0
- package/.agents/skills/embedded-captions/scripts/safe-zones.cjs +813 -0
- package/.agents/skills/embedded-captions/scripts/transcribe.cjs +349 -0
- package/.agents/skills/embedded-captions/themes/PORTING.md +49 -0
- package/.agents/skills/embedded-captions/themes/README.md +244 -0
- package/.agents/skills/embedded-captions/themes/anchor.json +27 -0
- package/.agents/skills/embedded-captions/themes/arcade.json +51 -0
- package/.agents/skills/embedded-captions/themes/aurora.json +41 -0
- package/.agents/skills/embedded-captions/themes/biolume.json +35 -0
- package/.agents/skills/embedded-captions/themes/brush.json +74 -0
- package/.agents/skills/embedded-captions/themes/chalkboard.json +52 -0
- package/.agents/skills/embedded-captions/themes/dossier.json +56 -0
- package/.agents/skills/embedded-captions/themes/graffiti.json +62 -0
- package/.agents/skills/embedded-captions/themes/hologram.json +41 -0
- package/.agents/skills/embedded-captions/themes/inkwater.json +43 -0
- package/.agents/skills/embedded-captions/themes/laser.json +46 -0
- package/.agents/skills/embedded-captions/themes/lastpage.json +33 -0
- package/.agents/skills/embedded-captions/themes/neonsign.json +53 -0
- package/.agents/skills/embedded-captions/themes/nightcity.json +56 -0
- package/.agents/skills/embedded-captions/themes/ordnance.json +54 -0
- package/.agents/skills/embedded-captions/themes/papercut.json +37 -0
- package/.agents/skills/embedded-captions/themes/popup.json +46 -0
- package/.agents/skills/embedded-captions/themes/ransom.json +62 -0
- package/.agents/skills/embedded-captions/themes/scoreboard.json +54 -0
- package/.agents/skills/embedded-captions/themes/spectrum.json +46 -0
- package/.agents/skills/embedded-captions/themes/stardust.json +29 -0
- package/.agents/skills/embedded-captions/themes/stomp.json +43 -0
- package/.agents/skills/embedded-captions/themes/terminal.json +49 -0
- package/.agents/skills/embedded-captions/themes/thunder.json +44 -0
- package/.agents/skills/embedded-captions/themes/transit.json +44 -0
- package/.agents/skills/embedded-captions/themes/vhs.json +47 -0
- package/.agents/skills/faceless-explainer/SKILL.md +209 -0
- package/.agents/skills/faceless-explainer/references/cut-catalog.md +215 -0
- package/.agents/skills/faceless-explainer/references/motion-language.md +156 -0
- package/.agents/skills/faceless-explainer/references/story-design.md +248 -0
- package/.agents/skills/faceless-explainer/references/visual-design.md +146 -0
- package/.agents/skills/faceless-explainer/scripts/assemble-index.mjs +570 -0
- package/.agents/skills/faceless-explainer/scripts/audio.mjs +260 -0
- package/.agents/skills/faceless-explainer/scripts/build-frame.mjs +536 -0
- package/.agents/skills/faceless-explainer/scripts/captions.mjs +508 -0
- package/.agents/skills/faceless-explainer/scripts/lib/assets.mjs +55 -0
- package/.agents/skills/faceless-explainer/scripts/lib/dimensions.mjs +45 -0
- package/.agents/skills/faceless-explainer/scripts/lib/storyboard.mjs +249 -0
- package/.agents/skills/faceless-explainer/scripts/lib/tokens.mjs +204 -0
- package/.agents/skills/faceless-explainer/scripts/lib/transition-registry.mjs +38 -0
- package/.agents/skills/faceless-explainer/scripts/lib/transitions.json +71 -0
- package/.agents/skills/faceless-explainer/scripts/transitions.mjs +320 -0
- package/.agents/skills/faceless-explainer/sub-agents/frame-worker.md +75 -0
- package/.agents/skills/farmville-saas-ux/SKILL.md +156 -0
- package/.agents/skills/farmville-saas-ux/assets/starter.html +294 -0
- package/.agents/skills/farmville-saas-ux/references/components.md +340 -0
- package/.agents/skills/farmville-saas-ux/references/porting-guide.md +121 -0
- package/.agents/skills/farmville-saas-ux/references/tokens.md +271 -0
- package/.agents/skills/flockposter/LICENSE +17 -0
- package/.agents/skills/flockposter/README.md +72 -0
- package/.agents/skills/flockposter/SKILL.md +562 -0
- package/.agents/skills/flockposter/examples/cli-instagram-reel.sh +21 -0
- package/.agents/skills/flockposter/examples/cli-multi-platform.json +69 -0
- package/.agents/skills/flockposter/examples/cli-tiktok-upload-only.sh +22 -0
- package/.agents/skills/flockposter/examples/cli-x-thread.sh +17 -0
- package/.agents/skills/flockposter/examples/cli-youtube-short.sh +21 -0
- package/.agents/skills/flockposter/examples/public-api-instagram-reel.json +28 -0
- package/.agents/skills/general-video/SKILL.md +143 -0
- package/.agents/skills/hyperframes/SKILL.md +160 -0
- package/.agents/skills/hyperframes-animation/SKILL.md +84 -0
- package/.agents/skills/hyperframes-animation/adapters/animate-text.md +64 -0
- package/.agents/skills/hyperframes-animation/adapters/animejs.md +114 -0
- package/.agents/skills/hyperframes-animation/adapters/css-animations.md +143 -0
- package/.agents/skills/hyperframes-animation/adapters/gsap-easing-and-stagger.md +118 -0
- package/.agents/skills/hyperframes-animation/adapters/gsap-timeline-and-labels.md +96 -0
- package/.agents/skills/hyperframes-animation/adapters/gsap-transforms-and-perf.md +96 -0
- package/.agents/skills/hyperframes-animation/adapters/gsap.md +105 -0
- package/.agents/skills/hyperframes-animation/adapters/html-in-canvas-patterns.md +507 -0
- package/.agents/skills/hyperframes-animation/adapters/lottie.md +117 -0
- package/.agents/skills/hyperframes-animation/adapters/three.md +131 -0
- package/.agents/skills/hyperframes-animation/adapters/typegpu.md +178 -0
- package/.agents/skills/hyperframes-animation/adapters/waapi.md +101 -0
- package/.agents/skills/hyperframes-animation/blueprints/comparison-split.md +27 -0
- package/.agents/skills/hyperframes-animation/blueprints/constellation-hub.md +44 -0
- package/.agents/skills/hyperframes-animation/blueprints/cta-morph-press.md +28 -0
- package/.agents/skills/hyperframes-animation/blueprints/cursor-ui-demo.md +57 -0
- package/.agents/skills/hyperframes-animation/blueprints/dataviz-countup.md +46 -0
- package/.agents/skills/hyperframes-animation/blueprints/device-surface-showcase.md +53 -0
- package/.agents/skills/hyperframes-animation/blueprints/grid-card-assemble.md +66 -0
- package/.agents/skills/hyperframes-animation/blueprints/kinetic-type-beats.md +78 -0
- package/.agents/skills/hyperframes-animation/blueprints/logo-assemble-lockup.md +69 -0
- package/.agents/skills/hyperframes-animation/blueprints/overwhelm-surround.md +28 -0
- package/.agents/skills/hyperframes-animation/blueprints/spatial-pan-stations.md +37 -0
- package/.agents/skills/hyperframes-animation/blueprints/ticker-takeover.md +29 -0
- package/.agents/skills/hyperframes-animation/blueprints/titlecard-reveal.md +42 -0
- package/.agents/skills/hyperframes-animation/blueprints/typewriter-reveal.md +51 -0
- package/.agents/skills/hyperframes-animation/blueprints/video-text-pivot.md +30 -0
- package/.agents/skills/hyperframes-animation/blueprints-index.md +146 -0
- package/.agents/skills/hyperframes-animation/examples/assets/avatars/02.avif +0 -0
- package/.agents/skills/hyperframes-animation/examples/assets/brands/github.avif +0 -0
- package/.agents/skills/hyperframes-animation/examples/assets/brands/nvidia.avif +0 -0
- package/.agents/skills/hyperframes-animation/examples/assets/brands/visa.avif +0 -0
- package/.agents/skills/hyperframes-animation/examples/assets/brands/zoominfo.avif +0 -0
- package/.agents/skills/hyperframes-animation/examples/brand-reveal-assemble-zoom.html +382 -0
- package/.agents/skills/hyperframes-animation/examples/comparison-split-cards.html +649 -0
- package/.agents/skills/hyperframes-animation/examples/concept-demo-decode-pan.html +520 -0
- package/.agents/skills/hyperframes-animation/examples/cta-morph-press.html +468 -0
- package/.agents/skills/hyperframes-animation/examples/cta-orbit-collapse.html +1298 -0
- package/.agents/skills/hyperframes-animation/examples/demo-page-scroll-spotlight.html +759 -0
- package/.agents/skills/hyperframes-animation/examples/hook-counter-burst.html +729 -0
- package/.agents/skills/hyperframes-animation/examples/messaging-multi-phrase.html +352 -0
- package/.agents/skills/hyperframes-animation/examples/metric-video-text-pivot.html +779 -0
- package/.agents/skills/hyperframes-animation/examples/problem-mockup-overwhelm.html +1374 -0
- package/.agents/skills/hyperframes-animation/examples/proof-logo-chain.html +861 -0
- package/.agents/skills/hyperframes-animation/examples/takeover-ticker-displace.html +347 -0
- package/.agents/skills/hyperframes-animation/examples/workflow-approve-press.html +606 -0
- package/.agents/skills/hyperframes-animation/rules/3d-page-scroll.md +227 -0
- package/.agents/skills/hyperframes-animation/rules/3d-text-depth-layers.md +297 -0
- package/.agents/skills/hyperframes-animation/rules/ai-tracking-box.md +382 -0
- package/.agents/skills/hyperframes-animation/rules/ambient-glow-bloom.md +305 -0
- package/.agents/skills/hyperframes-animation/rules/asr-keyword-glow.md +286 -0
- package/.agents/skills/hyperframes-animation/rules/avatar-cloud-network.md +371 -0
- package/.agents/skills/hyperframes-animation/rules/camera-cursor-tracking.md +246 -0
- package/.agents/skills/hyperframes-animation/rules/card-morph-anchor.md +267 -0
- package/.agents/skills/hyperframes-animation/rules/center-outward-expansion.md +227 -0
- package/.agents/skills/hyperframes-animation/rules/context-sensitive-cursor.md +257 -0
- package/.agents/skills/hyperframes-animation/rules/coordinate-target-zoom.md +332 -0
- package/.agents/skills/hyperframes-animation/rules/counting-dynamic-scale.md +283 -0
- package/.agents/skills/hyperframes-animation/rules/css-marker-patterns.md +373 -0
- package/.agents/skills/hyperframes-animation/rules/cursor-click-ripple.md +262 -0
- package/.agents/skills/hyperframes-animation/rules/depth-of-field-blur.md +313 -0
- package/.agents/skills/hyperframes-animation/rules/depth-scatter-assemble.md +303 -0
- package/.agents/skills/hyperframes-animation/rules/discrete-text-sequence.md +273 -0
- package/.agents/skills/hyperframes-animation/rules/dynamic-content-sequencing.md +307 -0
- package/.agents/skills/hyperframes-animation/rules/gsap-effects.md +299 -0
- package/.agents/skills/hyperframes-animation/rules/hacker-flip-3d.md +223 -0
- package/.agents/skills/hyperframes-animation/rules/kinetic-beat-slam.md +183 -0
- package/.agents/skills/hyperframes-animation/rules/motion-blur-streak.md +328 -0
- package/.agents/skills/hyperframes-animation/rules/multi-phase-camera.md +273 -0
- package/.agents/skills/hyperframes-animation/rules/orbit-3d-entry.md +301 -0
- package/.agents/skills/hyperframes-animation/rules/physics-press-reaction.md +350 -0
- package/.agents/skills/hyperframes-animation/rules/press-release-spring.md +296 -0
- package/.agents/skills/hyperframes-animation/rules/reactive-displacement.md +277 -0
- package/.agents/skills/hyperframes-animation/rules/scale-swap-transition.md +298 -0
- package/.agents/skills/hyperframes-animation/rules/sine-wave-loop.md +278 -0
- package/.agents/skills/hyperframes-animation/rules/split-tilt-cards.md +277 -0
- package/.agents/skills/hyperframes-animation/rules/spring-pop-entrance.md +273 -0
- package/.agents/skills/hyperframes-animation/rules/stat-bars-and-fills.md +156 -0
- package/.agents/skills/hyperframes-animation/rules/svg-icon-enrichment.md +329 -0
- package/.agents/skills/hyperframes-animation/rules/svg-path-draw.md +274 -0
- package/.agents/skills/hyperframes-animation/rules/vertical-spring-ticker.md +239 -0
- package/.agents/skills/hyperframes-animation/rules/viewport-change.md +349 -0
- package/.agents/skills/hyperframes-animation/rules-index.md +86 -0
- package/.agents/skills/hyperframes-animation/scripts/animation-map.mjs +606 -0
- package/.agents/skills/hyperframes-animation/scripts/package-loader.mjs +288 -0
- package/.agents/skills/hyperframes-animation/scripts/package-loader.test.mjs +62 -0
- package/.agents/skills/hyperframes-animation/techniques.md +507 -0
- package/.agents/skills/hyperframes-animation/transitions/TRANSITION-REGISTRY.md +167 -0
- package/.agents/skills/hyperframes-animation/transitions/catalog.md +127 -0
- package/.agents/skills/hyperframes-animation/transitions/css-3d.md +12 -0
- package/.agents/skills/hyperframes-animation/transitions/css-blur.md +51 -0
- package/.agents/skills/hyperframes-animation/transitions/css-cover.md +43 -0
- package/.agents/skills/hyperframes-animation/transitions/css-destruction.md +95 -0
- package/.agents/skills/hyperframes-animation/transitions/css-dissolve.md +66 -0
- package/.agents/skills/hyperframes-animation/transitions/css-distortion.md +45 -0
- package/.agents/skills/hyperframes-animation/transitions/css-grid.md +10 -0
- package/.agents/skills/hyperframes-animation/transitions/css-light.md +49 -0
- package/.agents/skills/hyperframes-animation/transitions/css-mechanical.md +30 -0
- package/.agents/skills/hyperframes-animation/transitions/css-other.md +25 -0
- package/.agents/skills/hyperframes-animation/transitions/css-push.md +41 -0
- package/.agents/skills/hyperframes-animation/transitions/css-radial.md +37 -0
- package/.agents/skills/hyperframes-animation/transitions/css-scale.md +24 -0
- package/.agents/skills/hyperframes-animation/transitions/overview.md +153 -0
- package/.agents/skills/hyperframes-cli/SKILL.md +113 -0
- package/.agents/skills/hyperframes-cli/references/doctor-browser.md +45 -0
- package/.agents/skills/hyperframes-cli/references/init-and-scaffold.md +55 -0
- package/.agents/skills/hyperframes-cli/references/lambda.md +132 -0
- package/.agents/skills/hyperframes-cli/references/lint-validate-inspect.md +121 -0
- package/.agents/skills/hyperframes-cli/references/preview-render.md +149 -0
- package/.agents/skills/hyperframes-cli/references/upgrade-info-misc.md +75 -0
- package/.agents/skills/hyperframes-core/SKILL.md +78 -0
- package/.agents/skills/hyperframes-core/references/composition-patterns.md +263 -0
- package/.agents/skills/hyperframes-core/references/data-attributes.md +70 -0
- package/.agents/skills/hyperframes-core/references/determinism-rules.md +68 -0
- package/.agents/skills/hyperframes-core/references/full-screen-motion.md +62 -0
- package/.agents/skills/hyperframes-core/references/minimal-composition.md +66 -0
- package/.agents/skills/hyperframes-core/references/script-format.md +49 -0
- package/.agents/skills/hyperframes-core/references/storyboard-format.md +95 -0
- package/.agents/skills/hyperframes-core/references/sub-compositions.md +237 -0
- package/.agents/skills/hyperframes-core/references/subagent-dispatch.md +41 -0
- package/.agents/skills/hyperframes-core/references/tailwind.md +125 -0
- package/.agents/skills/hyperframes-core/references/tracks-and-clips.md +76 -0
- package/.agents/skills/hyperframes-core/references/variables-and-media.md +90 -0
- package/.agents/skills/hyperframes-creative/SKILL.md +70 -0
- package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/FRAME.md +289 -0
- package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/caption-skin.html +221 -0
- package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/frame-showcase.html +1214 -0
- package/.agents/skills/hyperframes-creative/frame-presets/blockframe/FRAME.md +275 -0
- package/.agents/skills/hyperframes-creative/frame-presets/blockframe/caption-skin.html +224 -0
- package/.agents/skills/hyperframes-creative/frame-presets/blockframe/frame-showcase.html +1347 -0
- package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/FRAME.md +307 -0
- package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/caption-skin.html +223 -0
- package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/frame-showcase.html +1372 -0
- package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/FRAME.md +275 -0
- package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/caption-skin.html +230 -0
- package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/frame-showcase.html +1140 -0
- package/.agents/skills/hyperframes-creative/frame-presets/broadside/FRAME.md +284 -0
- package/.agents/skills/hyperframes-creative/frame-presets/broadside/caption-skin.html +225 -0
- package/.agents/skills/hyperframes-creative/frame-presets/broadside/frame-showcase.html +1166 -0
- package/.agents/skills/hyperframes-creative/frame-presets/capsule/FRAME.md +284 -0
- package/.agents/skills/hyperframes-creative/frame-presets/capsule/caption-skin.html +224 -0
- package/.agents/skills/hyperframes-creative/frame-presets/capsule/frame-showcase.html +1465 -0
- package/.agents/skills/hyperframes-creative/frame-presets/cartesian/FRAME.md +278 -0
- package/.agents/skills/hyperframes-creative/frame-presets/cartesian/caption-skin.html +222 -0
- package/.agents/skills/hyperframes-creative/frame-presets/cartesian/frame-showcase.html +1263 -0
- package/.agents/skills/hyperframes-creative/frame-presets/claude/FRAME.md +277 -0
- package/.agents/skills/hyperframes-creative/frame-presets/claude/caption-skin.html +224 -0
- package/.agents/skills/hyperframes-creative/frame-presets/claude/frame-showcase.html +1439 -0
- package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/FRAME.md +265 -0
- package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/caption-skin.html +238 -0
- package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/frame-showcase.html +1468 -0
- package/.agents/skills/hyperframes-creative/frame-presets/coral/FRAME.md +314 -0
- package/.agents/skills/hyperframes-creative/frame-presets/coral/caption-skin.html +226 -0
- package/.agents/skills/hyperframes-creative/frame-presets/coral/frame-showcase.html +1268 -0
- package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/FRAME.md +333 -0
- package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/caption-skin.html +227 -0
- package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/frame-showcase.html +1330 -0
- package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/FRAME.md +274 -0
- package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/caption-skin.html +230 -0
- package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/frame-showcase.html +1444 -0
- package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/FRAME.md +265 -0
- package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/caption-skin.html +223 -0
- package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/frame-showcase.html +1273 -0
- package/.agents/skills/hyperframes-creative/palettes/bold-energetic.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/clean-corporate.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/dark-premium.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/jewel-rich.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/monochrome.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/nature-earth.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/neon-electric.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/pastel-soft.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/warm-editorial.md +14 -0
- package/.agents/skills/hyperframes-creative/references/audio-reactive.md +76 -0
- package/.agents/skills/hyperframes-creative/references/beat-direction.md +164 -0
- package/.agents/skills/hyperframes-creative/references/composition-patterns.md +199 -0
- package/.agents/skills/hyperframes-creative/references/data-in-motion.md +19 -0
- package/.agents/skills/hyperframes-creative/references/design-adherence.md +19 -0
- package/.agents/skills/hyperframes-creative/references/design-picker.md +123 -0
- package/.agents/skills/hyperframes-creative/references/design-spec.md +55 -0
- package/.agents/skills/hyperframes-creative/references/house-style.md +73 -0
- package/.agents/skills/hyperframes-creative/references/motion-principles.md +150 -0
- package/.agents/skills/hyperframes-creative/references/narration.md +92 -0
- package/.agents/skills/hyperframes-creative/references/prompt-expansion.md +68 -0
- package/.agents/skills/hyperframes-creative/references/typography.md +218 -0
- package/.agents/skills/hyperframes-creative/references/video-composition.md +63 -0
- package/.agents/skills/hyperframes-creative/references/visual-styles.md +457 -0
- package/.agents/skills/hyperframes-creative/scripts/contrast-report.mjs +353 -0
- package/.agents/skills/hyperframes-creative/scripts/extract-audio-data.py +188 -0
- package/.agents/skills/hyperframes-creative/scripts/package-loader.mjs +288 -0
- package/.agents/skills/hyperframes-creative/scripts/package-loader.test.mjs +62 -0
- package/.agents/skills/hyperframes-creative/templates/design-picker.html +1432 -0
- package/.agents/skills/hyperframes-keyframes/SKILL.md +237 -0
- package/.agents/skills/hyperframes-keyframes/agents/openai.yaml +4 -0
- package/.agents/skills/hyperframes-keyframes/references/keyframe-patterns.md +106 -0
- package/.agents/skills/motion-graphics/SKILL.md +172 -0
- package/.agents/skills/motion-graphics/agents/builder.md +40 -0
- package/.agents/skills/motion-graphics/agents/director.md +53 -0
- package/.agents/skills/motion-graphics/agents/finalize.md +17 -0
- package/.agents/skills/motion-graphics/catalog-map.md +40 -0
- package/.agents/skills/motion-graphics/categories/asset-fusion/module.md +37 -0
- package/.agents/skills/motion-graphics/categories/charts/module.md +21 -0
- package/.agents/skills/motion-graphics/categories/kinetic-type/module.md +19 -0
- package/.agents/skills/motion-graphics/categories/logo-reveal/module.md +17 -0
- package/.agents/skills/motion-graphics/categories/lower-thirds/module.md +16 -0
- package/.agents/skills/motion-graphics/categories/maps/bake-basemap.mjs +273 -0
- package/.agents/skills/motion-graphics/categories/maps/module.md +61 -0
- package/.agents/skills/motion-graphics/categories/news/module.md +56 -0
- package/.agents/skills/motion-graphics/categories/stat/module.md +21 -0
- package/.agents/skills/motion-graphics/categories/tweet/module.md +16 -0
- package/.agents/skills/motion-graphics/categories/webpage/module.md +21 -0
- package/.agents/skills/motion-graphics/grounding/PROTOCOL.md +64 -0
- package/.agents/skills/motion-graphics/grounding/locate.mjs +227 -0
- package/.agents/skills/motion-graphics/phases/source/guide.md +21 -0
- package/.agents/skills/motion-graphics/references/builder-contract.md +38 -0
- package/.agents/skills/motion-graphics/references/motion-vocabulary.md +43 -0
- package/.agents/skills/motion-graphics/references/shot-plan-ir.md +54 -0
- package/.agents/skills/motion-graphics/samples/asset-fusion/_ref-circle-highlight.html +306 -0
- package/.agents/skills/music/SKILL.md +416 -0
- package/.agents/skills/music/references/api_reference.md +519 -0
- package/.agents/skills/music/references/installation.md +65 -0
- package/.agents/skills/product-launch-video/SKILL.md +208 -0
- package/.agents/skills/product-launch-video/references/cut-catalog.md +220 -0
- package/.agents/skills/product-launch-video/references/motion-language.md +156 -0
- package/.agents/skills/product-launch-video/references/story-design.md +373 -0
- package/.agents/skills/product-launch-video/references/visual-design.md +126 -0
- package/.agents/skills/product-launch-video/scripts/assemble-index.mjs +570 -0
- package/.agents/skills/product-launch-video/scripts/audio.mjs +260 -0
- package/.agents/skills/product-launch-video/scripts/build-frame.mjs +536 -0
- package/.agents/skills/product-launch-video/scripts/captions.mjs +508 -0
- package/.agents/skills/product-launch-video/scripts/lib/assets.mjs +55 -0
- package/.agents/skills/product-launch-video/scripts/lib/dimensions.mjs +45 -0
- package/.agents/skills/product-launch-video/scripts/lib/pad-frame-duration.mjs +36 -0
- package/.agents/skills/product-launch-video/scripts/lib/pad-frame-duration.test.mjs +76 -0
- package/.agents/skills/product-launch-video/scripts/lib/storyboard.mjs +249 -0
- package/.agents/skills/product-launch-video/scripts/lib/tokens.mjs +204 -0
- package/.agents/skills/product-launch-video/scripts/lib/transition-registry.mjs +38 -0
- package/.agents/skills/product-launch-video/scripts/lib/transitions.json +71 -0
- package/.agents/skills/product-launch-video/scripts/stage-assets.mjs +39 -0
- package/.agents/skills/product-launch-video/scripts/transitions.mjs +327 -0
- package/.agents/skills/product-launch-video/sub-agents/frame-worker.md +75 -0
- package/.agents/skills/slideshow/SKILL.md +550 -0
- package/.agents/skills/slideshow/references/standalone-harness.md +1020 -0
- package/.agents/skills/talking-head-recut/NOTICE.md +39 -0
- package/.agents/skills/talking-head-recut/SKILL.md +1195 -0
- package/.agents/skills/talking-head-recut/assets/vendor/gsap.min.js +10 -0
- package/.agents/skills/talking-head-recut/references/DESIGN_INDEX.md +145 -0
- package/.agents/skills/talking-head-recut/references/frames/clean.html +92 -0
- package/.agents/skills/talking-head-recut/references/frames/hairline.html +171 -0
- package/.agents/skills/talking-head-recut/references/frames/polaroid.html +131 -0
- package/.agents/skills/talking-head-recut/references/layouts/overlay.html +164 -0
- package/.agents/skills/talking-head-recut/references/layouts/pip.html +143 -0
- package/.agents/skills/talking-head-recut/references/layouts/split.html +121 -0
- package/.agents/skills/talking-head-recut/references/layouts/stack.html +109 -0
- package/.agents/skills/talking-head-recut/references/styles/academic.html +172 -0
- package/.agents/skills/talking-head-recut/references/styles/audit.html +170 -0
- package/.agents/skills/talking-head-recut/references/styles/editorial.html +189 -0
- package/.agents/skills/talking-head-recut/references/styles/geom.html +181 -0
- package/.agents/skills/talking-head-recut/references/styles/minimal.html +130 -0
- package/.agents/skills/talking-head-recut/references/styles/spotlight.html +142 -0
- package/.agents/skills/talking-head-recut/references/styles/swiss.html +188 -0
- package/.agents/skills/talking-head-recut/references/styles/terminal.html +196 -0
- package/.agents/skills/talking-head-recut/references/styles/whiteboard.html +170 -0
- package/.agents/skills/talking-head-recut/references/styles/xhs.html +206 -0
- package/.agents/skills/text-to-speech/SKILL.md +226 -0
- package/.agents/skills/text-to-speech/references/installation.md +90 -0
- package/.agents/skills/text-to-speech/references/streaming.md +307 -0
- package/.agents/skills/text-to-speech/references/voice-settings.md +115 -0
- package/.agents/skills/vidfarm-director/SKILL.md +179 -0
- package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
- package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
- package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +117 -0
- package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +276 -0
- package/.agents/skills/vidfarm-director/references/core-workflows.md +301 -0
- package/.agents/skills/vidfarm-director/references/editor-workflows.md +367 -0
- package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
- package/.agents/skills/vidfarm-director/references/primitives.md +307 -0
- package/.agents/skills/vidfarm-director/references/rest-api.md +84 -0
- package/.agents/skills/vidfarm-media/SKILL.md +158 -0
- package/.agents/skills/vidfarm-media/references/tts.md +111 -0
- package/.agents/skills/vidfarm-media/scripts/audio.mjs +628 -0
- package/.agents/skills/vidfarm-media/scripts/wait-bgm.mjs +31 -0
- package/.agents/skills/website-to-video/SKILL.md +145 -0
- package/.agents/skills/website-to-video/assets/sfx/CREDITS.md +35 -0
- package/.agents/skills/website-to-video/assets/sfx/manifest.json +97 -0
- package/.agents/skills/website-to-video/references/beat-builder-guide.md +263 -0
- package/.agents/skills/website-to-video/references/capabilities.md +713 -0
- package/.agents/skills/website-to-video/references/step-0-capture.md +55 -0
- package/.agents/skills/website-to-video/references/step-1-design.md +333 -0
- package/.agents/skills/website-to-video/references/step-2-brief.md +178 -0
- package/.agents/skills/website-to-video/references/step-3-storyboard.md +572 -0
- package/.agents/skills/website-to-video/references/step-4-vo.md +225 -0
- package/.agents/skills/website-to-video/references/step-5-build.md +483 -0
- package/.agents/skills/website-to-video/references/step-6-validate.md +341 -0
- package/.agents/skills/website-to-video/scripts/w2h-verify.mjs +759 -0
- package/README.md +97 -0
- package/SKILL.director.md +1644 -0
- package/SKILL.md +83 -0
- package/demo/README.md +28 -0
- package/demo/dist/app.css +1 -0
- package/demo/dist/app.js +968 -0
- package/demo/dist/chunks/chunk-3OT2D42H.js +1 -0
- package/demo/dist/chunks/chunk-DXB73IDG.js +1 -0
- package/demo/dist/chunks/domEditingLayers-AT7G6F4L-ZTHJHB7R.js +1 -0
- package/demo/dist/chunks/hyperframes-player-7HKH6NSO.js +459 -0
- package/demo/dist/chunks/lib-XAQ37YOE.js +1 -0
- package/demo/dist/chunks/src-TJ2QYA4U.js +207 -0
- package/demo/dist/favicon.ico +0 -0
- package/demo/dist/icons/timeline/audio.svg +7 -0
- package/demo/dist/icons/timeline/captions.svg +5 -0
- package/demo/dist/icons/timeline/composition.svg +12 -0
- package/demo/dist/icons/timeline/image.svg +18 -0
- package/demo/dist/icons/timeline/music.svg +10 -0
- package/demo/dist/icons/timeline/text.svg +3 -0
- package/demo/dist/index.html +16 -0
- package/dist/src/cli.js +7258 -0
- package/dist/src/devcli/auth-store.js +67 -0
- package/dist/src/devcli/captions.js +310 -0
- package/dist/src/devcli/clip-store.js +368 -0
- package/dist/src/devcli/clips.js +1138 -0
- package/dist/src/devcli/composition-edit.js +1292 -0
- package/dist/src/devcli/cost-mode.js +149 -0
- package/dist/src/devcli/doctor.js +192 -0
- package/dist/src/devcli/hyperframes-cli.js +118 -0
- package/dist/src/devcli/local-backend.js +167 -0
- package/dist/src/devcli/local-frontend-server.js +681 -0
- package/dist/src/devcli/local-render.js +136 -0
- package/dist/src/devcli/skills.js +476 -0
- package/dist/src/devcli/speech.js +178 -0
- package/dist/src/devcli/stills.js +249 -0
- package/dist/src/devcli/telemetry.js +236 -0
- package/dist/src/devcli/timeline-edit.js +490 -0
- package/dist/src/devcli/transitions.js +205 -0
- package/dist/src/hyperframes/composition.js +780 -0
- package/dist/src/lib/crypto.js +45 -0
- package/dist/src/lib/dev-log.js +54 -0
- package/dist/src/lib/display-name.js +11 -0
- package/dist/src/lib/ids.js +24 -0
- package/dist/src/lib/images.js +19 -0
- package/dist/src/lib/json.js +15 -0
- package/dist/src/lib/package-root.js +47 -0
- package/dist/src/lib/render-media-prep.js +293 -0
- package/dist/src/lib/template-paths.js +28 -0
- package/dist/src/lib/time.js +7 -0
- package/dist/src/lib/url-clean.js +85 -0
- package/dist/src/services/captions.js +123 -0
- package/dist/src/services/clip-curation/cost.js +116 -0
- package/dist/src/services/clip-curation/ffmpeg.js +352 -0
- package/dist/src/services/clip-curation/gemini.js +418 -0
- package/dist/src/services/clip-curation/hunt.js +470 -0
- package/dist/src/services/clip-curation/index.js +19 -0
- package/dist/src/services/clip-curation/local-agent.js +248 -0
- package/dist/src/services/clip-curation/media-select.js +152 -0
- package/dist/src/services/clip-curation/presets.js +20 -0
- package/dist/src/services/clip-curation/presets.v1.json +59 -0
- package/dist/src/services/clip-curation/query.js +167 -0
- package/dist/src/services/clip-curation/refine.js +165 -0
- package/dist/src/services/clip-curation/scan.js +189 -0
- package/dist/src/services/clip-curation/source-naming.js +131 -0
- package/dist/src/services/clip-curation/taxonomy.js +73 -0
- package/dist/src/services/clip-curation/taxonomy.v1.json +102 -0
- package/dist/src/services/clip-curation/types.js +7 -0
- package/dist/src/services/composition-lint.js +199 -0
- package/dist/src/services/provider-errors.js +128 -0
- package/dist/src/services/speech.js +629 -0
- package/package.json +154 -0
- package/public/assets/discover-client-app.js +1 -0
- package/public/assets/favicon.ico +0 -0
- package/public/assets/file-directory-app.js +89 -0
- package/public/assets/homepage-app.js +54 -0
- package/public/assets/homepage-client-app.js +80 -0
- package/public/assets/landing-page-client-app.js +87 -0
- package/public/assets/logo-vidfarm.png +0 -0
- package/public/assets/page-runtime-client-app.js +94 -0
- package/public/assets/placeholders/scene-placeholder.png +0 -0
- package/public/serve-shells/editor.html +3146 -0
- package/public/serve-shells/library-files.html +3450 -0
- package/public/serve-shells/library-raws.html +4404 -0
- package/public/serve-shells/tools-clipper.html +3991 -0
- package/public/serve-shells/tools-image.html +5584 -0
- 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
|