dsh-hyperframes 0.3.1 → 0.4.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/README.en.md +6 -5
- package/README.md +7 -6
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -0
- package/package.json +63 -63
- package/skills/embedded-captions/SKILL.md +28 -3
- package/skills/embedded-captions/references/aesthetic-principles.md +2 -3
- package/skills/embedded-captions/references/rail.md +1 -1
- package/skills/embedded-captions/scripts/check-occlusion.cjs +4 -1
- package/skills/embedded-captions/scripts/check-overflow.cjs +1 -0
- package/skills/embedded-captions/scripts/check-rail-climax.cjs +5 -1
- package/skills/embedded-captions/scripts/fixtures/heroless/theme.json +11 -0
- package/skills/embedded-captions/scripts/hf-cli.cjs +33 -0
- package/skills/embedded-captions/scripts/make-theme.cjs +95 -57
- package/skills/embedded-captions/scripts/make-theme.test.mjs +131 -0
- package/skills/embedded-captions/scripts/matte.cjs +1 -13
- package/skills/embedded-captions/scripts/measure-layout.cjs +2 -1
- package/skills/embedded-captions/scripts/preview-frames.cjs +39 -11
- package/skills/embedded-captions/scripts/preview-frames.test.mjs +24 -0
- package/skills/embedded-captions/scripts/render-and-composite.sh +8 -52
- package/skills/embedded-captions/scripts/safe-zones.cjs +4 -1
- package/skills/embedded-captions/scripts/transcribe.cjs +5 -14
- package/skills/embedded-captions/scripts/transcribe.test.mjs +41 -0
- package/skills/faceless-explainer/SKILL.md +20 -16
- package/skills/faceless-explainer/references/story-design.md +1 -1
- package/skills/faceless-explainer/scripts/assemble-index.mjs +6 -4
- package/skills/faceless-explainer/scripts/assemble-index.test.mjs +57 -2
- package/skills/faceless-explainer/scripts/audio.mjs +2 -0
- package/skills/faceless-explainer/scripts/build-frame.mjs +6 -3
- package/skills/faceless-explainer/scripts/captions.mjs +7 -4
- package/skills/faceless-explainer/scripts/frame-packets.mjs +4 -4
- package/skills/faceless-explainer/scripts/lib/bgm-volume.mjs +4 -0
- package/skills/faceless-explainer/scripts/lib/captured-fonts.mjs +58 -0
- package/skills/{hyperframes-core → faceless-explainer}/scripts/lib/frame-packets-core.mjs +5 -23
- package/skills/faceless-explainer/scripts/lib/storyboard.mjs +1 -1
- package/skills/faceless-explainer/scripts/lib/transitions.json +3 -3
- package/skills/faceless-explainer/scripts/transitions.mjs +1 -0
- package/skills/faceless-explainer/scripts/transitions.test.mjs +2 -0
- package/skills/faceless-explainer/sub-agents/frame-worker.md +1 -1
- package/skills/figma/SKILL.md +2 -0
- package/skills/general-video/SKILL.md +13 -11
- package/skills/general-video/scripts/frame-packets.mjs +4 -4
- package/skills/general-video/scripts/lib/frame-packets-core.mjs +194 -0
- package/skills/general-video/sub-agents/frame-worker.md +1 -1
- package/skills/hyperframes/SKILL.md +40 -30
- package/skills/{hyperframes-core → hyperframes}/references/brief-contract.md +15 -15
- package/skills/{hyperframes-core → hyperframes}/references/brief-format.md +8 -8
- package/skills/hyperframes/references/capability-menu.md +2 -2
- package/skills/{hyperframes-core → hyperframes}/references/frame-worker-core.md +1 -1
- package/skills/hyperframes/references/intent-interview.md +16 -16
- package/skills/hyperframes/references/plugin-installation.md +43 -0
- package/skills/{hyperframes-core → hyperframes}/references/production-loop.md +11 -11
- package/skills/hyperframes/references/review-loop.md +37 -0
- package/skills/hyperframes/references/route-briefs.md +1 -1
- package/skills/hyperframes/references/routes/motion-graphics.md +1 -1
- package/skills/{hyperframes-core → hyperframes}/references/script-format.md +3 -3
- package/skills/hyperframes/references/skill-lifecycle.md +2 -0
- package/skills/{hyperframes-core → hyperframes}/references/storyboard-format.md +3 -34
- package/skills/hyperframes/scripts/lib/frame-packets-core.mjs +194 -0
- package/skills/hyperframes/scripts/plugin-cli.mjs +88 -0
- package/skills/hyperframes/scripts/plugin-cli.test.mjs +117 -0
- package/skills/hyperframes-animation/SKILL.md +5 -1
- package/skills/hyperframes-animation/adapters/gsap.md +1 -1
- package/skills/hyperframes-animation/adapters/lottie.md +15 -2
- package/skills/hyperframes-animation/adapters/three.md +14 -0
- package/skills/hyperframes-animation/blueprints/titlecard-reveal.md +1 -1
- package/skills/hyperframes-animation/references/motion-blur.md +133 -0
- package/skills/hyperframes-animation/rules/cursor-click-ripple.md +1 -1
- package/skills/hyperframes-animation/rules/dynamic-content-sequencing.md +1 -1
- package/skills/hyperframes-animation/rules/hacker-flip-3d.md +1 -1
- package/skills/hyperframes-animation/rules/kinetic-beat-slam.md +2 -2
- package/skills/hyperframes-animation/rules/motion-blur-streak.md +1 -1
- package/skills/hyperframes-animation/rules/vertical-spring-ticker.md +1 -1
- package/skills/hyperframes-animation/scripts/package-loader.mjs +47 -16
- package/skills/hyperframes-animation/scripts/package-loader.test.mjs +52 -0
- package/skills/hyperframes-animation/techniques.md +1 -1
- package/skills/hyperframes-animation/transitions/TRANSITION-REGISTRY.md +3 -3
- package/skills/hyperframes-animation/transitions/css-push.md +4 -4
- package/skills/hyperframes-audio/SKILL.md +30 -22
- package/skills/hyperframes-audio/scripts/carve.mjs +6 -3
- package/skills/hyperframes-audio/scripts/carve.test.mjs +16 -1
- package/skills/hyperframes-cli/SKILL.md +44 -35
- package/skills/hyperframes-cli/references/init-and-scaffold.md +8 -6
- package/skills/hyperframes-cli/references/lint-validate-inspect.md +3 -3
- package/skills/hyperframes-cli/references/preview-render.md +35 -32
- package/skills/hyperframes-cli/references/upgrade-info-misc.md +50 -1
- package/skills/hyperframes-core/SKILL.md +30 -24
- package/skills/hyperframes-core/references/composition-patterns.md +3 -42
- package/skills/hyperframes-core/references/creator-editing-recipes.md +131 -7
- package/skills/hyperframes-core/references/data-attributes.md +11 -11
- package/skills/hyperframes-core/references/determinism-rules.md +2 -19
- package/skills/hyperframes-core/references/full-screen-motion.md +0 -9
- package/skills/hyperframes-core/references/minimal-composition.md +2 -2
- package/skills/hyperframes-core/references/sub-compositions.md +3 -66
- package/skills/hyperframes-core/references/tailwind.md +1 -20
- package/skills/hyperframes-core/references/tracks-and-clips.md +2 -6
- package/skills/hyperframes-core/references/variables-and-media.md +3 -3
- package/skills/hyperframes-creative/SKILL.md +2 -0
- package/skills/hyperframes-creative/references/house-style.md +15 -15
- package/skills/hyperframes-creative/references/story-spine.md +3 -1
- package/skills/hyperframes-creative/references/storyboard-recipe.md +57 -0
- package/skills/hyperframes-creative/references/typography.md +1 -1
- package/skills/hyperframes-creative/scripts/package-loader.mjs +47 -16
- package/skills/hyperframes-creative/scripts/package-loader.test.mjs +52 -0
- package/skills/hyperframes-keyframes/SKILL.md +4 -2
- package/skills/hyperframes-registry/SKILL.md +3 -1
- package/skills/hyperframes-registry/references/contributing.md +8 -2
- package/skills/hyperframes-registry/references/discovery.md +20 -34
- package/skills/hyperframes-studio/SKILL.md +77 -0
- package/skills/media-use/SKILL.md +14 -12
- package/skills/media-use/audio/references/captions/authoring.md +4 -0
- package/skills/media-use/audio/references/remove-background.md +1 -1
- package/skills/media-use/audio/references/tts-to-captions.md +11 -2
- package/skills/media-use/audio/references/tts.md +84 -6
- package/skills/media-use/audio/scripts/audio.mjs +15 -6
- package/skills/media-use/audio/scripts/gemini-pipeline.test.mjs +113 -0
- package/skills/media-use/audio/scripts/lib/audio-meta.mjs +39 -0
- package/skills/media-use/audio/scripts/lib/audio-meta.test.mjs +136 -0
- package/skills/media-use/audio/scripts/lib/bgm-volume.mjs +4 -0
- package/skills/media-use/audio/scripts/lib/bgm.mjs +2 -6
- package/skills/media-use/audio/scripts/lib/gemini-auth.mjs +43 -0
- package/skills/media-use/audio/scripts/lib/gemini-auth.py +52 -0
- package/skills/media-use/audio/scripts/lib/gemini-auth.test.mjs +86 -0
- package/skills/media-use/audio/scripts/lib/gemini-auth_test.py +90 -0
- package/skills/media-use/audio/scripts/lib/gemini-tts.mjs +122 -0
- package/skills/media-use/audio/scripts/lib/gemini-tts.test.mjs +243 -0
- package/skills/media-use/audio/scripts/lib/heygen.mjs +2 -1
- package/skills/media-use/audio/scripts/lib/tts.mjs +15 -3
- package/skills/media-use/audio/scripts/wait-bgm.mjs +8 -4
- package/skills/media-use/audio/scripts/wait-bgm.test.mjs +65 -0
- package/skills/media-use/references/audio.md +3 -2
- package/skills/media-use/references/grading.md +7 -7
- package/skills/media-use/references/media-treatment-recipes.md +1 -1
- package/skills/media-use/references/memory.md +2 -2
- package/skills/media-use/references/meta.md +2 -2
- package/skills/media-use/references/operations.md +15 -13
- package/skills/media-use/references/resolve.md +22 -11
- package/skills/media-use/references/setup-providers.md +11 -11
- package/skills/media-use/scripts/audio-duck.mjs +19 -27
- package/skills/media-use/scripts/compatibility.test.mjs +96 -0
- package/skills/media-use/scripts/lib/config-lock.mjs +45 -0
- package/skills/media-use/scripts/lib/duck.mjs +16 -10
- package/skills/media-use/scripts/lib/media-fetch.mjs +85 -0
- package/skills/media-use/scripts/lib/media-home.mjs +20 -0
- package/skills/media-use/scripts/lib/npx-sync.mjs +28 -2
- package/skills/media-use/scripts/lib/prefs-store.mjs +2 -2
- package/skills/media-use/scripts/lib/recipe-store.mjs +2 -2
- package/skills/media-use/scripts/lib/telemetry.mjs +29 -41
- package/skills/media-use/scripts/lib/transcriptCutFade.mjs +21 -0
- package/skills/media-use/scripts/prefs.mjs +1 -1
- package/skills/media-use/scripts/recipe.mjs +1 -1
- package/skills/media-use/scripts/resolve-plugin.test.mjs +62 -0
- package/skills/media-use/scripts/resolve.mjs +27 -1267
- package/skills/media-use/scripts/transcribe.mjs +39 -25
- package/skills/media-use/scripts/transcribe.test.mjs +112 -0
- package/skills/media-use/scripts/transcript-cut.mjs +39 -8
- package/skills/media-use/scripts/transcript-cut.test.mjs +117 -0
- package/skills/motion-graphics/SKILL.md +3 -1
- package/skills/motion-graphics/agents/builder.md +1 -0
- package/skills/motion-graphics/agents/director.md +2 -1
- package/skills/motion-graphics/catalog-map.md +25 -13
- package/skills/motion-graphics/categories/charts/module.md +2 -1
- package/skills/motion-graphics/categories/lower-thirds/module.md +4 -0
- package/skills/motion-graphics/categories/news/module.md +1 -1
- package/skills/motion-graphics/references/motion-vocabulary.md +4 -0
- package/skills/music-to-video/SKILL.md +8 -6
- package/skills/music-to-video/references/montage.md +3 -6
- package/skills/music-to-video/references/motion-primitives/3d-card-flip/index.html +10 -111
- package/skills/music-to-video/references/motion-primitives/3d-card-flip/scene.html +119 -0
- package/skills/music-to-video/references/motion-primitives/bg-flow-field/index.html +11 -284
- package/skills/music-to-video/references/motion-primitives/bg-flow-field/scene.html +294 -0
- package/skills/music-to-video/references/motion-primitives/binary-decrypt/index.html +10 -76
- package/skills/music-to-video/references/motion-primitives/binary-decrypt/scene.html +85 -0
- package/skills/music-to-video/references/motion-primitives/blur-resolve/index.html +10 -52
- package/skills/music-to-video/references/motion-primitives/blur-resolve/scene.html +60 -0
- package/skills/music-to-video/references/motion-primitives/braam-punch/index.html +10 -92
- package/skills/music-to-video/references/motion-primitives/braam-punch/scene.html +100 -0
- package/skills/music-to-video/references/motion-primitives/chromatic-split/index.html +10 -103
- package/skills/music-to-video/references/motion-primitives/chromatic-split/scene.html +111 -0
- package/skills/music-to-video/references/motion-primitives/chrome-sweep/index.html +10 -60
- package/skills/music-to-video/references/motion-primitives/chrome-sweep/scene.html +68 -0
- package/skills/music-to-video/references/motion-primitives/counting-punch/index.html +10 -110
- package/skills/music-to-video/references/motion-primitives/counting-punch/scene.html +119 -0
- package/skills/music-to-video/references/motion-primitives/crash-zoom-in/index.html +10 -112
- package/skills/music-to-video/references/motion-primitives/crash-zoom-in/scene.html +120 -0
- package/skills/music-to-video/references/motion-primitives/datamosh-smear/index.html +10 -121
- package/skills/music-to-video/references/motion-primitives/datamosh-smear/scene.html +131 -0
- package/skills/music-to-video/references/motion-primitives/directional-fill/index.html +10 -90
- package/skills/music-to-video/references/motion-primitives/directional-fill/scene.html +103 -0
- package/skills/music-to-video/references/motion-primitives/dolly-zoom/index.html +10 -94
- package/skills/music-to-video/references/motion-primitives/dolly-zoom/scene.html +102 -0
- package/skills/music-to-video/references/motion-primitives/electric-arc/index.html +10 -100
- package/skills/music-to-video/references/motion-primitives/electric-arc/scene.html +105 -0
- package/skills/music-to-video/references/motion-primitives/flash-cut/index.html +10 -89
- package/skills/music-to-video/references/motion-primitives/flash-cut/scene.html +97 -0
- package/skills/music-to-video/references/motion-primitives/gooey-metaball/index.html +10 -197
- package/skills/music-to-video/references/motion-primitives/gooey-metaball/scene.html +206 -0
- package/skills/music-to-video/references/motion-primitives/hard-cut/index.html +10 -74
- package/skills/music-to-video/references/motion-primitives/hard-cut/scene.html +82 -0
- package/skills/music-to-video/references/motion-primitives/hypercut-whip/index.html +10 -36
- package/skills/music-to-video/references/motion-primitives/hypercut-whip/scene.html +44 -0
- package/skills/music-to-video/references/motion-primitives/iris-open/index.html +10 -61
- package/skills/music-to-video/references/motion-primitives/iris-open/scene.html +69 -0
- package/skills/music-to-video/references/motion-primitives/kinetic-letter-in/index.html +10 -55
- package/skills/music-to-video/references/motion-primitives/kinetic-letter-in/scene.html +64 -0
- package/skills/music-to-video/references/motion-primitives/liquid-morph/index.html +10 -88
- package/skills/music-to-video/references/motion-primitives/liquid-morph/scene.html +96 -0
- package/skills/music-to-video/references/motion-primitives/mask-reveal/index.html +10 -66
- package/skills/music-to-video/references/motion-primitives/mask-reveal/scene.html +74 -0
- package/skills/music-to-video/references/motion-primitives/mosaic-pack/index.html +10 -81
- package/skills/music-to-video/references/motion-primitives/mosaic-pack/scene.html +89 -0
- package/skills/music-to-video/references/motion-primitives/neon-flicker/index.html +10 -65
- package/skills/music-to-video/references/motion-primitives/neon-flicker/scene.html +73 -0
- package/skills/music-to-video/references/motion-primitives/outline-to-fill/index.html +10 -74
- package/skills/music-to-video/references/motion-primitives/outline-to-fill/scene.html +82 -0
- package/skills/music-to-video/references/motion-primitives/palette-flip/index.html +10 -89
- package/skills/music-to-video/references/motion-primitives/palette-flip/scene.html +97 -0
- package/skills/music-to-video/references/motion-primitives/particle-burst/index.html +10 -98
- package/skills/music-to-video/references/motion-primitives/particle-burst/scene.html +106 -0
- package/skills/music-to-video/references/motion-primitives/pixel-dissolve/index.html +10 -78
- package/skills/music-to-video/references/motion-primitives/pixel-dissolve/scene.html +86 -0
- package/skills/music-to-video/references/motion-primitives/radial-burst-lines/index.html +10 -117
- package/skills/music-to-video/references/motion-primitives/radial-burst-lines/scene.html +130 -0
- package/skills/music-to-video/references/motion-primitives/screen-shake/index.html +10 -70
- package/skills/music-to-video/references/motion-primitives/screen-shake/scene.html +78 -0
- package/skills/music-to-video/references/motion-primitives/slot-machine-reveal/index.html +10 -87
- package/skills/music-to-video/references/motion-primitives/slot-machine-reveal/scene.html +100 -0
- package/skills/music-to-video/references/motion-primitives/spotlight-sweep/index.html +10 -94
- package/skills/music-to-video/references/motion-primitives/spotlight-sweep/scene.html +102 -0
- package/skills/music-to-video/references/motion-primitives/staggered-exit/index.html +10 -74
- package/skills/music-to-video/references/motion-primitives/staggered-exit/scene.html +82 -0
- package/skills/music-to-video/references/motion-primitives/text-spectral-rays/index.html +11 -271
- package/skills/music-to-video/references/motion-primitives/text-spectral-rays/scene.html +280 -0
- package/skills/music-to-video/references/motion-primitives/text-wave-distort/index.html +10 -92
- package/skills/music-to-video/references/motion-primitives/text-wave-distort/scene.html +106 -0
- package/skills/music-to-video/references/motion-primitives/tile-mosaic/index.html +10 -101
- package/skills/music-to-video/references/motion-primitives/tile-mosaic/scene.html +109 -0
- package/skills/music-to-video/references/motion-primitives/typewriter-reveal/index.html +10 -79
- package/skills/music-to-video/references/motion-primitives/typewriter-reveal/scene.html +91 -0
- package/skills/music-to-video/references/motion-primitives/word-grid-burst/index.html +10 -83
- package/skills/music-to-video/references/motion-primitives/word-grid-burst/scene.html +96 -0
- package/skills/music-to-video/references/planning.md +2 -2
- package/skills/music-to-video/scripts/lib/storyboard.mjs +1 -1
- package/skills/music-to-video/sub-agents/frame-worker.md +2 -2
- package/skills/pr-to-video/SKILL.md +20 -16
- package/skills/pr-to-video/references/code-vocabulary.md +14 -12
- package/skills/pr-to-video/references/story-design.md +1 -1
- package/skills/pr-to-video/scripts/assemble-index.mjs +6 -4
- package/skills/pr-to-video/scripts/assemble-index.test.mjs +57 -2
- package/skills/pr-to-video/scripts/audio.mjs +2 -0
- package/skills/pr-to-video/scripts/build-frame.mjs +6 -3
- package/skills/pr-to-video/scripts/captions.mjs +7 -4
- package/skills/pr-to-video/scripts/captions.test.mjs +41 -0
- package/skills/pr-to-video/scripts/frame-packets.mjs +4 -4
- package/skills/pr-to-video/scripts/lib/bgm-volume.mjs +4 -0
- package/skills/pr-to-video/scripts/lib/captured-fonts.mjs +58 -0
- package/skills/pr-to-video/scripts/lib/frame-packets-core.mjs +194 -0
- package/skills/pr-to-video/scripts/lib/storyboard.mjs +1 -1
- package/skills/pr-to-video/scripts/lib/transitions.json +3 -3
- package/skills/pr-to-video/scripts/transitions.mjs +7 -5
- package/skills/pr-to-video/sub-agents/frame-worker.md +1 -1
- package/skills/product-launch-video/SKILL.md +20 -16
- package/skills/product-launch-video/references/story-design.md +1 -1
- package/skills/product-launch-video/scripts/assemble-index.mjs +6 -4
- package/skills/product-launch-video/scripts/assemble-index.test.mjs +57 -2
- package/skills/product-launch-video/scripts/audio.mjs +2 -0
- package/skills/product-launch-video/scripts/audio.test.mjs +16 -0
- package/skills/product-launch-video/scripts/build-frame-subsets.test.mjs +152 -0
- package/skills/product-launch-video/scripts/build-frame.mjs +6 -3
- package/skills/product-launch-video/scripts/captions.mjs +7 -4
- package/skills/product-launch-video/scripts/frame-packets.mjs +4 -4
- package/skills/product-launch-video/scripts/lib/bgm-volume.mjs +4 -0
- package/skills/product-launch-video/scripts/lib/captured-fonts.mjs +58 -0
- package/skills/product-launch-video/scripts/lib/frame-packets-core.mjs +194 -0
- package/skills/product-launch-video/scripts/lib/storyboard.mjs +1 -1
- package/skills/product-launch-video/scripts/lib/transitions.json +3 -3
- package/skills/product-launch-video/scripts/transitions.mjs +7 -5
- package/skills/product-launch-video/scripts/transitions.test.mjs +68 -0
- package/skills/product-launch-video/sub-agents/frame-worker.md +1 -1
- package/skills/remotion-to-hyperframes/SKILL.md +7 -4
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-1-title-card/expected.json +5 -5
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-1-title-card/hf-src/compositions/title.html +31 -0
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-1-title-card/hf-src/index.html +10 -25
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-2-multi-scene/README.md +8 -8
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-2-multi-scene/expected.json +9 -9
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-2-multi-scene/hf-src/compositions/scene-1.html +35 -0
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-2-multi-scene/hf-src/compositions/scene-2.html +35 -0
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-2-multi-scene/hf-src/compositions/scene-3.html +36 -0
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-2-multi-scene/hf-src/index.html +28 -71
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-3-data-driven/expected.json +3 -3
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-3-data-driven/hf-src/compositions/scene-1.html +52 -0
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-3-data-driven/hf-src/compositions/scene-2.html +99 -0
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-3-data-driven/hf-src/compositions/scene-3.html +55 -0
- package/skills/remotion-to-hyperframes/assets/test-corpus/tier-3-data-driven/hf-src/index.html +13 -212
- package/skills/remotion-to-hyperframes/references/api-map.md +16 -16
- package/skills/remotion-to-hyperframes/references/limitations.md +7 -7
- package/skills/remotion-to-hyperframes/references/lottie.md +5 -7
- package/skills/remotion-to-hyperframes/references/media.md +3 -3
- package/skills/remotion-to-hyperframes/references/parameters.md +5 -0
- package/skills/remotion-to-hyperframes/references/sequencing.md +71 -18
- package/skills/slideshow/SKILL.md +4 -1
- package/skills/slideshow/references/standalone-harness.md +0 -3
- package/skills/talking-head-recut/SKILL.md +10 -1
- package/skills/hyperframes-core/references/review-loop.md +0 -41
- package/skills/media-use/scripts/lib/adopt.mjs +0 -128
- package/skills/media-use/scripts/lib/adopt.test.mjs +0 -95
- package/skills/media-use/scripts/lib/bgm-provider.mjs +0 -20
- package/skills/media-use/scripts/lib/brand-provider.mjs +0 -59
- package/skills/media-use/scripts/lib/bundled-sfx-provider.mjs +0 -108
- package/skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs +0 -85
- package/skills/media-use/scripts/lib/cache.mjs +0 -150
- package/skills/media-use/scripts/lib/candidates.mjs +0 -91
- package/skills/media-use/scripts/lib/candidates.test.mjs +0 -155
- package/skills/media-use/scripts/lib/codex-provider.mjs +0 -161
- package/skills/media-use/scripts/lib/codex-provider.test.mjs +0 -34
- package/skills/media-use/scripts/lib/coverage.test.mjs +0 -117
- package/skills/media-use/scripts/lib/cube-build.mjs +0 -181
- package/skills/media-use/scripts/lib/cube-build.test.mjs +0 -80
- package/skills/media-use/scripts/lib/cube-validate.mjs +0 -198
- package/skills/media-use/scripts/lib/cube-validate.test.mjs +0 -125
- package/skills/media-use/scripts/lib/cutlist.test.mjs +0 -148
- package/skills/media-use/scripts/lib/duck.test.mjs +0 -118
- package/skills/media-use/scripts/lib/error-diffusion.test.mjs +0 -102
- package/skills/media-use/scripts/lib/freeze.mjs +0 -70
- package/skills/media-use/scripts/lib/freeze.test.mjs +0 -46
- package/skills/media-use/scripts/lib/grade-analyzer.mjs +0 -264
- package/skills/media-use/scripts/lib/grade-analyzer.test.mjs +0 -285
- package/skills/media-use/scripts/lib/heygen-cli.mjs +0 -191
- package/skills/media-use/scripts/lib/heygen-cli.test.mjs +0 -301
- package/skills/media-use/scripts/lib/heygen-search.mjs +0 -45
- package/skills/media-use/scripts/lib/heygen-search.test.mjs +0 -49
- package/skills/media-use/scripts/lib/heygen-video-provider.mjs +0 -127
- package/skills/media-use/scripts/lib/heygen-video-provider.test.mjs +0 -344
- package/skills/media-use/scripts/lib/image-provider.mjs +0 -44
- package/skills/media-use/scripts/lib/local-models.mjs +0 -318
- package/skills/media-use/scripts/lib/local-models.test.mjs +0 -220
- package/skills/media-use/scripts/lib/local-run.mjs +0 -78
- package/skills/media-use/scripts/lib/local-run.test.mjs +0 -121
- package/skills/media-use/scripts/lib/logo-provider.mjs +0 -222
- package/skills/media-use/scripts/lib/logo-provider.test.mjs +0 -137
- package/skills/media-use/scripts/lib/ltx-video-provider.mjs +0 -94
- package/skills/media-use/scripts/lib/ltx-video-provider.test.mjs +0 -316
- package/skills/media-use/scripts/lib/lut-preset-provider.mjs +0 -266
- package/skills/media-use/scripts/lib/lut-preset-provider.test.mjs +0 -176
- package/skills/media-use/scripts/lib/manifest.test.mjs +0 -388
- package/skills/media-use/scripts/lib/match.mjs +0 -47
- package/skills/media-use/scripts/lib/mflux-provider.mjs +0 -128
- package/skills/media-use/scripts/lib/mflux-provider.test.mjs +0 -174
- package/skills/media-use/scripts/lib/misses.mjs +0 -49
- package/skills/media-use/scripts/lib/misses.test.mjs +0 -78
- package/skills/media-use/scripts/lib/npx-sync.test.mjs +0 -50
- package/skills/media-use/scripts/lib/parakeet-words.test.mjs +0 -44
- package/skills/media-use/scripts/lib/prefs-store.test.mjs +0 -189
- package/skills/media-use/scripts/lib/probe.mjs +0 -39
- package/skills/media-use/scripts/lib/probe.test.mjs +0 -31
- package/skills/media-use/scripts/lib/providers.mjs +0 -5
- package/skills/media-use/scripts/lib/recipe-store.test.mjs +0 -367
- package/skills/media-use/scripts/lib/registry.mjs +0 -218
- package/skills/media-use/scripts/lib/registry.test.mjs +0 -261
- package/skills/media-use/scripts/lib/search.mjs +0 -24
- package/skills/media-use/scripts/lib/search.test.mjs +0 -32
- package/skills/media-use/scripts/lib/sfx-provider.mjs +0 -23
- package/skills/media-use/scripts/lib/specs.mjs +0 -132
- package/skills/media-use/scripts/lib/specs.test.mjs +0 -157
- package/skills/media-use/scripts/lib/stats.mjs +0 -117
- package/skills/media-use/scripts/lib/stats.test.mjs +0 -165
- package/skills/media-use/scripts/lib/telemetry.test.mjs +0 -373
- package/skills/media-use/scripts/lib/tts-local-provider.mjs +0 -92
- package/skills/media-use/scripts/lib/tts-local-provider.test.mjs +0 -72
- package/skills/media-use/scripts/lib/usage.mjs +0 -57
- package/skills/media-use/scripts/lib/usage.test.mjs +0 -54
- package/skills/media-use/scripts/lib/voice-provider.mjs +0 -57
- package/skills/media-use/scripts/lib/voice-provider.test.mjs +0 -46
- package/skills/media-use/scripts/resolve.test.mjs +0 -1120
- /package/skills/{hyperframes-core → hyperframes}/references/subagent-dispatch.md +0 -0
|
@@ -36,6 +36,10 @@ Use another voice only for a documented reason, and write the reason down.
|
|
|
36
36
|
|
|
37
37
|
## Available routes
|
|
38
38
|
|
|
39
|
+
Gemini is an explicit alternative to the automatic provider order below. A
|
|
40
|
+
request to use Gemini already chooses the provider; do not redirect that user
|
|
41
|
+
to HeyGen sign-in. Read the Gemini section for its credential requirement.
|
|
42
|
+
|
|
39
43
|
| Order | Provider | Env trigger | Voice IDs | Word timestamps | Audio format |
|
|
40
44
|
| ----- | ----------------- | ------------------------------------------- | ------------------------------------------- | ----------------------------------------- | -------------------- |
|
|
41
45
|
| 1 | HeyGen (Starfish) | `$HEYGEN_API_KEY` / `~/.heygen/credentials` | UUIDs from `GET /v3/voices?engine=starfish` | **Yes** (`word_timestamps[]` in response) | mp3 → wav via ffmpeg |
|
|
@@ -83,12 +87,86 @@ node skills/media-use/audio/scripts/heygen-tts.mjs --list # public starfish vo
|
|
|
83
87
|
|
|
84
88
|
## When to use which provider
|
|
85
89
|
|
|
86
|
-
| Goal | Use
|
|
87
|
-
| --------------------------------------------------------- |
|
|
88
|
-
| Best voice quality + word timestamps in one call | **HeyGen**
|
|
89
|
-
| Drop-in cloud TTS, big voice catalog | **ElevenLabs**
|
|
90
|
-
| Offline, no API key, fast iteration | **Kokoro**
|
|
91
|
-
|
|
|
90
|
+
| Goal | Use |
|
|
91
|
+
| --------------------------------------------------------- | --------------------------------------------------------- |
|
|
92
|
+
| Best voice quality + word timestamps in one call | **HeyGen** |
|
|
93
|
+
| Drop-in cloud TTS, big voice catalog | **ElevenLabs** |
|
|
94
|
+
| Offline, no API key, fast iteration | **Kokoro** |
|
|
95
|
+
| Directed delivery with Gemini prebuilt or custom voices | **Gemini** (explicit selection; transcription for timing) |
|
|
96
|
+
| Non-English multilingual with deterministic phonemization | **Kokoro** (`ef_dora`, `jf_alpha`, `zf_xiaobei`, …) |
|
|
97
|
+
|
|
98
|
+
## Gemini narration
|
|
99
|
+
|
|
100
|
+
Use the shared audio engine, not `hyperframes tts`. Authenticate with either:
|
|
101
|
+
|
|
102
|
+
- `GEMINI_API_KEY` or `GOOGLE_API_KEY` (first set key wins).
|
|
103
|
+
- A service-account JSON file at `GOOGLE_APPLICATION_CREDENTIALS`, or injected
|
|
104
|
+
JSON in `GCS_CREDS`. Install `google-auth requests` in the Python 3 environment
|
|
105
|
+
used by the helper. A configured file takes precedence over injected JSON.
|
|
106
|
+
|
|
107
|
+
API keys take precedence over service accounts: unset both key variables to use
|
|
108
|
+
OAuth. Never put credentials in a request file or composition. The helper
|
|
109
|
+
obtains a fresh OAuth token for each generation with the
|
|
110
|
+
`generative-language.retriever` scope. The quota project resolves from
|
|
111
|
+
`GOOGLE_CLOUD_PROJECT`, then `GCLOUD_PROJECT_ID`, then the service-account JSON.
|
|
112
|
+
User ADC files and metadata-server authentication are not supported.
|
|
113
|
+
|
|
114
|
+
Both routes call the Gemini Developer Interactions API, not Cloud TTS or Vertex
|
|
115
|
+
AI. Those APIs have separate model catalogs and access requirements.
|
|
116
|
+
|
|
117
|
+
Save this as `audio_request.json` in the project:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"provider": "gemini",
|
|
122
|
+
"tts_model": "gemini-3.8-flash-tts",
|
|
123
|
+
"voice": "Kore",
|
|
124
|
+
"lang": "en",
|
|
125
|
+
"style": "Warm, clear, conversational. Leave a short pause between sentences.",
|
|
126
|
+
"lines": [
|
|
127
|
+
{ "id": "intro", "text": "Every word has a moment. Let the picture follow the voice." }
|
|
128
|
+
],
|
|
129
|
+
"bgm": { "mode": "none" }
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
node <SKILL_DIR>/audio/scripts/audio.mjs \
|
|
135
|
+
--request ./audio_request.json --hyperframes . --out ./audio_meta.json --only tts
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The engine saves `assets/voice/intro.wav`, measures its duration, and transcribes
|
|
139
|
+
it into `voices[].words` in `audio_meta.json`. Check that every requested line
|
|
140
|
+
has audio and nonempty word timings before building a captioned video. Review
|
|
141
|
+
the timings against the actual audio; transcription is estimated alignment,
|
|
142
|
+
not native TTS timestamps. Do not distribute words evenly across a clip.
|
|
143
|
+
|
|
144
|
+
- **Models:** `gemini-3.8-flash-tts` (default), `gemini-3.8-flash-lite-tts`,
|
|
145
|
+
`gemini-3.1-flash-tts-preview`, `gemini-2.5-pro-preview-tts`, and
|
|
146
|
+
`gemini-2.5-flash-preview-tts`. Use these exact Developer API IDs; Cloud TTS
|
|
147
|
+
aliases such as `gemini-2.5-flash-tts` are not accepted.
|
|
148
|
+
- **Voice:** `Kore` by default; pass another prebuilt voice or an existing custom
|
|
149
|
+
voice ID for 3.8. Older models require prebuilt voices. Creating or replicating
|
|
150
|
+
voices is outside this helper.
|
|
151
|
+
- **Delivery:** Put directions in `style`, not in spoken `text`. Each line can
|
|
152
|
+
override `style`. 3.8 uses structured annotations; older models receive a
|
|
153
|
+
delivery prompt before the transcript. Check that directions were not spoken.
|
|
154
|
+
Use style for pacing; numeric `speed` must be omitted or 1.
|
|
155
|
+
- **Audio:** 3.8 returns a complete WAV. Older models return mono 16-bit PCM,
|
|
156
|
+
which the helper wraps as WAV at the returned sample rate without resampling.
|
|
157
|
+
No generation calls run during playback or rendering.
|
|
158
|
+
- **Timing:** This adapter requests no native word timestamps. It uses the
|
|
159
|
+
existing transcription pass. `lang` selects transcription language; Gemini
|
|
160
|
+
infers speech language from the text.
|
|
161
|
+
- **Workflow adapters:** Product-launch, faceless-explainer, and PR-video audio
|
|
162
|
+
scripts accept `--provider gemini --voice Kore --tts-model gemini-3.8-flash-tts
|
|
163
|
+
--style "Warm and clear"`.
|
|
164
|
+
|
|
165
|
+
An API error is reported as a failed line; an explicitly chosen Gemini voice
|
|
166
|
+
never silently falls back to another provider. Check the engine's anomalies
|
|
167
|
+
and output metadata, not only its exit code.
|
|
168
|
+
|
|
169
|
+
API contract: [Google's speech generation guide](https://ai.google.dev/gemini-api/docs/speech-generation).
|
|
92
170
|
|
|
93
171
|
## ffmpeg requirement
|
|
94
172
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
//
|
|
16
16
|
// ── audio_request.json (input) ────────────────────────────────────────────────
|
|
17
17
|
// {
|
|
18
|
-
// "provider": "auto", // auto|heygen|elevenlabs|kokoro (override: --provider)
|
|
18
|
+
// "provider": "auto", // auto|heygen|elevenlabs|kokoro|gemini (override: --provider)
|
|
19
19
|
// "lang": "en", "speed": 1.0,
|
|
20
20
|
// "lines": [ // one TTS unit each; id joins back to the caller's model
|
|
21
21
|
// { "id": "01", "text": "...", "sfx": ["whoosh", "ui click"] }
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
// the generate path it is spawned detached (bgm_pending:true) — run wait-bgm.mjs
|
|
40
40
|
// before assembling.
|
|
41
41
|
|
|
42
|
-
import { existsSync, mkdirSync, readFileSync
|
|
42
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
43
43
|
import { dirname, join, resolve } from "node:path";
|
|
44
44
|
import { fileURLToPath } from "node:url";
|
|
45
45
|
import { heygenAuthHeaders, heygenCredential, loadEnvFromDir } from "./lib/heygen.mjs";
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
import { generateBgmDetached, inferBgmPrompt, retrieveBgm } from "./lib/bgm.mjs";
|
|
55
55
|
import { resolveSfx } from "./lib/sfx.mjs";
|
|
56
56
|
import { mapWithConcurrency } from "./lib/concurrency.mjs";
|
|
57
|
+
import { openAudioMeta } from "./lib/audio-meta.mjs";
|
|
57
58
|
|
|
58
59
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
59
60
|
const argv = process.argv.slice(2);
|
|
@@ -112,10 +113,10 @@ const speed = Number(speedOverride ?? request.speed ?? 1.0) || 1.0;
|
|
|
112
113
|
// ── env + HeyGen availability (the single switch) ─────────────────────────────
|
|
113
114
|
loadEnvFromDir(hyperframesDir);
|
|
114
115
|
const heygenOK = heygenCredential() !== null;
|
|
115
|
-
const headers = heygenOK ? heygenAuthHeaders() : null;
|
|
116
116
|
|
|
117
117
|
// ── merge base: preserve sections not selected by --only ──────────────────────
|
|
118
|
-
const
|
|
118
|
+
const audioMeta = openAudioMeta(outPath);
|
|
119
|
+
const prev = audioMeta.value;
|
|
119
120
|
const anomalies = [];
|
|
120
121
|
|
|
121
122
|
// ── TTS ───────────────────────────────────────────────────────────────────────
|
|
@@ -151,6 +152,8 @@ if (only.has("tts") && lines.length) {
|
|
|
151
152
|
voiceId,
|
|
152
153
|
lang,
|
|
153
154
|
speed,
|
|
155
|
+
model: flag("tts-model", request.tts_model),
|
|
156
|
+
style: flag("style", line.style ?? request.style),
|
|
154
157
|
wavAbs: abs,
|
|
155
158
|
hyperframesDir,
|
|
156
159
|
});
|
|
@@ -208,7 +211,12 @@ if (only.has("bgm")) {
|
|
|
208
211
|
console.error(`· bgm: disabled`);
|
|
209
212
|
} else if (mode === "retrieve") {
|
|
210
213
|
try {
|
|
211
|
-
bgm = await retrieveBgm({
|
|
214
|
+
bgm = await retrieveBgm({
|
|
215
|
+
query: request.bgm?.query,
|
|
216
|
+
headers: heygenAuthHeaders(),
|
|
217
|
+
hyperframesDir,
|
|
218
|
+
hasVoice,
|
|
219
|
+
});
|
|
212
220
|
if (bgm) {
|
|
213
221
|
bgmFields.bgm_provider = "heygen";
|
|
214
222
|
bgmFields.bgm_mode = "retrieve";
|
|
@@ -260,6 +268,7 @@ if (only.has("sfx")) {
|
|
|
260
268
|
.map((name) => ({ id: String(l.id), name: String(name).trim() }))
|
|
261
269
|
.filter((c) => c.name),
|
|
262
270
|
);
|
|
271
|
+
const headers = heygenOK && cues.length ? heygenAuthHeaders() : null;
|
|
263
272
|
const res = await resolveSfx({ cues, heygenOK, headers, hyperframesDir, sfxLibDir });
|
|
264
273
|
sfx = res.sfx;
|
|
265
274
|
anomalies.push(...res.anomalies);
|
|
@@ -279,7 +288,7 @@ const meta = {
|
|
|
279
288
|
total_duration_s: totalDuration,
|
|
280
289
|
};
|
|
281
290
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
282
|
-
|
|
291
|
+
audioMeta.write(meta);
|
|
283
292
|
|
|
284
293
|
console.log(`✓ audio engine → ${outPath}`);
|
|
285
294
|
console.log(` heygen: ${heygenOK ? "yes" : "no"} · ran: ${[...only].join(",")}`);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { mkdtempSync, writeFileSync, readFileSync, mkdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
// Exercise the actual engine and provider together; only the paid API and
|
|
9
|
+
// external audio tools are fixtures. This is not a live synthesis/render test.
|
|
10
|
+
for (const expired of [false, true]) {
|
|
11
|
+
for (const only of ["tts", "tts,bgm,sfx"]) {
|
|
12
|
+
test(`Gemini engine returns caption metadata with ${expired ? "expired" : "absent"} HeyGen credentials (${only})`, (t) => {
|
|
13
|
+
const dir = mkdtempSync(join(tmpdir(), "hf-gemini-pipeline-"));
|
|
14
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
15
|
+
const config = join(dir, "heygen");
|
|
16
|
+
mkdirSync(config);
|
|
17
|
+
if (expired)
|
|
18
|
+
writeFileSync(
|
|
19
|
+
join(config, "credentials"),
|
|
20
|
+
JSON.stringify({
|
|
21
|
+
oauth: { access_token: "expired-fixture", expires_at: "2000-01-01T00:00:00Z" },
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
const bin = join(dir, "bin");
|
|
25
|
+
mkdirSync(bin);
|
|
26
|
+
const preload = join(dir, "fetch.mjs");
|
|
27
|
+
writeFileSync(
|
|
28
|
+
preload,
|
|
29
|
+
`
|
|
30
|
+
import assert from 'node:assert/strict';
|
|
31
|
+
globalThis.fetch = async (url, options) => {
|
|
32
|
+
assert.equal(url, 'https://generativelanguage.googleapis.com/v1beta/interactions');
|
|
33
|
+
const body = JSON.parse(options.body);
|
|
34
|
+
assert.equal(body.model, 'gemini-3.8-flash-lite-tts');
|
|
35
|
+
assert.equal(body.input[0].content[0].annotations[0].style, 'Line direction');
|
|
36
|
+
const bytes = Buffer.alloc(48);
|
|
37
|
+
bytes.write('RIFF'); bytes.write('WAVE', 8);
|
|
38
|
+
return Response.json({status:'completed', steps:[{type:'model_output', content:[
|
|
39
|
+
{type:'audio', mime_type:'audio/wav', data:bytes.toString('base64')}
|
|
40
|
+
]}]});
|
|
41
|
+
};
|
|
42
|
+
`,
|
|
43
|
+
);
|
|
44
|
+
writeFileSync(
|
|
45
|
+
join(bin, "npx"),
|
|
46
|
+
`#!${process.execPath}
|
|
47
|
+
const assert = require('node:assert/strict');
|
|
48
|
+
const fs = require('node:fs');
|
|
49
|
+
const path = require('node:path');
|
|
50
|
+
const args = process.argv.slice(2);
|
|
51
|
+
assert.deepEqual(args.slice(0, 3), ['hyperframes', 'transcribe', 'assets/voice/intro.wav']);
|
|
52
|
+
assert.ok(fs.existsSync(args[2]));
|
|
53
|
+
assert.equal(args[args.indexOf('--model')+1], 'small.en');
|
|
54
|
+
fs.writeFileSync(path.join(args[args.indexOf('--dir')+1], 'transcript.json'), JSON.stringify([
|
|
55
|
+
{text:'Hello',start:0.1,end:0.4}, {text:'there.',start:0.5,end:0.9}
|
|
56
|
+
]));
|
|
57
|
+
`,
|
|
58
|
+
{ mode: 0o755 },
|
|
59
|
+
);
|
|
60
|
+
writeFileSync(join(bin, "ffprobe"), "#!/bin/sh\necho 1.25\n", { mode: 0o755 });
|
|
61
|
+
writeFileSync(join(dir, ".env"), "");
|
|
62
|
+
writeFileSync(
|
|
63
|
+
join(dir, "audio_request.json"),
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
provider: "gemini",
|
|
66
|
+
tts_model: "gemini-3.8-flash-lite-tts",
|
|
67
|
+
style: "Global direction",
|
|
68
|
+
lines: [{ id: "intro", text: "Hello there.", style: "Line direction" }],
|
|
69
|
+
bgm: { mode: "none" },
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
const result = spawnSync(
|
|
73
|
+
process.execPath,
|
|
74
|
+
[
|
|
75
|
+
"--import",
|
|
76
|
+
preload,
|
|
77
|
+
new URL("./audio.mjs", import.meta.url).pathname,
|
|
78
|
+
"--hyperframes",
|
|
79
|
+
dir,
|
|
80
|
+
"--only",
|
|
81
|
+
only,
|
|
82
|
+
],
|
|
83
|
+
{
|
|
84
|
+
encoding: "utf8",
|
|
85
|
+
env: {
|
|
86
|
+
...process.env,
|
|
87
|
+
HEYGEN_CONFIG_DIR: config,
|
|
88
|
+
HEYGEN_API_KEY: "",
|
|
89
|
+
HYPERFRAMES_API_KEY: "",
|
|
90
|
+
GEMINI_API_KEY: "fixture-key",
|
|
91
|
+
PATH: `${bin}:${process.env.PATH}`,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
assert.equal(result.status, 0, result.stderr);
|
|
96
|
+
const meta = JSON.parse(readFileSync(join(dir, "audio_meta.json"), "utf8"));
|
|
97
|
+
assert.equal(meta.tts_provider, "gemini");
|
|
98
|
+
assert.equal(meta.voice_id, "Kore");
|
|
99
|
+
assert.deepEqual(meta.voices, [
|
|
100
|
+
{
|
|
101
|
+
id: "intro",
|
|
102
|
+
path: "assets/voice/intro.wav",
|
|
103
|
+
duration_s: 1.25,
|
|
104
|
+
words: [
|
|
105
|
+
{ id: "w0", text: "Hello", start: 0.1, end: 0.4 },
|
|
106
|
+
{ id: "w1", text: "there.", start: 0.5, end: 0.9 },
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
assert.equal(meta.total_duration_s, 1.25);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { closeSync, ftruncateSync, openSync, readFileSync, writeSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
// Keep the merge base and output on the same file even if its pathname changes
|
|
4
|
+
// while audio generation runs. The CLI owns this handle until write or exit.
|
|
5
|
+
export function openAudioMeta(path) {
|
|
6
|
+
let fd;
|
|
7
|
+
try {
|
|
8
|
+
fd = openSync(path, "r+");
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (error.code !== "ENOENT") throw error;
|
|
11
|
+
}
|
|
12
|
+
let value = {};
|
|
13
|
+
if (fd !== undefined) {
|
|
14
|
+
try {
|
|
15
|
+
value = JSON.parse(readFileSync(fd, "utf8"));
|
|
16
|
+
} catch (error) {
|
|
17
|
+
closeSync(fd);
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
value,
|
|
23
|
+
write(meta) {
|
|
24
|
+
const bytes = Buffer.from(JSON.stringify(meta, null, 2));
|
|
25
|
+
// Defer new-file creation until generation succeeds. Never overwrite a
|
|
26
|
+
// file (or follow a link) that appeared since the missing merge base.
|
|
27
|
+
if (fd === undefined) fd = openSync(path, "wx");
|
|
28
|
+
try {
|
|
29
|
+
let offset = 0;
|
|
30
|
+
while (offset < bytes.length) {
|
|
31
|
+
offset += writeSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
32
|
+
}
|
|
33
|
+
ftruncateSync(fd, bytes.length);
|
|
34
|
+
} finally {
|
|
35
|
+
closeSync(fd);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
statSync,
|
|
9
|
+
symlinkSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { openAudioMeta } from "./audio-meta.mjs";
|
|
17
|
+
|
|
18
|
+
function fixture(t) {
|
|
19
|
+
const dir = mkdtempSync(join(tmpdir(), "audio-meta-"));
|
|
20
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("updates the read file from byte zero, truncates, and retains its mode", (t) => {
|
|
25
|
+
const path = join(fixture(t), "meta.json");
|
|
26
|
+
writeFileSync(path, JSON.stringify({ voices: ["long existing voice metadata"] }), {
|
|
27
|
+
mode: 0o600,
|
|
28
|
+
});
|
|
29
|
+
const originalMode = statSync(path).mode;
|
|
30
|
+
const handle = openAudioMeta(path);
|
|
31
|
+
assert.deepEqual(handle.value, { voices: ["long existing voice metadata"] });
|
|
32
|
+
handle.write({ voices: ["声"] });
|
|
33
|
+
assert.equal(readFileSync(path, "utf8"), JSON.stringify({ voices: ["声"] }, null, 2));
|
|
34
|
+
assert.equal(statSync(path).mode, originalMode);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("pathname replacement cannot redirect the existing-file write", (t) => {
|
|
38
|
+
const dir = fixture(t);
|
|
39
|
+
const path = join(dir, "meta.json");
|
|
40
|
+
const moved = join(dir, "original.json");
|
|
41
|
+
writeFileSync(path, "{}");
|
|
42
|
+
const handle = openAudioMeta(path);
|
|
43
|
+
renameSync(path, moved);
|
|
44
|
+
writeFileSync(path, "replacement must survive");
|
|
45
|
+
handle.write({ voices: [] });
|
|
46
|
+
assert.equal(readFileSync(path, "utf8"), "replacement must survive");
|
|
47
|
+
assert.deepEqual(JSON.parse(readFileSync(moved, "utf8")), { voices: [] });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("existing symlink outputs retain their original target even after retargeting", (t) => {
|
|
51
|
+
const dir = fixture(t);
|
|
52
|
+
const path = join(dir, "meta.json");
|
|
53
|
+
const target = join(dir, "target.json");
|
|
54
|
+
const victim = join(dir, "victim.json");
|
|
55
|
+
writeFileSync(target, "{}");
|
|
56
|
+
writeFileSync(victim, "untouched");
|
|
57
|
+
symlinkSync(target, path);
|
|
58
|
+
const handle = openAudioMeta(path);
|
|
59
|
+
rmSync(path);
|
|
60
|
+
symlinkSync(victim, path);
|
|
61
|
+
handle.write({ bgm: null });
|
|
62
|
+
assert.equal(readFileSync(victim, "utf8"), "untouched");
|
|
63
|
+
assert.deepEqual(JSON.parse(readFileSync(target, "utf8")), { bgm: null });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("missing output is created only when saved", (t) => {
|
|
67
|
+
const path = join(fixture(t), "meta.json");
|
|
68
|
+
const handle = openAudioMeta(path);
|
|
69
|
+
assert.deepEqual(handle.value, {});
|
|
70
|
+
assert.throws(() => statSync(path), { code: "ENOENT" });
|
|
71
|
+
handle.write({ sfx: [] });
|
|
72
|
+
assert.deepEqual(JSON.parse(readFileSync(path, "utf8")), { sfx: [] });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
for (const replacement of ["file", "symlink"]) {
|
|
76
|
+
test(`new output does not overwrite a racing ${replacement}`, (t) => {
|
|
77
|
+
const dir = fixture(t);
|
|
78
|
+
const path = join(dir, "meta.json");
|
|
79
|
+
const victim = join(dir, "victim.json");
|
|
80
|
+
const handle = openAudioMeta(path);
|
|
81
|
+
writeFileSync(victim, "untouched");
|
|
82
|
+
if (replacement === "symlink") symlinkSync(victim, path);
|
|
83
|
+
else writeFileSync(path, "untouched");
|
|
84
|
+
assert.throws(() => handle.write({}), { code: "EEXIST" });
|
|
85
|
+
assert.equal(readFileSync(path, "utf8"), "untouched");
|
|
86
|
+
assert.equal(readFileSync(victim, "utf8"), "untouched");
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test("malformed merge base remains an error without changing bytes", (t) => {
|
|
91
|
+
const path = join(fixture(t), "meta.json");
|
|
92
|
+
writeFileSync(path, "bad json");
|
|
93
|
+
assert.throws(() => openAudioMeta(path), SyntaxError);
|
|
94
|
+
assert.equal(readFileSync(path, "utf8"), "bad json");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("CLI partial run retains unselected voices, BGM and SFX", (t) => {
|
|
98
|
+
const dir = fixture(t);
|
|
99
|
+
const path = join(dir, "meta.json");
|
|
100
|
+
const request = join(dir, "request.json");
|
|
101
|
+
const previous = {
|
|
102
|
+
voices: [{ id: "a", duration_s: 2 }],
|
|
103
|
+
bgm: { path: "bgm.wav" },
|
|
104
|
+
sfx: [{ name: "click" }],
|
|
105
|
+
tts_provider: "kokoro",
|
|
106
|
+
voice_id: "voice-a",
|
|
107
|
+
};
|
|
108
|
+
writeFileSync(path, JSON.stringify(previous));
|
|
109
|
+
writeFileSync(request, "{}");
|
|
110
|
+
const result = spawnSync(
|
|
111
|
+
process.execPath,
|
|
112
|
+
[
|
|
113
|
+
fileURLToPath(new URL("../audio.mjs", import.meta.url)),
|
|
114
|
+
"--request",
|
|
115
|
+
request,
|
|
116
|
+
"--hyperframes",
|
|
117
|
+
dir,
|
|
118
|
+
"--out",
|
|
119
|
+
path,
|
|
120
|
+
"--only",
|
|
121
|
+
"none",
|
|
122
|
+
],
|
|
123
|
+
{
|
|
124
|
+
encoding: "utf8",
|
|
125
|
+
env: {
|
|
126
|
+
...process.env,
|
|
127
|
+
HEYGEN_CONFIG_DIR: dir,
|
|
128
|
+
HEYGEN_API_KEY: "",
|
|
129
|
+
HYPERFRAMES_API_KEY: "",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
assert.equal(result.status, 0, result.stderr);
|
|
134
|
+
const actual = JSON.parse(readFileSync(path, "utf8"));
|
|
135
|
+
for (const key of Object.keys(previous)) assert.deepEqual(actual[key], previous[key]);
|
|
136
|
+
});
|
|
@@ -20,12 +20,8 @@ import { pythonInvocation } from "./python.mjs";
|
|
|
20
20
|
const r3 = (x) => Number(x.toFixed(3));
|
|
21
21
|
const lyriaKey = () => process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || "";
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// beneath, so BGM sits forward at 0.9. Callers may override per composition.
|
|
26
|
-
export const BGM_BED_VOLUME = 0.12;
|
|
27
|
-
export const BGM_SILENT_VOLUME = 0.9;
|
|
28
|
-
export const bgmDefaultVolume = (hasVoice) => (hasVoice ? BGM_BED_VOLUME : BGM_SILENT_VOLUME);
|
|
23
|
+
import { bgmDefaultVolume } from "./bgm-volume.mjs";
|
|
24
|
+
export { BGM_BED_VOLUME, BGM_SILENT_VOLUME, bgmDefaultVolume } from "./bgm-volume.mjs";
|
|
29
25
|
|
|
30
26
|
const BGM_PY_DEPS = ["transformers", "torch", "soundfile", "numpy"];
|
|
31
27
|
const BGM_PY_PROBE =
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { pythonInvocation } from "./python.mjs";
|
|
4
|
+
|
|
5
|
+
export function geminiConfigured() {
|
|
6
|
+
return Boolean(
|
|
7
|
+
process.env.GEMINI_API_KEY ||
|
|
8
|
+
process.env.GOOGLE_API_KEY ||
|
|
9
|
+
process.env.GOOGLE_APPLICATION_CREDENTIALS ||
|
|
10
|
+
process.env.GCS_CREDS,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function geminiAuth({ run = spawnSync } = {}) {
|
|
15
|
+
const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
16
|
+
if (key) return { headers: { "x-goog-api-key": key }, secret: key };
|
|
17
|
+
if (!geminiConfigured()) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Gemini TTS needs GEMINI_API_KEY or GOOGLE_API_KEY, or service-account credentials (GOOGLE_APPLICATION_CREDENTIALS or GCS_CREDS)",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const { cmd, args } = pythonInvocation([
|
|
23
|
+
fileURLToPath(new URL("./gemini-auth.py", import.meta.url)),
|
|
24
|
+
]);
|
|
25
|
+
const result = run(cmd, args, { encoding: "utf8", timeout: 60_000, maxBuffer: 1024 * 1024 });
|
|
26
|
+
let data;
|
|
27
|
+
try {
|
|
28
|
+
data = JSON.parse(result.stdout);
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Gemini service-account authentication failed; check Python and install google-auth requests",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (result.status !== 0 || !data.token) {
|
|
35
|
+
// Do not echo subprocess output: it could contain credentials.
|
|
36
|
+
throw new Error(
|
|
37
|
+
"Gemini service-account authentication failed; check service-account JSON, permissions, and Python packages google-auth requests",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const headers = { Authorization: `Bearer ${data.token}` };
|
|
41
|
+
if (data.project) headers["x-goog-user-project"] = data.project;
|
|
42
|
+
return { headers, secret: data.token };
|
|
43
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Obtain Gemini OAuth headers without persisting credentials or tokens."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
# Keep the credential subprocess protocol UTF-8 on every platform.
|
|
8
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
9
|
+
if hasattr(_stream, "reconfigure"):
|
|
10
|
+
_stream.reconfigure(encoding="utf-8", errors=_stream.errors)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def authenticate():
|
|
14
|
+
try:
|
|
15
|
+
from google.auth.transport.requests import Request
|
|
16
|
+
from google.oauth2 import service_account
|
|
17
|
+
except ImportError:
|
|
18
|
+
return {"error": "Service-account auth needs Python packages: pip install google-auth requests"}
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
path = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
|
|
22
|
+
if path:
|
|
23
|
+
with open(path, encoding="utf-8") as source:
|
|
24
|
+
info = json.load(source)
|
|
25
|
+
else:
|
|
26
|
+
info = json.loads(os.environ["GCS_CREDS"])
|
|
27
|
+
if info.get("type") != "service_account":
|
|
28
|
+
return {"error": "Gemini requires service-account JSON, not user ADC credentials"}
|
|
29
|
+
# Only exchange credentials with Google's OAuth server.
|
|
30
|
+
info["token_uri"] = "https://oauth2.googleapis.com/token"
|
|
31
|
+
credentials = service_account.Credentials.from_service_account_info(
|
|
32
|
+
info,
|
|
33
|
+
scopes=[
|
|
34
|
+
"https://www.googleapis.com/auth/generative-language.retriever",
|
|
35
|
+
],
|
|
36
|
+
)
|
|
37
|
+
credentials.refresh(Request())
|
|
38
|
+
project = (
|
|
39
|
+
os.environ.get("GOOGLE_CLOUD_PROJECT")
|
|
40
|
+
or os.environ.get("GCLOUD_PROJECT_ID")
|
|
41
|
+
or info.get("project_id")
|
|
42
|
+
)
|
|
43
|
+
return {"token": credentials.token, "project": project}
|
|
44
|
+
except Exception:
|
|
45
|
+
# Library exceptions can contain credential data. Never forward them.
|
|
46
|
+
return {"error": "Gemini service-account authentication failed; check credentials, permissions, and network access"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
result = authenticate()
|
|
51
|
+
print(json.dumps(result))
|
|
52
|
+
sys.exit(1 if "error" in result else 0)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { pythonInvocation } from "./python.mjs";
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { geminiAuth, geminiConfigured } from "./gemini-auth.mjs";
|
|
7
|
+
import { pickProvider } from "./tts.mjs";
|
|
8
|
+
|
|
9
|
+
function env(t) {
|
|
10
|
+
const saved = { ...process.env };
|
|
11
|
+
for (const key of [
|
|
12
|
+
"GEMINI_API_KEY",
|
|
13
|
+
"GOOGLE_API_KEY",
|
|
14
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
15
|
+
"GCS_CREDS",
|
|
16
|
+
])
|
|
17
|
+
delete process.env[key];
|
|
18
|
+
t.after(() => {
|
|
19
|
+
process.env = saved;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("API keys take precedence over service accounts without invoking Python", (t) => {
|
|
24
|
+
env(t);
|
|
25
|
+
process.env.GEMINI_API_KEY = "gemini-key";
|
|
26
|
+
process.env.GOOGLE_API_KEY = "google-key";
|
|
27
|
+
process.env.GCS_CREDS = "private-json";
|
|
28
|
+
const run = () => assert.fail("must not spawn Python");
|
|
29
|
+
assert.deepEqual(geminiAuth({ run }).headers, { "x-goog-api-key": "gemini-key" });
|
|
30
|
+
delete process.env.GEMINI_API_KEY;
|
|
31
|
+
assert.deepEqual(geminiAuth({ run }).headers, { "x-goog-api-key": "google-key" });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("service-account configuration selects Gemini and returns scoped helper headers", (t) => {
|
|
35
|
+
env(t);
|
|
36
|
+
assert.equal(geminiConfigured(), false);
|
|
37
|
+
assert.throws(() => geminiAuth(), /needs GEMINI_API_KEY/);
|
|
38
|
+
for (const key of ["GOOGLE_APPLICATION_CREDENTIALS", "GCS_CREDS"]) {
|
|
39
|
+
process.env[key] = "credential-source";
|
|
40
|
+
assert.equal(pickProvider("gemini"), "gemini");
|
|
41
|
+
const auth = geminiAuth({
|
|
42
|
+
run: (_, args, options) => {
|
|
43
|
+
assert.ok(args.at(-1).endsWith("gemini-auth.py"));
|
|
44
|
+
assert.ok(!args.includes("credential-source"));
|
|
45
|
+
assert.equal(options.timeout, 60000);
|
|
46
|
+
return {
|
|
47
|
+
status: 0,
|
|
48
|
+
stdout: JSON.stringify({ token: "access-token", project: "quota-project" }),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
assert.deepEqual(auth.headers, {
|
|
53
|
+
Authorization: "Bearer access-token",
|
|
54
|
+
"x-goog-user-project": "quota-project",
|
|
55
|
+
});
|
|
56
|
+
delete process.env[key];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("failed, malformed, or missing Python output cannot leak secrets or fall back", (t) => {
|
|
61
|
+
env(t);
|
|
62
|
+
process.env.GCS_CREDS = "private-json";
|
|
63
|
+
for (const result of [
|
|
64
|
+
{ status: 1, stdout: '{"error":"private-json"}', stderr: "private-json" },
|
|
65
|
+
{ status: null, stdout: "private-json", error: new Error("private-json") },
|
|
66
|
+
{ status: 0, stdout: "{}" },
|
|
67
|
+
]) {
|
|
68
|
+
assert.throws(
|
|
69
|
+
() => geminiAuth({ run: () => result }),
|
|
70
|
+
(error) => {
|
|
71
|
+
assert.match(error.message, /authentication failed/);
|
|
72
|
+
assert.ok(!error.message.includes("private-json"));
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Keep the Python credential-boundary suite reachable from the normal Node CI runner.
|
|
80
|
+
test("Python service-account boundary validates credentials, scope, transport and errors", () => {
|
|
81
|
+
const { cmd, args } = pythonInvocation([
|
|
82
|
+
fileURLToPath(new URL("./gemini-auth_test.py", import.meta.url)),
|
|
83
|
+
]);
|
|
84
|
+
const result = spawnSync(cmd, args, { encoding: "utf8", timeout: 30_000 });
|
|
85
|
+
assert.equal(result.status, 0, result.stderr || result.error?.message);
|
|
86
|
+
});
|