@mevdragon/vidfarm-devcli 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/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/vidfarm-media/SKILL.md +85 -0
- package/.agents/skills/vidfarm-media/references/tts.md +54 -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/SKILL.director.md +60 -4
- package/SKILL.platform.md +7 -2
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +485 -86
- package/dist/src/account-pages-legacy.js +9 -14
- package/dist/src/app.js +659 -159
- package/dist/src/cli.js +618 -17
- package/dist/src/composition-runtime.js +286 -6
- package/dist/src/devcli/composition-edit.js +176 -8
- package/dist/src/devcli/doctor.js +192 -0
- package/dist/src/devcli/hyperframes-cli.js +118 -0
- package/dist/src/devcli/skills.js +476 -0
- package/dist/src/devcli/stills.js +249 -0
- package/dist/src/devcli/transitions.js +205 -0
- package/dist/src/editor-chat.js +260 -4
- package/dist/src/frontend/template-editor-chat.js +6 -5
- package/dist/src/homepage.js +2 -48
- package/dist/src/hyperframes/composition.js +188 -7
- package/dist/src/services/composition-lint.js +199 -0
- package/dist/src/services/hyperframes.js +127 -28
- package/dist/src/services/serverless-records.js +7 -0
- package/dist/src/template-editor-shell.js +87 -48
- package/package.json +2 -1
- package/public/assets/page-runtime-client-app.js +31 -31
package/dist/src/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import { createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
5
|
import { pipeline as streamPipeline } from "node:stream/promises";
|
|
@@ -23,7 +23,7 @@ import { writeForkManifest } from "./services/fork-manifest.js";
|
|
|
23
23
|
import { renderHelpPage } from "./help-page.js";
|
|
24
24
|
import { renderHomepage } from "./homepage.js";
|
|
25
25
|
import { COMPOSITION_RUNTIME_SCRIPT_BODY } from "./composition-runtime.js";
|
|
26
|
-
import { CAPTION_ANIM_CSS, CAPTION_STYLE_MARKER, KEN_BURNS_CSS, KEN_BURNS_STYLE_MARKER, TRANSITION_CSS, TRANSITION_STYLE_MARKER } from "./hyperframes/composition.js";
|
|
26
|
+
import { CAPTION_ANIM_CSS, CAPTION_STYLE_MARKER, KEN_BURNS_CSS, KEN_BURNS_STYLE_MARKER, TRANSITION_CSS, TRANSITION_STYLE_MARKER, upgradeLegacyTransitionCss } from "./hyperframes/composition.js";
|
|
27
27
|
import { normalizeCompositionZIndexHtml, sanitizeCompositionHtml } from "./services/composition-sanitize.js";
|
|
28
28
|
import { applyMarkupUsd } from "./services/billing-pricing.js";
|
|
29
29
|
import { primitiveRegistry } from "./primitive-registry.js";
|
|
@@ -35,7 +35,7 @@ import { resolveRootFileCandidates } from "./lib/package-root.js";
|
|
|
35
35
|
import { stripTrackingParams } from "./lib/url-clean.js";
|
|
36
36
|
import { addSeconds, nowIso } from "./lib/time.js";
|
|
37
37
|
import { templateRegistry } from "./registry.js";
|
|
38
|
-
import { buildEditorChatSystemInstruction, compactEditorChatModelMessages, defaultEditorChatModel, EDITOR_CHAT_PROVIDER_PRIORITY, extractEditorChatCompositionBrief, formatEditorChatCurrentDateTime, toEditorChatModelMessages } from "./editor-chat.js";
|
|
38
|
+
import { buildAsyncJobHandoffText, buildEditorChatGuardrailNotice, buildEditorChatSystemInstruction, collectVerifiedHttpJobIds, compactEditorChatModelMessages, createLoadSkillTool, createStreamingJobIdRedactor, defaultEditorChatModel, EDITOR_CHAT_PROVIDER_PRIORITY, extractEditorChatCompositionBrief, formatEditorChatCurrentDateTime, lintReplaceCompositionAction, readAsyncJobHandoff, toEditorChatModelMessages } from "./editor-chat.js";
|
|
39
39
|
import { apiCallHistoryStore } from "./services/api-call-history.js";
|
|
40
40
|
import { AuthService } from "./services/auth.js";
|
|
41
41
|
import { BillingService } from "./services/billing.js";
|
|
@@ -54,7 +54,7 @@ import { loadSpeechAudioLocally, MAX_LOCAL_TRANSCRIBE_AUDIO_BYTES } from "./devc
|
|
|
54
54
|
import { DescribeExecutionCommand, SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
|
|
55
55
|
import { clipRecords, makeUserPreset } from "./services/clip-records.js";
|
|
56
56
|
import { matchScenesToClips, searchUserClips, vectorIndex } from "./services/clip-search.js";
|
|
57
|
-
import { BUILTIN_PRESETS, buildEffectiveGuidance, ClipModelClient, detectLocalAgent, effectiveHuntDurationSec, estimateScanCostFromDuration, hasFfmpeg, LocalAgentClipClient, normalizeCropFocus, normalizeHuntSpec, parseClipHuntPrompt, probeVideo, resolveDurationBand, scanVideo } from "./services/clip-curation/index.js";
|
|
57
|
+
import { BUILTIN_PRESETS, buildClipEmbeddingText, buildEffectiveGuidance, ClipModelClient, cosineSimilarity, detectLocalAgent, effectiveHuntDurationSec, estimateScanCostFromDuration, hasFfmpeg, LocalAgentClipClient, normalizeCropFocus, normalizeHuntSpec, parseClipHuntPrompt, probeVideo, resolveDurationBand, scanVideo } from "./services/clip-curation/index.js";
|
|
58
58
|
import { analyzeCastMembers, annotateScenesFromSource, buildSmartDecomposedHtml, HyperframesService, smartDecomposeVideo, stampCompositionIdentity } from "./services/hyperframes.js";
|
|
59
59
|
import { buildPendingSceneAnnotationsState } from "./services/scene-annotations.js";
|
|
60
60
|
import { createPrimitiveJobContext } from "./primitive-context.js";
|
|
@@ -401,7 +401,16 @@ const userAttachmentFinalizeSchema = z.object({
|
|
|
401
401
|
content_type: z.string().trim().min(1).max(255),
|
|
402
402
|
size_bytes: z.coerce.number().int().min(1).max(1024 * 1024 * 1024),
|
|
403
403
|
storage_key: z.string().min(1).max(2000),
|
|
404
|
-
folder_path: z.string().trim().max(500).optional()
|
|
404
|
+
folder_path: z.string().trim().max(500).optional(),
|
|
405
|
+
notes: z.string().trim().max(4000).optional()
|
|
406
|
+
});
|
|
407
|
+
const userAttachmentNotesSchema = z.object({
|
|
408
|
+
notes: z.string().max(4000).nullable().optional()
|
|
409
|
+
});
|
|
410
|
+
const userAttachmentSearchSchema = z.object({
|
|
411
|
+
query: z.string().min(1).max(2000),
|
|
412
|
+
folder_path: z.string().trim().max(500).optional(),
|
|
413
|
+
limit: z.coerce.number().int().min(1).max(100).optional()
|
|
405
414
|
});
|
|
406
415
|
const userFileFolderSchema = z.object({
|
|
407
416
|
folder_path: z.string().trim().min(1).max(500)
|
|
@@ -2917,6 +2926,9 @@ function serializeUserAttachment(c, attachment) {
|
|
|
2917
2926
|
sizeBytes: attachment.sizeBytes,
|
|
2918
2927
|
createdAt: attachment.createdAt,
|
|
2919
2928
|
folderPath: attachment.folderPath ?? "",
|
|
2929
|
+
notes: attachment.notes ?? null,
|
|
2930
|
+
// Flag only — the raw vector never ships to the client (mirrors serializeClip).
|
|
2931
|
+
hasNotesEmbedding: Boolean(attachment.notesEmbedding && attachment.notesEmbedding.length > 0),
|
|
2920
2932
|
viewUrl: attachment.publicUrl || storage.getPublicUrl(attachment.storageKey) || buildUserAttachmentViewUrl(c, attachment.storageKey)
|
|
2921
2933
|
};
|
|
2922
2934
|
}
|
|
@@ -3597,7 +3609,12 @@ function normalizeEditorChatPath(input) {
|
|
|
3597
3609
|
// and NEVER runs GhostCut on the long-form source — "no text" is a
|
|
3598
3610
|
// scene-selection filter; caption removal stays a per-finished-clip
|
|
3599
3611
|
// primitive (/api/v1/primitives/videos/remove-captions).
|
|
3600
|
-
CLIPS_PREFIX
|
|
3612
|
+
CLIPS_PREFIX,
|
|
3613
|
+
// Public skill knowledge: the vendored skill packs (load_skill fetches
|
|
3614
|
+
// these too) and per-template SKILL.md manifests. Read-only markdown.
|
|
3615
|
+
"/skill/",
|
|
3616
|
+
"/skill-pack/",
|
|
3617
|
+
"/discover/skills/"
|
|
3601
3618
|
];
|
|
3602
3619
|
const parsed = trimmed.startsWith("http://") || trimmed.startsWith("https://")
|
|
3603
3620
|
? new URL(trimmed)
|
|
@@ -3975,16 +3992,19 @@ function browseFilesTextContentType(fileName) {
|
|
|
3975
3992
|
}
|
|
3976
3993
|
function createBrowseFilesTool(input) {
|
|
3977
3994
|
return tool({
|
|
3978
|
-
description: "Browse AND write the user's My Files filesystem — their personal library of uploaded assets (mp4, mov, webm, png, jpg, jpeg, gif, webp, svg, mp3, wav, m4a, aac, pdf, md, txt, csv, and more) organized into virtual folders. Use action=list to enumerate files and virtual folders so you can discover what assets exist; pass folder_path to scope the listing to one folder (omit or empty for the root). Use action=read to fetch one file by file_id (preferred; copy it from a prior list result) or by file_name: it returns the file's durable view_url plus metadata, and for text files (md, txt, csv, srt, vtt, json) it also returns the full text_content inline so you can read it. For images, video, audio, and PDFs it returns view_url only — reference that URL directly (drop it into composition layers, or pass it into primitive routes like images/edit source_image_url, videos/generate input_references, or videos/download); you cannot read their raw bytes as text. Use action=write to SAVE a text file into My Files — pass file_name (e.g. About.md), content (the full text),
|
|
3995
|
+
description: "Browse AND write the user's My Files filesystem — their personal library of uploaded assets (mp4, mov, webm, png, jpg, jpeg, gif, webp, svg, mp3, wav, m4a, aac, pdf, md, txt, csv, and more) organized into virtual folders. Use action=list to enumerate files and virtual folders so you can discover what assets exist; pass folder_path to scope the listing to one folder (omit or empty for the root). Use action=search to find files by MEANING when you don't know where something lives: pass query (plain language, e.g. 'character reference sheet for the mascot' or 'logo on transparent background') and it matches keywords AND vector-embedded metadata notes across every file's name, folder, and notes — prefer search over paging through list when My Files is large or the user references an asset vaguely. Use action=read to fetch one file by file_id (preferred; copy it from a prior list/search result) or by file_name: it returns the file's durable view_url plus metadata, and for text files (md, txt, csv, srt, vtt, json) it also returns the full text_content inline so you can read it. For images, video, audio, and PDFs it returns view_url only — reference that URL directly (drop it into composition layers, or pass it into primitive routes like images/edit source_image_url, videos/generate input_references, or videos/download); you cannot read their raw bytes as text. Use action=write to SAVE a text file into My Files — pass file_name (e.g. About.md), content (the full text), optional folder_path to namescope it under a product/offer folder, and optional notes. This is how you persist Getting Started context the user can reuse later: a product About.md or Interview.md, and awareness-levels.md / persuasive-angles.md / ad-hooks.md distilled from brainstorm output. write saves text files (md, txt, csv, json, srt, vtt) via content, or IMPORTS any media asset into the library by passing its durable URL as source_url — e.g. save a finished image job's output as character_sprite_card.png so it persists in My Files instead of living only in a job result. write returns the new file_id and view_url. Use action=annotate to set metadata notes on ANY existing file (including binary assets): notes say what the file is, who/what it depicts, and when to use it, and they are vector-embedded so future sessions can search their way back to the asset. Annotate every asset worth finding again — especially character references: keep a character_sprite_card.png plus an about_character.md per recurring character in that character's folder, annotate both, and pass the sprite card's view_url into generation reference inputs (images prompt_attachments / videos input_references) for character consistency. Always list or search before you read so you use a real file_id; never invent file ids or view URLs.",
|
|
3979
3996
|
inputSchema: z.object({
|
|
3980
|
-
action: z.enum(["list", "read", "write"]).describe("list = enumerate files and folders; read = fetch one file's view_url and, for text files, its contents; write = save a text file into My Files."),
|
|
3981
|
-
folder_path: z.string().optional().describe("Folder to scope a list to, the folder of the file to read, or the folder to write into. Omit or leave empty for the root."),
|
|
3982
|
-
file_id: z.string().optional().describe("Attachment id to read, copied from a prior list result. Preferred over file_name."),
|
|
3983
|
-
file_name: z.string().optional().describe("File name to read when you do not have the id, or the name to save under for write (e.g. About.md).
|
|
3984
|
-
content: z.string().optional().describe("For write: the full text content of the file to save (e.g. Markdown for About.md). Required for
|
|
3985
|
-
|
|
3997
|
+
action: z.enum(["list", "read", "write", "annotate", "search"]).describe("list = enumerate files and folders; read = fetch one file's view_url and, for text files, its contents; write = save a text file into My Files; annotate = set the metadata notes on one existing file; search = find files by meaning across names, folders, and notes."),
|
|
3998
|
+
folder_path: z.string().optional().describe("Folder to scope a list or search to, the folder of the file to read, or the folder to write into. Omit or leave empty for the root."),
|
|
3999
|
+
file_id: z.string().optional().describe("Attachment id to read or annotate, copied from a prior list/search result. Preferred over file_name."),
|
|
4000
|
+
file_name: z.string().optional().describe("File name to read/annotate when you do not have the id, or the name to save under for write (e.g. About.md). Matched within folder_path when one is provided."),
|
|
4001
|
+
content: z.string().optional().describe("For write: the full text content of the file to save (e.g. Markdown for About.md). Required for text writes; mutually exclusive with source_url."),
|
|
4002
|
+
source_url: z.string().optional().describe("For write: a durable media URL (from a finished primitive job result or an existing asset) to IMPORT into My Files as file_name — this is how you save a generated character_sprite_card.png or other binary asset into the library. Mutually exclusive with content."),
|
|
4003
|
+
notes: z.string().optional().describe("For annotate (required) or write (optional): metadata notes describing what the file is, who/what it depicts, and when to use it — e.g. 'Sprite card for Zara, our mascot: front/side/back views, teal jacket. Use as the reference image whenever generating Zara.' Notes are vector-embedded so search finds the file by meaning. For annotate, an empty string clears existing notes."),
|
|
4004
|
+
query: z.string().optional().describe("For search: what you're looking for, in plain language. Required for search; ignored otherwise."),
|
|
4005
|
+
explanation: z.string().optional().describe("Short reason for the browse, save, annotation, or search.")
|
|
3986
4006
|
}),
|
|
3987
|
-
execute: async ({ action, folder_path, file_id, file_name, content, explanation }) => {
|
|
4007
|
+
execute: async ({ action, folder_path, file_id, file_name, content, source_url, notes, query, explanation }) => {
|
|
3988
4008
|
const listUrl = new URL(`${USER_PREFIX}/me/attachments`, config.PUBLIC_BASE_URL);
|
|
3989
4009
|
const headers = new Headers();
|
|
3990
4010
|
headers.set("vidfarm-api-key", input.vidfarmApiKey);
|
|
@@ -3997,22 +4017,59 @@ function createBrowseFilesTool(input) {
|
|
|
3997
4017
|
if (action === "write") {
|
|
3998
4018
|
const name = (file_name ?? "").trim();
|
|
3999
4019
|
if (!name) {
|
|
4000
|
-
return { action, explanation: explanation ?? null, status: 400, error: "write requires file_name (e.g. About.md)." };
|
|
4020
|
+
return { action, explanation: explanation ?? null, status: 400, error: "write requires file_name (e.g. About.md, or character_sprite_card.png with source_url)." };
|
|
4001
4021
|
}
|
|
4002
|
-
|
|
4003
|
-
|
|
4022
|
+
const importSrc = (source_url ?? "").trim();
|
|
4023
|
+
let fileBlob;
|
|
4024
|
+
if (importSrc) {
|
|
4025
|
+
// Import a durable media URL (e.g. a finished primitive job's output)
|
|
4026
|
+
// into My Files. The api-key header only travels to our own origin.
|
|
4027
|
+
let sourceUrl;
|
|
4028
|
+
try {
|
|
4029
|
+
sourceUrl = new URL(importSrc, config.PUBLIC_BASE_URL);
|
|
4030
|
+
}
|
|
4031
|
+
catch {
|
|
4032
|
+
return { action, explanation: explanation ?? null, status: 400, error: `source_url is not a valid URL: ${importSrc.slice(0, 200)}` };
|
|
4033
|
+
}
|
|
4034
|
+
const sourceHeaders = new Headers();
|
|
4035
|
+
if (sourceUrl.origin === new URL(config.PUBLIC_BASE_URL).origin) {
|
|
4036
|
+
sourceHeaders.set("vidfarm-api-key", input.vidfarmApiKey);
|
|
4037
|
+
}
|
|
4038
|
+
const sourceResponse = await fetch(sourceUrl, {
|
|
4039
|
+
method: "GET",
|
|
4040
|
+
headers: sourceHeaders,
|
|
4041
|
+
signal: mergeAbortSignals(input.abortSignal, timeoutController.signal)
|
|
4042
|
+
});
|
|
4043
|
+
if (!sourceResponse.ok) {
|
|
4044
|
+
return { action, explanation: explanation ?? null, status: sourceResponse.status, error: `Could not fetch source_url (${sourceResponse.status}). Use a durable URL from a finished job result or an existing asset.` };
|
|
4045
|
+
}
|
|
4046
|
+
const sourceBytes = await sourceResponse.arrayBuffer();
|
|
4047
|
+
if (sourceBytes.byteLength > 50 * 1024 * 1024) {
|
|
4048
|
+
return { action, explanation: explanation ?? null, status: 400, error: "source_url asset is larger than the 50 MB My Files limit." };
|
|
4049
|
+
}
|
|
4050
|
+
const sourceType = (sourceResponse.headers.get("content-type") ?? "").split(";")[0].trim();
|
|
4051
|
+
fileBlob = new Blob([sourceBytes], sourceType ? { type: sourceType } : undefined);
|
|
4004
4052
|
}
|
|
4005
|
-
|
|
4006
|
-
|
|
4053
|
+
else {
|
|
4054
|
+
if (typeof content !== "string") {
|
|
4055
|
+
return { action, explanation: explanation ?? null, status: 400, error: "write requires content (the full text of the file) or source_url (a durable media URL to import)." };
|
|
4056
|
+
}
|
|
4057
|
+
if (!isBrowseFilesTextFile(name, "")) {
|
|
4058
|
+
return { action, explanation: explanation ?? null, status: 400, error: `write with content only supports text files (md, txt, csv, json, srt, vtt). To save media like "${name}", pass its durable URL as source_url instead.` };
|
|
4059
|
+
}
|
|
4060
|
+
fileBlob = new Blob([content], { type: browseFilesTextContentType(name) });
|
|
4007
4061
|
}
|
|
4008
4062
|
const uploadUrl = new URL(`${USER_PREFIX}/me/attachments/upload`, config.PUBLIC_BASE_URL);
|
|
4009
4063
|
const uploadHeaders = new Headers();
|
|
4010
4064
|
uploadHeaders.set("vidfarm-api-key", input.vidfarmApiKey);
|
|
4011
4065
|
const form = new FormData();
|
|
4012
|
-
form.append("file",
|
|
4066
|
+
form.append("file", fileBlob, name);
|
|
4013
4067
|
if (normalizedFolder) {
|
|
4014
4068
|
form.append("folder_path", normalizedFolder);
|
|
4015
4069
|
}
|
|
4070
|
+
if (typeof notes === "string" && notes.trim()) {
|
|
4071
|
+
form.append("notes", notes.trim());
|
|
4072
|
+
}
|
|
4016
4073
|
const writeResponse = await fetch(uploadUrl, {
|
|
4017
4074
|
method: "POST",
|
|
4018
4075
|
headers: uploadHeaders,
|
|
@@ -4047,11 +4104,65 @@ function createBrowseFilesTool(input) {
|
|
|
4047
4104
|
content_type: saved.contentType,
|
|
4048
4105
|
size_bytes: saved.sizeBytes,
|
|
4049
4106
|
folder_path: saved.folderPath ?? "",
|
|
4107
|
+
notes: saved.notes ?? null,
|
|
4050
4108
|
view_url: saved.viewUrl,
|
|
4051
4109
|
created_at: saved.createdAt
|
|
4052
4110
|
}
|
|
4053
4111
|
};
|
|
4054
4112
|
}
|
|
4113
|
+
if (action === "search") {
|
|
4114
|
+
const q = (query ?? "").trim();
|
|
4115
|
+
if (!q) {
|
|
4116
|
+
return { action, explanation: explanation ?? null, status: 400, error: "search requires query (what you're looking for, in plain language)." };
|
|
4117
|
+
}
|
|
4118
|
+
const searchUrl = new URL(`${USER_PREFIX}/me/attachments/search`, config.PUBLIC_BASE_URL);
|
|
4119
|
+
const searchHeaders = new Headers();
|
|
4120
|
+
searchHeaders.set("vidfarm-api-key", input.vidfarmApiKey);
|
|
4121
|
+
searchHeaders.set("Content-Type", "application/json");
|
|
4122
|
+
const searchResponse = await fetch(searchUrl, {
|
|
4123
|
+
method: "POST",
|
|
4124
|
+
headers: searchHeaders,
|
|
4125
|
+
body: JSON.stringify({ query: q, ...(normalizedFolder ? { folder_path: normalizedFolder } : {}), limit: 20 }),
|
|
4126
|
+
signal: mergeAbortSignals(input.abortSignal, timeoutController.signal)
|
|
4127
|
+
});
|
|
4128
|
+
const searchText = await searchResponse.text();
|
|
4129
|
+
let searchJson = null;
|
|
4130
|
+
try {
|
|
4131
|
+
searchJson = searchText ? JSON.parse(searchText) : null;
|
|
4132
|
+
}
|
|
4133
|
+
catch {
|
|
4134
|
+
searchJson = null;
|
|
4135
|
+
}
|
|
4136
|
+
if (!searchResponse.ok || !Array.isArray(searchJson?.results)) {
|
|
4137
|
+
return {
|
|
4138
|
+
action,
|
|
4139
|
+
explanation: explanation ?? null,
|
|
4140
|
+
status: searchResponse.status,
|
|
4141
|
+
error: `My Files search failed (${searchResponse.status}). ${(searchJson?.error ?? searchText).slice(0, 500)}`
|
|
4142
|
+
};
|
|
4143
|
+
}
|
|
4144
|
+
return {
|
|
4145
|
+
action,
|
|
4146
|
+
explanation: explanation ?? null,
|
|
4147
|
+
status: 200,
|
|
4148
|
+
query: q,
|
|
4149
|
+
folder_path: normalizedFolder,
|
|
4150
|
+
semantic: Boolean(searchJson.semantic),
|
|
4151
|
+
result_count: searchJson.results.length,
|
|
4152
|
+
results: searchJson.results.map((file) => ({
|
|
4153
|
+
file_id: file.id,
|
|
4154
|
+
file_name: file.fileName,
|
|
4155
|
+
content_type: file.contentType,
|
|
4156
|
+
size_bytes: file.sizeBytes,
|
|
4157
|
+
folder_path: file.folderPath ?? "",
|
|
4158
|
+
notes: file.notes ?? null,
|
|
4159
|
+
similarity: file.similarity ?? null,
|
|
4160
|
+
keyword_match: Boolean(file.keyword_match),
|
|
4161
|
+
view_url: file.viewUrl,
|
|
4162
|
+
created_at: file.createdAt
|
|
4163
|
+
}))
|
|
4164
|
+
};
|
|
4165
|
+
}
|
|
4055
4166
|
const listResponse = await fetch(listUrl, {
|
|
4056
4167
|
method: "GET",
|
|
4057
4168
|
headers,
|
|
@@ -4086,6 +4197,7 @@ function createBrowseFilesTool(input) {
|
|
|
4086
4197
|
content_type: file.contentType,
|
|
4087
4198
|
size_bytes: file.sizeBytes,
|
|
4088
4199
|
folder_path: file.folderPath ?? "",
|
|
4200
|
+
notes: file.notes ?? null,
|
|
4089
4201
|
view_url: file.viewUrl,
|
|
4090
4202
|
created_at: file.createdAt
|
|
4091
4203
|
}))
|
|
@@ -4104,7 +4216,56 @@ function createBrowseFilesTool(input) {
|
|
|
4104
4216
|
status: 404,
|
|
4105
4217
|
error: file_id || file_name
|
|
4106
4218
|
? "No matching file in My Files. Call action=list first to copy an exact file_id."
|
|
4107
|
-
:
|
|
4219
|
+
: `${action} requires file_id (preferred) or file_name.`
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
if (action === "annotate") {
|
|
4223
|
+
if (typeof notes !== "string") {
|
|
4224
|
+
return { action, explanation: explanation ?? null, status: 400, error: "annotate requires notes (the metadata to attach; empty string clears)." };
|
|
4225
|
+
}
|
|
4226
|
+
const annotateUrl = new URL(`${USER_PREFIX}/me/attachments/${encodeURIComponent(match.id)}`, config.PUBLIC_BASE_URL);
|
|
4227
|
+
const annotateHeaders = new Headers();
|
|
4228
|
+
annotateHeaders.set("vidfarm-api-key", input.vidfarmApiKey);
|
|
4229
|
+
annotateHeaders.set("Content-Type", "application/json");
|
|
4230
|
+
const annotateResponse = await fetch(annotateUrl, {
|
|
4231
|
+
method: "PATCH",
|
|
4232
|
+
headers: annotateHeaders,
|
|
4233
|
+
body: JSON.stringify({ notes }),
|
|
4234
|
+
signal: mergeAbortSignals(input.abortSignal, timeoutController.signal)
|
|
4235
|
+
});
|
|
4236
|
+
const annotateText = await annotateResponse.text();
|
|
4237
|
+
let annotateJson = null;
|
|
4238
|
+
try {
|
|
4239
|
+
annotateJson = annotateText ? JSON.parse(annotateText) : null;
|
|
4240
|
+
}
|
|
4241
|
+
catch {
|
|
4242
|
+
annotateJson = null;
|
|
4243
|
+
}
|
|
4244
|
+
if (!annotateResponse.ok || !annotateJson?.attachment) {
|
|
4245
|
+
return {
|
|
4246
|
+
action,
|
|
4247
|
+
explanation: explanation ?? null,
|
|
4248
|
+
status: annotateResponse.status,
|
|
4249
|
+
error: `Could not annotate file (${annotateResponse.status}). ${(annotateJson?.error ?? annotateText).slice(0, 500)}`
|
|
4250
|
+
};
|
|
4251
|
+
}
|
|
4252
|
+
const annotated = annotateJson.attachment;
|
|
4253
|
+
return {
|
|
4254
|
+
action,
|
|
4255
|
+
explanation: explanation ?? null,
|
|
4256
|
+
status: 200,
|
|
4257
|
+
annotated: true,
|
|
4258
|
+
notes_embedded: Boolean(annotateJson.notes_embedded),
|
|
4259
|
+
file: {
|
|
4260
|
+
file_id: annotated.id,
|
|
4261
|
+
file_name: annotated.fileName,
|
|
4262
|
+
content_type: annotated.contentType,
|
|
4263
|
+
size_bytes: annotated.sizeBytes,
|
|
4264
|
+
folder_path: annotated.folderPath ?? "",
|
|
4265
|
+
notes: annotated.notes ?? null,
|
|
4266
|
+
view_url: annotated.viewUrl,
|
|
4267
|
+
created_at: annotated.createdAt
|
|
4268
|
+
}
|
|
4108
4269
|
};
|
|
4109
4270
|
}
|
|
4110
4271
|
const fileInfo = {
|
|
@@ -4113,6 +4274,7 @@ function createBrowseFilesTool(input) {
|
|
|
4113
4274
|
content_type: match.contentType,
|
|
4114
4275
|
size_bytes: match.sizeBytes,
|
|
4115
4276
|
folder_path: match.folderPath ?? "",
|
|
4277
|
+
notes: match.notes ?? null,
|
|
4116
4278
|
view_url: match.viewUrl,
|
|
4117
4279
|
created_at: match.createdAt
|
|
4118
4280
|
};
|
|
@@ -4231,8 +4393,9 @@ function createEditorActionTool() {
|
|
|
4231
4393
|
"replace_composition_html",
|
|
4232
4394
|
"export_composition",
|
|
4233
4395
|
"generate_layer",
|
|
4234
|
-
"set_captions"
|
|
4235
|
-
|
|
4396
|
+
"set_captions",
|
|
4397
|
+
"set_transitions"
|
|
4398
|
+
]).describe("Which mutation to perform. Use generate_layer to AI-generate a video/image and auto-place it on the timeline (fill a gap or replace a scene). Use export_composition to trigger the same Export button available in the editor UI — this queues an AWS Lambda render and the finished MP4 URL will surface in the next editor_context as last_export_url. Use set_captions to lay down a full run of ANIMATED word-by-word captions (TikTok/CapCut style): pass captions[] cues (or text + start/duration to auto-page), plus caption_style; it replaces any existing animated caption layers. To restyle existing captions without changing text/timing, use set_layer_style with caption_* fields on each caption layer. Use set_transitions to configure scene transitions across the WHOLE timeline in one call: transition = the junction preset applied at every cut (every scene clip except each track's first; 'none' clears), transition_intro = the first clip's entrance, transition_outro = the last clip's exit."),
|
|
4236
4399
|
layer_key: z.string().optional().describe("Existing layer key from editor_context (required for remove/set/duplicate/split). MAY be either the element_id (data-hf-id) or the slug (data-hf-slug). For add_layer, optionally provide a stable id (starts with a letter, [A-Za-z0-9_-], <=64 chars) to reuse in later tool calls this turn."),
|
|
4237
4400
|
slug: z.string().optional().describe("snake_case slug for the layer. On add_layer, seeds data-hf-slug. On set_layer_identity, sets it. Slugs give viral DNA a stable handle for the element."),
|
|
4238
4401
|
note: z.string().optional().describe("Human-facing note explaining the layer's role, referenced by viral DNA. On add_layer seeds data-hf-note. On set_layer_identity, sets it. Empty string removes."),
|
|
@@ -4265,8 +4428,12 @@ function createEditorActionTool() {
|
|
|
4265
4428
|
object_position: z.string().optional().describe("CSS object-position for image/video (e.g., center, top, bottom left)."),
|
|
4266
4429
|
ken_burns: z.enum(["zoom-in", "zoom-out", "pan-left", "pan-right", "pan-up", "pan-down", "zoom-in-left", "zoom-in-right", "none"]).optional().describe("Ken Burns motion for still-image layers: a slow pan/zoom spanning the clip's full duration. Apply with set_layer_media on an existing image, or seed it on add_layer/generate_layer when kind/media_type is image. 'none' removes the effect. When the user says 'animate the images' / 'ken burns', apply it to every still image, varying presets across adjacent clips (zoom-in, pan-left, zoom-out, ...) for a documentary feel."),
|
|
4267
4430
|
ken_burns_intensity: z.number().optional().describe("Ken Burns strength: extra zoom / pan travel as a fraction of the frame, 0.04-0.5 (default 0.18; ~0.1 subtle, ~0.3 dramatic). Only meaningful alongside ken_burns."),
|
|
4268
|
-
transition: z.enum(["crossfade", "fade-black", "slide-left", "slide-right", "slide-up", "slide-down", "wipe-left", "wipe-right", "wipe-up", "wipe-down", "zoom-in", "zoom-out", "circle-open", "none"]).optional().describe("Scene transition INTO this clip at its start, from the previous clip on the same track. Set it on the INCOMING clip (the one after the cut) with set_layer_media, or seed it on add_layer/generate_layer. Works on video and image scene clips; 'none' removes it. The previous butt-cut clip is automatically held on screen beneath the incoming one for the transition window — do not retime anything yourself.
|
|
4269
|
-
transition_duration: z.number().optional().describe("Transition length in seconds, 0.1-2.5 (default 0.5; ~0.3 snappy, ~1.0 slow cinematic). Only meaningful alongside transition."),
|
|
4431
|
+
transition: z.enum(["crossfade", "fade-black", "fade-white", "flash", "smoke", "blur", "slide-left", "slide-right", "slide-up", "slide-down", "whip-left", "whip-right", "wipe-left", "wipe-right", "wipe-up", "wipe-down", "zoom-in", "zoom-out", "circle-open", "none"]).optional().describe("Scene transition INTO this clip at its start, from the previous clip on the same track. Set it on the INCOMING clip (the one after the cut) with set_layer_media, or seed it on add_layer/generate_layer. On set_transitions it is the junction preset applied at EVERY cut. Works on video and image scene clips; 'none' removes it. The previous butt-cut clip is automatically held on screen beneath the incoming one for the transition window — do not retime anything yourself. Preset energy: crossfade/fade-black/fade-white/blur = calm, smoke = dreamy, slide/wipe = energetic, whip/flash = fast-paced social, zoom/circle-open = punchy."),
|
|
4432
|
+
transition_duration: z.number().optional().describe("Transition length in seconds, 0.1-2.5 (default 0.5; ~0.3 snappy, ~1.0 slow cinematic). Only meaningful alongside transition/transition_intro."),
|
|
4433
|
+
transition_out: z.enum(["fade", "fade-black", "fade-white", "flash", "smoke", "blur", "slide-left", "slide-right", "slide-up", "slide-down", "wipe-left", "wipe-right", "wipe-up", "wipe-down", "zoom-in", "zoom-out", "circle-close", "none"]).optional().describe("Exit transition OUT of this clip at its end (fade to black, slide away, ...). Set it with set_layer_media on the clip that should animate away, or seed it on add_layer/generate_layer. The animation plays over the clip's last transition_out_duration seconds; no retiming needed. Most cuts only need the incoming clip's transition — use transition_out for ending a video (fade to black), for clips followed by a gap, or for deliberate dip-to-black moments (outgoing fade-black + incoming fade-black)."),
|
|
4434
|
+
transition_out_duration: z.number().optional().describe("Exit transition length in seconds, 0.1-2.5 (default 0.5). Only meaningful alongside transition_out/transition_outro."),
|
|
4435
|
+
transition_intro: z.enum(["crossfade", "fade-black", "fade-white", "flash", "smoke", "blur", "slide-left", "slide-right", "slide-up", "slide-down", "whip-left", "whip-right", "wipe-left", "wipe-right", "wipe-up", "wipe-down", "zoom-in", "zoom-out", "circle-open", "none"]).optional().describe("set_transitions only: entrance for the very FIRST scene clip (fade in from black = fade-black). 'none' clears it."),
|
|
4436
|
+
transition_outro: z.enum(["fade", "fade-black", "fade-white", "flash", "smoke", "blur", "slide-left", "slide-right", "slide-up", "slide-down", "wipe-left", "wipe-right", "wipe-up", "wipe-down", "zoom-in", "zoom-out", "circle-close", "none"]).optional().describe("set_transitions only: exit for the very LAST scene clip (fade to black = fade-black). 'none' clears it."),
|
|
4270
4437
|
caption_style: z.enum(["spotlight", "karaoke", "word-pop", "stack-up", "neon", "bounce"]).optional().describe("Animated caption preset look: spotlight (active word gets a highlight pill, bold outline text), karaoke (words fill with color as spoken), word-pop (one word at a time, punchy scale-in), stack-up (words fade/rise in and stay), neon (active word glow pulse), bounce (active word bounces). Applies animation + colors + background treatment in one go; individual caption_*/color/background/background_style fields override specifics."),
|
|
4271
4438
|
caption_animation: z.enum(["highlight-pop", "karaoke-fill", "word-pop", "cumulative-reveal", "glow-pulse", "bounce-wave", "none"]).optional().describe("Word-by-word caption animation, if you want to set it directly instead of via caption_style. Works on set_captions, add_layer (kind=text), and set_layer_style. 'none' removes the animation and flattens the layer back to static text."),
|
|
4272
4439
|
caption_active_color: z.string().optional().describe("CSS color the active word takes while it is spoken (karaoke fill color, highlight text color, glow color)."),
|
|
@@ -4301,95 +4468,14 @@ function createEditorActionTool() {
|
|
|
4301
4468
|
explanation: z.string().describe("One-sentence reason shown in the UI.")
|
|
4302
4469
|
}),
|
|
4303
4470
|
execute: async (args) => {
|
|
4471
|
+
const linted = lintReplaceCompositionAction(args);
|
|
4472
|
+
if (linted) {
|
|
4473
|
+
return linted;
|
|
4474
|
+
}
|
|
4304
4475
|
return { ...args };
|
|
4305
4476
|
}
|
|
4306
4477
|
});
|
|
4307
4478
|
}
|
|
4308
|
-
const JOB_ID_PATTERN = /\bjob_[a-z0-9]+\b/gi;
|
|
4309
|
-
const HTTP_TOOL_CLAIM_PATTERN = /\b(?:http_request|REST API call|API call|GET\s+https?:\/\/|POST\s+https?:\/\/|GET\s+\/api\/|POST\s+\/api\/)/i;
|
|
4310
|
-
function collectJobIdsFromUnknown(value, output = new Set()) {
|
|
4311
|
-
if (typeof value === "string") {
|
|
4312
|
-
for (const match of value.matchAll(JOB_ID_PATTERN)) {
|
|
4313
|
-
output.add(match[0]);
|
|
4314
|
-
}
|
|
4315
|
-
return output;
|
|
4316
|
-
}
|
|
4317
|
-
if (Array.isArray(value)) {
|
|
4318
|
-
for (const entry of value) {
|
|
4319
|
-
collectJobIdsFromUnknown(entry, output);
|
|
4320
|
-
}
|
|
4321
|
-
return output;
|
|
4322
|
-
}
|
|
4323
|
-
if (value && typeof value === "object") {
|
|
4324
|
-
for (const entry of Object.values(value)) {
|
|
4325
|
-
collectJobIdsFromUnknown(entry, output);
|
|
4326
|
-
}
|
|
4327
|
-
}
|
|
4328
|
-
return output;
|
|
4329
|
-
}
|
|
4330
|
-
function collectVerifiedHttpJobIds(toolResult) {
|
|
4331
|
-
if (!toolResult || typeof toolResult !== "object") {
|
|
4332
|
-
return new Set();
|
|
4333
|
-
}
|
|
4334
|
-
const record = toolResult;
|
|
4335
|
-
return collectJobIdsFromUnknown(record.response?.body ?? record.result?.body);
|
|
4336
|
-
}
|
|
4337
|
-
function readAsyncJobHandoff(output) {
|
|
4338
|
-
if (!output || typeof output !== "object") {
|
|
4339
|
-
return null;
|
|
4340
|
-
}
|
|
4341
|
-
const record = output;
|
|
4342
|
-
const body = record.response?.body;
|
|
4343
|
-
if (!body || typeof body !== "object") {
|
|
4344
|
-
return null;
|
|
4345
|
-
}
|
|
4346
|
-
const job = body;
|
|
4347
|
-
if (typeof job.job_id !== "string" || typeof job.status !== "string") {
|
|
4348
|
-
return null;
|
|
4349
|
-
}
|
|
4350
|
-
if (job.status === "succeeded" || job.status === "failed" || job.status === "cancelled") {
|
|
4351
|
-
return null;
|
|
4352
|
-
}
|
|
4353
|
-
return {
|
|
4354
|
-
jobId: job.job_id,
|
|
4355
|
-
tracer: typeof job.tracer === "string" ? job.tracer : null,
|
|
4356
|
-
status: job.status,
|
|
4357
|
-
isNewQueuedJob: record.request?.method === "POST" && record.response?.status === 202
|
|
4358
|
-
};
|
|
4359
|
-
}
|
|
4360
|
-
function buildAsyncJobHandoffText(input) {
|
|
4361
|
-
if (!input.isNewQueuedJob) {
|
|
4362
|
-
return `Render \`${input.jobId}\` is ${input.status}.`;
|
|
4363
|
-
}
|
|
4364
|
-
return [
|
|
4365
|
-
`Started render \`${input.jobId}\`${input.tracer ? ` with tracer \`${input.tracer}\`` : ""}.`,
|
|
4366
|
-
"If it takes a while, ask for an update later and I can check it for you."
|
|
4367
|
-
].join("\n\n");
|
|
4368
|
-
}
|
|
4369
|
-
function buildEditorChatGuardrailNotice(input) {
|
|
4370
|
-
const mentionedJobIds = collectJobIdsFromUnknown(input.assistantText);
|
|
4371
|
-
const unverifiedJobIds = [...mentionedJobIds].filter((jobId) => !input.verifiedJobIds.has(jobId));
|
|
4372
|
-
const notices = [];
|
|
4373
|
-
if (input.redactedJobIds.size) {
|
|
4374
|
-
notices.push("Guardrail: I withheld unverified job ID text because it did not come from a visible HTTP response card.");
|
|
4375
|
-
}
|
|
4376
|
-
else if (unverifiedJobIds.length) {
|
|
4377
|
-
notices.push("Guardrail: I mentioned an unverified job ID. Ignore it unless it appears in a visible HTTP response card.");
|
|
4378
|
-
}
|
|
4379
|
-
if (!input.hasHttpRequestResult && HTTP_TOOL_CLAIM_PATTERN.test(input.assistantText)) {
|
|
4380
|
-
notices.push("Guardrail: no `http_request` tool result was recorded for this response, so any claimed REST/API call should be treated as not executed.");
|
|
4381
|
-
}
|
|
4382
|
-
return notices.length ? `\n\n${notices.join("\n\n")}` : "";
|
|
4383
|
-
}
|
|
4384
|
-
function redactUnverifiedJobIds(input) {
|
|
4385
|
-
return input.text.replace(JOB_ID_PATTERN, (jobId) => {
|
|
4386
|
-
if (input.verifiedJobIds.has(jobId)) {
|
|
4387
|
-
return jobId;
|
|
4388
|
-
}
|
|
4389
|
-
input.redactedJobIds.add(jobId);
|
|
4390
|
-
return "[unverified job id]";
|
|
4391
|
-
});
|
|
4392
|
-
}
|
|
4393
4479
|
function normalizeHistoryAttachments(attachments) {
|
|
4394
4480
|
return Array.isArray(attachments)
|
|
4395
4481
|
? attachments.map((attachment) => ({
|
|
@@ -4433,6 +4519,10 @@ async function streamEditorChatAgent(input, send) {
|
|
|
4433
4519
|
vidfarmApiKey: input.vidfarmApiKey,
|
|
4434
4520
|
abortSignal: input.abortSignal
|
|
4435
4521
|
}),
|
|
4522
|
+
load_skill: createLoadSkillTool({
|
|
4523
|
+
publicBaseUrl: config.PUBLIC_BASE_URL,
|
|
4524
|
+
abortSignal: input.abortSignal
|
|
4525
|
+
}),
|
|
4436
4526
|
frontend_action: createEditorFrontendActionTool({
|
|
4437
4527
|
tracerRegistry
|
|
4438
4528
|
}),
|
|
@@ -4459,6 +4549,7 @@ async function streamEditorChatAgent(input, send) {
|
|
|
4459
4549
|
let hasHttpRequestResult = false;
|
|
4460
4550
|
const verifiedJobIds = new Set();
|
|
4461
4551
|
const redactedJobIds = new Set();
|
|
4552
|
+
const jobIdRedactor = createStreamingJobIdRedactor({ verifiedJobIds, redactedJobIds });
|
|
4462
4553
|
const toolExchangeOrder = [];
|
|
4463
4554
|
const toolExchangeMap = new Map();
|
|
4464
4555
|
const httpExchanges = [];
|
|
@@ -4473,16 +4564,14 @@ async function streamEditorChatAgent(input, send) {
|
|
|
4473
4564
|
case "text-delta":
|
|
4474
4565
|
assistantTextForAudit += part.text;
|
|
4475
4566
|
{
|
|
4476
|
-
const safeText =
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
delta: safeText
|
|
4485
|
-
});
|
|
4567
|
+
const safeText = jobIdRedactor.push(part.text);
|
|
4568
|
+
if (safeText) {
|
|
4569
|
+
assistantTextForUser += safeText;
|
|
4570
|
+
send({
|
|
4571
|
+
type: "text-delta",
|
|
4572
|
+
delta: safeText
|
|
4573
|
+
});
|
|
4574
|
+
}
|
|
4486
4575
|
}
|
|
4487
4576
|
break;
|
|
4488
4577
|
case "tool-call":
|
|
@@ -4576,6 +4665,14 @@ async function streamEditorChatAgent(input, send) {
|
|
|
4576
4665
|
break;
|
|
4577
4666
|
case "finish":
|
|
4578
4667
|
{
|
|
4668
|
+
const heldText = jobIdRedactor.flush();
|
|
4669
|
+
if (heldText) {
|
|
4670
|
+
assistantTextForUser += heldText;
|
|
4671
|
+
send({
|
|
4672
|
+
type: "text-delta",
|
|
4673
|
+
delta: heldText
|
|
4674
|
+
});
|
|
4675
|
+
}
|
|
4579
4676
|
const guardrailNotice = buildEditorChatGuardrailNotice({
|
|
4580
4677
|
assistantText: assistantTextForAudit,
|
|
4581
4678
|
verifiedJobIds,
|
|
@@ -4691,7 +4788,11 @@ function rewriteHyperframesDraftCompositionHtml(html, draftPath) {
|
|
|
4691
4788
|
}
|
|
4692
4789
|
}
|
|
4693
4790
|
// And the scene-transition keyframes: toggling data-transition on a clip
|
|
4694
|
-
// must animate immediately on older stored compositions.
|
|
4791
|
+
// must animate immediately on older stored compositions. Compositions that
|
|
4792
|
+
// embed the V1 block get it swapped for the current one in place; if the
|
|
4793
|
+
// swap misses (drifted block), the marker check below appends V2 after it,
|
|
4794
|
+
// which wins by source order.
|
|
4795
|
+
rewritten = upgradeLegacyTransitionCss(rewritten);
|
|
4695
4796
|
if (!rewritten.includes(TRANSITION_STYLE_MARKER)) {
|
|
4696
4797
|
const transitionTag = `<style data-vf-transitions="true">${TRANSITION_CSS}</style>`;
|
|
4697
4798
|
if (/<\/head>/i.test(rewritten)) {
|
|
@@ -5511,21 +5612,193 @@ const PUBLIC_SKILL_FILES = {
|
|
|
5511
5612
|
"vidfarm-director": "SKILL.director.md",
|
|
5512
5613
|
"vidfarm-platform": "SKILL.platform.md"
|
|
5513
5614
|
};
|
|
5615
|
+
// Vendored skill packs (the heygen-scrubbed hyperframes pack + vidfarm-media)
|
|
5616
|
+
// mirrored from .agents/skills. Vidfarm is the distribution origin: the devcli
|
|
5617
|
+
// (`vidfarm skills add`) and the chat copilot's load_skill tool both pull from
|
|
5618
|
+
// these routes, so customers never fetch skills from an upstream vendor org.
|
|
5619
|
+
const SKILL_PACK_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,64}$/;
|
|
5620
|
+
const SKILL_PACK_IGNORED_ENTRIES = new Set([".DS_Store", "node_modules", ".git"]);
|
|
5621
|
+
function resolveSkillPackDir(name) {
|
|
5622
|
+
if (!SKILL_PACK_NAME_PATTERN.test(name)) {
|
|
5623
|
+
return null;
|
|
5624
|
+
}
|
|
5625
|
+
for (const candidate of resolveRootFileCandidates(path.join(".agents", "skills", name))) {
|
|
5626
|
+
try {
|
|
5627
|
+
if (statSync(candidate).isDirectory()) {
|
|
5628
|
+
return candidate;
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
catch {
|
|
5632
|
+
continue;
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
return null;
|
|
5636
|
+
}
|
|
5637
|
+
function listSkillPackFiles(dir, prefix = "") {
|
|
5638
|
+
const files = [];
|
|
5639
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
5640
|
+
if (SKILL_PACK_IGNORED_ENTRIES.has(entry.name)) {
|
|
5641
|
+
continue;
|
|
5642
|
+
}
|
|
5643
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
5644
|
+
const absolute = path.join(dir, entry.name);
|
|
5645
|
+
if (entry.isDirectory()) {
|
|
5646
|
+
files.push(...listSkillPackFiles(absolute, relative));
|
|
5647
|
+
}
|
|
5648
|
+
else if (entry.isFile()) {
|
|
5649
|
+
files.push(relative);
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
return files.sort();
|
|
5653
|
+
}
|
|
5654
|
+
function readSkillPackDescription(dir) {
|
|
5655
|
+
try {
|
|
5656
|
+
const skillMd = readFileSync(path.join(dir, "SKILL.md"), "utf8");
|
|
5657
|
+
const match = skillMd.match(/^description:\s*(.+)$/m);
|
|
5658
|
+
return match ? match[1].trim().slice(0, 300) : "";
|
|
5659
|
+
}
|
|
5660
|
+
catch {
|
|
5661
|
+
return "";
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5514
5664
|
app.get("/skill/:name", (c) => {
|
|
5515
5665
|
const name = c.req.param("name").replace(/\.md$/i, "");
|
|
5516
5666
|
const filename = PUBLIC_SKILL_FILES[name];
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5667
|
+
const contents = filename
|
|
5668
|
+
? readRootSkillFile(filename)
|
|
5669
|
+
: (() => {
|
|
5670
|
+
const packDir = resolveSkillPackDir(name);
|
|
5671
|
+
if (!packDir) {
|
|
5672
|
+
return "";
|
|
5673
|
+
}
|
|
5674
|
+
try {
|
|
5675
|
+
return readFileSync(path.join(packDir, "SKILL.md"), "utf8");
|
|
5676
|
+
}
|
|
5677
|
+
catch {
|
|
5678
|
+
return "";
|
|
5679
|
+
}
|
|
5680
|
+
})();
|
|
5521
5681
|
if (!contents) {
|
|
5522
|
-
return c.text("Skill file unavailable", 404);
|
|
5682
|
+
return c.text(filename ? "Skill file unavailable" : "Unknown skill", 404);
|
|
5523
5683
|
}
|
|
5524
5684
|
return c.body(contents, 200, {
|
|
5525
5685
|
"content-type": "text/markdown; charset=utf-8",
|
|
5526
5686
|
"cache-control": "no-cache"
|
|
5527
5687
|
});
|
|
5528
5688
|
});
|
|
5689
|
+
app.get("/skill-pack/index.json", (c) => {
|
|
5690
|
+
const skills = [];
|
|
5691
|
+
for (const candidate of resolveRootFileCandidates(path.join(".agents", "skills"))) {
|
|
5692
|
+
let entries;
|
|
5693
|
+
try {
|
|
5694
|
+
entries = readdirSync(candidate);
|
|
5695
|
+
}
|
|
5696
|
+
catch {
|
|
5697
|
+
continue;
|
|
5698
|
+
}
|
|
5699
|
+
for (const entry of entries.sort()) {
|
|
5700
|
+
if (!SKILL_PACK_NAME_PATTERN.test(entry) || skills.some((skill) => skill.name === entry)) {
|
|
5701
|
+
continue;
|
|
5702
|
+
}
|
|
5703
|
+
const dir = path.join(candidate, entry);
|
|
5704
|
+
try {
|
|
5705
|
+
if (!statSync(dir).isDirectory()) {
|
|
5706
|
+
continue;
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
catch {
|
|
5710
|
+
continue;
|
|
5711
|
+
}
|
|
5712
|
+
skills.push({ name: entry, description: readSkillPackDescription(dir) });
|
|
5713
|
+
}
|
|
5714
|
+
break;
|
|
5715
|
+
}
|
|
5716
|
+
return c.json({ skills });
|
|
5717
|
+
});
|
|
5718
|
+
// Manifest hashing reads every file in a pack (hyperframes-animation alone is
|
|
5719
|
+
// ~100 files) — memoize per pack dir, invalidated by a cheap stat signature,
|
|
5720
|
+
// so `vidfarm skills update` sweeps don't re-hash 5MB per request.
|
|
5721
|
+
const skillPackManifestCache = new Map();
|
|
5722
|
+
app.get("/skill-pack/:name/manifest.json", (c) => {
|
|
5723
|
+
const packDir = resolveSkillPackDir(c.req.param("name"));
|
|
5724
|
+
if (!packDir) {
|
|
5725
|
+
return c.json({ error: "Unknown skill pack." }, 404);
|
|
5726
|
+
}
|
|
5727
|
+
const relativePaths = listSkillPackFiles(packDir);
|
|
5728
|
+
const signature = relativePaths
|
|
5729
|
+
.map((relative) => {
|
|
5730
|
+
try {
|
|
5731
|
+
const stats = statSync(path.join(packDir, relative));
|
|
5732
|
+
return `${relative}:${stats.size}:${stats.mtimeMs}`;
|
|
5733
|
+
}
|
|
5734
|
+
catch {
|
|
5735
|
+
return `${relative}:missing`;
|
|
5736
|
+
}
|
|
5737
|
+
})
|
|
5738
|
+
.join("|");
|
|
5739
|
+
const cached = skillPackManifestCache.get(packDir);
|
|
5740
|
+
if (cached && cached.signature === signature) {
|
|
5741
|
+
return c.json(cached.manifest);
|
|
5742
|
+
}
|
|
5743
|
+
const files = relativePaths.map((relative) => {
|
|
5744
|
+
const bytes = readFileSync(path.join(packDir, relative));
|
|
5745
|
+
return {
|
|
5746
|
+
path: relative,
|
|
5747
|
+
size: bytes.length,
|
|
5748
|
+
sha256: createHash("sha256").update(bytes).digest("hex")
|
|
5749
|
+
};
|
|
5750
|
+
});
|
|
5751
|
+
const manifest = { name: path.basename(packDir), files };
|
|
5752
|
+
skillPackManifestCache.set(packDir, { signature, manifest });
|
|
5753
|
+
return c.json(manifest);
|
|
5754
|
+
});
|
|
5755
|
+
app.get("/skill-pack/:name/files/*", (c) => {
|
|
5756
|
+
const packDir = resolveSkillPackDir(c.req.param("name"));
|
|
5757
|
+
if (!packDir) {
|
|
5758
|
+
return c.text("Unknown skill pack.", 404);
|
|
5759
|
+
}
|
|
5760
|
+
const marker = "/files/";
|
|
5761
|
+
const rawPath = c.req.path.slice(c.req.path.indexOf(marker) + marker.length);
|
|
5762
|
+
let relative;
|
|
5763
|
+
try {
|
|
5764
|
+
relative = rawPath
|
|
5765
|
+
.split("/")
|
|
5766
|
+
.map((segment) => decodeURIComponent(segment))
|
|
5767
|
+
.join("/");
|
|
5768
|
+
}
|
|
5769
|
+
catch {
|
|
5770
|
+
return c.text("Bad file path.", 400);
|
|
5771
|
+
}
|
|
5772
|
+
const resolved = path.resolve(packDir, relative);
|
|
5773
|
+
if (resolved !== packDir && !resolved.startsWith(packDir + path.sep)) {
|
|
5774
|
+
return c.text("Bad file path.", 400);
|
|
5775
|
+
}
|
|
5776
|
+
let bytes;
|
|
5777
|
+
try {
|
|
5778
|
+
bytes = readFileSync(resolved);
|
|
5779
|
+
}
|
|
5780
|
+
catch {
|
|
5781
|
+
return c.text("File not found.", 404);
|
|
5782
|
+
}
|
|
5783
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
5784
|
+
const contentType = ext === ".md" || ext === ".markdown" ? "text/markdown; charset=utf-8"
|
|
5785
|
+
: ext === ".json" ? "application/json; charset=utf-8"
|
|
5786
|
+
: ext === ".mjs" || ext === ".cjs" || ext === ".js" ? "text/javascript; charset=utf-8"
|
|
5787
|
+
: ext === ".html" ? "text/html; charset=utf-8"
|
|
5788
|
+
: ext === ".css" ? "text/css; charset=utf-8"
|
|
5789
|
+
: ext === ".txt" || ext === ".srt" || ext === ".vtt" || ext === ".sh" || ext === ".yaml" || ext === ".yml" ? "text/plain; charset=utf-8"
|
|
5790
|
+
: ext === ".png" ? "image/png"
|
|
5791
|
+
: ext === ".avif" ? "image/avif"
|
|
5792
|
+
: ext === ".webp" ? "image/webp"
|
|
5793
|
+
: ext === ".jpg" || ext === ".jpeg" ? "image/jpeg"
|
|
5794
|
+
: ext === ".mp3" ? "audio/mpeg"
|
|
5795
|
+
: ext === ".woff2" ? "font/woff2"
|
|
5796
|
+
: "application/octet-stream";
|
|
5797
|
+
return c.body(new Uint8Array(bytes), 200, {
|
|
5798
|
+
"content-type": contentType,
|
|
5799
|
+
"cache-control": "no-cache"
|
|
5800
|
+
});
|
|
5801
|
+
});
|
|
5529
5802
|
if (config.SENTRY_DEBUG_ROUTE === "true") {
|
|
5530
5803
|
app.get("/debug-sentry", () => {
|
|
5531
5804
|
Sentry.logger.info("User triggered test error", {
|
|
@@ -6435,7 +6708,13 @@ app.post(`${COMPOSITIONS_PREFIX}/:forkId/captions`, async (c) => {
|
|
|
6435
6708
|
const text = str("text");
|
|
6436
6709
|
if (text) {
|
|
6437
6710
|
const windowStart = Math.max(0, num("start") ?? 0);
|
|
6438
|
-
const
|
|
6711
|
+
const availableWindow = compositionDuration - windowStart;
|
|
6712
|
+
// When the composition declares no usable window (missing data-duration,
|
|
6713
|
+
// or windowStart at/after the end), fall back to a word-count estimate
|
|
6714
|
+
// (~0.32s/word, same as the /audio/captions primitive) instead of
|
|
6715
|
+
// cramming the whole script into a 0.4s window of overlapping cues.
|
|
6716
|
+
const estimatedWindow = Math.max(0.4, text.trim().split(/\s+/).filter(Boolean).length * 0.32);
|
|
6717
|
+
const windowDuration = num("duration") ?? (availableWindow > 0.4 ? availableWindow : estimatedWindow);
|
|
6439
6718
|
cues = cuesFromText(text, windowStart + offsetSec, windowDuration, { maxWordsPerCue: maxWords });
|
|
6440
6719
|
sourceNote = "text";
|
|
6441
6720
|
}
|
|
@@ -14772,6 +15051,77 @@ app.get(`${USER_PREFIX}/me/generated-media`, async (c) => {
|
|
|
14772
15051
|
next_cursor: page.nextCursor
|
|
14773
15052
|
});
|
|
14774
15053
|
});
|
|
15054
|
+
// OpenAI's embedding models are named text-embedding-3-* / text-embedding-ada-*;
|
|
15055
|
+
// Gemini's family INCLUDES text-embedding-004 (plus gemini-embedding-*), so a
|
|
15056
|
+
// bare startsWith("text-embedding") misroutes a Gemini-built library to OpenAI
|
|
15057
|
+
// and returns cross-space (garbage) cosine scores. Match the OpenAI naming
|
|
15058
|
+
// precisely; everything else is gemini.
|
|
15059
|
+
function inferEmbeddingProviderFromModel(model) {
|
|
15060
|
+
return model && /^text-embedding-(3|ada)/i.test(model) ? "openai" : "gemini";
|
|
15061
|
+
}
|
|
15062
|
+
// Embed-capable client for populating/searching a clip library. Matches the
|
|
15063
|
+
// library's existing embedding space when one exists (so vectors stay
|
|
15064
|
+
// cosine-comparable); otherwise takes the first embed-capable saved key
|
|
15065
|
+
// (gemini → openai; openrouter can't embed). Null → no key, keyword-only.
|
|
15066
|
+
async function buildClipLibraryEmbedder(customerId) {
|
|
15067
|
+
const existing = await clipRecords.getLibraryEmbeddingModel(customerId);
|
|
15068
|
+
const providers = existing ? [inferEmbeddingProviderFromModel(existing)] : ["gemini", "openai"];
|
|
15069
|
+
for (const provider of providers) {
|
|
15070
|
+
const apiKey = await resolveCustomerProviderKey(customerId, provider);
|
|
15071
|
+
if (!apiKey)
|
|
15072
|
+
continue;
|
|
15073
|
+
const client = new ClipModelClient({ provider, apiKey });
|
|
15074
|
+
if (client.canEmbed)
|
|
15075
|
+
return client;
|
|
15076
|
+
}
|
|
15077
|
+
return null;
|
|
15078
|
+
}
|
|
15079
|
+
// Embedder for My Files notes — the same BYOK seam clip search uses. Prefer the
|
|
15080
|
+
// provider matching the embedding model the file library was built with (from
|
|
15081
|
+
// the newest annotated attachment) so query vectors stay cosine-comparable; a
|
|
15082
|
+
// fresh library takes the first embed-capable key (gemini → openai; openrouter
|
|
15083
|
+
// can't embed). Null (no key) fail-softs notes search to keyword-only.
|
|
15084
|
+
async function buildFileNotesEmbedder(customerId) {
|
|
15085
|
+
const attachments = await serverlessRecords.listUserAttachments(customerId);
|
|
15086
|
+
const libModel = attachments.find((a) => a.notesEmbeddingModel && Array.isArray(a.notesEmbedding) && a.notesEmbedding.length > 0)?.notesEmbeddingModel ?? null;
|
|
15087
|
+
const providers = libModel ? [inferEmbeddingProviderFromModel(libModel)] : ["gemini", "openai"];
|
|
15088
|
+
for (const provider of providers) {
|
|
15089
|
+
const apiKey = await resolveCustomerProviderKey(customerId, provider);
|
|
15090
|
+
if (!apiKey)
|
|
15091
|
+
continue;
|
|
15092
|
+
const client = new ClipModelClient({ provider, apiKey });
|
|
15093
|
+
if (client.canEmbed)
|
|
15094
|
+
return client;
|
|
15095
|
+
}
|
|
15096
|
+
return null;
|
|
15097
|
+
}
|
|
15098
|
+
// What we embed for a My Files entry: folder + file name + the notes, so
|
|
15099
|
+
// "the sprite card for our mascot" is findable from any phrasing. Fail-soft:
|
|
15100
|
+
// no key or an embed error still saves the notes (keyword-searchable).
|
|
15101
|
+
async function embedAttachmentNotes(customerId, input) {
|
|
15102
|
+
if (!input.notes.trim())
|
|
15103
|
+
return null;
|
|
15104
|
+
try {
|
|
15105
|
+
const embedder = await buildFileNotesEmbedder(customerId);
|
|
15106
|
+
if (!embedder?.embeddingModelId)
|
|
15107
|
+
return null;
|
|
15108
|
+
const text = [input.folderPath, input.fileName, input.notes].filter(Boolean).join("\n");
|
|
15109
|
+
const embedding = await embedder.embedQuery(text);
|
|
15110
|
+
return embedding && embedding.length > 0 ? { embedding, model: embedder.embeddingModelId } : null;
|
|
15111
|
+
}
|
|
15112
|
+
catch (error) {
|
|
15113
|
+
devLog("attachment_notes.embed_failed", devErrorFields(error));
|
|
15114
|
+
return null;
|
|
15115
|
+
}
|
|
15116
|
+
}
|
|
15117
|
+
function attachmentKeywordMatch(attachment, q) {
|
|
15118
|
+
const hay = `${attachment.fileName} ${attachment.folderPath ?? ""} ${attachment.notes ?? ""}`.toLowerCase();
|
|
15119
|
+
return q
|
|
15120
|
+
.toLowerCase()
|
|
15121
|
+
.split(/\s+/)
|
|
15122
|
+
.filter(Boolean)
|
|
15123
|
+
.every((term) => hay.includes(term));
|
|
15124
|
+
}
|
|
14775
15125
|
app.get(`${USER_PREFIX}/me/attachments`, async (c) => {
|
|
14776
15126
|
const customer = requireCustomer(c);
|
|
14777
15127
|
const attachments = (await serverlessRecords.listUserAttachments(customer.id)).map((attachment) => serializeUserAttachment(c, attachment));
|
|
@@ -14850,6 +15200,7 @@ app.post(`${USER_PREFIX}/me/attachments/upload`, async (c) => {
|
|
|
14850
15200
|
const fileName = sanitizeFileName(file.name || "upload.bin");
|
|
14851
15201
|
const contentType = file.type || inferAttachmentContentType(fileName);
|
|
14852
15202
|
const folderPath = sanitizeStorageSubpath(String(formData.get("folder_path") ?? ""));
|
|
15203
|
+
const notes = String(formData.get("notes") ?? "").trim().slice(0, 4000) || null;
|
|
14853
15204
|
if (!isAllowedAttachment(fileName, contentType)) {
|
|
14854
15205
|
return c.json({ error: `Unsupported attachment type: ${fileName}` }, 400);
|
|
14855
15206
|
}
|
|
@@ -14861,6 +15212,7 @@ app.post(`${USER_PREFIX}/me/attachments/upload`, async (c) => {
|
|
|
14861
15212
|
const attachmentId = randomUUID();
|
|
14862
15213
|
const storageKey = storage.userAttachmentKey(customer.id, attachmentId, fileName, folderPath);
|
|
14863
15214
|
const stored = await storage.putBuffer(storageKey, buffer, contentType);
|
|
15215
|
+
const embedded = notes ? await embedAttachmentNotes(customer.id, { fileName, folderPath, notes }) : null;
|
|
14864
15216
|
await serverlessRecords.createUserAttachment({
|
|
14865
15217
|
id: attachmentId,
|
|
14866
15218
|
customerId: customer.id,
|
|
@@ -14869,7 +15221,10 @@ app.post(`${USER_PREFIX}/me/attachments/upload`, async (c) => {
|
|
|
14869
15221
|
sizeBytes: buffer.byteLength,
|
|
14870
15222
|
storageKey,
|
|
14871
15223
|
folderPath,
|
|
14872
|
-
publicUrl: stored.url || storage.getPublicUrl(storageKey) || buildUserAttachmentViewUrl(c, storageKey)
|
|
15224
|
+
publicUrl: stored.url || storage.getPublicUrl(storageKey) || buildUserAttachmentViewUrl(c, storageKey),
|
|
15225
|
+
notes,
|
|
15226
|
+
notesEmbedding: embedded?.embedding ?? null,
|
|
15227
|
+
notesEmbeddingModel: embedded?.model ?? null
|
|
14873
15228
|
});
|
|
14874
15229
|
const attachment = await serverlessRecords.getUserAttachment(customer.id, attachmentId);
|
|
14875
15230
|
if (!attachment) {
|
|
@@ -14898,6 +15253,8 @@ app.post(`${USER_PREFIX}/me/attachments`, async (c) => {
|
|
|
14898
15253
|
if (!isAllowedAttachment(fileName, contentType)) {
|
|
14899
15254
|
return c.json({ error: `Unsupported attachment type: ${fileName}` }, 400);
|
|
14900
15255
|
}
|
|
15256
|
+
const notes = body.notes?.trim() || null;
|
|
15257
|
+
const embedded = notes ? await embedAttachmentNotes(customer.id, { fileName, folderPath, notes }) : null;
|
|
14901
15258
|
await serverlessRecords.createUserAttachment({
|
|
14902
15259
|
id: body.attachment_id,
|
|
14903
15260
|
customerId: customer.id,
|
|
@@ -14906,7 +15263,10 @@ app.post(`${USER_PREFIX}/me/attachments`, async (c) => {
|
|
|
14906
15263
|
sizeBytes: body.size_bytes,
|
|
14907
15264
|
storageKey: expectedStorageKey,
|
|
14908
15265
|
folderPath,
|
|
14909
|
-
publicUrl: storage.getPublicUrl(expectedStorageKey) || buildUserAttachmentViewUrl(c, expectedStorageKey)
|
|
15266
|
+
publicUrl: storage.getPublicUrl(expectedStorageKey) || buildUserAttachmentViewUrl(c, expectedStorageKey),
|
|
15267
|
+
notes,
|
|
15268
|
+
notesEmbedding: embedded?.embedding ?? null,
|
|
15269
|
+
notesEmbeddingModel: embedded?.model ?? null
|
|
14910
15270
|
});
|
|
14911
15271
|
const attachment = await serverlessRecords.getUserAttachment(customer.id, body.attachment_id);
|
|
14912
15272
|
if (!attachment) {
|
|
@@ -14940,6 +15300,89 @@ app.patch(`${USER_PREFIX}/me/attachments/folders`, async (c) => {
|
|
|
14940
15300
|
await serverlessRecords.moveUserAttachmentFolder(customer.id, fromFolder, toFolder);
|
|
14941
15301
|
return c.json({ ok: true, from_folder_path: fromFolder, to_folder_path: toFolder });
|
|
14942
15302
|
});
|
|
15303
|
+
// PATCH /me/attachments/:attachmentId — set/clear the metadata notes on one My
|
|
15304
|
+
// Files entry and (re)embed them for vector search. Notes are what make assets
|
|
15305
|
+
// findable by MEANING later — e.g. annotate a character_sprite_card.png with
|
|
15306
|
+
// who the character is so /me/attachments/search finds it from any phrasing.
|
|
15307
|
+
// (Registered after the /folders PATCH so "folders" never matches as an id.)
|
|
15308
|
+
app.patch(`${USER_PREFIX}/me/attachments/:attachmentId`, async (c) => {
|
|
15309
|
+
const customer = requireCustomer(c);
|
|
15310
|
+
try {
|
|
15311
|
+
const body = userAttachmentNotesSchema.parse(await c.req.json());
|
|
15312
|
+
const attachment = await serverlessRecords.getUserAttachment(customer.id, c.req.param("attachmentId"));
|
|
15313
|
+
if (!attachment) {
|
|
15314
|
+
return c.json({ error: "Attachment not found." }, 404);
|
|
15315
|
+
}
|
|
15316
|
+
const notes = (body.notes ?? "").trim() || null;
|
|
15317
|
+
const embedded = notes
|
|
15318
|
+
? await embedAttachmentNotes(customer.id, { fileName: attachment.fileName, folderPath: attachment.folderPath, notes })
|
|
15319
|
+
: null;
|
|
15320
|
+
const updated = await serverlessRecords.updateUserAttachmentNotes(customer.id, attachment.id, {
|
|
15321
|
+
notes,
|
|
15322
|
+
notesEmbedding: embedded?.embedding ?? null,
|
|
15323
|
+
notesEmbeddingModel: embedded?.model ?? null
|
|
15324
|
+
});
|
|
15325
|
+
if (!updated) {
|
|
15326
|
+
return c.json({ error: "Attachment not found." }, 404);
|
|
15327
|
+
}
|
|
15328
|
+
return c.json({ ok: true, attachment: serializeUserAttachment(c, updated), notes_embedded: Boolean(embedded) });
|
|
15329
|
+
}
|
|
15330
|
+
catch (error) {
|
|
15331
|
+
return c.json({ error: error instanceof Error ? error.message : "Unable to update attachment notes." }, 400);
|
|
15332
|
+
}
|
|
15333
|
+
});
|
|
15334
|
+
// POST /me/attachments/search — find My Files entries by meaning, mirroring
|
|
15335
|
+
// /clips/search: keyword filter over name/folder/notes, semantically re-ranked
|
|
15336
|
+
// via the notes embeddings; when nothing keyword-matches, relax to a pure
|
|
15337
|
+
// semantic pass. Embeddings are BYOK — no saved key fail-softs to keyword-only.
|
|
15338
|
+
app.post(`${USER_PREFIX}/me/attachments/search`, async (c) => {
|
|
15339
|
+
const customer = requireCustomer(c);
|
|
15340
|
+
try {
|
|
15341
|
+
const body = userAttachmentSearchSchema.parse(await c.req.json());
|
|
15342
|
+
const query = body.query.trim();
|
|
15343
|
+
if (!query) {
|
|
15344
|
+
return c.json({ error: "Provide a search query." }, 400);
|
|
15345
|
+
}
|
|
15346
|
+
const folderPath = sanitizeStorageSubpath(body.folder_path);
|
|
15347
|
+
const limit = Math.max(1, Math.min(100, body.limit ?? 20));
|
|
15348
|
+
const all = await serverlessRecords.listUserAttachments(customer.id);
|
|
15349
|
+
const scoped = folderPath ? all.filter((a) => (a.folderPath ?? "") === folderPath) : all;
|
|
15350
|
+
let queryEmbedding;
|
|
15351
|
+
try {
|
|
15352
|
+
const embedder = await buildFileNotesEmbedder(customer.id);
|
|
15353
|
+
if (embedder)
|
|
15354
|
+
queryEmbedding = await embedder.embedQuery(query);
|
|
15355
|
+
}
|
|
15356
|
+
catch (error) {
|
|
15357
|
+
devLog("attachment_search.embed_failed", devErrorFields(error));
|
|
15358
|
+
}
|
|
15359
|
+
const keywordIds = new Set(scoped.filter((a) => attachmentKeywordMatch(a, query)).map((a) => a.id));
|
|
15360
|
+
const pool = keywordIds.size > 0 ? scoped.filter((a) => keywordIds.has(a.id)) : scoped;
|
|
15361
|
+
const hits = pool
|
|
15362
|
+
.map((a) => ({
|
|
15363
|
+
attachment: a,
|
|
15364
|
+
keyword: keywordIds.has(a.id),
|
|
15365
|
+
similarity: queryEmbedding && Array.isArray(a.notesEmbedding) && a.notesEmbedding.length > 0
|
|
15366
|
+
? cosineSimilarity(a.notesEmbedding, queryEmbedding)
|
|
15367
|
+
: undefined
|
|
15368
|
+
}))
|
|
15369
|
+
.filter((h) => h.keyword || (h.similarity ?? 0) > 0)
|
|
15370
|
+
.sort((x, y) => ((y.similarity ?? 0) + (y.keyword ? 0.2 : 0)) - ((x.similarity ?? 0) + (x.keyword ? 0.2 : 0)))
|
|
15371
|
+
.slice(0, limit);
|
|
15372
|
+
return c.json({
|
|
15373
|
+
results: hits.map((h) => ({
|
|
15374
|
+
...serializeUserAttachment(c, h.attachment),
|
|
15375
|
+
similarity: h.similarity ?? null,
|
|
15376
|
+
keyword_match: h.keyword
|
|
15377
|
+
})),
|
|
15378
|
+
semantic: Boolean(queryEmbedding),
|
|
15379
|
+
folder_path: folderPath || null
|
|
15380
|
+
});
|
|
15381
|
+
}
|
|
15382
|
+
catch (error) {
|
|
15383
|
+
return c.json({ error: error instanceof Error ? error.message : "My Files search failed." }, 400);
|
|
15384
|
+
}
|
|
15385
|
+
});
|
|
14943
15386
|
app.delete(`${USER_PREFIX}/me/attachments/folders`, async (c) => {
|
|
14944
15387
|
const customer = requireCustomer(c);
|
|
14945
15388
|
const body = userFileFolderSchema.parse(await c.req.json());
|
|
@@ -15519,20 +15962,31 @@ function renderClipsPage(input) {
|
|
|
15519
15962
|
.clips-import-note[data-tone="success"] { color: #3f7d55; }
|
|
15520
15963
|
.clips-import-actions { display: flex; justify-content: flex-end; gap: 10px; }
|
|
15521
15964
|
|
|
15522
|
-
@media (max-width: 1180px) {
|
|
15965
|
+
@media (min-width: 1025px) and (max-width: 1180px) {
|
|
15966
|
+
/* Rail stacks inline below the content in this band only; at ≤1024px
|
|
15967
|
+
the shared shell turns the rail into the FAB + popup overlay. */
|
|
15523
15968
|
.clips-frame { padding-left: 0; }
|
|
15524
15969
|
.editor-right-rail { position: relative; inset: auto; width: 100%; height: 540px; margin-top: 14px; border-radius: 24px; overflow: hidden; }
|
|
15525
15970
|
.editor-chat-panel { border-right: 0; border-radius: 24px; }
|
|
15526
|
-
}
|
|
15527
|
-
@media (min-width: 1025px) and (max-width: 1180px) {
|
|
15528
15971
|
html, body { height: auto; min-height: 100%; overflow: auto; overflow-x: hidden; }
|
|
15529
15972
|
.app-shell { height: auto; min-height: 100svh; }
|
|
15530
15973
|
.page-stack, .frame { height: auto; min-height: 0; overflow: visible; }
|
|
15531
15974
|
.clips-scroll { overflow: visible; padding-right: 0; }
|
|
15532
15975
|
}
|
|
15533
15976
|
@media (max-width: 1024px) {
|
|
15977
|
+
.clips-frame { padding-left: 0; }
|
|
15534
15978
|
.clips-shell { grid-template-rows: auto auto; }
|
|
15535
15979
|
.clips-scroll { overflow: visible; padding-right: 0; }
|
|
15980
|
+
.clips-toolbar-row { gap: 8px; }
|
|
15981
|
+
.clips-search { flex: 1 1 100%; min-width: 0; }
|
|
15982
|
+
.clips-search input { min-width: 0; font-size: 16px; }
|
|
15983
|
+
.clips-import-form input,
|
|
15984
|
+
.clips-import-form textarea,
|
|
15985
|
+
.clips-import-form select { font-size: 16px; }
|
|
15986
|
+
}
|
|
15987
|
+
@media (max-width: 560px) {
|
|
15988
|
+
.clips-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
15989
|
+
.clip-body { padding: 10px; }
|
|
15536
15990
|
}
|
|
15537
15991
|
`,
|
|
15538
15992
|
body: `
|
|
@@ -15846,7 +16300,7 @@ async function pickClipProviderFromKeys(customerId) {
|
|
|
15846
16300
|
*/
|
|
15847
16301
|
async function buildMatchEmbedder(customerId) {
|
|
15848
16302
|
const libModel = await clipRecords.getLibraryEmbeddingModel(customerId);
|
|
15849
|
-
const provider = libModel
|
|
16303
|
+
const provider = inferEmbeddingProviderFromModel(libModel);
|
|
15850
16304
|
const key = await resolveCustomerProviderKey(customerId, provider);
|
|
15851
16305
|
if (!key)
|
|
15852
16306
|
return null;
|
|
@@ -16216,29 +16670,54 @@ app.post(`${CLIPS_PREFIX}/scan`, async (c) => {
|
|
|
16216
16670
|
const computeEstimate = effectiveDurationSec
|
|
16217
16671
|
? estimateClipScanComputeUsd(effectiveDurationSec, huntSpec.duration_band?.target_sec)
|
|
16218
16672
|
: null;
|
|
16219
|
-
|
|
16220
|
-
|
|
16221
|
-
|
|
16222
|
-
|
|
16223
|
-
|
|
16224
|
-
|
|
16225
|
-
|
|
16226
|
-
|
|
16227
|
-
|
|
16228
|
-
|
|
16229
|
-
|
|
16230
|
-
|
|
16231
|
-
|
|
16232
|
-
|
|
16233
|
-
|
|
16234
|
-
|
|
16235
|
-
|
|
16236
|
-
|
|
16237
|
-
|
|
16238
|
-
|
|
16239
|
-
|
|
16240
|
-
|
|
16241
|
-
|
|
16673
|
+
// Records first: a Dynamo failure here aborts submission before anything
|
|
16674
|
+
// is billed, and the warm probe lambda's later status:"scanning" write is
|
|
16675
|
+
// never clobbered back to "pending" by a post-start putScanRecords.
|
|
16676
|
+
await putScanRecords({ scanStatus: "running" });
|
|
16677
|
+
let executionArn;
|
|
16678
|
+
try {
|
|
16679
|
+
const res = await clipScanSfn.send(new StartExecutionCommand({
|
|
16680
|
+
stateMachineArn: config.VIDFARM_CLIP_SCAN_STATE_MACHINE_ARN,
|
|
16681
|
+
name: scanId.replace(/[^A-Za-z0-9-_]/g, "-").slice(0, 80),
|
|
16682
|
+
input: JSON.stringify({
|
|
16683
|
+
scan_id: scanId,
|
|
16684
|
+
owner_id: customer.id,
|
|
16685
|
+
source_video_id: sourceVideoId,
|
|
16686
|
+
source_filename: filename,
|
|
16687
|
+
raw_s3_uri: rawS3Uri ?? "",
|
|
16688
|
+
tier,
|
|
16689
|
+
provider,
|
|
16690
|
+
tag_api_key: tagApiKey,
|
|
16691
|
+
embedding_provider: client.embeddingProvider ?? "",
|
|
16692
|
+
embedding_api_key: embeddingKey ?? "",
|
|
16693
|
+
guidance_prompt: guidancePrompt,
|
|
16694
|
+
frames_per_scene: framesPerScene,
|
|
16695
|
+
include_audio: includeAudio,
|
|
16696
|
+
hunt_spec: huntSpec,
|
|
16697
|
+
tracer
|
|
16698
|
+
})
|
|
16699
|
+
}));
|
|
16700
|
+
executionArn = res.executionArn;
|
|
16701
|
+
}
|
|
16702
|
+
catch (error) {
|
|
16703
|
+
// The execution never started — roll the scan (and its source) to failed
|
|
16704
|
+
// so pollers do not spin on a "running" record that will never progress,
|
|
16705
|
+
// mirroring the lazy-reconcile failure path.
|
|
16706
|
+
const orphan = await clipRecords.getScan(customer.id, scanId);
|
|
16707
|
+
if (orphan)
|
|
16708
|
+
await clipRecords.putScan({ ...orphan, status: "failed", error: "Failed to start the scan pipeline.", updated_at: nowIso() });
|
|
16709
|
+
const orphanSource = await clipRecords.getSource(customer.id, sourceVideoId);
|
|
16710
|
+
if (orphanSource && orphanSource.status !== "complete")
|
|
16711
|
+
await clipRecords.putSource({ ...orphanSource, status: "failed", updated_at: nowIso() });
|
|
16712
|
+
throw error;
|
|
16713
|
+
}
|
|
16714
|
+
// Attach the execution ARN for lazy SFN reconciliation (best-effort — the
|
|
16715
|
+
// record is already pollable; spread the current record so a runner that
|
|
16716
|
+
// has already advanced the status is not regressed).
|
|
16717
|
+
const startedScan = await clipRecords.getScan(customer.id, scanId);
|
|
16718
|
+
if (startedScan)
|
|
16719
|
+
await clipRecords.putScan({ ...startedScan, execution_arn: executionArn, updated_at: nowIso() });
|
|
16720
|
+
return scanResponse({ status: "running", executionArn, pipeline: "cloud", estimate, computeEstimate });
|
|
16242
16721
|
}
|
|
16243
16722
|
// ── vidfarm serve (no deployed pipeline): LOCAL-FIRST, cloud as backup ──
|
|
16244
16723
|
// Same policy as `vidfarm clips scan`: the user's local claude/codex CLI
|
|
@@ -16540,6 +17019,11 @@ async function mirrorUpstreamClipsToLocal(input) {
|
|
|
16540
17019
|
if (!feed.ok || !feed.json)
|
|
16541
17020
|
throw new Error(`Upstream clip feed unavailable (HTTP ${feed.status}).`);
|
|
16542
17021
|
let stored = 0;
|
|
17022
|
+
// Upstream /clips/feed strips the embedding vector, so re-embed each mirrored
|
|
17023
|
+
// clip on the owner's BYOK key — otherwise it lands with no embedding and is
|
|
17024
|
+
// permanently invisible to semantic clip search (keyword-only). Fail-soft:
|
|
17025
|
+
// no embed-capable key → store without embedding (keyword-only), as before.
|
|
17026
|
+
const embedder = await buildClipLibraryEmbedder(ownerId);
|
|
16543
17027
|
for (const raw of feed.json.clips ?? []) {
|
|
16544
17028
|
const clipId = typeof raw.clip_id === "string" ? raw.clip_id : null;
|
|
16545
17029
|
const viewUrl = typeof raw.view_url === "string" ? raw.view_url : null;
|
|
@@ -16559,16 +17043,32 @@ async function mirrorUpstreamClipsToLocal(input) {
|
|
|
16559
17043
|
await storage.putBuffer(thumbKey, new Uint8Array(await thumbRes.arrayBuffer()), "image/jpeg");
|
|
16560
17044
|
}
|
|
16561
17045
|
}
|
|
16562
|
-
const { view_url: _v, thumbnail_url: _t, ...rest } = raw;
|
|
17046
|
+
const { view_url: _v, thumbnail_url: _t, embedding: _e, embedding_model: _em, ...rest } = raw;
|
|
16563
17047
|
void _v;
|
|
16564
17048
|
void _t;
|
|
16565
|
-
|
|
17049
|
+
void _e;
|
|
17050
|
+
void _em;
|
|
17051
|
+
const clip = {
|
|
16566
17052
|
...rest,
|
|
16567
17053
|
owner_id: ownerId,
|
|
16568
17054
|
source_video_id: scan.source_video_id,
|
|
16569
17055
|
file_path: clipKey,
|
|
16570
17056
|
thumbnail_path: thumbKey
|
|
16571
|
-
}
|
|
17057
|
+
};
|
|
17058
|
+
if (embedder?.embeddingModelId) {
|
|
17059
|
+
try {
|
|
17060
|
+
const [vector] = await embedder.embedDocuments([buildClipEmbeddingText(clip.tags, clip.description)]);
|
|
17061
|
+
if (vector?.length) {
|
|
17062
|
+
clip.embedding = vector;
|
|
17063
|
+
clip.embedding_model = embedder.embeddingModelId;
|
|
17064
|
+
}
|
|
17065
|
+
}
|
|
17066
|
+
catch (error) {
|
|
17067
|
+
devLog("clip_scan.upstream_mirror_embed_failed", { scan_id: scan.scan_id, clip_id: clipId, ...devErrorFields(error) }, "warn");
|
|
17068
|
+
}
|
|
17069
|
+
}
|
|
17070
|
+
await clipRecords.putClip(clip);
|
|
17071
|
+
await vectorIndex.upsert(ownerId, clip);
|
|
16572
17072
|
stored++;
|
|
16573
17073
|
}
|
|
16574
17074
|
const source = await clipRecords.getSource(ownerId, scan.source_video_id);
|