@mevdragon/vidfarm-devcli 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +37 -0
- package/SKILL.platform.md +6 -1
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +369 -85
- package/dist/src/app.js +630 -140
- package/dist/src/cli.js +529 -12
- package/dist/src/composition-runtime.js +262 -3
- package/dist/src/config.js +13 -0
- 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/editor-chat.js +256 -0
- package/dist/src/frontend/homepage-client.js +50 -1
- package/dist/src/frontend/homepage-shared.js +197 -0
- package/dist/src/frontend/homepage-store.js +10 -1
- package/dist/src/frontend/homepage-view.js +43 -5
- package/dist/src/homepage.js +51 -18
- package/dist/src/hyperframes/composition.js +5 -5
- package/dist/src/services/composition-lint.js +199 -0
- package/dist/src/services/hyperframes.js +67 -13
- package/dist/src/services/serverless-records.js +9 -0
- package/package.json +2 -1
- package/public/assets/homepage-client-app.js +23 -23
package/dist/src/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import { createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
5
|
import { pipeline as streamPipeline } from "node:stream/promises";
|
|
@@ -22,6 +22,7 @@ import { assertForkAccess, resolveForkAccess, ForkAccessDeniedError, ForkNotFoun
|
|
|
22
22
|
import { writeForkManifest } from "./services/fork-manifest.js";
|
|
23
23
|
import { renderHelpPage } from "./help-page.js";
|
|
24
24
|
import { renderHomepage } from "./homepage.js";
|
|
25
|
+
import { POPULAR_INSPIRATION_GROUPS } from "./frontend/homepage-shared.js";
|
|
25
26
|
import { COMPOSITION_RUNTIME_SCRIPT_BODY } from "./composition-runtime.js";
|
|
26
27
|
import { CAPTION_ANIM_CSS, CAPTION_STYLE_MARKER, KEN_BURNS_CSS, KEN_BURNS_STYLE_MARKER, TRANSITION_CSS, TRANSITION_STYLE_MARKER, upgradeLegacyTransitionCss } from "./hyperframes/composition.js";
|
|
27
28
|
import { normalizeCompositionZIndexHtml, sanitizeCompositionHtml } from "./services/composition-sanitize.js";
|
|
@@ -35,7 +36,7 @@ import { resolveRootFileCandidates } from "./lib/package-root.js";
|
|
|
35
36
|
import { stripTrackingParams } from "./lib/url-clean.js";
|
|
36
37
|
import { addSeconds, nowIso } from "./lib/time.js";
|
|
37
38
|
import { templateRegistry } from "./registry.js";
|
|
38
|
-
import { buildEditorChatSystemInstruction, compactEditorChatModelMessages, defaultEditorChatModel, EDITOR_CHAT_PROVIDER_PRIORITY, extractEditorChatCompositionBrief, formatEditorChatCurrentDateTime, toEditorChatModelMessages } from "./editor-chat.js";
|
|
39
|
+
import { buildAsyncJobHandoffText, buildEditorChatGuardrailNotice, buildEditorChatSystemInstruction, collectVerifiedHttpJobIds, compactEditorChatModelMessages, createLoadSkillTool, createStreamingJobIdRedactor, defaultEditorChatModel, EDITOR_CHAT_PROVIDER_PRIORITY, extractEditorChatCompositionBrief, formatEditorChatCurrentDateTime, lintReplaceCompositionAction, readAsyncJobHandoff, toEditorChatModelMessages } from "./editor-chat.js";
|
|
39
40
|
import { apiCallHistoryStore } from "./services/api-call-history.js";
|
|
40
41
|
import { AuthService } from "./services/auth.js";
|
|
41
42
|
import { BillingService } from "./services/billing.js";
|
|
@@ -54,12 +55,12 @@ import { loadSpeechAudioLocally, MAX_LOCAL_TRANSCRIBE_AUDIO_BYTES } from "./devc
|
|
|
54
55
|
import { DescribeExecutionCommand, SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
|
|
55
56
|
import { clipRecords, makeUserPreset } from "./services/clip-records.js";
|
|
56
57
|
import { matchScenesToClips, searchUserClips, vectorIndex } from "./services/clip-search.js";
|
|
57
|
-
import { BUILTIN_PRESETS, buildEffectiveGuidance, ClipModelClient, cosineSimilarity, detectLocalAgent, effectiveHuntDurationSec, estimateScanCostFromDuration, hasFfmpeg, LocalAgentClipClient, normalizeCropFocus, normalizeHuntSpec, parseClipHuntPrompt, probeVideo, resolveDurationBand, scanVideo } from "./services/clip-curation/index.js";
|
|
58
|
+
import { BUILTIN_PRESETS, buildClipEmbeddingText, buildEffectiveGuidance, ClipModelClient, cosineSimilarity, detectLocalAgent, effectiveHuntDurationSec, estimateScanCostFromDuration, hasFfmpeg, LocalAgentClipClient, normalizeCropFocus, normalizeHuntSpec, parseClipHuntPrompt, probeVideo, resolveDurationBand, scanVideo } from "./services/clip-curation/index.js";
|
|
58
59
|
import { analyzeCastMembers, annotateScenesFromSource, buildSmartDecomposedHtml, HyperframesService, smartDecomposeVideo, stampCompositionIdentity } from "./services/hyperframes.js";
|
|
59
60
|
import { buildPendingSceneAnnotationsState } from "./services/scene-annotations.js";
|
|
60
61
|
import { createPrimitiveJobContext } from "./primitive-context.js";
|
|
61
62
|
import { buildPendingCastState, castMemberFromRaw, extractCastStill, isolateCastMember } from "./services/cast.js";
|
|
62
|
-
import { MediaDurationExceededError, probeMediaAsset } from "./services/media-processing.js";
|
|
63
|
+
import { MediaDurationExceededError, probeMediaAsset, trimVideoAsset } from "./services/media-processing.js";
|
|
63
64
|
import { estimateGhostcutCostUsd, isGhostcutConfigured, mirrorGhostcutVideoToStorage, pollStatus as pollGhostcutStatus, submitSubtitleRemoval } from "./services/ghostcut.js";
|
|
64
65
|
import { hasUpstreamKey, isUpstreamEnabled, proxyRequestToUpstream, seedFromUpstream, upstreamHost, upstreamJson } from "./services/upstream.js";
|
|
65
66
|
import { TemplateSourceService } from "./services/template-sources.js";
|
|
@@ -3609,7 +3610,12 @@ function normalizeEditorChatPath(input) {
|
|
|
3609
3610
|
// and NEVER runs GhostCut on the long-form source — "no text" is a
|
|
3610
3611
|
// scene-selection filter; caption removal stays a per-finished-clip
|
|
3611
3612
|
// primitive (/api/v1/primitives/videos/remove-captions).
|
|
3612
|
-
CLIPS_PREFIX
|
|
3613
|
+
CLIPS_PREFIX,
|
|
3614
|
+
// Public skill knowledge: the vendored skill packs (load_skill fetches
|
|
3615
|
+
// these too) and per-template SKILL.md manifests. Read-only markdown.
|
|
3616
|
+
"/skill/",
|
|
3617
|
+
"/skill-pack/",
|
|
3618
|
+
"/discover/skills/"
|
|
3613
3619
|
];
|
|
3614
3620
|
const parsed = trimmed.startsWith("http://") || trimmed.startsWith("https://")
|
|
3615
3621
|
? new URL(trimmed)
|
|
@@ -4463,95 +4469,14 @@ function createEditorActionTool() {
|
|
|
4463
4469
|
explanation: z.string().describe("One-sentence reason shown in the UI.")
|
|
4464
4470
|
}),
|
|
4465
4471
|
execute: async (args) => {
|
|
4472
|
+
const linted = lintReplaceCompositionAction(args);
|
|
4473
|
+
if (linted) {
|
|
4474
|
+
return linted;
|
|
4475
|
+
}
|
|
4466
4476
|
return { ...args };
|
|
4467
4477
|
}
|
|
4468
4478
|
});
|
|
4469
4479
|
}
|
|
4470
|
-
const JOB_ID_PATTERN = /\bjob_[a-z0-9]+\b/gi;
|
|
4471
|
-
const HTTP_TOOL_CLAIM_PATTERN = /\b(?:http_request|REST API call|API call|GET\s+https?:\/\/|POST\s+https?:\/\/|GET\s+\/api\/|POST\s+\/api\/)/i;
|
|
4472
|
-
function collectJobIdsFromUnknown(value, output = new Set()) {
|
|
4473
|
-
if (typeof value === "string") {
|
|
4474
|
-
for (const match of value.matchAll(JOB_ID_PATTERN)) {
|
|
4475
|
-
output.add(match[0]);
|
|
4476
|
-
}
|
|
4477
|
-
return output;
|
|
4478
|
-
}
|
|
4479
|
-
if (Array.isArray(value)) {
|
|
4480
|
-
for (const entry of value) {
|
|
4481
|
-
collectJobIdsFromUnknown(entry, output);
|
|
4482
|
-
}
|
|
4483
|
-
return output;
|
|
4484
|
-
}
|
|
4485
|
-
if (value && typeof value === "object") {
|
|
4486
|
-
for (const entry of Object.values(value)) {
|
|
4487
|
-
collectJobIdsFromUnknown(entry, output);
|
|
4488
|
-
}
|
|
4489
|
-
}
|
|
4490
|
-
return output;
|
|
4491
|
-
}
|
|
4492
|
-
function collectVerifiedHttpJobIds(toolResult) {
|
|
4493
|
-
if (!toolResult || typeof toolResult !== "object") {
|
|
4494
|
-
return new Set();
|
|
4495
|
-
}
|
|
4496
|
-
const record = toolResult;
|
|
4497
|
-
return collectJobIdsFromUnknown(record.response?.body ?? record.result?.body);
|
|
4498
|
-
}
|
|
4499
|
-
function readAsyncJobHandoff(output) {
|
|
4500
|
-
if (!output || typeof output !== "object") {
|
|
4501
|
-
return null;
|
|
4502
|
-
}
|
|
4503
|
-
const record = output;
|
|
4504
|
-
const body = record.response?.body;
|
|
4505
|
-
if (!body || typeof body !== "object") {
|
|
4506
|
-
return null;
|
|
4507
|
-
}
|
|
4508
|
-
const job = body;
|
|
4509
|
-
if (typeof job.job_id !== "string" || typeof job.status !== "string") {
|
|
4510
|
-
return null;
|
|
4511
|
-
}
|
|
4512
|
-
if (job.status === "succeeded" || job.status === "failed" || job.status === "cancelled") {
|
|
4513
|
-
return null;
|
|
4514
|
-
}
|
|
4515
|
-
return {
|
|
4516
|
-
jobId: job.job_id,
|
|
4517
|
-
tracer: typeof job.tracer === "string" ? job.tracer : null,
|
|
4518
|
-
status: job.status,
|
|
4519
|
-
isNewQueuedJob: record.request?.method === "POST" && record.response?.status === 202
|
|
4520
|
-
};
|
|
4521
|
-
}
|
|
4522
|
-
function buildAsyncJobHandoffText(input) {
|
|
4523
|
-
if (!input.isNewQueuedJob) {
|
|
4524
|
-
return `Render \`${input.jobId}\` is ${input.status}.`;
|
|
4525
|
-
}
|
|
4526
|
-
return [
|
|
4527
|
-
`Started render \`${input.jobId}\`${input.tracer ? ` with tracer \`${input.tracer}\`` : ""}.`,
|
|
4528
|
-
"If it takes a while, ask for an update later and I can check it for you."
|
|
4529
|
-
].join("\n\n");
|
|
4530
|
-
}
|
|
4531
|
-
function buildEditorChatGuardrailNotice(input) {
|
|
4532
|
-
const mentionedJobIds = collectJobIdsFromUnknown(input.assistantText);
|
|
4533
|
-
const unverifiedJobIds = [...mentionedJobIds].filter((jobId) => !input.verifiedJobIds.has(jobId));
|
|
4534
|
-
const notices = [];
|
|
4535
|
-
if (input.redactedJobIds.size) {
|
|
4536
|
-
notices.push("Guardrail: I withheld unverified job ID text because it did not come from a visible HTTP response card.");
|
|
4537
|
-
}
|
|
4538
|
-
else if (unverifiedJobIds.length) {
|
|
4539
|
-
notices.push("Guardrail: I mentioned an unverified job ID. Ignore it unless it appears in a visible HTTP response card.");
|
|
4540
|
-
}
|
|
4541
|
-
if (!input.hasHttpRequestResult && HTTP_TOOL_CLAIM_PATTERN.test(input.assistantText)) {
|
|
4542
|
-
notices.push("Guardrail: no `http_request` tool result was recorded for this response, so any claimed REST/API call should be treated as not executed.");
|
|
4543
|
-
}
|
|
4544
|
-
return notices.length ? `\n\n${notices.join("\n\n")}` : "";
|
|
4545
|
-
}
|
|
4546
|
-
function redactUnverifiedJobIds(input) {
|
|
4547
|
-
return input.text.replace(JOB_ID_PATTERN, (jobId) => {
|
|
4548
|
-
if (input.verifiedJobIds.has(jobId)) {
|
|
4549
|
-
return jobId;
|
|
4550
|
-
}
|
|
4551
|
-
input.redactedJobIds.add(jobId);
|
|
4552
|
-
return "[unverified job id]";
|
|
4553
|
-
});
|
|
4554
|
-
}
|
|
4555
4480
|
function normalizeHistoryAttachments(attachments) {
|
|
4556
4481
|
return Array.isArray(attachments)
|
|
4557
4482
|
? attachments.map((attachment) => ({
|
|
@@ -4595,6 +4520,10 @@ async function streamEditorChatAgent(input, send) {
|
|
|
4595
4520
|
vidfarmApiKey: input.vidfarmApiKey,
|
|
4596
4521
|
abortSignal: input.abortSignal
|
|
4597
4522
|
}),
|
|
4523
|
+
load_skill: createLoadSkillTool({
|
|
4524
|
+
publicBaseUrl: config.PUBLIC_BASE_URL,
|
|
4525
|
+
abortSignal: input.abortSignal
|
|
4526
|
+
}),
|
|
4598
4527
|
frontend_action: createEditorFrontendActionTool({
|
|
4599
4528
|
tracerRegistry
|
|
4600
4529
|
}),
|
|
@@ -4621,6 +4550,7 @@ async function streamEditorChatAgent(input, send) {
|
|
|
4621
4550
|
let hasHttpRequestResult = false;
|
|
4622
4551
|
const verifiedJobIds = new Set();
|
|
4623
4552
|
const redactedJobIds = new Set();
|
|
4553
|
+
const jobIdRedactor = createStreamingJobIdRedactor({ verifiedJobIds, redactedJobIds });
|
|
4624
4554
|
const toolExchangeOrder = [];
|
|
4625
4555
|
const toolExchangeMap = new Map();
|
|
4626
4556
|
const httpExchanges = [];
|
|
@@ -4635,16 +4565,14 @@ async function streamEditorChatAgent(input, send) {
|
|
|
4635
4565
|
case "text-delta":
|
|
4636
4566
|
assistantTextForAudit += part.text;
|
|
4637
4567
|
{
|
|
4638
|
-
const safeText =
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
delta: safeText
|
|
4647
|
-
});
|
|
4568
|
+
const safeText = jobIdRedactor.push(part.text);
|
|
4569
|
+
if (safeText) {
|
|
4570
|
+
assistantTextForUser += safeText;
|
|
4571
|
+
send({
|
|
4572
|
+
type: "text-delta",
|
|
4573
|
+
delta: safeText
|
|
4574
|
+
});
|
|
4575
|
+
}
|
|
4648
4576
|
}
|
|
4649
4577
|
break;
|
|
4650
4578
|
case "tool-call":
|
|
@@ -4738,6 +4666,14 @@ async function streamEditorChatAgent(input, send) {
|
|
|
4738
4666
|
break;
|
|
4739
4667
|
case "finish":
|
|
4740
4668
|
{
|
|
4669
|
+
const heldText = jobIdRedactor.flush();
|
|
4670
|
+
if (heldText) {
|
|
4671
|
+
assistantTextForUser += heldText;
|
|
4672
|
+
send({
|
|
4673
|
+
type: "text-delta",
|
|
4674
|
+
delta: heldText
|
|
4675
|
+
});
|
|
4676
|
+
}
|
|
4741
4677
|
const guardrailNotice = buildEditorChatGuardrailNotice({
|
|
4742
4678
|
assistantText: assistantTextForAudit,
|
|
4743
4679
|
verifiedJobIds,
|
|
@@ -5002,6 +4938,28 @@ app.get("/editor/:templateId", async (c) => {
|
|
|
5002
4938
|
if (!forkRecord || forkRecord.deletedAt) {
|
|
5003
4939
|
effectiveForkParam = null;
|
|
5004
4940
|
}
|
|
4941
|
+
else if (customer) {
|
|
4942
|
+
// A caller who can VIEW but not EDIT this fork (e.g. a public template's
|
|
4943
|
+
// default fork opened via a shared deep link) must not be pinned onto it:
|
|
4944
|
+
// the editor would happily let them "edit", but every composition.html PUT
|
|
4945
|
+
// is 403'd and the change silently reverts on reload — the exact "the AI
|
|
4946
|
+
// said it changed the text but it didn't" failure. Drop the fork and fall
|
|
4947
|
+
// through to per-user resolution so they get (or mint) their own editable
|
|
4948
|
+
// copy instead.
|
|
4949
|
+
try {
|
|
4950
|
+
const access = await resolveForkAccess({
|
|
4951
|
+
forkId: effectiveForkParam,
|
|
4952
|
+
customerId: customer.id,
|
|
4953
|
+
shareToken: readShareToken(c) ?? undefined
|
|
4954
|
+
});
|
|
4955
|
+
if (!access.capabilities.canEdit) {
|
|
4956
|
+
effectiveForkParam = null;
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
catch {
|
|
4960
|
+
effectiveForkParam = null;
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
5005
4963
|
}
|
|
5006
4964
|
if (!effectiveForkParam) {
|
|
5007
4965
|
// Per-user default fork resolution: prefer caller's most-recently-updated
|
|
@@ -5018,8 +4976,14 @@ app.get("/editor/:templateId", async (c) => {
|
|
|
5018
4976
|
if (mostRecent)
|
|
5019
4977
|
targetForkId = mostRecent.id;
|
|
5020
4978
|
}
|
|
5021
|
-
|
|
4979
|
+
// Only fall back to the template's canonical default fork when the caller can
|
|
4980
|
+
// actually edit it — i.e. they own the template. A non-owner sent to the
|
|
4981
|
+
// default fork would land back on a read-only fork (and, since it's also the
|
|
4982
|
+
// page we just dropped, loop). Leaving targetForkId null routes them to the
|
|
4983
|
+
// bare editor, where the client mints their own editable fork on first save.
|
|
4984
|
+
if (!targetForkId && template.customerId === customer?.id) {
|
|
5022
4985
|
targetForkId = template.defaultForkId;
|
|
4986
|
+
}
|
|
5023
4987
|
if (targetForkId) {
|
|
5024
4988
|
// Do NOT auto-attach a thread_id here: the caller's browser is the
|
|
5025
4989
|
// source of truth for "which chat is active" via localStorage. Injecting
|
|
@@ -5127,6 +5091,11 @@ app.get("/discover/feed", async (c) => {
|
|
|
5127
5091
|
}
|
|
5128
5092
|
const templates = await serverlessRecords.listTemplatesFeed({ limit, query });
|
|
5129
5093
|
for (const template of templates) {
|
|
5094
|
+
// Curated "Popular" picks live only under the Popular toggle
|
|
5095
|
+
// (GET /discover/popular) — keep them out of the continuously-grown Feed
|
|
5096
|
+
// so the two views stay distinct.
|
|
5097
|
+
if (typeof template.popularGroup === "string" && template.popularGroup.trim().length > 0)
|
|
5098
|
+
continue;
|
|
5130
5099
|
const entry = buildTemplateHomepageEntry(template);
|
|
5131
5100
|
if (entry)
|
|
5132
5101
|
templateEntries.push(entry);
|
|
@@ -5143,7 +5112,7 @@ app.get("/discover/feed", async (c) => {
|
|
|
5143
5112
|
const upstreamFeed = (await upstreamJson(`/discover/feed?${search.toString()}`)).json?.templates ?? [];
|
|
5144
5113
|
const seen = new Set(templateEntries.map((entry) => entry.templateId));
|
|
5145
5114
|
for (const entry of upstreamFeed) {
|
|
5146
|
-
if (entry && typeof entry.templateId === "string" && !seen.has(entry.templateId)) {
|
|
5115
|
+
if (entry && typeof entry.templateId === "string" && !seen.has(entry.templateId) && !entry.popularGroup) {
|
|
5147
5116
|
templateEntries.push(entry);
|
|
5148
5117
|
seen.add(entry.templateId);
|
|
5149
5118
|
}
|
|
@@ -5158,16 +5127,60 @@ app.get("/discover/feed", async (c) => {
|
|
|
5158
5127
|
next_cursor: null
|
|
5159
5128
|
});
|
|
5160
5129
|
});
|
|
5130
|
+
// Curated "Popular" feed: the editorial list surfaced under the /discover
|
|
5131
|
+
// Popular toggle. Same template records as Feed, just the ones flagged with a
|
|
5132
|
+
// popularGroup — rendered client-side as autoplay TemplateCards grouped by that
|
|
5133
|
+
// label. Public + identical for everyone, so it caches shared. Mirrors upstream
|
|
5134
|
+
// (vidfarm serve) exactly like /discover/feed so a locally-empty box still sees
|
|
5135
|
+
// the cloud-seeded picks. Seed with POST /api/v1/admin/popular/seed.
|
|
5136
|
+
app.get("/discover/popular", async (c) => {
|
|
5137
|
+
const templateEntries = [];
|
|
5138
|
+
const seen = new Set();
|
|
5139
|
+
const popular = await serverlessRecords.listPopularTemplates();
|
|
5140
|
+
for (const template of popular) {
|
|
5141
|
+
const entry = buildTemplateHomepageEntry(template);
|
|
5142
|
+
if (entry) {
|
|
5143
|
+
templateEntries.push(entry);
|
|
5144
|
+
seen.add(entry.templateId);
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
if (isUpstreamEnabled()) {
|
|
5148
|
+
const upstream = (await upstreamJson(`/discover/popular`)).json?.templates ?? [];
|
|
5149
|
+
for (const entry of upstream) {
|
|
5150
|
+
if (entry && typeof entry.templateId === "string" && !seen.has(entry.templateId) && entry.popularGroup) {
|
|
5151
|
+
templateEntries.push(entry);
|
|
5152
|
+
seen.add(entry.templateId);
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
// Stable editorial order: popularRank is a global sequence assigned by the
|
|
5157
|
+
// seed (group index * 1000 + item index), so a plain rank sort preserves both
|
|
5158
|
+
// group order and within-group order. Group label is only a tiebreak.
|
|
5159
|
+
templateEntries.sort((a, b) => {
|
|
5160
|
+
const rankCompare = (a.popularRank ?? 0) - (b.popularRank ?? 0);
|
|
5161
|
+
if (rankCompare !== 0)
|
|
5162
|
+
return rankCompare;
|
|
5163
|
+
return String(a.popularGroup ?? "").localeCompare(String(b.popularGroup ?? ""));
|
|
5164
|
+
});
|
|
5165
|
+
c.header("cache-control", "public, max-age=60");
|
|
5166
|
+
return c.json({
|
|
5167
|
+
templates: templateEntries,
|
|
5168
|
+
next_cursor: null
|
|
5169
|
+
});
|
|
5170
|
+
});
|
|
5161
5171
|
function buildTemplateHomepageEntry(template) {
|
|
5162
5172
|
// Undecomposed templates (no defaultForkId yet) still appear on /discover so
|
|
5163
5173
|
// users can open them and mint the first fork via the Decompose modal.
|
|
5164
5174
|
if (!template.videoUrl)
|
|
5165
5175
|
return null;
|
|
5166
5176
|
const isPrivate = template.visibility === "private";
|
|
5177
|
+
const isPopular = typeof template.popularGroup === "string" && template.popularGroup.trim().length > 0;
|
|
5167
5178
|
// Older records stored the source host ("youtube.com") as a title fallback —
|
|
5168
5179
|
// never show that as a card title; the template id is the canonical default.
|
|
5169
5180
|
const storedTitle = template.title && template.title !== template.sourceHost ? template.title : null;
|
|
5170
|
-
|
|
5181
|
+
// Public feed cards default to the template id, but curated Popular picks show
|
|
5182
|
+
// their editorial title (the whole point of the Popular list).
|
|
5183
|
+
const title = (isPrivate || isPopular) ? (storedTitle || template.id) : template.id;
|
|
5171
5184
|
const previewUrl = template.thumbnailUrl || template.videoUrl || null;
|
|
5172
5185
|
return {
|
|
5173
5186
|
title,
|
|
@@ -5187,6 +5200,8 @@ function buildTemplateHomepageEntry(template) {
|
|
|
5187
5200
|
summary: template.searchSummary ?? null,
|
|
5188
5201
|
visibility: template.visibility,
|
|
5189
5202
|
origin: template.origin ?? "inspiration",
|
|
5203
|
+
popularGroup: template.popularGroup ?? null,
|
|
5204
|
+
popularRank: template.popularRank ?? null,
|
|
5190
5205
|
notes: template.notes,
|
|
5191
5206
|
status: "ready"
|
|
5192
5207
|
};
|
|
@@ -5554,6 +5569,8 @@ function buildPendingInspirationHomepageEntry(inspiration) {
|
|
|
5554
5569
|
durationSeconds: inspiration.durationSeconds ?? null,
|
|
5555
5570
|
visibility: "private",
|
|
5556
5571
|
origin: inspiration.origin ?? "inspiration",
|
|
5572
|
+
popularGroup: inspiration.popularGroup ?? null,
|
|
5573
|
+
popularRank: inspiration.popularRank ?? null,
|
|
5557
5574
|
notes: inspiration.notes,
|
|
5558
5575
|
status: inspiration.status === "failed" ? "failed" : "processing"
|
|
5559
5576
|
};
|
|
@@ -5677,21 +5694,193 @@ const PUBLIC_SKILL_FILES = {
|
|
|
5677
5694
|
"vidfarm-director": "SKILL.director.md",
|
|
5678
5695
|
"vidfarm-platform": "SKILL.platform.md"
|
|
5679
5696
|
};
|
|
5697
|
+
// Vendored skill packs (the heygen-scrubbed hyperframes pack + vidfarm-media)
|
|
5698
|
+
// mirrored from .agents/skills. Vidfarm is the distribution origin: the devcli
|
|
5699
|
+
// (`vidfarm skills add`) and the chat copilot's load_skill tool both pull from
|
|
5700
|
+
// these routes, so customers never fetch skills from an upstream vendor org.
|
|
5701
|
+
const SKILL_PACK_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,64}$/;
|
|
5702
|
+
const SKILL_PACK_IGNORED_ENTRIES = new Set([".DS_Store", "node_modules", ".git"]);
|
|
5703
|
+
function resolveSkillPackDir(name) {
|
|
5704
|
+
if (!SKILL_PACK_NAME_PATTERN.test(name)) {
|
|
5705
|
+
return null;
|
|
5706
|
+
}
|
|
5707
|
+
for (const candidate of resolveRootFileCandidates(path.join(".agents", "skills", name))) {
|
|
5708
|
+
try {
|
|
5709
|
+
if (statSync(candidate).isDirectory()) {
|
|
5710
|
+
return candidate;
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
catch {
|
|
5714
|
+
continue;
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
return null;
|
|
5718
|
+
}
|
|
5719
|
+
function listSkillPackFiles(dir, prefix = "") {
|
|
5720
|
+
const files = [];
|
|
5721
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
5722
|
+
if (SKILL_PACK_IGNORED_ENTRIES.has(entry.name)) {
|
|
5723
|
+
continue;
|
|
5724
|
+
}
|
|
5725
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
5726
|
+
const absolute = path.join(dir, entry.name);
|
|
5727
|
+
if (entry.isDirectory()) {
|
|
5728
|
+
files.push(...listSkillPackFiles(absolute, relative));
|
|
5729
|
+
}
|
|
5730
|
+
else if (entry.isFile()) {
|
|
5731
|
+
files.push(relative);
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
return files.sort();
|
|
5735
|
+
}
|
|
5736
|
+
function readSkillPackDescription(dir) {
|
|
5737
|
+
try {
|
|
5738
|
+
const skillMd = readFileSync(path.join(dir, "SKILL.md"), "utf8");
|
|
5739
|
+
const match = skillMd.match(/^description:\s*(.+)$/m);
|
|
5740
|
+
return match ? match[1].trim().slice(0, 300) : "";
|
|
5741
|
+
}
|
|
5742
|
+
catch {
|
|
5743
|
+
return "";
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5680
5746
|
app.get("/skill/:name", (c) => {
|
|
5681
5747
|
const name = c.req.param("name").replace(/\.md$/i, "");
|
|
5682
5748
|
const filename = PUBLIC_SKILL_FILES[name];
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5749
|
+
const contents = filename
|
|
5750
|
+
? readRootSkillFile(filename)
|
|
5751
|
+
: (() => {
|
|
5752
|
+
const packDir = resolveSkillPackDir(name);
|
|
5753
|
+
if (!packDir) {
|
|
5754
|
+
return "";
|
|
5755
|
+
}
|
|
5756
|
+
try {
|
|
5757
|
+
return readFileSync(path.join(packDir, "SKILL.md"), "utf8");
|
|
5758
|
+
}
|
|
5759
|
+
catch {
|
|
5760
|
+
return "";
|
|
5761
|
+
}
|
|
5762
|
+
})();
|
|
5687
5763
|
if (!contents) {
|
|
5688
|
-
return c.text("Skill file unavailable", 404);
|
|
5764
|
+
return c.text(filename ? "Skill file unavailable" : "Unknown skill", 404);
|
|
5689
5765
|
}
|
|
5690
5766
|
return c.body(contents, 200, {
|
|
5691
5767
|
"content-type": "text/markdown; charset=utf-8",
|
|
5692
5768
|
"cache-control": "no-cache"
|
|
5693
5769
|
});
|
|
5694
5770
|
});
|
|
5771
|
+
app.get("/skill-pack/index.json", (c) => {
|
|
5772
|
+
const skills = [];
|
|
5773
|
+
for (const candidate of resolveRootFileCandidates(path.join(".agents", "skills"))) {
|
|
5774
|
+
let entries;
|
|
5775
|
+
try {
|
|
5776
|
+
entries = readdirSync(candidate);
|
|
5777
|
+
}
|
|
5778
|
+
catch {
|
|
5779
|
+
continue;
|
|
5780
|
+
}
|
|
5781
|
+
for (const entry of entries.sort()) {
|
|
5782
|
+
if (!SKILL_PACK_NAME_PATTERN.test(entry) || skills.some((skill) => skill.name === entry)) {
|
|
5783
|
+
continue;
|
|
5784
|
+
}
|
|
5785
|
+
const dir = path.join(candidate, entry);
|
|
5786
|
+
try {
|
|
5787
|
+
if (!statSync(dir).isDirectory()) {
|
|
5788
|
+
continue;
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
catch {
|
|
5792
|
+
continue;
|
|
5793
|
+
}
|
|
5794
|
+
skills.push({ name: entry, description: readSkillPackDescription(dir) });
|
|
5795
|
+
}
|
|
5796
|
+
break;
|
|
5797
|
+
}
|
|
5798
|
+
return c.json({ skills });
|
|
5799
|
+
});
|
|
5800
|
+
// Manifest hashing reads every file in a pack (hyperframes-animation alone is
|
|
5801
|
+
// ~100 files) — memoize per pack dir, invalidated by a cheap stat signature,
|
|
5802
|
+
// so `vidfarm skills update` sweeps don't re-hash 5MB per request.
|
|
5803
|
+
const skillPackManifestCache = new Map();
|
|
5804
|
+
app.get("/skill-pack/:name/manifest.json", (c) => {
|
|
5805
|
+
const packDir = resolveSkillPackDir(c.req.param("name"));
|
|
5806
|
+
if (!packDir) {
|
|
5807
|
+
return c.json({ error: "Unknown skill pack." }, 404);
|
|
5808
|
+
}
|
|
5809
|
+
const relativePaths = listSkillPackFiles(packDir);
|
|
5810
|
+
const signature = relativePaths
|
|
5811
|
+
.map((relative) => {
|
|
5812
|
+
try {
|
|
5813
|
+
const stats = statSync(path.join(packDir, relative));
|
|
5814
|
+
return `${relative}:${stats.size}:${stats.mtimeMs}`;
|
|
5815
|
+
}
|
|
5816
|
+
catch {
|
|
5817
|
+
return `${relative}:missing`;
|
|
5818
|
+
}
|
|
5819
|
+
})
|
|
5820
|
+
.join("|");
|
|
5821
|
+
const cached = skillPackManifestCache.get(packDir);
|
|
5822
|
+
if (cached && cached.signature === signature) {
|
|
5823
|
+
return c.json(cached.manifest);
|
|
5824
|
+
}
|
|
5825
|
+
const files = relativePaths.map((relative) => {
|
|
5826
|
+
const bytes = readFileSync(path.join(packDir, relative));
|
|
5827
|
+
return {
|
|
5828
|
+
path: relative,
|
|
5829
|
+
size: bytes.length,
|
|
5830
|
+
sha256: createHash("sha256").update(bytes).digest("hex")
|
|
5831
|
+
};
|
|
5832
|
+
});
|
|
5833
|
+
const manifest = { name: path.basename(packDir), files };
|
|
5834
|
+
skillPackManifestCache.set(packDir, { signature, manifest });
|
|
5835
|
+
return c.json(manifest);
|
|
5836
|
+
});
|
|
5837
|
+
app.get("/skill-pack/:name/files/*", (c) => {
|
|
5838
|
+
const packDir = resolveSkillPackDir(c.req.param("name"));
|
|
5839
|
+
if (!packDir) {
|
|
5840
|
+
return c.text("Unknown skill pack.", 404);
|
|
5841
|
+
}
|
|
5842
|
+
const marker = "/files/";
|
|
5843
|
+
const rawPath = c.req.path.slice(c.req.path.indexOf(marker) + marker.length);
|
|
5844
|
+
let relative;
|
|
5845
|
+
try {
|
|
5846
|
+
relative = rawPath
|
|
5847
|
+
.split("/")
|
|
5848
|
+
.map((segment) => decodeURIComponent(segment))
|
|
5849
|
+
.join("/");
|
|
5850
|
+
}
|
|
5851
|
+
catch {
|
|
5852
|
+
return c.text("Bad file path.", 400);
|
|
5853
|
+
}
|
|
5854
|
+
const resolved = path.resolve(packDir, relative);
|
|
5855
|
+
if (resolved !== packDir && !resolved.startsWith(packDir + path.sep)) {
|
|
5856
|
+
return c.text("Bad file path.", 400);
|
|
5857
|
+
}
|
|
5858
|
+
let bytes;
|
|
5859
|
+
try {
|
|
5860
|
+
bytes = readFileSync(resolved);
|
|
5861
|
+
}
|
|
5862
|
+
catch {
|
|
5863
|
+
return c.text("File not found.", 404);
|
|
5864
|
+
}
|
|
5865
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
5866
|
+
const contentType = ext === ".md" || ext === ".markdown" ? "text/markdown; charset=utf-8"
|
|
5867
|
+
: ext === ".json" ? "application/json; charset=utf-8"
|
|
5868
|
+
: ext === ".mjs" || ext === ".cjs" || ext === ".js" ? "text/javascript; charset=utf-8"
|
|
5869
|
+
: ext === ".html" ? "text/html; charset=utf-8"
|
|
5870
|
+
: ext === ".css" ? "text/css; charset=utf-8"
|
|
5871
|
+
: ext === ".txt" || ext === ".srt" || ext === ".vtt" || ext === ".sh" || ext === ".yaml" || ext === ".yml" ? "text/plain; charset=utf-8"
|
|
5872
|
+
: ext === ".png" ? "image/png"
|
|
5873
|
+
: ext === ".avif" ? "image/avif"
|
|
5874
|
+
: ext === ".webp" ? "image/webp"
|
|
5875
|
+
: ext === ".jpg" || ext === ".jpeg" ? "image/jpeg"
|
|
5876
|
+
: ext === ".mp3" ? "audio/mpeg"
|
|
5877
|
+
: ext === ".woff2" ? "font/woff2"
|
|
5878
|
+
: "application/octet-stream";
|
|
5879
|
+
return c.body(new Uint8Array(bytes), 200, {
|
|
5880
|
+
"content-type": contentType,
|
|
5881
|
+
"cache-control": "no-cache"
|
|
5882
|
+
});
|
|
5883
|
+
});
|
|
5695
5884
|
if (config.SENTRY_DEBUG_ROUTE === "true") {
|
|
5696
5885
|
app.get("/debug-sentry", () => {
|
|
5697
5886
|
Sentry.logger.info("User triggered test error", {
|
|
@@ -6601,7 +6790,13 @@ app.post(`${COMPOSITIONS_PREFIX}/:forkId/captions`, async (c) => {
|
|
|
6601
6790
|
const text = str("text");
|
|
6602
6791
|
if (text) {
|
|
6603
6792
|
const windowStart = Math.max(0, num("start") ?? 0);
|
|
6604
|
-
const
|
|
6793
|
+
const availableWindow = compositionDuration - windowStart;
|
|
6794
|
+
// When the composition declares no usable window (missing data-duration,
|
|
6795
|
+
// or windowStart at/after the end), fall back to a word-count estimate
|
|
6796
|
+
// (~0.32s/word, same as the /audio/captions primitive) instead of
|
|
6797
|
+
// cramming the whole script into a 0.4s window of overlapping cues.
|
|
6798
|
+
const estimatedWindow = Math.max(0.4, text.trim().split(/\s+/).filter(Boolean).length * 0.32);
|
|
6799
|
+
const windowDuration = num("duration") ?? (availableWindow > 0.4 ? availableWindow : estimatedWindow);
|
|
6605
6800
|
cues = cuesFromText(text, windowStart + offsetSec, windowDuration, { maxWordsPerCue: maxWords });
|
|
6606
6801
|
sourceNote = "text";
|
|
6607
6802
|
}
|
|
@@ -7018,6 +7213,13 @@ app.post(`${INSPIRATIONS_PREFIX}/:inspirationId/decompose`, async (c) => {
|
|
|
7018
7213
|
// so a forced re-run may start. Must exceed the longest real decompose but stay
|
|
7019
7214
|
// under the API lambda's 15-min timeout.
|
|
7020
7215
|
const DECOMPOSE_PROCESSING_STALE_MS = 10 * 60 * 1000;
|
|
7216
|
+
// Inspiration ingest + auto-decompose no longer hard-cap the source at 120s.
|
|
7217
|
+
// Users can bring longer inspiration; an operator can still re-impose a ceiling
|
|
7218
|
+
// via INSPIRATION_MAX_DURATION_SEC (>0). 0 → unlimited, translated to Infinity so
|
|
7219
|
+
// probeMediaAsset never rejects a source purely on length.
|
|
7220
|
+
const INSPIRATION_PROBE_MAX_DURATION_SECONDS = config.INSPIRATION_MAX_DURATION_SEC > 0
|
|
7221
|
+
? config.INSPIRATION_MAX_DURATION_SEC
|
|
7222
|
+
: Number.POSITIVE_INFINITY;
|
|
7021
7223
|
async function readDecomposeJobState(forkId) {
|
|
7022
7224
|
const text = await storage.readText(storage.compositionForkWorkingKey(forkId, "decompose.json"));
|
|
7023
7225
|
if (!text)
|
|
@@ -7137,7 +7339,10 @@ app.post(`${COMPOSITIONS_PREFIX}/:forkId/auto-decompose`, async (c) => {
|
|
|
7137
7339
|
let probedDurationSeconds = null;
|
|
7138
7340
|
if (sourceUrl) {
|
|
7139
7341
|
try {
|
|
7140
|
-
const probe = await probeMediaAsset({
|
|
7342
|
+
const probe = await probeMediaAsset({
|
|
7343
|
+
sourceUrl,
|
|
7344
|
+
maxDurationSeconds: INSPIRATION_PROBE_MAX_DURATION_SECONDS
|
|
7345
|
+
});
|
|
7141
7346
|
const probed = probe.metadata.durationSeconds;
|
|
7142
7347
|
if (Number.isFinite(probed) && probed && probed > 0) {
|
|
7143
7348
|
probedDurationSeconds = probed;
|
|
@@ -7185,7 +7390,17 @@ app.post(`${COMPOSITIONS_PREFIX}/:forkId/auto-decompose`, async (c) => {
|
|
|
7185
7390
|
ghostcutSkipReason = "ghostcut_not_configured";
|
|
7186
7391
|
console.warn(`ghostcut: skipping subtitle removal for fork ${fork.id} — GHOSTCUT_KEY/GHOSTCUT_SECRET not configured. Composition will keep the original video with baked-in text.`);
|
|
7187
7392
|
}
|
|
7188
|
-
if (
|
|
7393
|
+
else if (config.GHOSTCUT_DECOMPOSE_MAX_DURATION_SEC > 0 &&
|
|
7394
|
+
probedDurationSeconds !== null &&
|
|
7395
|
+
probedDurationSeconds > config.GHOSTCUT_DECOMPOSE_MAX_DURATION_SEC) {
|
|
7396
|
+
// GhostCut is per-30s-chunk and meant for short clips. Long inspiration is
|
|
7397
|
+
// now allowed through ingest/decompose, but laundering a multi-minute video
|
|
7398
|
+
// through GhostCut is disproportionately expensive — skip it and keep the
|
|
7399
|
+
// original burned-in captions instead.
|
|
7400
|
+
ghostcutSkipReason = `source_too_long:${probedDurationSeconds.toFixed(1)}s>${config.GHOSTCUT_DECOMPOSE_MAX_DURATION_SEC}s`;
|
|
7401
|
+
console.warn(`ghostcut: skipping subtitle removal for fork ${fork.id} — source is ${probedDurationSeconds.toFixed(1)}s, over the ${config.GHOSTCUT_DECOMPOSE_MAX_DURATION_SEC}s decompose ceiling. Composition keeps the original burned-in captions.`);
|
|
7402
|
+
}
|
|
7403
|
+
if (sourceUrl && sourceIsVideo && isGhostcutConfigured() && !ghostcutSkipReason) {
|
|
7189
7404
|
try {
|
|
7190
7405
|
const submission = await submitSubtitleRemoval(sourceUrl);
|
|
7191
7406
|
ghostcutTaskId = submission.taskId;
|
|
@@ -9005,6 +9220,8 @@ function serializeInspiration(inspiration) {
|
|
|
9005
9220
|
summary: inspiration.searchSummary ?? null,
|
|
9006
9221
|
visibility: inspiration.visibility ?? "public",
|
|
9007
9222
|
origin: inspiration.origin ?? "inspiration",
|
|
9223
|
+
popular_group: inspiration.popularGroup ?? null,
|
|
9224
|
+
popular_rank: inspiration.popularRank ?? null,
|
|
9008
9225
|
notes: inspiration.notes ?? null,
|
|
9009
9226
|
ingester_customer_id: inspiration.customerId,
|
|
9010
9227
|
error: inspiration.errorMessage,
|
|
@@ -11109,7 +11326,10 @@ async function ensureInspirationReadyThenFork(input) {
|
|
|
11109
11326
|
// the true length instead of a placeholder.
|
|
11110
11327
|
if (!inspiration.durationSeconds) {
|
|
11111
11328
|
try {
|
|
11112
|
-
const probe = await probeMediaAsset({
|
|
11329
|
+
const probe = await probeMediaAsset({
|
|
11330
|
+
sourceUrl: inspiration.videoUrl,
|
|
11331
|
+
maxDurationSeconds: INSPIRATION_PROBE_MAX_DURATION_SECONDS
|
|
11332
|
+
});
|
|
11113
11333
|
const probedDuration = probe.metadata.durationSeconds;
|
|
11114
11334
|
if (Number.isFinite(probedDuration) && probedDuration && probedDuration > 0) {
|
|
11115
11335
|
inspiration = (await serverlessRecords.updateInspiration({
|
|
@@ -11188,6 +11408,205 @@ function buildTemplateEditorUrl(publicBaseUrl, templateId) {
|
|
|
11188
11408
|
const path = `/editor/${encodeURIComponent(templateId)}`;
|
|
11189
11409
|
return publicBaseUrl ? `${publicBaseUrl.replace(/\/$/, "")}${path}` : path;
|
|
11190
11410
|
}
|
|
11411
|
+
// Curated Popular picks are short-form: a source longer than the 120s ingest
|
|
11412
|
+
// limit is clipped to this many seconds (kept safely under 120 for the probe).
|
|
11413
|
+
const POPULAR_MAX_CLIP_SEC = 115;
|
|
11414
|
+
// Seed the curated "Popular" catalog from POPULAR_INSPIRATION_GROUPS: ingest
|
|
11415
|
+
// each URL once into a real PUBLIC template (same path as admin TikTok
|
|
11416
|
+
// submissions) and stamp it with the editorial title + popularGroup +
|
|
11417
|
+
// popularRank so it always shows under the /discover Popular toggle and
|
|
11418
|
+
// autoplays inline exactly like a Feed card. Over-length sources are clipped to
|
|
11419
|
+
// POPULAR_MAX_CLIP_SEC. Idempotent — find-or-create by URL, then (re)stamp.
|
|
11420
|
+
// Requires RAPIDAPI_KEY (the download primitive), so run this on staging/prod.
|
|
11421
|
+
app.post(`${API_PREFIX}/admin/popular/seed`, async (c) => {
|
|
11422
|
+
try {
|
|
11423
|
+
requireSuperagency(c);
|
|
11424
|
+
}
|
|
11425
|
+
catch (error) {
|
|
11426
|
+
return c.json({ error: error instanceof Error ? error.message : "Forbidden" }, 403);
|
|
11427
|
+
}
|
|
11428
|
+
const body = asRecord(await c.req.json().catch(() => ({}))) ?? {};
|
|
11429
|
+
const requestedOwnerEmail = readNonEmptyString(body.owner_email)?.trim().toLowerCase();
|
|
11430
|
+
const ownerEmail = requestedOwnerEmail ?? config.adminEmails[0]?.trim().toLowerCase();
|
|
11431
|
+
if (!ownerEmail)
|
|
11432
|
+
return c.json({ error: "No owner_email supplied and no admin email configured." }, 400);
|
|
11433
|
+
const ownerCustomer = await serverlessRecords.getCustomerByEmail(ownerEmail);
|
|
11434
|
+
if (!ownerCustomer)
|
|
11435
|
+
return c.json({ error: `Owner not found: ${ownerEmail}` }, 404);
|
|
11436
|
+
const primitive = primitiveRegistry.get("primitive:video_download");
|
|
11437
|
+
if (!primitive)
|
|
11438
|
+
return c.json({ error: "video_download primitive missing" }, 500);
|
|
11439
|
+
const operation = primitive.operations["run"];
|
|
11440
|
+
if (!operation)
|
|
11441
|
+
return c.json({ error: "video_download.run missing" }, 500);
|
|
11442
|
+
const publicBaseUrl = config.PUBLIC_BASE_URL?.trim() || "";
|
|
11443
|
+
try {
|
|
11444
|
+
await assertWalletCanQueueJobs(ownerCustomer.id);
|
|
11445
|
+
}
|
|
11446
|
+
catch (error) {
|
|
11447
|
+
if (error instanceof InsufficientWalletFundsError)
|
|
11448
|
+
return insufficientFundsResponse(c, error);
|
|
11449
|
+
throw error;
|
|
11450
|
+
}
|
|
11451
|
+
// Flatten the manifest into one ranked list. popularRank is a global sequence
|
|
11452
|
+
// (group index * 1000 + item index) so a plain rank sort restores both the
|
|
11453
|
+
// group order and the order within each group.
|
|
11454
|
+
const allItems = POPULAR_INSPIRATION_GROUPS.flatMap((group, gi) => group.items.map((item, ri) => ({ title: item.title, url: item.url, group: group.label, rank: gi * 1000 + ri })));
|
|
11455
|
+
// Optional { groups: [...] } filter seeds one category at a time so a large
|
|
11456
|
+
// catalog stays well under the Lambda timeout (each URL downloads inline).
|
|
11457
|
+
// Reconcile below still runs against the FULL manifest, so a partial seed
|
|
11458
|
+
// never retires the groups it skipped.
|
|
11459
|
+
const onlyGroups = Array.isArray(body.groups) ? new Set(body.groups.map((g) => String(g))) : null;
|
|
11460
|
+
const items = onlyGroups ? allItems.filter((it) => onlyGroups.has(it.group)) : allItems;
|
|
11461
|
+
const results = [];
|
|
11462
|
+
for (const item of items) {
|
|
11463
|
+
const originalUrl = stripTrackingParams(item.url);
|
|
11464
|
+
const sourceHost = safeUrlHostname(originalUrl);
|
|
11465
|
+
if (!sourceHost) {
|
|
11466
|
+
results.push({ source_url: item.url, title: item.title, error: "invalid URL" });
|
|
11467
|
+
continue;
|
|
11468
|
+
}
|
|
11469
|
+
let inspiration = await serverlessRecords.findInspirationByOriginalUrl(originalUrl);
|
|
11470
|
+
// (Re)start the download only when there's no usable video yet — a genuine
|
|
11471
|
+
// download failure (no videoUrl) retries, but a pick that DID download and
|
|
11472
|
+
// only tripped the duration limit keeps its bytes and gets clipped below.
|
|
11473
|
+
const needsIngest = !inspiration
|
|
11474
|
+
|| (!inspiration.videoUrl && inspiration.status !== "downloading");
|
|
11475
|
+
if (needsIngest) {
|
|
11476
|
+
const payload = operation.inputSchema.parse({ source_url: originalUrl, save_manifest: true });
|
|
11477
|
+
let job;
|
|
11478
|
+
try {
|
|
11479
|
+
job = await jobs.createRootJob({
|
|
11480
|
+
templateId: primitive.id,
|
|
11481
|
+
operationName: "run",
|
|
11482
|
+
workflowName: operation.workflow,
|
|
11483
|
+
tracer: `popular_seed_ingest:${ownerCustomer.id}`,
|
|
11484
|
+
payload,
|
|
11485
|
+
webhookUrl: null,
|
|
11486
|
+
customer: ownerCustomer,
|
|
11487
|
+
providerHint: undefined
|
|
11488
|
+
});
|
|
11489
|
+
}
|
|
11490
|
+
catch (error) {
|
|
11491
|
+
results.push({ source_url: item.url, title: item.title, error: error instanceof Error ? error.message : String(error) });
|
|
11492
|
+
continue;
|
|
11493
|
+
}
|
|
11494
|
+
inspiration = inspiration
|
|
11495
|
+
? ((await serverlessRecords.updateInspiration({
|
|
11496
|
+
inspirationId: inspiration.id,
|
|
11497
|
+
patch: { status: "downloading", downloadJobId: job.id, errorMessage: null }
|
|
11498
|
+
})) ?? inspiration)
|
|
11499
|
+
: await serverlessRecords.createInspiration({
|
|
11500
|
+
customerId: ownerCustomer.id,
|
|
11501
|
+
sourceUrl: item.url,
|
|
11502
|
+
originalUrl,
|
|
11503
|
+
sourceHost,
|
|
11504
|
+
downloadJobId: job.id,
|
|
11505
|
+
trendTagline: item.title
|
|
11506
|
+
});
|
|
11507
|
+
}
|
|
11508
|
+
// Stamp the editorial marker + curated title up front so a still-downloading
|
|
11509
|
+
// pick already surfaces under Popular with the right label (pending cards
|
|
11510
|
+
// read trendTagline, which finalize never clobbers).
|
|
11511
|
+
inspiration = (await serverlessRecords.updateInspiration({
|
|
11512
|
+
inspirationId: inspiration.id,
|
|
11513
|
+
patch: { title: item.title, trendTagline: item.title, popularGroup: item.group, popularRank: item.rank, visibility: "public" }
|
|
11514
|
+
})) ?? inspiration;
|
|
11515
|
+
// On a local box (no Step Functions) the download job would queue forever —
|
|
11516
|
+
// run it in-process so ensureInspirationReadyThenFork can finalize.
|
|
11517
|
+
if (!config.VIDFARM_JOB_STATE_MACHINE_ARN && inspiration.status === "downloading" && inspiration.downloadJobId) {
|
|
11518
|
+
const pendingJobId = inspiration.downloadJobId;
|
|
11519
|
+
void runPrimitiveJobInProcess(pendingJobId).catch((error) => {
|
|
11520
|
+
console.error("popular seed download failed", pendingJobId, error instanceof Error ? error.message : error);
|
|
11521
|
+
});
|
|
11522
|
+
}
|
|
11523
|
+
const finalized = await ensureInspirationReadyThenFork({ inspiration, ownerCustomer, publicBaseUrl, note: null });
|
|
11524
|
+
let templateId = typeof finalized.template_id === "string" ? finalized.template_id : null;
|
|
11525
|
+
let clipped = false;
|
|
11526
|
+
let sourceDurationSec = null;
|
|
11527
|
+
// Curated Popular picks are short-form. If the source downloaded fine but was
|
|
11528
|
+
// rejected ONLY for exceeding the 120s ingest limit, clip it to <=115s and
|
|
11529
|
+
// finalize the clip — so a long reference video still becomes a Popular card.
|
|
11530
|
+
if (!templateId) {
|
|
11531
|
+
const rejected = await serverlessRecords.getInspiration(inspiration.id);
|
|
11532
|
+
const tooLong = Boolean(rejected?.videoUrl)
|
|
11533
|
+
&& ((Number.isFinite(rejected?.durationSeconds) && Number(rejected?.durationSeconds) > POPULAR_MAX_CLIP_SEC)
|
|
11534
|
+
|| /exceeds/i.test(rejected?.errorMessage ?? ""));
|
|
11535
|
+
if (Number.isFinite(rejected?.durationSeconds))
|
|
11536
|
+
sourceDurationSec = Number(rejected?.durationSeconds);
|
|
11537
|
+
else {
|
|
11538
|
+
const m = /([0-9.]+)s exceeds/.exec(rejected?.errorMessage ?? "");
|
|
11539
|
+
if (m)
|
|
11540
|
+
sourceDurationSec = Number(m[1]);
|
|
11541
|
+
}
|
|
11542
|
+
if (rejected?.videoUrl && tooLong) {
|
|
11543
|
+
try {
|
|
11544
|
+
const trimmed = await trimVideoAsset({ sourceUrl: rejected.videoUrl, startMs: 0, durationMs: POPULAR_MAX_CLIP_SEC * 1000 });
|
|
11545
|
+
// Store under the public "primitives/" prefix so getPublicUrl can hand
|
|
11546
|
+
// back a directly-fetchable S3 URL (other prefixes resolve to null).
|
|
11547
|
+
const clipKey = joinStorageKey("primitives", "popular-clips", `${rejected.id}.mp4`);
|
|
11548
|
+
const stored = await storage.putBuffer(clipKey, trimmed.bytes, "video/mp4");
|
|
11549
|
+
const clipUrl = stored.url ?? storage.getPublicUrl(clipKey);
|
|
11550
|
+
if (!clipUrl)
|
|
11551
|
+
console.error("popular seed clip: no public URL for", clipKey);
|
|
11552
|
+
const clipDurRaw = trimmed.metadata.trimmed?.durationSeconds;
|
|
11553
|
+
const clipDur = Number.isFinite(clipDurRaw) && clipDurRaw ? Number(clipDurRaw) : POPULAR_MAX_CLIP_SEC;
|
|
11554
|
+
if (clipUrl) {
|
|
11555
|
+
const readyInsp = await serverlessRecords.updateInspiration({
|
|
11556
|
+
inspirationId: rejected.id,
|
|
11557
|
+
patch: {
|
|
11558
|
+
videoUrl: clipUrl,
|
|
11559
|
+
videoStorageKey: clipKey,
|
|
11560
|
+
thumbnailUrl: null,
|
|
11561
|
+
durationSeconds: clipDur,
|
|
11562
|
+
status: "ready",
|
|
11563
|
+
errorMessage: null
|
|
11564
|
+
}
|
|
11565
|
+
});
|
|
11566
|
+
if (readyInsp) {
|
|
11567
|
+
const reFinalized = await ensureInspirationReadyThenFork({ inspiration: readyInsp, ownerCustomer, publicBaseUrl, note: null });
|
|
11568
|
+
templateId = typeof reFinalized.template_id === "string" ? reFinalized.template_id : null;
|
|
11569
|
+
clipped = Boolean(templateId);
|
|
11570
|
+
}
|
|
11571
|
+
}
|
|
11572
|
+
}
|
|
11573
|
+
catch (error) {
|
|
11574
|
+
console.error("popular seed clip failed", rejected.id, error instanceof Error ? error.message : error);
|
|
11575
|
+
}
|
|
11576
|
+
}
|
|
11577
|
+
}
|
|
11578
|
+
if (templateId) {
|
|
11579
|
+
await serverlessRecords.updateTemplate({
|
|
11580
|
+
templateId,
|
|
11581
|
+
patch: { title: item.title, popularGroup: item.group, popularRank: item.rank, visibility: "public" }
|
|
11582
|
+
});
|
|
11583
|
+
}
|
|
11584
|
+
results.push({
|
|
11585
|
+
source_url: item.url,
|
|
11586
|
+
title: item.title,
|
|
11587
|
+
group: item.group,
|
|
11588
|
+
rank: item.rank,
|
|
11589
|
+
template_id: templateId,
|
|
11590
|
+
status: templateId ? "ready" : (finalized.status ?? null),
|
|
11591
|
+
clipped,
|
|
11592
|
+
source_duration_sec: sourceDurationSec,
|
|
11593
|
+
pending: finalized.pending ?? false
|
|
11594
|
+
});
|
|
11595
|
+
}
|
|
11596
|
+
// Reconcile: any previously-seeded pick whose URL is no longer in the manifest
|
|
11597
|
+
// (a replaced or removed entry) gets un-flagged so it drops out of the Popular
|
|
11598
|
+
// view — the manifest is the single source of truth. Compared after the same
|
|
11599
|
+
// tracking-param stripping the stored originalUrl went through.
|
|
11600
|
+
const manifestUrls = new Set(allItems.map((it) => stripTrackingParams(it.url)));
|
|
11601
|
+
const retired = [];
|
|
11602
|
+
for (const tmpl of await serverlessRecords.listPopularTemplates()) {
|
|
11603
|
+
if (!manifestUrls.has(tmpl.originalUrl)) {
|
|
11604
|
+
await serverlessRecords.updateTemplate({ templateId: tmpl.id, patch: { popularGroup: null, popularRank: null } });
|
|
11605
|
+
retired.push({ template_id: tmpl.id, title: tmpl.title, original_url: tmpl.originalUrl });
|
|
11606
|
+
}
|
|
11607
|
+
}
|
|
11608
|
+
return c.json({ ok: true, owner_customer_id: ownerCustomer.id, count: results.length, items: results, retired }, 202);
|
|
11609
|
+
});
|
|
11191
11610
|
function buildRawTikTokCompositionHtml(input) {
|
|
11192
11611
|
const duration = Number.isFinite(input.durationSeconds) && input.durationSeconds > 0
|
|
11193
11612
|
? Number(input.durationSeconds.toFixed(3))
|
|
@@ -14938,6 +15357,31 @@ app.get(`${USER_PREFIX}/me/generated-media`, async (c) => {
|
|
|
14938
15357
|
next_cursor: page.nextCursor
|
|
14939
15358
|
});
|
|
14940
15359
|
});
|
|
15360
|
+
// OpenAI's embedding models are named text-embedding-3-* / text-embedding-ada-*;
|
|
15361
|
+
// Gemini's family INCLUDES text-embedding-004 (plus gemini-embedding-*), so a
|
|
15362
|
+
// bare startsWith("text-embedding") misroutes a Gemini-built library to OpenAI
|
|
15363
|
+
// and returns cross-space (garbage) cosine scores. Match the OpenAI naming
|
|
15364
|
+
// precisely; everything else is gemini.
|
|
15365
|
+
function inferEmbeddingProviderFromModel(model) {
|
|
15366
|
+
return model && /^text-embedding-(3|ada)/i.test(model) ? "openai" : "gemini";
|
|
15367
|
+
}
|
|
15368
|
+
// Embed-capable client for populating/searching a clip library. Matches the
|
|
15369
|
+
// library's existing embedding space when one exists (so vectors stay
|
|
15370
|
+
// cosine-comparable); otherwise takes the first embed-capable saved key
|
|
15371
|
+
// (gemini → openai; openrouter can't embed). Null → no key, keyword-only.
|
|
15372
|
+
async function buildClipLibraryEmbedder(customerId) {
|
|
15373
|
+
const existing = await clipRecords.getLibraryEmbeddingModel(customerId);
|
|
15374
|
+
const providers = existing ? [inferEmbeddingProviderFromModel(existing)] : ["gemini", "openai"];
|
|
15375
|
+
for (const provider of providers) {
|
|
15376
|
+
const apiKey = await resolveCustomerProviderKey(customerId, provider);
|
|
15377
|
+
if (!apiKey)
|
|
15378
|
+
continue;
|
|
15379
|
+
const client = new ClipModelClient({ provider, apiKey });
|
|
15380
|
+
if (client.canEmbed)
|
|
15381
|
+
return client;
|
|
15382
|
+
}
|
|
15383
|
+
return null;
|
|
15384
|
+
}
|
|
14941
15385
|
// Embedder for My Files notes — the same BYOK seam clip search uses. Prefer the
|
|
14942
15386
|
// provider matching the embedding model the file library was built with (from
|
|
14943
15387
|
// the newest annotated attachment) so query vectors stay cosine-comparable; a
|
|
@@ -14946,7 +15390,7 @@ app.get(`${USER_PREFIX}/me/generated-media`, async (c) => {
|
|
|
14946
15390
|
async function buildFileNotesEmbedder(customerId) {
|
|
14947
15391
|
const attachments = await serverlessRecords.listUserAttachments(customerId);
|
|
14948
15392
|
const libModel = attachments.find((a) => a.notesEmbeddingModel && Array.isArray(a.notesEmbedding) && a.notesEmbedding.length > 0)?.notesEmbeddingModel ?? null;
|
|
14949
|
-
const providers = libModel ? [libModel
|
|
15393
|
+
const providers = libModel ? [inferEmbeddingProviderFromModel(libModel)] : ["gemini", "openai"];
|
|
14950
15394
|
for (const provider of providers) {
|
|
14951
15395
|
const apiKey = await resolveCustomerProviderKey(customerId, provider);
|
|
14952
15396
|
if (!apiKey)
|
|
@@ -16162,7 +16606,7 @@ async function pickClipProviderFromKeys(customerId) {
|
|
|
16162
16606
|
*/
|
|
16163
16607
|
async function buildMatchEmbedder(customerId) {
|
|
16164
16608
|
const libModel = await clipRecords.getLibraryEmbeddingModel(customerId);
|
|
16165
|
-
const provider = libModel
|
|
16609
|
+
const provider = inferEmbeddingProviderFromModel(libModel);
|
|
16166
16610
|
const key = await resolveCustomerProviderKey(customerId, provider);
|
|
16167
16611
|
if (!key)
|
|
16168
16612
|
return null;
|
|
@@ -16532,29 +16976,54 @@ app.post(`${CLIPS_PREFIX}/scan`, async (c) => {
|
|
|
16532
16976
|
const computeEstimate = effectiveDurationSec
|
|
16533
16977
|
? estimateClipScanComputeUsd(effectiveDurationSec, huntSpec.duration_band?.target_sec)
|
|
16534
16978
|
: null;
|
|
16535
|
-
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
|
|
16539
|
-
|
|
16540
|
-
|
|
16541
|
-
|
|
16542
|
-
|
|
16543
|
-
|
|
16544
|
-
|
|
16545
|
-
|
|
16546
|
-
|
|
16547
|
-
|
|
16548
|
-
|
|
16549
|
-
|
|
16550
|
-
|
|
16551
|
-
|
|
16552
|
-
|
|
16553
|
-
|
|
16554
|
-
|
|
16555
|
-
|
|
16556
|
-
|
|
16557
|
-
|
|
16979
|
+
// Records first: a Dynamo failure here aborts submission before anything
|
|
16980
|
+
// is billed, and the warm probe lambda's later status:"scanning" write is
|
|
16981
|
+
// never clobbered back to "pending" by a post-start putScanRecords.
|
|
16982
|
+
await putScanRecords({ scanStatus: "running" });
|
|
16983
|
+
let executionArn;
|
|
16984
|
+
try {
|
|
16985
|
+
const res = await clipScanSfn.send(new StartExecutionCommand({
|
|
16986
|
+
stateMachineArn: config.VIDFARM_CLIP_SCAN_STATE_MACHINE_ARN,
|
|
16987
|
+
name: scanId.replace(/[^A-Za-z0-9-_]/g, "-").slice(0, 80),
|
|
16988
|
+
input: JSON.stringify({
|
|
16989
|
+
scan_id: scanId,
|
|
16990
|
+
owner_id: customer.id,
|
|
16991
|
+
source_video_id: sourceVideoId,
|
|
16992
|
+
source_filename: filename,
|
|
16993
|
+
raw_s3_uri: rawS3Uri ?? "",
|
|
16994
|
+
tier,
|
|
16995
|
+
provider,
|
|
16996
|
+
tag_api_key: tagApiKey,
|
|
16997
|
+
embedding_provider: client.embeddingProvider ?? "",
|
|
16998
|
+
embedding_api_key: embeddingKey ?? "",
|
|
16999
|
+
guidance_prompt: guidancePrompt,
|
|
17000
|
+
frames_per_scene: framesPerScene,
|
|
17001
|
+
include_audio: includeAudio,
|
|
17002
|
+
hunt_spec: huntSpec,
|
|
17003
|
+
tracer
|
|
17004
|
+
})
|
|
17005
|
+
}));
|
|
17006
|
+
executionArn = res.executionArn;
|
|
17007
|
+
}
|
|
17008
|
+
catch (error) {
|
|
17009
|
+
// The execution never started — roll the scan (and its source) to failed
|
|
17010
|
+
// so pollers do not spin on a "running" record that will never progress,
|
|
17011
|
+
// mirroring the lazy-reconcile failure path.
|
|
17012
|
+
const orphan = await clipRecords.getScan(customer.id, scanId);
|
|
17013
|
+
if (orphan)
|
|
17014
|
+
await clipRecords.putScan({ ...orphan, status: "failed", error: "Failed to start the scan pipeline.", updated_at: nowIso() });
|
|
17015
|
+
const orphanSource = await clipRecords.getSource(customer.id, sourceVideoId);
|
|
17016
|
+
if (orphanSource && orphanSource.status !== "complete")
|
|
17017
|
+
await clipRecords.putSource({ ...orphanSource, status: "failed", updated_at: nowIso() });
|
|
17018
|
+
throw error;
|
|
17019
|
+
}
|
|
17020
|
+
// Attach the execution ARN for lazy SFN reconciliation (best-effort — the
|
|
17021
|
+
// record is already pollable; spread the current record so a runner that
|
|
17022
|
+
// has already advanced the status is not regressed).
|
|
17023
|
+
const startedScan = await clipRecords.getScan(customer.id, scanId);
|
|
17024
|
+
if (startedScan)
|
|
17025
|
+
await clipRecords.putScan({ ...startedScan, execution_arn: executionArn, updated_at: nowIso() });
|
|
17026
|
+
return scanResponse({ status: "running", executionArn, pipeline: "cloud", estimate, computeEstimate });
|
|
16558
17027
|
}
|
|
16559
17028
|
// ── vidfarm serve (no deployed pipeline): LOCAL-FIRST, cloud as backup ──
|
|
16560
17029
|
// Same policy as `vidfarm clips scan`: the user's local claude/codex CLI
|
|
@@ -16856,6 +17325,11 @@ async function mirrorUpstreamClipsToLocal(input) {
|
|
|
16856
17325
|
if (!feed.ok || !feed.json)
|
|
16857
17326
|
throw new Error(`Upstream clip feed unavailable (HTTP ${feed.status}).`);
|
|
16858
17327
|
let stored = 0;
|
|
17328
|
+
// Upstream /clips/feed strips the embedding vector, so re-embed each mirrored
|
|
17329
|
+
// clip on the owner's BYOK key — otherwise it lands with no embedding and is
|
|
17330
|
+
// permanently invisible to semantic clip search (keyword-only). Fail-soft:
|
|
17331
|
+
// no embed-capable key → store without embedding (keyword-only), as before.
|
|
17332
|
+
const embedder = await buildClipLibraryEmbedder(ownerId);
|
|
16859
17333
|
for (const raw of feed.json.clips ?? []) {
|
|
16860
17334
|
const clipId = typeof raw.clip_id === "string" ? raw.clip_id : null;
|
|
16861
17335
|
const viewUrl = typeof raw.view_url === "string" ? raw.view_url : null;
|
|
@@ -16875,16 +17349,32 @@ async function mirrorUpstreamClipsToLocal(input) {
|
|
|
16875
17349
|
await storage.putBuffer(thumbKey, new Uint8Array(await thumbRes.arrayBuffer()), "image/jpeg");
|
|
16876
17350
|
}
|
|
16877
17351
|
}
|
|
16878
|
-
const { view_url: _v, thumbnail_url: _t, ...rest } = raw;
|
|
17352
|
+
const { view_url: _v, thumbnail_url: _t, embedding: _e, embedding_model: _em, ...rest } = raw;
|
|
16879
17353
|
void _v;
|
|
16880
17354
|
void _t;
|
|
16881
|
-
|
|
17355
|
+
void _e;
|
|
17356
|
+
void _em;
|
|
17357
|
+
const clip = {
|
|
16882
17358
|
...rest,
|
|
16883
17359
|
owner_id: ownerId,
|
|
16884
17360
|
source_video_id: scan.source_video_id,
|
|
16885
17361
|
file_path: clipKey,
|
|
16886
17362
|
thumbnail_path: thumbKey
|
|
16887
|
-
}
|
|
17363
|
+
};
|
|
17364
|
+
if (embedder?.embeddingModelId) {
|
|
17365
|
+
try {
|
|
17366
|
+
const [vector] = await embedder.embedDocuments([buildClipEmbeddingText(clip.tags, clip.description)]);
|
|
17367
|
+
if (vector?.length) {
|
|
17368
|
+
clip.embedding = vector;
|
|
17369
|
+
clip.embedding_model = embedder.embeddingModelId;
|
|
17370
|
+
}
|
|
17371
|
+
}
|
|
17372
|
+
catch (error) {
|
|
17373
|
+
devLog("clip_scan.upstream_mirror_embed_failed", { scan_id: scan.scan_id, clip_id: clipId, ...devErrorFields(error) }, "warn");
|
|
17374
|
+
}
|
|
17375
|
+
}
|
|
17376
|
+
await clipRecords.putClip(clip);
|
|
17377
|
+
await vectorIndex.upsert(ownerId, clip);
|
|
16888
17378
|
stored++;
|
|
16889
17379
|
}
|
|
16890
17380
|
const source = await clipRecords.getSource(ownerId, scan.source_video_id);
|