dsh-hyperframes 0.3.2 → 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 +4 -3
- package/README.md +5 -4
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -0
- package/package.json +2 -2
- 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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
|
|
9
|
+
const workflows = ["product-launch-video", "faceless-explainer", "pr-to-video"];
|
|
10
|
+
const css = `<style>
|
|
11
|
+
/* cyrillic */
|
|
12
|
+
@font-face { font-family: 'Manrope'; font-style: normal; font-weight: 200 800;
|
|
13
|
+
src: url(assets/fonts/aaa.woff2) format('woff2'); unicode-range: U+0400-045F; }
|
|
14
|
+
/* latin */
|
|
15
|
+
@font-face { font-family: 'Manrope'; font-style: normal; font-weight: 200 800;
|
|
16
|
+
src: url("assets/fonts/zzz.woff2") format('woff2'); unicode-range: U+0000-00FF; }
|
|
17
|
+
</style>`;
|
|
18
|
+
|
|
19
|
+
for (const skill of workflows) {
|
|
20
|
+
const scripts = new URL("../../" + skill + "/scripts/", import.meta.url);
|
|
21
|
+
const { brandFontFaces } = await import(new URL("captions.mjs", scripts));
|
|
22
|
+
test(skill + " stages every captured subset and captions preserve the same coverage", () => {
|
|
23
|
+
const project = mkdtempSync(join(tmpdir(), "frame-subsets-"));
|
|
24
|
+
try {
|
|
25
|
+
mkdirSync(join(project, "capture/extracted"), { recursive: true });
|
|
26
|
+
mkdirSync(join(project, "capture/assets/fonts"), { recursive: true });
|
|
27
|
+
writeFileSync(
|
|
28
|
+
join(project, "capture/extracted/tokens.json"),
|
|
29
|
+
JSON.stringify({ fonts: [{ family: "Manrope" }] }),
|
|
30
|
+
);
|
|
31
|
+
writeFileSync(join(project, "capture/extracted/page.html"), css);
|
|
32
|
+
writeFileSync(join(project, "capture/assets/fonts/aaa.woff2"), "cyrillic bytes");
|
|
33
|
+
writeFileSync(join(project, "capture/assets/fonts/zzz.woff2"), "latin bytes");
|
|
34
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
35
|
+
execFileSync(
|
|
36
|
+
process.execPath,
|
|
37
|
+
[
|
|
38
|
+
fileURLToPath(new URL("build-frame.mjs", scripts)),
|
|
39
|
+
"--preset",
|
|
40
|
+
"capsule",
|
|
41
|
+
"--hyperframes",
|
|
42
|
+
project,
|
|
43
|
+
],
|
|
44
|
+
{ stdio: "pipe" },
|
|
45
|
+
);
|
|
46
|
+
const frame = readFileSync(join(project, "frame.md"), "utf8");
|
|
47
|
+
const captions = brandFontFaces(join(project, "frame.md"), project);
|
|
48
|
+
for (const output of [frame, captions]) {
|
|
49
|
+
assert.match(output, /unicode-range:\s*U\+0400-045F/);
|
|
50
|
+
assert.match(output, /unicode-range:\s*U\+0000-00FF/);
|
|
51
|
+
assert.equal((output.match(/font-weight:\s*200 800/g) ?? []).length, 2);
|
|
52
|
+
assert.equal((output.match(/@font-face/g) ?? []).length, 2);
|
|
53
|
+
for (const match of output.matchAll(/url\(["']?(assets\/fonts\/[^"')]+)["']?\)/g)) {
|
|
54
|
+
assert.ok(
|
|
55
|
+
["cyrillic bytes", "latin bytes"].includes(
|
|
56
|
+
readFileSync(join(project, match[1]), "utf8"),
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
assert.equal(readdirSync(join(project, "assets/fonts")).length, 2);
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
rmSync(project, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test("captured font helper copies stay equal to the owner", () => {
|
|
70
|
+
const owner = readFileSync(new URL("./lib/captured-fonts.mjs", import.meta.url), "utf8");
|
|
71
|
+
for (const skill of workflows.slice(1)) {
|
|
72
|
+
assert.equal(
|
|
73
|
+
readFileSync(
|
|
74
|
+
new URL("../../" + skill + "/scripts/lib/captured-fonts.mjs", import.meta.url),
|
|
75
|
+
"utf8",
|
|
76
|
+
),
|
|
77
|
+
owner,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("captured rules stage only available local sources for requested families", async () => {
|
|
83
|
+
const { stageCapturedFonts } = await import("./lib/captured-fonts.mjs");
|
|
84
|
+
const project = mkdtempSync(join(tmpdir(), "captured-font-sources-"));
|
|
85
|
+
try {
|
|
86
|
+
mkdirSync(join(project, "capture/extracted"), { recursive: true });
|
|
87
|
+
mkdirSync(join(project, "capture/assets/fonts"), { recursive: true });
|
|
88
|
+
writeFileSync(join(project, "capture/assets/fonts/local.woff2"), "first");
|
|
89
|
+
writeFileSync(
|
|
90
|
+
join(project, "capture/extracted/page.html"),
|
|
91
|
+
`<style>
|
|
92
|
+
@font-face { font-family: Other; src: url(assets/fonts/local.woff2); }
|
|
93
|
+
@font-face { font-family: Manrope; font-weight: 600; unicode-range: U+0000-00FF;
|
|
94
|
+
src: local(Manrope), url(https://example.com/remote.woff2), url(assets/fonts/missing.woff2), url(assets/fonts/local.woff2) format("woff2"); }
|
|
95
|
+
</style>`,
|
|
96
|
+
);
|
|
97
|
+
const first = stageCapturedFonts(project, ["Manrope"]);
|
|
98
|
+
assert.equal(first.faces.length, 1);
|
|
99
|
+
assert.deepEqual([...first.families], ["manrope"]);
|
|
100
|
+
assert.doesNotMatch(first.faces[0], /Other|https:|missing|local\(/);
|
|
101
|
+
assert.match(first.faces[0], /unicode-range: U\+0000-00FF/);
|
|
102
|
+
const [file] = first.files;
|
|
103
|
+
assert.equal(readFileSync(join(project, file), "utf8"), "first");
|
|
104
|
+
writeFileSync(join(project, "capture/assets/fonts/local.woff2"), "refreshed");
|
|
105
|
+
stageCapturedFonts(project, ["Manrope"]);
|
|
106
|
+
assert.equal(readFileSync(join(project, file), "utf8"), "refreshed");
|
|
107
|
+
} finally {
|
|
108
|
+
rmSync(project, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
for (const skill of workflows) {
|
|
113
|
+
test(
|
|
114
|
+
skill + " keeps captured family ownership when downloads are missing or names overlap",
|
|
115
|
+
async () => {
|
|
116
|
+
const { brandFontFaces } = await import(
|
|
117
|
+
new URL("../../" + skill + "/scripts/captions.mjs", import.meta.url)
|
|
118
|
+
);
|
|
119
|
+
const project = mkdtempSync(join(tmpdir(), "font-ownership-"));
|
|
120
|
+
try {
|
|
121
|
+
mkdirSync(join(project, "capture/extracted"), { recursive: true });
|
|
122
|
+
mkdirSync(join(project, "capture/assets/fonts"), { recursive: true });
|
|
123
|
+
writeFileSync(
|
|
124
|
+
join(project, "frame.md"),
|
|
125
|
+
'typography:\n display: { fontFamily: "TT Norms Pro Mono", weight: 400 }\n body: { fontFamily: "TT Norms Pro", weight: 400 }\n',
|
|
126
|
+
);
|
|
127
|
+
writeFileSync(
|
|
128
|
+
join(project, "capture/assets/fonts/TT_Norms_Pro_Mono_Regular.woff2"),
|
|
129
|
+
"mono",
|
|
130
|
+
);
|
|
131
|
+
writeFileSync(
|
|
132
|
+
join(project, "capture/extracted/page.html"),
|
|
133
|
+
`<style>
|
|
134
|
+
@font-face { font-family: 'TT Norms Pro Mono'; src: url(assets/fonts/TT_Norms_Pro_Mono_Regular.woff2); unicode-range: U+0000-00FF; }
|
|
135
|
+
</style>`,
|
|
136
|
+
);
|
|
137
|
+
const mixed = brandFontFaces(join(project, "frame.md"), project);
|
|
138
|
+
assert.equal((mixed.match(/@font-face/g) ?? []).length, 1);
|
|
139
|
+
assert.doesNotMatch(mixed, /font-family: 'TT Norms Pro';/);
|
|
140
|
+
writeFileSync(
|
|
141
|
+
join(project, "capture/extracted/page.html"),
|
|
142
|
+
`<style>
|
|
143
|
+
@font-face { font-family: 'TT Norms Pro Mono'; src: url(assets/fonts/missing.woff2); }
|
|
144
|
+
</style>`,
|
|
145
|
+
);
|
|
146
|
+
assert.equal(brandFontFaces(join(project, "frame.md"), project), "");
|
|
147
|
+
} finally {
|
|
148
|
+
rmSync(project, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
UA_DEFAULT_COLORS,
|
|
43
43
|
} from "./lib/tokens.mjs";
|
|
44
44
|
|
|
45
|
+
import { stageCapturedFonts } from "./lib/captured-fonts.mjs";
|
|
46
|
+
|
|
45
47
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
46
48
|
const argv = process.argv.slice(2);
|
|
47
49
|
const flag = (name, def) => {
|
|
@@ -479,11 +481,12 @@ if (brandFonts.length) {
|
|
|
479
481
|
const famOf = (f) =>
|
|
480
482
|
fams.length === 1 ? fams[0] : ranked.find((x) => norm(f).includes(norm(x)));
|
|
481
483
|
const outDir = join(hyperframesDir, "assets/fonts");
|
|
482
|
-
const
|
|
483
|
-
const
|
|
484
|
+
const captured = stageCapturedFonts(hyperframesDir, fams);
|
|
485
|
+
const faces = [...captured.faces];
|
|
486
|
+
const stagedNames = new Set(captured.files);
|
|
484
487
|
for (const { d, f } of files) {
|
|
485
488
|
const fam = famOf(f);
|
|
486
|
-
if (!fam) continue;
|
|
489
|
+
if (!fam || captured.families.has(fam.toLowerCase())) continue;
|
|
487
490
|
const { n, w } = weightInfo(f);
|
|
488
491
|
const style = styleOf(f);
|
|
489
492
|
const clean = `${fam.replace(/[^A-Za-z0-9]/g, "")}-${w}${style === "italic" ? "-Italic" : ""}.${extOf(f)}`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { stageCapturedFonts } from "./lib/captured-fonts.mjs";
|
|
2
3
|
// captions.mjs — build the captions sub-composition from STORYBOARD + audio_meta.
|
|
3
4
|
//
|
|
4
5
|
// One mode: `build`. Reads STORYBOARD.md (frame order + durations → cumulative
|
|
@@ -263,7 +264,7 @@ function buildFromSkin(skin, groups, total, W, H, tokens, die, faces = "", fonts
|
|
|
263
264
|
out = fillOnce(
|
|
264
265
|
out,
|
|
265
266
|
/var GROUPS = \[\];/,
|
|
266
|
-
`var GROUPS = ${JSON.stringify(groups)};`,
|
|
267
|
+
`var GROUPS = ${JSON.stringify(groups).replace(/</g, "\\u003c")};`,
|
|
267
268
|
"`var GROUPS = [];` hole",
|
|
268
269
|
);
|
|
269
270
|
out = fillOnce(out, /var DURATION = 0;/, `var DURATION = ${total};`, "`var DURATION = 0;` hole");
|
|
@@ -344,7 +345,8 @@ function brandFontFaces(framePath, hyperframesDir) {
|
|
|
344
345
|
// underscore/hyphen-named brand font (e.g. TT Norms Pro), which is exactly the
|
|
345
346
|
// font_family_without_font_face bug.
|
|
346
347
|
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
347
|
-
const
|
|
348
|
+
const captured = stageCapturedFonts(hyperframesDir, families);
|
|
349
|
+
const faces = [...captured.faces];
|
|
348
350
|
const seen = new Set();
|
|
349
351
|
const claimed = new Set(); // each file is claimed by the MOST SPECIFIC family only
|
|
350
352
|
// Match the longest family key first so "TT Norms Pro" can't swallow the files that
|
|
@@ -363,12 +365,13 @@ function brandFontFaces(framePath, hyperframesDir) {
|
|
|
363
365
|
if (!/\.(woff2|woff|ttf|otf)$/i.test(f)) continue;
|
|
364
366
|
if (claimed.has(f)) continue; // a more specific family already took this file
|
|
365
367
|
if (!norm(f.replace(/\.(woff2|woff|ttf|otf)$/i, "")).startsWith(key)) continue;
|
|
368
|
+
claimed.add(f);
|
|
369
|
+
if (captured.families.has(fam.toLowerCase())) continue;
|
|
366
370
|
const w = weightOf(f);
|
|
367
371
|
const style = styleOf(f);
|
|
368
372
|
const dedup = `${fam}-${w}-${style}`;
|
|
369
373
|
if (seen.has(dedup)) continue; // one src per face; assets/fonts wins over capture
|
|
370
374
|
seen.add(dedup);
|
|
371
|
-
claimed.add(f);
|
|
372
375
|
faces.push(
|
|
373
376
|
` @font-face { font-family: '${fam}'; src: url('${d.rel}/${f}') format('${fmtOf(f)}'); font-weight: ${w}; font-style: ${style}; font-display: block; }`,
|
|
374
377
|
);
|
|
@@ -480,7 +483,7 @@ function buildCaptionsHtml(groups, total, W, H) {
|
|
|
480
483
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js" integrity="sha384-sG0Hv1tP1lZCk9KQmrIbY/XNwi+OY84GQqhMscbnsoBFqAz8KNCil1kvfL3Hbbk2" crossorigin="anonymous"></script>
|
|
481
484
|
<script>
|
|
482
485
|
(function () {
|
|
483
|
-
var GROUPS = ${JSON.stringify(groups)};
|
|
486
|
+
var GROUPS = ${JSON.stringify(groups).replace(/</g, "\\u003c")};
|
|
484
487
|
var cap = document.getElementById("cap");
|
|
485
488
|
var tl = gsap.timeline({ paused: true });
|
|
486
489
|
GROUPS.forEach(function (g) {
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// Thin wrapper over the shared packet builder in hyperframes
|
|
3
|
+
// Thin wrapper over the shared packet builder in hyperframes — this file only
|
|
4
4
|
// pins the paths that are specific to this workflow skill. The logic (frame
|
|
5
5
|
// splitting, rule citation, packet bounds, `_role.md` assembly) has one owner:
|
|
6
|
-
// ../../hyperframes
|
|
6
|
+
// ../../hyperframes/scripts/lib/frame-packets-core.mjs
|
|
7
7
|
|
|
8
8
|
import { dirname, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import * as core from "
|
|
10
|
+
import * as core from "./lib/frame-packets-core.mjs";
|
|
11
11
|
|
|
12
12
|
const SKILL_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
13
|
const CONFIG = {
|
|
14
14
|
animationDir: resolve(SKILL_DIR, "../hyperframes-animation"),
|
|
15
|
-
corePath: resolve(SKILL_DIR, "../hyperframes
|
|
15
|
+
corePath: resolve(SKILL_DIR, "../hyperframes/references/frame-worker-core.md"),
|
|
16
16
|
deltaPath: resolve(SKILL_DIR, "sub-agents/frame-worker.md"),
|
|
17
17
|
};
|
|
18
18
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
// Capture preserves subset and variable-axis descriptors in extracted/page.html.
|
|
5
|
+
// Filenames alone cannot identify a face's character coverage.
|
|
6
|
+
export function stageCapturedFonts(projectDir, families) {
|
|
7
|
+
const page = join(projectDir, "capture/extracted/page.html");
|
|
8
|
+
const result = { faces: [], families: new Set(), files: new Set() };
|
|
9
|
+
if (!existsSync(page)) return result;
|
|
10
|
+
const wanted = new Set(families.map((family) => family.toLowerCase()));
|
|
11
|
+
const html = readFileSync(page, "utf8");
|
|
12
|
+
const copied = new Set();
|
|
13
|
+
for (const style of html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)) {
|
|
14
|
+
const css = style[1].replace(/\/\*[\s\S]*?\*\//g, "");
|
|
15
|
+
for (const match of css.matchAll(/@font-face\s*\{([^}]+)\}/gi)) {
|
|
16
|
+
const body = match[1];
|
|
17
|
+
const familyValue = /(?:^|;)\s*font-family\s*:\s*([^;]+)/i.exec(body)?.[1]?.trim();
|
|
18
|
+
if (!familyValue) continue;
|
|
19
|
+
const family = familyValue.replace(/^(['"])(.*)\1$/, "$2");
|
|
20
|
+
if (!wanted.has(family.toLowerCase())) continue;
|
|
21
|
+
result.families.add(family.toLowerCase());
|
|
22
|
+
const source = /(?:^|;)\s*src\s*:\s*([^;]+)/i.exec(body);
|
|
23
|
+
if (!source) continue;
|
|
24
|
+
const sources = localFontSources(source[1], projectDir);
|
|
25
|
+
if (!sources.length) {
|
|
26
|
+
console.warn("Captured font has no downloaded source: " + family);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
for (const file of sources) {
|
|
30
|
+
if (copied.has(file.name)) continue;
|
|
31
|
+
if (!copied.size) mkdirSync(join(projectDir, "assets/fonts"), { recursive: true });
|
|
32
|
+
copyFileSync(file.source, join(projectDir, file.target));
|
|
33
|
+
copied.add(file.name);
|
|
34
|
+
result.files.add(file.target);
|
|
35
|
+
}
|
|
36
|
+
const src = sources.map((file) => file.css).join(", ");
|
|
37
|
+
const rule = "@font-face{" + body.replace(source[1], src) + "}";
|
|
38
|
+
if (!result.faces.includes(rule)) result.faces.push(rule);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function localFontSources(source, projectDir) {
|
|
45
|
+
const files = [];
|
|
46
|
+
for (const match of source.matchAll(
|
|
47
|
+
/url\(\s*(?:"([^"]+)"|'([^']+)'|([^\s)]+))\s*\)(\s*format\([^)]*\))?/gi,
|
|
48
|
+
)) {
|
|
49
|
+
const url = match[1] ?? match[2] ?? match[3];
|
|
50
|
+
const name = /^assets\/fonts\/([a-z0-9_.-]+\.(?:woff2?|ttf|otf))$/i.exec(url)?.[1];
|
|
51
|
+
if (!name) continue;
|
|
52
|
+
const path = join(projectDir, "capture/assets/fonts", name);
|
|
53
|
+
if (!existsSync(path)) continue;
|
|
54
|
+
const target = "assets/fonts/captured-" + name;
|
|
55
|
+
files.push({ name, source: path, target, css: 'url("' + target + '")' + (match[4] ?? "") });
|
|
56
|
+
}
|
|
57
|
+
return files;
|
|
58
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Frame packets inline the storyboard frame, blueprint and cited recipes.
|
|
2
|
+
// Workflow wrappers supply resource paths and workflow-specific sections.
|
|
3
|
+
// The role combines the shared worker contract with the workflow delta.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { basename, join, resolve } from "node:path";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
15
|
+
|
|
16
|
+
export function field(block, name) {
|
|
17
|
+
const match = block.match(new RegExp(`^-\\s+${name}:\\s*(.+)$`, "im"));
|
|
18
|
+
return match?.[1]?.trim() ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function splitFrames(storyboard) {
|
|
22
|
+
const matches = [...storyboard.matchAll(/^## Frame\s+([^\n]+)$/gm)];
|
|
23
|
+
return matches.map((match, index) => {
|
|
24
|
+
const start = match.index;
|
|
25
|
+
const end = matches[index + 1]?.index ?? storyboard.length;
|
|
26
|
+
return {
|
|
27
|
+
heading: match[1].trim(),
|
|
28
|
+
block: storyboard.slice(start, end).trim(),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function frameId(frame) {
|
|
34
|
+
const src = field(frame.block, "src");
|
|
35
|
+
if (!src) throw new Error(`${frame.heading}: missing src`);
|
|
36
|
+
return basename(src).replace(/\.html?$/i, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function selectedFile(path, heading) {
|
|
40
|
+
if (!path || !existsSync(path)) return "";
|
|
41
|
+
return `\n## ${heading}\n\n${readFileSync(path, "utf8").trim()}\n`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function escapeRegExp(id) {
|
|
45
|
+
return id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function knownRuleIds(animationDir) {
|
|
49
|
+
const rulesDir = join(animationDir, "rules");
|
|
50
|
+
if (!existsSync(rulesDir)) {
|
|
51
|
+
console.warn(
|
|
52
|
+
`frame-packets: no rules dir at ${rulesDir} — packets will inline no motion recipes`,
|
|
53
|
+
);
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return readdirSync(rulesDir)
|
|
57
|
+
.filter((name) => name.endsWith(".md"))
|
|
58
|
+
.map((name) => name.replace(/\.md$/, ""));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function citedRules(block, ruleIds) {
|
|
62
|
+
const explicit = (field(block, "rules") ?? "")
|
|
63
|
+
.split(/[,\s]+/)
|
|
64
|
+
.map((rule) => rule.trim())
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
const mentioned = ruleIds.filter((id) =>
|
|
67
|
+
new RegExp(`(?<![\\w-])${escapeRegExp(id)}(?![\\w-])`, "i").test(block),
|
|
68
|
+
);
|
|
69
|
+
return [...new Set([...explicit, ...mentioned])].filter((id) => ruleIds.includes(id));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// visual-design.md tells the author to write the blueprint as `<id> (Reproduce)`
|
|
73
|
+
// or `<id> (Adapt)` — the qualifier is direction for the frame worker, not part of
|
|
74
|
+
// the filename. Parse the field into the id it names (or null for `compose`), so
|
|
75
|
+
// no caller ever resolves a raw field value against the blueprints directory.
|
|
76
|
+
export function blueprintId(block) {
|
|
77
|
+
const raw = field(block, "blueprint");
|
|
78
|
+
if (!raw) return null;
|
|
79
|
+
const id = raw.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
80
|
+
return id && id.toLowerCase() !== "compose" ? id : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resourceSections(block, { animationDir, ruleIds, frameId }) {
|
|
84
|
+
let sections = "";
|
|
85
|
+
const blueprint = blueprintId(block);
|
|
86
|
+
if (blueprint) {
|
|
87
|
+
const blueprintsDir = join(animationDir, "blueprints");
|
|
88
|
+
const path = join(blueprintsDir, `${blueprint}.md`);
|
|
89
|
+
// An installed library must contain the named blueprint.
|
|
90
|
+
// An absent on-demand library warns, like missing motion rules.
|
|
91
|
+
if (!existsSync(blueprintsDir)) {
|
|
92
|
+
console.warn(
|
|
93
|
+
`frame-packets: no blueprints dir at ${blueprintsDir} — packets will inline no blueprint`,
|
|
94
|
+
);
|
|
95
|
+
} else if (!existsSync(path)) {
|
|
96
|
+
throw new Error(`${frameId ?? "frame"}: blueprint "${blueprint}" has no file at ${path}`);
|
|
97
|
+
} else {
|
|
98
|
+
sections += selectedFile(path, `Selected blueprint: ${blueprint}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const rule of citedRules(block, ruleIds)) {
|
|
102
|
+
sections += selectedFile(
|
|
103
|
+
join(animationDir, "rules", `${rule}.md`),
|
|
104
|
+
`Selected motion rule: ${rule}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return sections;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function buildRolePayload({ corePath, deltaPath, outDir }) {
|
|
111
|
+
const core = readFileSync(corePath, "utf8").trim();
|
|
112
|
+
const delta = readFileSync(deltaPath, "utf8").trim();
|
|
113
|
+
const role = `${core}\n\n---\n\n${delta}\n`;
|
|
114
|
+
mkdirSync(outDir, { recursive: true });
|
|
115
|
+
const path = join(outDir, "_role.md");
|
|
116
|
+
writeFileSync(path, role);
|
|
117
|
+
return { path, bytes: Buffer.byteLength(role) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildFramePackets({
|
|
121
|
+
projectDir,
|
|
122
|
+
storyboardPath = join(projectDir, "STORYBOARD.md"),
|
|
123
|
+
outDir = join(projectDir, ".hyperframes", "frame-packets"),
|
|
124
|
+
maxPacketBytes = 48_000,
|
|
125
|
+
animationDir,
|
|
126
|
+
corePath,
|
|
127
|
+
deltaPath,
|
|
128
|
+
// Per-workflow hooks (all optional):
|
|
129
|
+
// designTruthLine(projectDir) -> the packet's design-truth input line
|
|
130
|
+
// validateFrame(frame, id) -> throw to reject a frame before packing
|
|
131
|
+
// extraSections(block) -> extra packet sections appended after the rule recipes
|
|
132
|
+
designTruthLine = (dir) => `- Design tokens: ${join(resolve(dir), "frame.md")}`,
|
|
133
|
+
validateFrame,
|
|
134
|
+
extraSections,
|
|
135
|
+
}) {
|
|
136
|
+
const storyboard = readFileSync(storyboardPath, "utf8");
|
|
137
|
+
const frames = splitFrames(storyboard);
|
|
138
|
+
if (frames.length === 0) throw new Error("STORYBOARD.md has no frame blocks");
|
|
139
|
+
const ruleIds = knownRuleIds(animationDir);
|
|
140
|
+
|
|
141
|
+
const packets = frames.map((frame) => {
|
|
142
|
+
const id = frameId(frame);
|
|
143
|
+
if (validateFrame) validateFrame(frame, id);
|
|
144
|
+
const packet = `# Frame packet: ${id}\n\n## Project inputs\n\n- Project: ${resolve(projectDir)}\n${designTruthLine(projectDir)}\n- RULES_DIR: ${join(animationDir, "rules")}\n\n## Assigned storyboard block\n\n${frame.block}\n${resourceSections(frame.block, { animationDir, ruleIds, frameId: id })}${extraSections ? extraSections(frame.block) : ""}`;
|
|
145
|
+
const bytes = Buffer.byteLength(packet);
|
|
146
|
+
if (bytes > maxPacketBytes) {
|
|
147
|
+
throw new Error(`${id}: frame packet is ${bytes} bytes (limit ${maxPacketBytes})`);
|
|
148
|
+
}
|
|
149
|
+
return { frameId: id, path: join(outDir, `${id}.md`), bytes, packet };
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
mkdirSync(outDir, { recursive: true });
|
|
153
|
+
for (const { path, packet } of packets) writeFileSync(path, packet);
|
|
154
|
+
buildRolePayload({ corePath, deltaPath, outDir });
|
|
155
|
+
return packets.map(({ packet: _packet, ...result }) => result);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function flag(argv, name, fallback) {
|
|
159
|
+
const index = argv.indexOf(`--${name}`);
|
|
160
|
+
return index >= 0 && argv[index + 1] ? argv[index + 1] : fallback;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// realpath both sides: on macOS /tmp → /private/tmp, and node resolves the main
|
|
164
|
+
// module's symlinks in import.meta.url while argv[1] keeps the invoked spelling —
|
|
165
|
+
// a raw compare silently skips main() when invoked through any symlinked path.
|
|
166
|
+
export function isMainModule(importMetaUrl) {
|
|
167
|
+
if (!process.argv[1]) return false;
|
|
168
|
+
try {
|
|
169
|
+
return pathToFileURL(realpathSync(process.argv[1])).href === importMetaUrl;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function runCli({ buildFramePackets: build, buildRolePayload: buildRole }) {
|
|
176
|
+
const argv = process.argv.slice(2);
|
|
177
|
+
const projectDir = resolve(flag(argv, "project", "."));
|
|
178
|
+
const outDir = resolve(flag(argv, "out-dir", join(projectDir, ".hyperframes", "frame-packets")));
|
|
179
|
+
try {
|
|
180
|
+
const packets = build({
|
|
181
|
+
projectDir,
|
|
182
|
+
storyboardPath: resolve(flag(argv, "storyboard", join(projectDir, "STORYBOARD.md"))),
|
|
183
|
+
outDir,
|
|
184
|
+
});
|
|
185
|
+
const role = buildRole({ outDir });
|
|
186
|
+
console.log(`✓ frame packets: ${packets.length} bounded packet(s)`);
|
|
187
|
+
for (const packet of packets)
|
|
188
|
+
console.log(` ${packet.frameId}: ${packet.bytes} bytes → ${packet.path}`);
|
|
189
|
+
console.log(` worker role: ${role.bytes} bytes → ${role.path}`);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error(`✗ frame packets: ${error.message}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// ship standalone: installed via `npx skills add`, a skill's scripts can't reach
|
|
6
6
|
// the monorepo's core package, and the core export points at .ts source that
|
|
7
7
|
// `node` (which runs these scripts) can't load. CANONICAL contract = the core
|
|
8
|
-
// parser + skills/hyperframes
|
|
8
|
+
// parser + skills/hyperframes/references/storyboard-format.md; keep this in
|
|
9
9
|
// lockstep. Behavior: never throws, accepts freeform narrative, recognizes
|
|
10
10
|
// Frame/Beat/Scene headings at H2/H3, preserves unknown keys verbatim under
|
|
11
11
|
// `extra` (keys lowercased). Pure node — no deps.
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"note": "Directional. The injector picks __DX__/__DY__ from the direction and emits the horizontal OR vertical pair (not both).",
|
|
35
35
|
"gsap_template_horizontal": [
|
|
36
36
|
"tl.to(__OLD__, { x: __DX__, duration: __DUR__, ease: \"power3.inOut\" }, __T__);",
|
|
37
|
-
"tl.fromTo(__NEW__, { x: __DXIN__, opacity: 1 }, { x: 0, duration: __DUR__, ease: \"power3.inOut\" }, __T__);"
|
|
37
|
+
"tl.fromTo(__NEW__, { x: __DXIN__, opacity: 1 }, { x: 0, opacity: 1, duration: __DUR__, ease: \"power3.inOut\" }, __T__);"
|
|
38
38
|
],
|
|
39
39
|
"gsap_template_vertical": [
|
|
40
40
|
"tl.to(__OLD__, { y: __DY__, duration: __DUR__, ease: \"power3.inOut\" }, __T__);",
|
|
41
|
-
"tl.fromTo(__NEW__, { y: __DYIN__, opacity: 1 }, { y: 0, duration: __DUR__, ease: \"power3.inOut\" }, __T__);"
|
|
41
|
+
"tl.fromTo(__NEW__, { y: __DYIN__, opacity: 1 }, { y: 0, opacity: 1, duration: __DUR__, ease: \"power3.inOut\" }, __T__);"
|
|
42
42
|
]
|
|
43
43
|
},
|
|
44
44
|
{
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"note": "Old compresses to a vertical line on the left edge; new expands from the right edge. Incoming starts off (scaleX 0) so its higher-track stacking is harmless.",
|
|
62
62
|
"gsap_template": [
|
|
63
63
|
"tl.to(__OLD__, { scaleX: 0, transformOrigin: \"left center\", duration: __DUR__, ease: \"power3.inOut\" }, __T__);",
|
|
64
|
-
"tl.fromTo(__NEW__, { scaleX: 0, transformOrigin: \"right center\", opacity: 1 }, { scaleX: 1, transformOrigin: \"right center\", duration: __DUR__, ease: \"power3.inOut\" }, __T__);"
|
|
64
|
+
"tl.fromTo(__NEW__, { scaleX: 0, transformOrigin: \"right center\", opacity: 1 }, { scaleX: 1, transformOrigin: \"right center\", opacity: 1, duration: __DUR__, ease: \"power3.inOut\" }, __T__);"
|
|
65
65
|
]
|
|
66
66
|
}
|
|
67
67
|
],
|
|
@@ -117,16 +117,18 @@ function extendFrameTail(hyperframesDir, frame, baseDuration, targetDuration, di
|
|
|
117
117
|
let extended = 0;
|
|
118
118
|
const rewritten = html.replace(/<([A-Za-z][\w:-]*)\b([^>]*)>/g, (tag, name, attrs) => {
|
|
119
119
|
const durationMatch = attrs.match(/\bdata-duration="([\d.]+)"/);
|
|
120
|
-
if (!durationMatch) return tag;
|
|
121
|
-
const duration = Number(durationMatch[1]);
|
|
122
|
-
if (!Number.isFinite(duration)) return tag;
|
|
123
|
-
|
|
124
120
|
const compositionMatch = attrs.match(/\bdata-composition-id="([^"]+)"/);
|
|
125
121
|
if (compositionMatch?.[1] === compId && !foundRoot) {
|
|
126
122
|
foundRoot = true;
|
|
127
|
-
return
|
|
123
|
+
return durationMatch
|
|
124
|
+
? tag.replace(/\bdata-duration="[\d.]+"/, `data-duration="${targetDuration}"`)
|
|
125
|
+
: tag.replace(/(\s*\/?>)$/, ` data-duration="${targetDuration}"$1`);
|
|
128
126
|
}
|
|
129
127
|
|
|
128
|
+
if (!durationMatch) return tag;
|
|
129
|
+
const duration = Number(durationMatch[1]);
|
|
130
|
+
if (!Number.isFinite(duration)) return tag;
|
|
131
|
+
|
|
130
132
|
if (name.toLowerCase() === "audio") return tag;
|
|
131
133
|
const startMatch = attrs.match(/\bdata-start="([\d.]+)"/);
|
|
132
134
|
if (!startMatch) return tag;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
|
|
8
|
+
const workflowScripts = [
|
|
9
|
+
["product-launch-video", new URL("./transitions.mjs", import.meta.url).pathname],
|
|
10
|
+
["pr-to-video", new URL("../../pr-to-video/scripts/transitions.mjs", import.meta.url).pathname],
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function runInject(script, rootId) {
|
|
14
|
+
const project = mkdtempSync(join(tmpdir(), "hf-transition-root-"));
|
|
15
|
+
const framesDir = join(project, "compositions", "frames");
|
|
16
|
+
mkdirSync(framesDir, { recursive: true });
|
|
17
|
+
writeFileSync(
|
|
18
|
+
join(project, "STORYBOARD.md"),
|
|
19
|
+
`---\nformat: 1920x1080\n---\n\n## Frame 1 — First\n\n- duration: 3s\n- transition_in: cut\n- src: compositions/frames/frame-01.html\n\n## Frame 2 — Second\n\n- duration: 3s\n- transition_in: crossfade 0.5s\n- src: compositions/frames/frame-02.html\n`,
|
|
20
|
+
);
|
|
21
|
+
writeFileSync(
|
|
22
|
+
join(framesDir, "frame-01.html"),
|
|
23
|
+
`<div data-composition-id="${rootId}" data-width="1920" data-height="1080"><div class="clip" data-start="0" data-duration="3"></div></div>`,
|
|
24
|
+
);
|
|
25
|
+
writeFileSync(
|
|
26
|
+
join(framesDir, "frame-02.html"),
|
|
27
|
+
'<div data-composition-id="frame-02" data-duration="3"></div>',
|
|
28
|
+
);
|
|
29
|
+
writeFileSync(
|
|
30
|
+
join(project, "index.html"),
|
|
31
|
+
`<div data-composition-id="main" data-duration="6">
|
|
32
|
+
<div id="el-frame-01" data-start="0" data-duration="3" data-track-index="0"></div>
|
|
33
|
+
<div id="el-frame-02" data-start="3" data-duration="3" data-track-index="0"></div>
|
|
34
|
+
</div><script>window.__timelines = {}; window.__timelines["main"] = gsap.timeline({ paused: true });</script>`,
|
|
35
|
+
);
|
|
36
|
+
const result = spawnSync(
|
|
37
|
+
process.execPath,
|
|
38
|
+
[script, "inject", "--storyboard", join(project, "STORYBOARD.md"), "--hyperframes", project],
|
|
39
|
+
{ encoding: "utf8" },
|
|
40
|
+
);
|
|
41
|
+
return {
|
|
42
|
+
project,
|
|
43
|
+
result,
|
|
44
|
+
frameHtml: () => readFileSync(join(framesDir, "frame-01.html"), "utf8"),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const [workflow, script] of workflowScripts) {
|
|
49
|
+
test(`${workflow} inserts duration on a valid durationless frame root`, () => {
|
|
50
|
+
const fixture = runInject(script, "frame-01");
|
|
51
|
+
try {
|
|
52
|
+
assert.equal(fixture.result.status, 0, fixture.result.stderr);
|
|
53
|
+
assert.match(fixture.frameHtml(), /data-composition-id="frame-01"[^>]*data-duration="3\.5"/);
|
|
54
|
+
} finally {
|
|
55
|
+
rmSync(fixture.project, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test(`${workflow} still rejects a genuinely mismatched frame root`, () => {
|
|
60
|
+
const fixture = runInject(script, "wrong-root");
|
|
61
|
+
try {
|
|
62
|
+
assert.notEqual(fixture.result.status, 0);
|
|
63
|
+
assert.match(fixture.result.stderr, /has no data-composition-id="frame-01" root/);
|
|
64
|
+
} finally {
|
|
65
|
+
rmSync(fixture.project, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Frame worker — product-launch delta
|
|
2
2
|
|
|
3
|
-
> The shared law is the core contract above (the packet builder prepends `../hyperframes
|
|
3
|
+
> The shared law is the core contract above (the packet builder prepends `../hyperframes/references/frame-worker-core.md` to this file as `_role.md`) — read the two as one role. This file carries only what's specific to a product-launch frame; you run N-up, **one frame each** — your dispatch carries exactly one packet. Tempted to add a generic GSAP / timeline rule here? Wrong home — it belongs in the core contract or `hyperframes-core`.
|
|
4
4
|
|
|
5
5
|
## Your `focal:` / `roles:` — real captured media
|
|
6
6
|
|