dsh-remotion 0.3.4 → 0.4.1
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/CHANGELOG.md +26 -0
- package/README.en.md +26 -65
- package/README.md +25 -67
- package/docs/USAGE.en.md +93 -0
- package/docs/USAGE.md +97 -0
- package/docs/VALIDATION.md +24 -0
- package/docs/validation-language-2026-10-03.md +16 -0
- package/lib/client.js +105 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +5 -1
- package/lib/media-store.d.ts +25 -0
- package/lib/media-store.js +174 -0
- package/lib/media-templates.d.ts +52 -0
- package/lib/media-templates.js +91 -0
- package/lib/media-workbench.d.ts +42 -0
- package/lib/media-workbench.js +476 -0
- package/package.json +18 -5
- package/scripts/build-client.mjs +4 -0
- package/skills/remotion-best-practices/SKILL.md +54 -54
- package/skills/remotion-best-practices/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/assets/remotion-icon.svg +4 -4
- package/skills/remotion-best-practices/remotion-captions/REFERENCE.md +36 -36
- package/skills/remotion-best-practices/remotion-captions/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/remotion-captions/assets/remotion-icon.svg +4 -4
- package/skills/remotion-best-practices/remotion-captions/display-captions.md +190 -190
- package/skills/remotion-best-practices/remotion-captions/import-srt-captions.md +73 -73
- package/skills/remotion-best-practices/remotion-captions/transcribe-captions.md +70 -70
- package/skills/remotion-best-practices/remotion-create/REFERENCE.md +85 -85
- package/skills/remotion-best-practices/remotion-create/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/remotion-create/assets/remotion-icon.svg +4 -4
- package/skills/remotion-best-practices/remotion-create/tailwind.md +11 -11
- package/skills/remotion-best-practices/remotion-create/video-layout.md +9 -9
- package/skills/remotion-best-practices/remotion-docs/REFERENCE.md +46 -46
- package/skills/remotion-best-practices/remotion-docs/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/remotion-docs/assets/remotion-icon.svg +4 -4
- package/skills/remotion-best-practices/remotion-interactivity/REFERENCE.md +315 -315
- package/skills/remotion-best-practices/remotion-interactivity/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/remotion-interactivity/assets/remotion-icon.svg +4 -4
- package/skills/remotion-best-practices/remotion-maps/REFERENCE.md +36 -36
- package/skills/remotion-best-practices/remotion-maps/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/remotion-maps/assets/remotion-icon.svg +4 -4
- package/skills/remotion-best-practices/remotion-maps/techniques/cesium/TECHNIQUE.md +89 -89
- package/skills/remotion-best-practices/remotion-maps/techniques/cesium/assets/CesiumFlythrough.tsx +284 -284
- package/skills/remotion-best-practices/remotion-maps/techniques/cesium/assets/cesium-path.json +299 -299
- package/skills/remotion-best-practices/remotion-maps/techniques/cesium/assets/city-path.json +12 -12
- package/skills/remotion-best-practices/remotion-maps/techniques/cesium/assets/example-Root.tsx +39 -39
- package/skills/remotion-best-practices/remotion-maps/techniques/cesium/assets/flight-path.ts +35 -35
- package/skills/remotion-best-practices/remotion-maps/techniques/cesium/assets/sample-river.geojson +215 -215
- package/skills/remotion-best-practices/remotion-maps/techniques/cesium/references/3d-data-sources.md +43 -43
- package/skills/remotion-best-practices/remotion-maps/techniques/cesium/references/3d-flyover-architecture.md +128 -128
- package/skills/remotion-best-practices/remotion-maps/techniques/cesium/references/3d-troubleshooting.md +57 -57
- package/skills/remotion-best-practices/remotion-maps/techniques/cesium/scripts/prep-cesium-path.mjs +161 -161
- package/skills/remotion-best-practices/remotion-maps/techniques/mapbox/TECHNIQUE.md +449 -449
- package/skills/remotion-best-practices/remotion-maps/techniques/mapbox/references/render-stability.md +64 -64
- package/skills/remotion-best-practices/remotion-maps/techniques/maplibre/TECHNIQUE.md +484 -484
- package/skills/remotion-best-practices/remotion-maps/techniques/maplibre/references/render-stability.md +64 -64
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/TECHNIQUE.md +80 -80
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/CountryLabel.tsx +70 -70
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/MapTilerVectorElement.ts +80 -80
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/RiverReveal.tsx +363 -363
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/example-Root.tsx +22 -22
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/sample-data/country-meta.json +7535 -7535
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/sample-data/yarlung-flow.json +605 -605
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/assets/tokens.ts +34 -34
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/references/map-data-sources.md +91 -91
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/references/map-explainer-architecture.md +129 -129
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/references/map-geo-prep.md +85 -85
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/references/render-stability.md +64 -64
- package/skills/remotion-best-practices/remotion-maps/techniques/maptiler/scripts/prep-geo.mjs +202 -202
- package/skills/remotion-best-practices/remotion-maps/techniques/static-map/TECHNIQUE.md +37 -37
- package/skills/remotion-best-practices/remotion-markup/3d.md +86 -86
- package/skills/remotion-best-practices/remotion-markup/REFERENCE.md +358 -358
- package/skills/remotion-best-practices/remotion-markup/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/remotion-markup/assets/remotion-icon.svg +4 -4
- package/skills/remotion-best-practices/remotion-markup/audio-visualization.md +198 -198
- package/skills/remotion-best-practices/remotion-markup/audio.md +169 -169
- package/skills/remotion-best-practices/remotion-markup/calculate-metadata.md +136 -136
- package/skills/remotion-best-practices/remotion-markup/compositions.md +123 -123
- package/skills/remotion-best-practices/remotion-markup/connected-compositions.md +79 -79
- package/skills/remotion-best-practices/remotion-markup/cropping.md +34 -34
- package/skills/remotion-best-practices/remotion-markup/effects.md +251 -251
- package/skills/remotion-best-practices/remotion-markup/embedding-videos.md +171 -171
- package/skills/remotion-best-practices/remotion-markup/ffmpeg.md +38 -38
- package/skills/remotion-best-practices/remotion-markup/gifs.md +187 -187
- package/skills/remotion-best-practices/remotion-markup/google-fonts.md +76 -76
- package/skills/remotion-best-practices/remotion-markup/html-in-canvas.md +128 -128
- package/skills/remotion-best-practices/remotion-markup/images.md +73 -73
- package/skills/remotion-best-practices/remotion-markup/light-leaks.md +106 -106
- package/skills/remotion-best-practices/remotion-markup/local-fonts.md +69 -69
- package/skills/remotion-best-practices/remotion-markup/lottie.md +73 -73
- package/skills/remotion-best-practices/remotion-markup/measuring-dom-nodes.md +38 -38
- package/skills/remotion-best-practices/remotion-markup/measuring-text.md +146 -146
- package/skills/remotion-best-practices/remotion-markup/motion-blur.md +60 -60
- package/skills/remotion-best-practices/remotion-markup/multi-scene-video.md +81 -81
- package/skills/remotion-best-practices/remotion-markup/parameters.md +111 -111
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/REFERENCE.md +36 -36
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/assets/remotion-icon.svg +4 -4
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/TECHNIQUE.md +89 -89
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/assets/CesiumFlythrough.tsx +284 -284
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/assets/cesium-path.json +299 -299
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/assets/city-path.json +12 -12
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/assets/example-Root.tsx +39 -39
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/assets/flight-path.ts +35 -35
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/assets/sample-river.geojson +215 -215
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/references/3d-data-sources.md +43 -43
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/references/3d-flyover-architecture.md +128 -128
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/references/3d-troubleshooting.md +57 -57
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/cesium/scripts/prep-cesium-path.mjs +161 -161
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/mapbox/TECHNIQUE.md +449 -449
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/mapbox/references/render-stability.md +64 -64
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maplibre/TECHNIQUE.md +484 -484
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maplibre/references/render-stability.md +64 -64
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/TECHNIQUE.md +80 -80
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/CountryLabel.tsx +70 -70
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/MapTilerVectorElement.ts +80 -80
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/RiverReveal.tsx +363 -363
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/example-Root.tsx +22 -22
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/sample-data/country-meta.json +7535 -7535
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/sample-data/yarlung-flow.json +605 -605
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/assets/tokens.ts +34 -34
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/references/map-data-sources.md +91 -91
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/references/map-explainer-architecture.md +129 -129
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/references/map-geo-prep.md +85 -85
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/references/render-stability.md +64 -64
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/maptiler/scripts/prep-geo.mjs +202 -202
- package/skills/remotion-best-practices/remotion-markup/remotion-maps/techniques/static-map/TECHNIQUE.md +37 -37
- package/skills/remotion-best-practices/remotion-markup/sequencing.md +169 -169
- package/skills/remotion-best-practices/remotion-markup/sfx.md +51 -51
- package/skills/remotion-best-practices/remotion-markup/silence-detection.md +71 -71
- package/skills/remotion-best-practices/remotion-markup/text-highlights.md +56 -56
- package/skills/remotion-best-practices/remotion-markup/timing.md +111 -111
- package/skills/remotion-best-practices/remotion-markup/transitions.md +239 -239
- package/skills/remotion-best-practices/remotion-markup/video-editing.md +90 -90
- package/skills/remotion-best-practices/remotion-markup/voiceover.md +99 -99
- package/skills/remotion-best-practices/remotion-multimedia/REFERENCE.md +20 -20
- package/skills/remotion-best-practices/remotion-multimedia/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/remotion-multimedia/assets/remotion-icon.svg +4 -4
- package/skills/remotion-best-practices/remotion-multimedia/get-audio-duration.md +56 -56
- package/skills/remotion-best-practices/remotion-multimedia/get-video-dimensions.md +66 -66
- package/skills/remotion-best-practices/remotion-multimedia/get-video-duration.md +58 -58
- package/skills/remotion-best-practices/remotion-render/REFERENCE.md +35 -35
- package/skills/remotion-best-practices/remotion-render/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/remotion-render/assets/remotion-icon.svg +4 -4
- package/skills/remotion-best-practices/remotion-render/transparent-videos.md +106 -106
- package/skills/remotion-best-practices/remotion-saas/REFERENCE.md +33 -33
- package/skills/remotion-best-practices/remotion-saas/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/remotion-saas/assets/remotion-icon.svg +4 -4
- package/skills/remotion-best-practices/remotion-saas/framework.md +19 -19
- package/skills/remotion-best-practices/remotion-saas/player.md +33 -33
- package/skills/remotion-best-practices/remotion-saas/rendering.md +69 -69
- package/skills/remotion-best-practices/remotion-studio/REFERENCE.md +24 -24
- package/skills/remotion-best-practices/remotion-studio/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/remotion-studio/assets/remotion-icon.svg +4 -4
- package/skills/remotion-best-practices/remotion-upgrade/REFERENCE.md +31 -31
- package/skills/remotion-best-practices/remotion-upgrade/agents/openai.yaml +7 -7
- package/skills/remotion-best-practices/remotion-upgrade/assets/remotion-icon.svg +4 -4
- package/skills/remotion-captions/SKILL.md +31 -31
- package/skills/remotion-captions/agents/openai.yaml +7 -7
- package/skills/remotion-captions/assets/remotion-icon.svg +4 -4
- package/skills/remotion-captions/display-captions.md +190 -190
- package/skills/remotion-captions/import-srt-captions.md +73 -73
- package/skills/remotion-captions/transcribe-captions.md +70 -70
- package/skills/remotion-create/SKILL.md +80 -80
- package/skills/remotion-create/agents/openai.yaml +7 -7
- package/skills/remotion-create/assets/remotion-icon.svg +4 -4
- package/skills/remotion-create/tailwind.md +11 -11
- package/skills/remotion-create/video-layout.md +9 -9
- package/skills/remotion-docs/SKILL.md +41 -41
- package/skills/remotion-docs/agents/openai.yaml +7 -7
- package/skills/remotion-docs/assets/remotion-icon.svg +4 -4
- package/skills/remotion-interactivity/SKILL.md +310 -310
- package/skills/remotion-interactivity/agents/openai.yaml +7 -7
- package/skills/remotion-interactivity/assets/remotion-icon.svg +4 -4
- package/skills/remotion-maps/SKILL.md +31 -31
- package/skills/remotion-maps/agents/openai.yaml +7 -7
- package/skills/remotion-maps/assets/remotion-icon.svg +4 -4
- package/skills/remotion-maps/techniques/cesium/TECHNIQUE.md +89 -89
- package/skills/remotion-maps/techniques/cesium/assets/CesiumFlythrough.tsx +284 -284
- package/skills/remotion-maps/techniques/cesium/assets/cesium-path.json +299 -299
- package/skills/remotion-maps/techniques/cesium/assets/city-path.json +12 -12
- package/skills/remotion-maps/techniques/cesium/assets/example-Root.tsx +39 -39
- package/skills/remotion-maps/techniques/cesium/assets/flight-path.ts +35 -35
- package/skills/remotion-maps/techniques/cesium/assets/sample-river.geojson +215 -215
- package/skills/remotion-maps/techniques/cesium/references/3d-data-sources.md +43 -43
- package/skills/remotion-maps/techniques/cesium/references/3d-flyover-architecture.md +128 -128
- package/skills/remotion-maps/techniques/cesium/references/3d-troubleshooting.md +57 -57
- package/skills/remotion-maps/techniques/cesium/scripts/prep-cesium-path.mjs +161 -161
- package/skills/remotion-maps/techniques/mapbox/TECHNIQUE.md +449 -449
- package/skills/remotion-maps/techniques/mapbox/references/render-stability.md +64 -64
- package/skills/remotion-maps/techniques/maplibre/TECHNIQUE.md +484 -484
- package/skills/remotion-maps/techniques/maplibre/references/render-stability.md +64 -64
- package/skills/remotion-maps/techniques/maptiler/TECHNIQUE.md +80 -80
- package/skills/remotion-maps/techniques/maptiler/assets/CountryLabel.tsx +70 -70
- package/skills/remotion-maps/techniques/maptiler/assets/MapTilerVectorElement.ts +80 -80
- package/skills/remotion-maps/techniques/maptiler/assets/RiverReveal.tsx +363 -363
- package/skills/remotion-maps/techniques/maptiler/assets/example-Root.tsx +22 -22
- package/skills/remotion-maps/techniques/maptiler/assets/sample-data/country-meta.json +7535 -7535
- package/skills/remotion-maps/techniques/maptiler/assets/sample-data/yarlung-flow.json +605 -605
- package/skills/remotion-maps/techniques/maptiler/assets/tokens.ts +34 -34
- package/skills/remotion-maps/techniques/maptiler/references/map-data-sources.md +91 -91
- package/skills/remotion-maps/techniques/maptiler/references/map-explainer-architecture.md +129 -129
- package/skills/remotion-maps/techniques/maptiler/references/map-geo-prep.md +85 -85
- package/skills/remotion-maps/techniques/maptiler/references/render-stability.md +64 -64
- package/skills/remotion-maps/techniques/maptiler/scripts/prep-geo.mjs +202 -202
- package/skills/remotion-maps/techniques/static-map/TECHNIQUE.md +37 -37
- package/skills/remotion-markup/3d.md +86 -86
- package/skills/remotion-markup/SKILL.md +353 -353
- package/skills/remotion-markup/agents/openai.yaml +7 -7
- package/skills/remotion-markup/assets/remotion-icon.svg +4 -4
- package/skills/remotion-markup/audio-visualization.md +198 -198
- package/skills/remotion-markup/audio.md +169 -169
- package/skills/remotion-markup/calculate-metadata.md +136 -136
- package/skills/remotion-markup/compositions.md +123 -123
- package/skills/remotion-markup/connected-compositions.md +79 -79
- package/skills/remotion-markup/cropping.md +34 -34
- package/skills/remotion-markup/effects.md +251 -251
- package/skills/remotion-markup/embedding-videos.md +171 -171
- package/skills/remotion-markup/ffmpeg.md +38 -38
- package/skills/remotion-markup/gifs.md +187 -187
- package/skills/remotion-markup/google-fonts.md +76 -76
- package/skills/remotion-markup/html-in-canvas.md +128 -128
- package/skills/remotion-markup/images.md +73 -73
- package/skills/remotion-markup/light-leaks.md +106 -106
- package/skills/remotion-markup/local-fonts.md +69 -69
- package/skills/remotion-markup/lottie.md +73 -73
- package/skills/remotion-markup/measuring-dom-nodes.md +38 -38
- package/skills/remotion-markup/measuring-text.md +146 -146
- package/skills/remotion-markup/motion-blur.md +60 -60
- package/skills/remotion-markup/multi-scene-video.md +81 -81
- package/skills/remotion-markup/parameters.md +111 -111
- package/skills/remotion-markup/remotion-maps/REFERENCE.md +36 -36
- package/skills/remotion-markup/remotion-maps/agents/openai.yaml +7 -7
- package/skills/remotion-markup/remotion-maps/assets/remotion-icon.svg +4 -4
- package/skills/remotion-markup/remotion-maps/techniques/cesium/TECHNIQUE.md +89 -89
- package/skills/remotion-markup/remotion-maps/techniques/cesium/assets/CesiumFlythrough.tsx +284 -284
- package/skills/remotion-markup/remotion-maps/techniques/cesium/assets/cesium-path.json +299 -299
- package/skills/remotion-markup/remotion-maps/techniques/cesium/assets/city-path.json +12 -12
- package/skills/remotion-markup/remotion-maps/techniques/cesium/assets/example-Root.tsx +39 -39
- package/skills/remotion-markup/remotion-maps/techniques/cesium/assets/flight-path.ts +35 -35
- package/skills/remotion-markup/remotion-maps/techniques/cesium/assets/sample-river.geojson +215 -215
- package/skills/remotion-markup/remotion-maps/techniques/cesium/references/3d-data-sources.md +43 -43
- package/skills/remotion-markup/remotion-maps/techniques/cesium/references/3d-flyover-architecture.md +128 -128
- package/skills/remotion-markup/remotion-maps/techniques/cesium/references/3d-troubleshooting.md +57 -57
- package/skills/remotion-markup/remotion-maps/techniques/cesium/scripts/prep-cesium-path.mjs +161 -161
- package/skills/remotion-markup/remotion-maps/techniques/mapbox/TECHNIQUE.md +449 -449
- package/skills/remotion-markup/remotion-maps/techniques/mapbox/references/render-stability.md +64 -64
- package/skills/remotion-markup/remotion-maps/techniques/maplibre/TECHNIQUE.md +484 -484
- package/skills/remotion-markup/remotion-maps/techniques/maplibre/references/render-stability.md +64 -64
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/TECHNIQUE.md +80 -80
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/CountryLabel.tsx +70 -70
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/MapTilerVectorElement.ts +80 -80
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/RiverReveal.tsx +363 -363
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/example-Root.tsx +22 -22
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/sample-data/country-meta.json +7535 -7535
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/sample-data/yarlung-flow.json +605 -605
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/assets/tokens.ts +34 -34
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/references/map-data-sources.md +91 -91
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/references/map-explainer-architecture.md +129 -129
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/references/map-geo-prep.md +85 -85
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/references/render-stability.md +64 -64
- package/skills/remotion-markup/remotion-maps/techniques/maptiler/scripts/prep-geo.mjs +202 -202
- package/skills/remotion-markup/remotion-maps/techniques/static-map/TECHNIQUE.md +37 -37
- package/skills/remotion-markup/sequencing.md +169 -169
- package/skills/remotion-markup/sfx.md +51 -51
- package/skills/remotion-markup/silence-detection.md +71 -71
- package/skills/remotion-markup/text-highlights.md +56 -56
- package/skills/remotion-markup/timing.md +111 -111
- package/skills/remotion-markup/transitions.md +239 -239
- package/skills/remotion-markup/video-editing.md +90 -90
- package/skills/remotion-markup/voiceover.md +99 -99
- package/skills/remotion-multimedia/SKILL.md +15 -15
- package/skills/remotion-multimedia/agents/openai.yaml +7 -7
- package/skills/remotion-multimedia/assets/remotion-icon.svg +4 -4
- package/skills/remotion-multimedia/get-audio-duration.md +56 -56
- package/skills/remotion-multimedia/get-video-dimensions.md +66 -66
- package/skills/remotion-multimedia/get-video-duration.md +58 -58
- package/skills/remotion-render/SKILL.md +30 -30
- package/skills/remotion-render/agents/openai.yaml +7 -7
- package/skills/remotion-render/assets/remotion-icon.svg +4 -4
- package/skills/remotion-render/transparent-videos.md +106 -106
- package/skills/remotion-saas/SKILL.md +28 -28
- package/skills/remotion-saas/agents/openai.yaml +7 -7
- package/skills/remotion-saas/assets/remotion-icon.svg +4 -4
- package/skills/remotion-saas/framework.md +19 -19
- package/skills/remotion-saas/player.md +33 -33
- package/skills/remotion-saas/rendering.md +69 -69
- package/skills/remotion-studio/SKILL.md +19 -19
- package/skills/remotion-studio/agents/openai.yaml +7 -7
- package/skills/remotion-studio/assets/remotion-icon.svg +4 -4
- package/skills/remotion-upgrade/SKILL.md +26 -26
- package/skills/remotion-upgrade/agents/openai.yaml +7 -7
- package/skills/remotion-upgrade/assets/remotion-icon.svg +4 -4
- package/templates/DESIGN.md +26 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Shared original browser source. build-client substitutes the engine identity.
|
|
2
|
+
window.__ModuleLoader__.load({ id: 'dsh-remotion', factory: require => {
|
|
3
|
+
const React = require('react'), { useState, useEffect, useRef } = React, h = React.createElement;
|
|
4
|
+
const engine = 'remotion', title = engine === 'hyperframes' ? 'HyperFrames' : 'Remotion', route = 'api/dsh-' + engine + '/workbench';
|
|
5
|
+
|
|
6
|
+
function mediaError(error, en) {
|
|
7
|
+
const message = typeof error === 'string' ? error : error?.message || String(error || '');
|
|
8
|
+
if (!en || !/[\u3400-\u9fff]/.test(message)) return message;
|
|
9
|
+
if (error?.status === 401) return 'Your session expired. Reopen DSH and try again.';
|
|
10
|
+
if (error?.status === 409 || /已.*更新|另一窗口|修订/.test(message)) return 'The project changed. Reopen the latest version and compare it with your preserved input.';
|
|
11
|
+
if (/标题/.test(message)) return 'Enter a title up to 72 characters.';
|
|
12
|
+
if (/时长/.test(message)) return 'Set a duration between 3 and 30 seconds.';
|
|
13
|
+
if (/正文/.test(message)) return 'Use body text up to 360 characters.';
|
|
14
|
+
if (/素材|PNG|JPEG|MP3|WAV|MP4/.test(message)) return 'Check the media format and file contents. Each asset must be ≤20 MB; a project supports 12 assets and 40 MB total, with one audio and one video background.';
|
|
15
|
+
if (/任务|官方|FFmpeg|Chrome|环境/.test(message)) return 'The official task did not finish. Open the job log and check the render environment, then retry. Your project is preserved.';
|
|
16
|
+
if (/工程记录|项目目录|文件/.test(message)) return 'The project files could not be read safely. Back them up and check their location and permissions before retrying.';
|
|
17
|
+
return 'The operation failed. Check the DSH logs and reload the project before retrying; your input is preserved.';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let cache = { project: null, fields: null, dirty: false, job: null, creating: false };
|
|
21
|
+
const empty = () => ({ template: 'title', title: '', body: '', format: 'landscape', duration: 8 });
|
|
22
|
+
const fieldsOf = p => ({ template: p.template, title: p.title, body: p.body, format: p.format, duration: p.duration });
|
|
23
|
+
const css = `.dmw{font:14px/1.6 system-ui,sans-serif;color:var(--fg,#142D39);min-width:0;padding:8px}.dmw *{box-sizing:border-box}.dmw h2{margin:0;font-size:23px}.dmw h3{font-size:17px;margin:8px 0}.dmw .muted{color:var(--fg-muted,#677881);font-size:13px}.dmw .bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0}.dmw button,.dmw .button{font:inherit;color:inherit;background:var(--bg-surface,#fff);border:1px solid var(--border,#c8d6d9);border-radius:9px;padding:8px 12px;cursor:pointer;white-space:normal;text-decoration:none}.dmw button.primary{background:#df5639;border-color:#df5639;color:white}.dmw button:disabled{opacity:.45;cursor:default}.dmw input:not([type=file]),.dmw textarea,.dmw select{font:inherit;color:inherit;width:100%;border:1px solid var(--border,#b9cccd);border-radius:8px;padding:9px;background:var(--bg,#fff)}.dmw label{display:block;margin:10px 0 4px}.dmw .card{border:1px solid var(--border,#ccd7d6);border-radius:12px;padding:14px;margin:12px 0;min-width:0;background:var(--bg-surface,#fff)}.dmw .notice{border-radius:8px;padding:10px;margin:10px 0;background:#eef5ed;color:#28573e;overflow-wrap:anywhere}.dmw .error{background:#fff0ec;color:#943b27}.dmw .row{display:grid;grid-template-columns:1fr 1fr;gap:14px}.dmw .projects{display:grid;gap:8px}.dmw .projects button{text-align:left;width:100%}.dmw img{width:90px;height:70px;object-fit:contain}.dmw .assets{display:flex;flex-direction:column;gap:8px}.dmw .asset{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.dmw video{width:100%;max-height:400px;background:#142D39;border-radius:10px}.dmw pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;margin:8px 0;max-height:220px;overflow:auto}.dmw :focus-visible{outline:3px solid #79CDB4;outline-offset:2px}[role=dialog]:has(.dmw){width:min(1160px,calc(100vw - 40px))!important;max-width:1160px!important}@media(max-width:700px){.dmw{padding:3px}.dmw .row{grid-template-columns:1fr}[role=dialog]:has(.dmw){width:100%!important;max-width:100%!important;flex-direction:column!important}[role=dialog]:has(.dmw)>nav+div{min-width:0;width:100%}[role=dialog]:has(.dmw)>nav+div>div:has(.dmw){padding:12px!important;min-width:0}[role=dialog]:has(.dmw) nav{width:100%!important;flex-basis:auto!important;border-right:0;max-height:170px;overflow:auto}[role=dialog]:has(.dmw) nav>div:has(>button){display:flex;flex-direction:row!important;flex-wrap:wrap;gap:4px}}`;
|
|
24
|
+
async function api(body) {
|
|
25
|
+
const response = await fetch(new URL(route, document.baseURI), { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', 'x-dsh-media': '1' }, body: JSON.stringify(body) });
|
|
26
|
+
let data; try { data = await response.json() } catch { throw new Error('无法读取插件响应,请检查是否已经重启 DSH。') }
|
|
27
|
+
if (!response.ok || !data.ok) { const error = new Error(data.message || '操作未完成'); error.status = response.status; throw error; } return data.value;
|
|
28
|
+
}
|
|
29
|
+
function urlFor(project, kind, extra = {}) { const url = new URL(route, document.baseURI); url.search = new URLSearchParams({ id: project.id, kind, ...extra }); return url.href; }
|
|
30
|
+
function Workbench({ locale }) {
|
|
31
|
+
const [lang, setLang] = useState('zh'), [projects, setProjects] = useState([]), [environment, setEnvironment] = useState(null), [templates, setTemplates] = useState([]);
|
|
32
|
+
const [project, setProject] = useState(cache.project), [fields, setFields] = useState(cache.fields || empty()), [dirty, setDirty] = useState(cache.dirty), [creating, setCreating] = useState(cache.creating);
|
|
33
|
+
const [job, setJob] = useState(cache.job), [busy, setBusy] = useState(false), [error, setError] = useState(''), [notice, setNotice] = useState(''), [pending, setPending] = useState(null), [conflict, setConflict] = useState(false);
|
|
34
|
+
const busyRef = useRef(false), fileRef = useRef(null), latest = useRef({ project, fields, dirty }); latest.current = { project, fields, dirty };
|
|
35
|
+
const t = (zh, en) => lang === 'en' ? en : zh, activeJob = job?.state === 'running', running = busy || activeJob && job.kind !== 'prepare';
|
|
36
|
+
const stage = lang === 'en' ? ({ '下载环境': 'Preparing environment', '检查 FFmpeg': 'Checking FFmpeg', '下载官方依赖': 'Downloading official dependencies', '检查官方环境': 'Checking official CLI', '检查或下载 Chrome': 'Checking or downloading Chrome', '下载专用渲染浏览器': 'Downloading the official rendering browser', '准备修订快照': 'Preparing saved revision', '启动官方预览': 'Starting official preview', '官方预览已就绪': 'Official preview is ready', '检查合成': 'Checking composition', '渲染 MP4': 'Rendering MP4', '已完成': 'Completed', '已取消': 'Cancelled', '已取消;工程已保留': 'Cancelled; project preserved', '官方任务未完成,请展开运行记录后重试。工程已保留。': 'The official task failed. Open the job log and retry; your project is preserved.', '未找到 FFmpeg,请安装并加入 PATH,重启 DSH 后重试。工程已保留。': 'FFmpeg was not found. Install it, add it to PATH, then restart DSH and retry; your project is preserved.' }[job?.stage] || mediaError(job?.stage, true)) : job?.stage;
|
|
37
|
+
useEffect(() => { const update = () => setLang(String(locale?.getSnapshot?.().active || '').startsWith('en') ? 'en' : 'zh'); update(); return locale?.subscribe?.(update); }, [locale]);
|
|
38
|
+
useEffect(() => { cache = { project, fields, dirty, job, creating }; }, [project, fields, dirty, job, creating]);
|
|
39
|
+
useEffect(() => { const exit = e => { if (latest.current.dirty) { e.preventDefault(); e.returnValue = ''; } }; window.addEventListener('beforeunload', exit); return () => window.removeEventListener('beforeunload', exit); }, []);
|
|
40
|
+
async function refresh() {
|
|
41
|
+
const data = await api({ action: 'list' }); setProjects(data.projects); setEnvironment(data.environment); setTemplates(data.templates);
|
|
42
|
+
const current = latest.current, saved = !current.dirty && data.projects.find(row => row.id === current.project?.id);
|
|
43
|
+
if (saved) { setProject(saved); setFields(fieldsOf(saved)); }
|
|
44
|
+
setJob(previous => data.jobs.find(row => row.id === previous?.id) || data.jobs.filter(row => row.state === 'running').at(-1) || data.jobs.at(-1) || null);
|
|
45
|
+
}
|
|
46
|
+
useEffect(() => { refresh().catch(e => setError(e)); }, []);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!job || job.state !== 'running') return;
|
|
49
|
+
let live = true, polling = false;
|
|
50
|
+
const timer = setInterval(async () => {
|
|
51
|
+
if (polling) return; polling = true;
|
|
52
|
+
try {
|
|
53
|
+
const next = await api({ action: 'job', job: job.id }); if (!live) return;
|
|
54
|
+
// Refresh output before the terminal state tears down this polling effect.
|
|
55
|
+
if (next.state !== 'running') await refresh();
|
|
56
|
+
else setJob(next);
|
|
57
|
+
} catch (e) { if (live) { setError(e); setJob(null); } } finally { polling = false; }
|
|
58
|
+
}, 1000);
|
|
59
|
+
return () => { live = false; clearInterval(timer); };
|
|
60
|
+
}, [job?.id, job?.state]);
|
|
61
|
+
async function perform(fn) { if (busyRef.current) return; busyRef.current = true; setBusy(true); setError(''); setNotice(''); try { return await fn(); } catch (e) { setError(e); setConflict(e.status === 409); } finally { busyRef.current = false; setBusy(false); } }
|
|
62
|
+
async function open(id) { const next = await api({ action: 'get', id }); setProject(next); setFields(fieldsOf(next)); setCreating(false); setDirty(false); setConflict(false); setPending(null); }
|
|
63
|
+
function navigate(target) { if (dirty) { setPending(target); return; } perform(() => move(target)); }
|
|
64
|
+
async function move(target) { if (target === 'new') { setProject(null); setFields(empty()); setCreating(true); } else if (target === 'list') { setProject(null); setCreating(false); await refresh(); } else await open(target); setDirty(false); setPending(null); }
|
|
65
|
+
async function save() { const next = await api(project ? { action: 'update', id: project.id, revision: project.revision, fields } : { action: 'create', fields }); setProject(next); setFields(fieldsOf(next)); setCreating(false); setDirty(false); setConflict(false); setNotice(t('工程已保存。', 'Project saved.')); await refresh(); return next; }
|
|
66
|
+
async function upload(event) { const file = event.target.files?.[0]; if (!file) return; await perform(async () => {
|
|
67
|
+
if (!project || dirty) throw new Error(t('请先保存工程,再上传素材。', 'Save your project before uploading media.'));
|
|
68
|
+
if (file.size > 20 * 1024 * 1024) throw new Error(t('单个素材不能超过 20 MB。', 'Each asset must be at most 20 MB.'));
|
|
69
|
+
const response = await fetch(new URL(route + '/upload', document.baseURI), { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/octet-stream', 'x-dsh-media': '1', 'x-project-id': project.id, 'x-project-revision': String(project.revision), 'x-file-name': encodeURIComponent(file.name) }, body: file }); const result = await response.json(); if (!response.ok || !result.ok) { const e = new Error(result.message); e.status = response.status; throw e; } setProject(result.value); setNotice(t('素材副本已保存,原文件未改变。', 'A media copy was saved; your original is unchanged.'));
|
|
70
|
+
}); event.target.value = ''; }
|
|
71
|
+
async function start(action) { await perform(async () => { if (action !== 'prepare' && dirty) throw new Error(t('请先保存修改。', 'Save changes first.')); setJob(await api({ action, id: project?.id, revision: project?.revision })); }); }
|
|
72
|
+
const render = project?.renders?.[0], stale = render && render.revision !== project.revision;
|
|
73
|
+
if (!environment) return h('div', { className: 'dmw' }, h('style', null, css), h('h2', null, title + t(' 视频工作台', ' video workbench')),
|
|
74
|
+
error ? h(React.Fragment, null, h('p', { className: 'notice error', role: 'alert' }, mediaError(error, lang === 'en')), h('button', { disabled: busy, onClick: () => perform(refresh) }, t('重新读取项目', 'Reload projects'))) : h('p', { role: 'status' }, t('正在读取工程与渲染环境…', 'Loading projects and render environment…')));
|
|
75
|
+
return h('div', { className: 'dmw' }, h('style', null, css), h('h2', null, title + t(' 视频工作台', ' video workbench')),
|
|
76
|
+
h('p', { className: 'muted' }, t('模板 → 素材 → 官方预览 → 本机 MP4。工程与素材保存在 DSH 本地数据目录。', 'Template → media → official preview → local MP4. Projects stay in the local DSH data folder.')),
|
|
77
|
+
error && h('div', { className: 'notice error', role: 'alert' }, mediaError(error, lang === 'en')), notice && h('div', { className: 'notice', role: 'status' }, notice),
|
|
78
|
+
pending && h('div', { className: 'card' }, h('p', null, t('有未保存内容,如何继续?', 'You have unsaved changes.')),
|
|
79
|
+
h('div', { className: 'bar' }, h('button', { disabled: running, onClick: () => perform(async () => { await save(); await move(pending); }) }, t('保存后继续', 'Save and continue')), h('button', { disabled: running, onClick: () => perform(() => move(pending)) }, t('放弃修改并继续', 'Discard and continue')), h('button', { onClick: () => setPending(null) }, t('留在当前', 'Stay here')))),
|
|
80
|
+
conflict && project && h('button', { disabled: running, onClick: () => { setPending(project.id); } }, t('重新打开最新版本(先处理当前输入)', 'Reopen latest version (review your current input first)')),
|
|
81
|
+
h('details', { className: 'card', open: !environment?.ready }, h('summary', null, t('渲染环境', 'Render environment') + (environment?.ready ? t(' · 已准备', ' · ready') : t(' · 首次需下载', ' · first download required'))),
|
|
82
|
+
h('p', { className: 'muted' }, t(environment?.hint, 'First use downloads the official CLI. ' + (engine === 'hyperframes' ? 'Windows uses the official rendering browser; other platforms reuse installed Chrome when available. FFmpeg must be in PATH.' : 'Installed Chrome is reused; a browser is downloaded when needed. Remotion retains its official license.'))), engine === 'remotion' && h('p', null, h('a', { href: 'https://www.remotion.dev/license', target: '_blank', rel: 'noreferrer' }, t('查看 Remotion 使用许可证', 'Read the Remotion license'))),
|
|
83
|
+
h('button', { disabled: busy || activeJob, onClick: () => start('prepare') }, environment?.ready ? t('重新检查 / 修复环境', 'Check / repair environment') : t('准备渲染环境', 'Prepare render environment'))),
|
|
84
|
+
job && h('div', { className: job.state === 'failed' ? 'card notice error' : 'card', role: job.state === 'failed' ? 'alert' : 'status' }, h('strong', null, t('任务状态:', 'Job status: ') + stage), job.url && h('p', null, h('a', { className: 'button', href: job.url, target: '_blank', rel: 'noreferrer' }, t('打开官方 Studio 预览', 'Open official Studio preview'))),
|
|
85
|
+
job.state === 'running' && h('button', { disabled: busy, onClick: () => perform(async () => { await api({ action: 'cancel', job: job.id }); setNotice(t('已请求停止;正在关闭本次任务。', 'Stopping this job.')); }) }, job.kind === 'preview' ? t('停止预览,返回编辑', 'Stop preview and return to editing') : t('取消任务', 'Cancel job')),
|
|
86
|
+
h('details', null, h('summary', null, t('查看运行记录', 'Show job log')), h('pre', null, job.lines.join('\n')))),
|
|
87
|
+
!project && !creating ? h(React.Fragment, null, h('div', { className: 'bar' }, h('button', { className: 'primary', disabled: running, onClick: () => navigate('new') }, t('新建视频', 'New video')), h('button', { disabled: running, onClick: () => perform(refresh) }, t('刷新项目', 'Refresh projects'))),
|
|
88
|
+
projects.length ? h('div', { className: 'projects' }, projects.map(p => h('button', { key: p.id, disabled: running, onClick: () => navigate(p.id) }, h('strong', null, p.title), h('div', { className: 'muted' }, p.duration + t(' 秒 · 修订 ', ' sec · revision ') + p.revision)))) : h('div', { className: 'card' }, h('h3', null, t('还没有视频工程', 'No video projects yet')), h('p', null, t('点击“新建视频”,先选模板并保存。环境准备可以稍后进行。', 'Choose New video, select a template and save. You can prepare the render environment later.')))) :
|
|
89
|
+
h(React.Fragment, null, h('div', { className: 'bar' }, h('button', { disabled: running, onClick: () => navigate('list') }, t('返回项目', 'Back to projects')), project && h('span', { className: 'muted' }, t('修订 ', 'Revision ') + project.revision), dirty && h('span', null, t('有未保存修改', 'Unsaved changes'))),
|
|
90
|
+
h('div', { className: 'card' }, h('h3', null, t('1. 模板与内容', '1. Template and content')),
|
|
91
|
+
h('label', { htmlFor: engine + '-template' }, t('模板', 'Template')), h('select', { id: engine + '-template', disabled: running, value: fields.template, onChange: e => { setFields({ ...fields, template: e.target.value }); setDirty(true); } }, templates.map(row => h('option', { key: row.id, value: row.id }, lang === 'en' ? row.en : row.zh))),
|
|
92
|
+
h('label', { htmlFor: engine + '-title' }, t('标题(最多 72 字符)', 'Title (up to 72 characters)')), h('input', { id: engine + '-title', value: fields.title, disabled: running, maxLength: 72, onChange: e => { setFields({ ...fields, title: e.target.value }); setDirty(true); } }),
|
|
93
|
+
h('label', { htmlFor: engine + '-body' }, t('正文(轮播时每行对应一页)', 'Body (one line per slideshow page)')), h('textarea', { id: engine + '-body', value: fields.body, disabled: running, maxLength: 360, rows: 4, onChange: e => { setFields({ ...fields, body: e.target.value }); setDirty(true); } }),
|
|
94
|
+
h('div', { className: 'row' }, h('div', null, h('label', { htmlFor: engine + '-format' }, t('画幅', 'Canvas')), h('select', { id: engine + '-format', value: fields.format, disabled: running, onChange: e => { setFields({ ...fields, format: e.target.value }); setDirty(true); } }, ['landscape','portrait','square'].map((key, i) => h('option', { key, value: key }, [t('横屏 16:9', 'Landscape 16:9'),t('竖屏 9:16', 'Portrait 9:16'),t('正方形', 'Square')][i])))), h('div', null, h('label', { htmlFor: engine + '-duration' }, t('时长(3–30 秒)', 'Duration (3–30 seconds)')), h('input', { id: engine + '-duration', type: 'number', min: 3, max: 30, value: fields.duration, disabled: running, onChange: e => { setFields({ ...fields, duration: Number(e.target.value) }); setDirty(true); } }))),
|
|
95
|
+
h('div', { className: 'bar' }, h('button', { className: 'primary', disabled: running || project && !dirty, onClick: () => perform(save) }, t('保存工程', 'Save project')), project && h('button', { disabled: running || !dirty, onClick: () => { setFields(fieldsOf(project)); setDirty(false); setError(''); } }, t('取消修改', 'Discard changes')))),
|
|
96
|
+
project && h('div', { className: 'card' }, h('h3', null, t('2. 素材', '2. Media')), h('p', { className: 'muted' }, t('PNG/JPEG 图片、一个背景音频和一个静音背景视频;每个 ≤20 MB,总共 ≤40 MB。轮播按上传顺序,每张至少两秒。', 'PNG/JPEG images, one background audio and one muted background video; each ≤20 MB, total ≤40 MB. Slideshow follows upload order, at least two seconds per image.')),
|
|
97
|
+
h('label', { htmlFor: engine + '-asset' }, t('上传素材副本', 'Upload a media copy')), h('input', { ref: fileRef, id: engine + '-asset', type: 'file', accept: '.png,.jpg,.jpeg,.mp3,.wav,.mp4', disabled: running || dirty, onChange: upload }),
|
|
98
|
+
h('div', { className: 'assets' }, project.assets.map(a => h('div', { className: 'asset', key: a.id }, a.kind === 'image' && h('img', { src: urlFor(project, 'asset', { asset: a.id }), alt: a.name }), h('span', null, a.name + ' · ' + Math.ceil(a.bytes / 1024) + ' KB'), h('button', { disabled: running || dirty, onClick: () => perform(async () => { setProject(await api({ action: 'removeAsset', id: project.id, revision: project.revision, asset: a.id })); }) }, t('移除上传副本', 'Remove uploaded copy')))))),
|
|
99
|
+
project && h('div', { className: 'card' }, h('h3', null, t('3. 预览与成品', '3. Preview and output')), h('p', { className: 'muted' }, t('保存后在官方 Studio 检查画面。关闭本次预览后再编辑或导出。MP4 按已保存修订生成;渲染不会占用前台窗口。', 'Check the saved project in official Studio. Stop this preview before editing or exporting. MP4 uses the saved revision and renders in the background.')),
|
|
100
|
+
h('div', { className: 'bar' }, h('button', { disabled: running || dirty || !environment?.ready, onClick: () => start('preview') }, t('启动官方预览', 'Start official preview')), h('button', { className: 'primary', disabled: running || dirty || !environment?.ready, onClick: () => start('render') }, t('导出 MP4', 'Export MP4')), h('a', { className: 'button', href: urlFor(project, 'source'), download: '' }, t('备份可编辑工程', 'Back up editable project'))),
|
|
101
|
+
render ? h(React.Fragment, null, h('p', { className: stale ? 'notice error' : 'muted' }, t('此 MP4 来自修订 ', 'This MP4 uses revision ') + render.revision + (stale ? t(',工程已有更新,请重新导出。', '; your project has newer changes. Export again.') : t(',请播放检查最终画面。', '. Play it to check the final output.'))), h('video', { controls: true, preload: 'metadata', src: urlFor(project, 'mp4', { render: render.id }) }), h('p', null, h('a', { className: 'button', href: urlFor(project, 'mp4', { render: render.id, download: '1' }), download: '' }, t('下载这份 MP4', 'Download this MP4')))) : h('p', { className: 'muted' }, t('尚无 MP4 成品。预览成功后点击“导出 MP4”。', 'No MP4 yet. Check the preview, then choose Export MP4.')))));
|
|
102
|
+
}
|
|
103
|
+
function apply(ctx) { let locale; try { locale = ctx.get?.('locale') || ctx.locale; } catch {} ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-' + engine, order: engine === 'hyperframes' ? 62 : 63, label: () => title, inject: () => ({ locale }) }, Workbench)); }
|
|
104
|
+
return { inject: ['slots','locale'], apply };
|
|
105
|
+
} });
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { type SkillsPluginContext } from './skill-bundle.js';
|
|
2
|
+
export { MediaWorkbench } from './media-workbench.js';
|
|
3
|
+
export { MediaStore } from './media-store.js';
|
|
4
|
+
export { MEDIA_TEMPLATES, MEDIA_VERSIONS } from './media-templates.js';
|
|
2
5
|
export { parseSkillFile } from './skill-bundle.js';
|
|
3
6
|
export type { SkillRegistration, SkillsPluginContext } from './skill-bundle.js';
|
|
4
7
|
export declare const name = "remotion-skills";
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { createSkillBundle } from './skill-bundle.js';
|
|
8
|
+
import { installMediaWorkbench } from './media-workbench.js';
|
|
9
|
+
export { MediaWorkbench } from './media-workbench.js';
|
|
10
|
+
export { MediaStore } from './media-store.js';
|
|
11
|
+
export { MEDIA_TEMPLATES, MEDIA_VERSIONS } from './media-templates.js';
|
|
8
12
|
export { parseSkillFile } from './skill-bundle.js';
|
|
9
13
|
export const name = 'remotion-skills';
|
|
10
14
|
export const inject = ['skills', 'tools'];
|
|
@@ -35,4 +39,4 @@ const bundle = createSkillBundle({
|
|
|
35
39
|
/** Re-read and validate disk resources; runtime state is checked by the health tool. */
|
|
36
40
|
export function checkBundledSkills() { return bundle.checkFiles(); }
|
|
37
41
|
/** Register this package's skills and its read-only health tool. */
|
|
38
|
-
export function apply(ctx) { bundle.apply(ctx); }
|
|
42
|
+
export function apply(ctx) { bundle.apply(ctx); installMediaWorkbench(ctx, 'remotion'); }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type MediaEngine, type MediaProject, type MediaAsset } from './media-templates.js';
|
|
2
|
+
export declare function mediaId(value: unknown): string;
|
|
3
|
+
export declare class MediaConflict extends Error {
|
|
4
|
+
status: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class MediaStore {
|
|
7
|
+
readonly root: string;
|
|
8
|
+
constructor(engine: MediaEngine, root?: string);
|
|
9
|
+
init(): Promise<void>;
|
|
10
|
+
path(id: string): Promise<string>;
|
|
11
|
+
checkPath(path: string): Promise<void>;
|
|
12
|
+
get(id: string): Promise<MediaProject>;
|
|
13
|
+
list(): Promise<MediaProject[]>;
|
|
14
|
+
private write;
|
|
15
|
+
create(value: unknown): Promise<MediaProject>;
|
|
16
|
+
edit(id: string, revision: unknown, fn: (p: MediaProject) => Promise<void> | void, bump?: boolean): Promise<MediaProject>;
|
|
17
|
+
update(id: string, revision: unknown, value: unknown): Promise<MediaProject>;
|
|
18
|
+
assetBytes(id: string, asset: MediaAsset): Promise<Buffer>;
|
|
19
|
+
upload(id: string, revision: unknown, name: string, bytes: Buffer): Promise<MediaProject>;
|
|
20
|
+
removeAsset(id: string, revision: unknown, assetId: string): Promise<MediaProject>;
|
|
21
|
+
snapshot(id: string, revision: unknown): Promise<{
|
|
22
|
+
project: MediaProject;
|
|
23
|
+
assets: Map<string, Buffer>;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, writeFile, rename, readdir, stat, lstat, unlink, realpath } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join, resolve, relative, isAbsolute, extname } from 'node:path';
|
|
5
|
+
import lockfile from 'proper-lockfile';
|
|
6
|
+
import { validateFields } from './media-templates.js';
|
|
7
|
+
export function mediaId(value) {
|
|
8
|
+
if (typeof value !== 'string' || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(value))
|
|
9
|
+
throw new Error('项目或任务编号无效。');
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
export class MediaConflict extends Error {
|
|
13
|
+
status = 409;
|
|
14
|
+
}
|
|
15
|
+
export class MediaStore {
|
|
16
|
+
root;
|
|
17
|
+
constructor(engine, root) { this.root = resolve(root ?? join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'data', 'dsh-' + engine)); }
|
|
18
|
+
async init() { await mkdir(join(this.root, 'projects'), { recursive: true, mode: 0o700 }); }
|
|
19
|
+
async path(id) {
|
|
20
|
+
await this.init();
|
|
21
|
+
const dir = join(this.root, 'projects', mediaId(id));
|
|
22
|
+
if ((await lstat(dir)).isSymbolicLink())
|
|
23
|
+
throw new Error('项目目录不能是符号链接。');
|
|
24
|
+
await this.checkPath(dir);
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
async checkPath(path) {
|
|
28
|
+
const base = await realpath(this.root), actual = await realpath(path), rel = relative(base, actual);
|
|
29
|
+
if (rel === '..' || rel.startsWith('..' + (process.platform === 'win32' ? '\\' : '/')) || isAbsolute(rel))
|
|
30
|
+
throw new Error('文件不在本插件项目目录内。');
|
|
31
|
+
}
|
|
32
|
+
async get(id) {
|
|
33
|
+
const file = join(await this.path(id), 'project.json');
|
|
34
|
+
await this.checkPath(file);
|
|
35
|
+
if ((await stat(file)).size > 65536)
|
|
36
|
+
throw new Error('工程记录过大,请保留文件后检查。');
|
|
37
|
+
const p = JSON.parse(await readFile(file, 'utf8'));
|
|
38
|
+
validateFields(p);
|
|
39
|
+
if (p.id !== id || !Number.isInteger(p.revision) || p.revision < 1 || !Array.isArray(p.assets) || p.assets.length > 12 || !Array.isArray(p.renders))
|
|
40
|
+
throw new Error('工程记录损坏;原文件已保留。');
|
|
41
|
+
for (const a of p.assets) {
|
|
42
|
+
mediaId(a.id);
|
|
43
|
+
if (!new RegExp('^' + a.id + '\\.(png|jpg|mp3|wav|mp4)$').test(a.file))
|
|
44
|
+
throw new Error('素材记录无效。');
|
|
45
|
+
}
|
|
46
|
+
for (const r of p.renders)
|
|
47
|
+
mediaId(r.id);
|
|
48
|
+
return p;
|
|
49
|
+
}
|
|
50
|
+
async list() {
|
|
51
|
+
await this.init();
|
|
52
|
+
const result = [];
|
|
53
|
+
for (const id of await readdir(join(this.root, 'projects'))) {
|
|
54
|
+
if (/^[0-9a-f-]{36}$/.test(id))
|
|
55
|
+
result.push(await this.get(id));
|
|
56
|
+
}
|
|
57
|
+
return result.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
58
|
+
}
|
|
59
|
+
async write(p) {
|
|
60
|
+
const dir = await this.path(p.id), temp = join(dir, randomUUID() + '.tmp');
|
|
61
|
+
try {
|
|
62
|
+
await writeFile(temp, JSON.stringify(p, null, 2), { flag: 'wx', mode: 0o600 });
|
|
63
|
+
await rename(temp, join(dir, 'project.json'));
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await unlink(temp).catch(() => { });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async create(value) {
|
|
70
|
+
await this.init();
|
|
71
|
+
const unlock = await lockfile.lock(join(this.root, 'projects'), { retries: { retries: 8, minTimeout: 30, maxTimeout: 60 } });
|
|
72
|
+
try {
|
|
73
|
+
if ((await this.list()).length >= 30)
|
|
74
|
+
throw new Error('工作台最多保存 30 个项目,请先备份现有工程。');
|
|
75
|
+
const p = { ...validateFields(value), id: randomUUID(), revision: 1, updated: new Date().toISOString(), assets: [], renders: [] };
|
|
76
|
+
await mkdir(join(this.root, 'projects', p.id), { mode: 0o700 });
|
|
77
|
+
await this.write(p);
|
|
78
|
+
return p;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await unlock();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async edit(id, revision, fn, bump = true) {
|
|
85
|
+
const dir = await this.path(id);
|
|
86
|
+
const unlock = await lockfile.lock(dir, { retries: { retries: 8, minTimeout: 30, maxTimeout: 60 } });
|
|
87
|
+
try {
|
|
88
|
+
const p = await this.get(id);
|
|
89
|
+
if (revision !== undefined && revision !== p.revision)
|
|
90
|
+
throw new MediaConflict('工程已在另一窗口更新。你的输入仍保留,请重新打开最新版本后比较。');
|
|
91
|
+
await fn(p);
|
|
92
|
+
if (bump)
|
|
93
|
+
p.revision++;
|
|
94
|
+
p.updated = new Date().toISOString();
|
|
95
|
+
await this.write(p);
|
|
96
|
+
return p;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await unlock();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async update(id, revision, value) { return this.edit(id, revision, p => { Object.assign(p, validateFields(value)); }); }
|
|
103
|
+
async assetBytes(id, asset) {
|
|
104
|
+
const file = join(await this.path(id), 'assets', asset.file);
|
|
105
|
+
await this.checkPath(file);
|
|
106
|
+
if ((await stat(file)).size !== asset.bytes || asset.bytes > 20 * 1024 * 1024)
|
|
107
|
+
throw new Error('素材已改变,请重新上传并检查。');
|
|
108
|
+
return readFile(file);
|
|
109
|
+
}
|
|
110
|
+
async upload(id, revision, name, bytes) {
|
|
111
|
+
if (!bytes.length || bytes.length > 20 * 1024 * 1024)
|
|
112
|
+
throw new Error('单个素材需为 1 字节–20 MB。');
|
|
113
|
+
const ext = extname(name).toLowerCase(), isPng = bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
114
|
+
const isJpg = bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;
|
|
115
|
+
let kind, suffix;
|
|
116
|
+
if (ext === '.png' && isPng) {
|
|
117
|
+
kind = 'image';
|
|
118
|
+
suffix = '.png';
|
|
119
|
+
}
|
|
120
|
+
else if (['.jpg', '.jpeg'].includes(ext) && isJpg) {
|
|
121
|
+
kind = 'image';
|
|
122
|
+
suffix = '.jpg';
|
|
123
|
+
}
|
|
124
|
+
else if (ext === '.wav' && bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WAVE') {
|
|
125
|
+
kind = 'audio';
|
|
126
|
+
suffix = '.wav';
|
|
127
|
+
}
|
|
128
|
+
else if (ext === '.mp3' && (bytes.toString('ascii', 0, 3) === 'ID3' || bytes[0] === 255 && (bytes[1] & 224) === 224)) {
|
|
129
|
+
kind = 'audio';
|
|
130
|
+
suffix = '.mp3';
|
|
131
|
+
}
|
|
132
|
+
else if (ext === '.mp4' && bytes.toString('ascii', 4, 8) === 'ftyp') {
|
|
133
|
+
kind = 'video';
|
|
134
|
+
suffix = '.mp4';
|
|
135
|
+
}
|
|
136
|
+
else
|
|
137
|
+
throw new Error('仅支持内容与扩展名一致的 PNG、JPEG、MP3、WAV 或 MP4。');
|
|
138
|
+
return this.edit(id, revision, async (p) => {
|
|
139
|
+
if (p.assets.length >= 12 || p.assets.reduce((n, a) => n + a.bytes, 0) + bytes.length > 40 * 1024 * 1024)
|
|
140
|
+
throw new Error('每个项目最多 12 个素材、合计 40 MB。');
|
|
141
|
+
if (kind !== 'image' && p.assets.some(a => a.kind === kind))
|
|
142
|
+
throw new Error(kind === 'audio' ? '已有背景音频,请先移除再替换。' : '已有背景视频,请先移除再替换。');
|
|
143
|
+
const assetId = randomUUID(), dir = join(await this.path(id), 'assets');
|
|
144
|
+
await mkdir(dir, { recursive: true });
|
|
145
|
+
const asset = { id: assetId, name: name.replace(/[\x00-\x1f/\\]/g, '_').slice(0, 100), file: assetId + suffix, kind, bytes: bytes.length };
|
|
146
|
+
await writeFile(join(dir, asset.file), bytes, { flag: 'wx', mode: 0o600 });
|
|
147
|
+
p.assets.push(asset);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async removeAsset(id, revision, assetId) {
|
|
151
|
+
let file;
|
|
152
|
+
const p = await this.edit(id, revision, async (p) => { const a = p.assets.find(a => a.id === mediaId(assetId)); if (!a)
|
|
153
|
+
throw new Error('素材不存在。'); file = join(await this.path(id), 'assets', a.file); p.assets = p.assets.filter(a => a.id !== assetId); });
|
|
154
|
+
// Unlink only this managed upload, never a source file or a resolved link target.
|
|
155
|
+
if (file)
|
|
156
|
+
await unlink(file).catch(() => { });
|
|
157
|
+
return p;
|
|
158
|
+
}
|
|
159
|
+
async snapshot(id, revision) {
|
|
160
|
+
const dir = await this.path(id), unlock = await lockfile.lock(dir, { retries: { retries: 8, minTimeout: 30, maxTimeout: 60 } });
|
|
161
|
+
try {
|
|
162
|
+
const project = await this.get(id);
|
|
163
|
+
if (project.revision !== revision)
|
|
164
|
+
throw new MediaConflict('项目已更新,请保存并重新预览。');
|
|
165
|
+
const assets = new Map();
|
|
166
|
+
for (const a of project.assets)
|
|
167
|
+
assets.set(a.file, await this.assetBytes(id, a));
|
|
168
|
+
return { project, assets };
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
await unlock();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Original managed templates. Visual identity: templates/DESIGN.md. */
|
|
2
|
+
export type MediaEngine = 'hyperframes' | 'remotion';
|
|
3
|
+
export interface MediaAsset {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
file: string;
|
|
7
|
+
kind: 'image' | 'audio' | 'video';
|
|
8
|
+
bytes: number;
|
|
9
|
+
}
|
|
10
|
+
export interface MediaFields {
|
|
11
|
+
template: string;
|
|
12
|
+
title: string;
|
|
13
|
+
body: string;
|
|
14
|
+
format: string;
|
|
15
|
+
duration: number;
|
|
16
|
+
}
|
|
17
|
+
export interface MediaProject extends MediaFields {
|
|
18
|
+
id: string;
|
|
19
|
+
revision: number;
|
|
20
|
+
updated: string;
|
|
21
|
+
assets: MediaAsset[];
|
|
22
|
+
renders: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
revision: number;
|
|
25
|
+
bytes: number;
|
|
26
|
+
created: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
export declare const MEDIA_VERSIONS: {
|
|
30
|
+
readonly hyperframes: "0.8.114";
|
|
31
|
+
readonly remotion: "4.0.532";
|
|
32
|
+
readonly gsap: "3.14.2";
|
|
33
|
+
};
|
|
34
|
+
export declare const MEDIA_TEMPLATES: readonly [{
|
|
35
|
+
readonly id: "title";
|
|
36
|
+
readonly zh: "标题卡";
|
|
37
|
+
readonly en: "Title card";
|
|
38
|
+
readonly description: "一句标题、一段介绍,适合开场与通知。";
|
|
39
|
+
}, {
|
|
40
|
+
readonly id: "product";
|
|
41
|
+
readonly zh: "产品介绍";
|
|
42
|
+
readonly en: "Product card";
|
|
43
|
+
readonly description: "深色画布与一张主图,适合产品亮点。";
|
|
44
|
+
}, {
|
|
45
|
+
readonly id: "slideshow";
|
|
46
|
+
readonly zh: "图文轮播";
|
|
47
|
+
readonly en: "Slideshow";
|
|
48
|
+
readonly description: "上传的图片依次展示,正文每行对应一页。";
|
|
49
|
+
}];
|
|
50
|
+
export declare const MEDIA_FORMATS: Record<string, [number, number]>;
|
|
51
|
+
export declare function validateFields(value: unknown): MediaFields;
|
|
52
|
+
export declare function projectSources(p: MediaProject, engine: MediaEngine): Record<string, string>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const MEDIA_VERSIONS = { hyperframes: '0.8.114', remotion: '4.0.532', gsap: '3.14.2' };
|
|
2
|
+
export const MEDIA_TEMPLATES = [
|
|
3
|
+
{ id: 'title', zh: '标题卡', en: 'Title card', description: '一句标题、一段介绍,适合开场与通知。' },
|
|
4
|
+
{ id: 'product', zh: '产品介绍', en: 'Product card', description: '深色画布与一张主图,适合产品亮点。' },
|
|
5
|
+
{ id: 'slideshow', zh: '图文轮播', en: 'Slideshow', description: '上传的图片依次展示,正文每行对应一页。' },
|
|
6
|
+
];
|
|
7
|
+
export const MEDIA_FORMATS = { landscape: [1280, 720], portrait: [720, 1280], square: [720, 720] };
|
|
8
|
+
export function validateFields(value) {
|
|
9
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
10
|
+
throw new Error('请填写项目内容。');
|
|
11
|
+
const v = value;
|
|
12
|
+
if (!MEDIA_TEMPLATES.some(row => row.id === v.template))
|
|
13
|
+
throw new Error('请选择支持的模板。');
|
|
14
|
+
if (typeof v.title !== 'string' || !v.title.trim() || v.title.length > 72)
|
|
15
|
+
throw new Error('标题需为 1–72 个字符。');
|
|
16
|
+
if (typeof v.body !== 'string' || v.body.length > 360)
|
|
17
|
+
throw new Error('正文最多 360 个字符。');
|
|
18
|
+
if (typeof v.format !== 'string' || !Object.hasOwn(MEDIA_FORMATS, v.format))
|
|
19
|
+
throw new Error('请选择支持的画幅。');
|
|
20
|
+
if (typeof v.duration !== 'number' || !Number.isInteger(v.duration) || v.duration < 3 || v.duration > 30)
|
|
21
|
+
throw new Error('时长需为 3–30 整秒。');
|
|
22
|
+
return { template: v.template, title: v.title.trim(), body: v.body.trim(), format: v.format, duration: v.duration };
|
|
23
|
+
}
|
|
24
|
+
const escapeHtml = (text) => text.replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
|
25
|
+
export function projectSources(p, engine) {
|
|
26
|
+
const [width, height] = MEDIA_FORMATS[p.format];
|
|
27
|
+
const images = p.assets.filter(a => a.kind === 'image'), audio = p.assets.find(a => a.kind === 'audio'), video = p.assets.find(a => a.kind === 'video');
|
|
28
|
+
const count = p.template === 'slideshow' ? Math.max(images.length, 1) : 1;
|
|
29
|
+
if (p.duration < count * 2)
|
|
30
|
+
throw new Error(`轮播 ${count} 张图片至少需要 ${count * 2} 秒,请增加时长。`);
|
|
31
|
+
const dark = p.template === 'product', portrait = width <= height;
|
|
32
|
+
const copyWidth = (width - (portrait ? 96 : 128)) * (!portrait && images.length ? .51 : 1);
|
|
33
|
+
const copyHeight = (height - (portrait ? 120 : 112)) * (portrait && images.length ? .55 : 1) - 36;
|
|
34
|
+
// Conservatively fit full-width CJK characters and explicit newlines in the safe area.
|
|
35
|
+
const linesAt = (text, size) => text.split('\n').reduce((n, line) => n + Math.max(1, Math.ceil([...line].length / Math.max(1, Math.floor(copyWidth / size)))), 0);
|
|
36
|
+
let titleSize = Math.round((portrait ? 52 : 68) * (p.title.length > 40 ? .72 : p.title.length > 24 ? .85 : 1));
|
|
37
|
+
let bodySize = p.body.length > 200 ? 21 : 27;
|
|
38
|
+
const bodies = count > 1 ? p.body.split('\n').slice(0, count) : [p.body];
|
|
39
|
+
const fits = () => 42 + linesAt(p.title, titleSize) * titleSize * 1.15 + 22 + Math.max(...bodies.map(body => linesAt(body, bodySize) * bodySize * 1.6)) <= copyHeight;
|
|
40
|
+
while (!fits() && (bodySize > 16 || titleSize > 28)) {
|
|
41
|
+
if (bodySize > 16)
|
|
42
|
+
bodySize--;
|
|
43
|
+
else
|
|
44
|
+
titleSize--;
|
|
45
|
+
}
|
|
46
|
+
if (!fits())
|
|
47
|
+
throw new Error('文字超过此画幅的可读空间。请缩短正文或分成轮播页后再导出。');
|
|
48
|
+
if (count > 1 && p.body.split('\n').length > count)
|
|
49
|
+
throw new Error('正文行数多于轮播图片数,请增加图片或合并文字。');
|
|
50
|
+
const css = `@font-face{font-family:"Microsoft YaHei";src:local("Microsoft YaHei")}*{box-sizing:border-box}body{margin:0;font-family:system-ui,"Microsoft YaHei",sans-serif} #root{position:relative;width:${width}px;height:${height}px;overflow:hidden;background:${dark ? '#142D39' : '#FFF8ED'};color:${dark ? '#FFF8ED' : '#142D39'}} .scene{position:absolute;inset:0;padding:${portrait ? '60px 48px' : '56px 64px'};display:flex;${portrait ? 'flex-direction:column;' : ''}align-items:center;justify-content:center;gap:36px} .copy{flex:1;min-width:0;width:100%} .eyebrow{color:#F26445;font-size:18px;letter-spacing:4px;font-weight:700;margin-bottom:18px} h1{font-size:${titleSize}px;line-height:1.15;margin:0;overflow-wrap:anywhere} p{font-size:${bodySize}px;line-height:1.6;margin:22px 0 0;white-space:pre-line;overflow-wrap:anywhere} .visual{${portrait ? 'height:40%;width:100%;' : 'width:45%;height:80%;'}flex-shrink:0;object-fit:contain;border-radius:18px} .accent{position:absolute;left:0;top:0;width:14px;height:100%;background:#F26445} .page{position:absolute;right:40px;bottom:22px;font-size:18px;opacity:.65}`;
|
|
51
|
+
const design = `# Design\nOriginal DSH managed templates.\n\nPaper #FFF8ED; ink #142D39; coral #F26445; mint #79CDB4. Local system-ui / Microsoft YaHei. Safe margins, wrapped text, deterministic entrances. No remote fonts, random or infinite animation.\n`;
|
|
52
|
+
const info = { ...p, width, height, fps: 24, titleSize, bodySize, dark, portrait };
|
|
53
|
+
const sources = {
|
|
54
|
+
'project.json': JSON.stringify(p, null, 2), 'DESIGN.md': design,
|
|
55
|
+
'README.md': `# ${p.title.replace(/[\r\n]/g, ' ')}\n\nManaged ${engine} project, revision ${p.revision}.\n\nRun npm install, then npm run preview or npm run render. Media files are local copies in public/assets. Generated template code is MIT. Renderer dependencies keep their own licenses; Remotion: https://www.remotion.dev/license .\n`,
|
|
56
|
+
};
|
|
57
|
+
if (engine === 'hyperframes') {
|
|
58
|
+
sources['meta.json'] = JSON.stringify({ id: p.id, name: p.title, createdAt: p.updated }, null, 2);
|
|
59
|
+
sources['hyperframes.json'] = JSON.stringify({ $schema: 'https://hyperframes.heygen.com/schema/hyperframes.json', paths: { blocks: 'compositions', components: 'compositions/components', assets: 'public/assets' }, media: { autoProxy: true } }, null, 2);
|
|
60
|
+
let clips = '';
|
|
61
|
+
for (let i = 0; i < count; i++) {
|
|
62
|
+
const start = i * p.duration / count, duration = p.duration / count;
|
|
63
|
+
const body = count > 1 ? (p.body.split('\n')[i] ?? '') : p.body;
|
|
64
|
+
const scene = 'scene-' + i;
|
|
65
|
+
clips += `<div id="${scene}" class="clip" data-composition-id="${scene}" data-composition-src="compositions/${scene}.html" data-start="${start}" data-duration="${duration}" data-track-index="1" style="position:absolute;inset:0;width:100%;height:100%"></div>`;
|
|
66
|
+
sources['compositions/' + scene + '.html'] = `<!doctype html><html lang="zh"><head><meta charset="utf-8"><style>${css}#root{background:transparent}</style></head><body><div id="root" data-composition-id="${scene}" data-start="0" data-duration="${duration}" data-width="${width}" data-height="${height}"><div class="scene"><div class="copy"><div class="eyebrow">${escapeHtml(p.template.toUpperCase())}</div><h1 id="title-${i}">${escapeHtml(p.title)}</h1><p id="body-${i}">${escapeHtml(body)}</p></div>${images.length ? `<img class="visual" src="public/assets/${images[i % images.length].file}" alt="">` : ''}<div class="page">${i + 1} / ${count}</div></div><div class="accent" data-layout-ignore></div></div><script src="vendor/gsap.min.js"></script><script>window.__timelines=window.__timelines||{};const tl=gsap.timeline({paused:true});tl.to({},{duration:${duration}},0);tl.from('#title-${i}',{y:24,opacity:0,duration:.45},.08);tl.from('#body-${i}',{y:16,opacity:0,duration:.4},.2);window.__timelines['${scene}']=tl;</script></body></html>`;
|
|
67
|
+
}
|
|
68
|
+
const media = `${video ? `<video id="bg-video" class="clip" style="position:absolute;inset:0;width:100%;height:100%;opacity:.22;object-fit:cover" src="public/assets/${video.file}" muted playsinline data-start="0" data-duration="${p.duration}" data-track-index="0"></video>` : ''}${audio ? `<audio id="music" class="clip" src="public/assets/${audio.file}" data-start="0" data-duration="${p.duration}" data-track-index="2" data-volume="0.4"></audio>` : ''}`;
|
|
69
|
+
sources['index.html'] = `<!doctype html><html lang="zh"><head><meta charset="utf-8"><style>${css}</style></head><body><div id="root" data-composition-id="managed-video" data-start="0" data-duration="${p.duration}" data-width="${width}" data-height="${height}" data-fps="24">${media}${clips}</div><script src="vendor/gsap.min.js"></script><script>window.__timelines=window.__timelines||{};const tl=gsap.timeline({paused:true});tl.to({},{duration:${p.duration}},0);window.__timelines['managed-video']=tl;</script></body></html>`;
|
|
70
|
+
sources['setup.mjs'] = `import{mkdirSync,copyFileSync}from'node:fs';mkdirSync('vendor',{recursive:true});copyFileSync('node_modules/gsap/dist/gsap.min.js','vendor/gsap.min.js');\n`;
|
|
71
|
+
sources['package.json'] = JSON.stringify({ private: true, type: 'module', dependencies: { hyperframes: MEDIA_VERSIONS.hyperframes, gsap: MEDIA_VERSIONS.gsap }, scripts: { prepare: 'node setup.mjs', preview: 'hyperframes preview --no-open --foreground', render: 'hyperframes browser ensure && hyperframes render --workers 1 --no-best-effort --strict --output final.mp4 --fps 24' } }, null, 2);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
sources['data.json'] = JSON.stringify(info);
|
|
75
|
+
sources['src/index.jsx'] = `import React from 'react';
|
|
76
|
+
import {registerRoot,Composition,AbsoluteFill,Img,Audio,OffthreadVideo,staticFile,useCurrentFrame,interpolate} from 'remotion';
|
|
77
|
+
import data from '../data.json';
|
|
78
|
+
function Video(){const frame=useCurrentFrame();const photos=data.assets.filter(a=>a.kind==='image'),music=data.assets.find(a=>a.kind==='audio'),video=data.assets.find(a=>a.kind==='video');const count=data.template==='slideshow'?Math.max(photos.length,1):1;const length=data.duration*24/count;const page=Math.min(count-1,Math.floor(frame/length));const local=frame-page*length;const opacity=interpolate(local,[0,12,length-10,length],[0,1,1,0],{extrapolateLeft:'clamp',extrapolateRight:'clamp'});const body=count>1?(data.body.split('\\n')[page]||''):data.body;
|
|
79
|
+
return <AbsoluteFill style={{background:data.dark?'#142D39':'#FFF8ED',color:data.dark?'#FFF8ED':'#142D39',fontFamily:'system-ui,Microsoft YaHei,sans-serif'}}>
|
|
80
|
+
{video&&<OffthreadVideo muted src={staticFile('assets/'+video.file)} style={{position:'absolute',width:'100%',height:'100%',objectFit:'cover',opacity:.22}}/>}
|
|
81
|
+
<div style={{position:'absolute',inset:0,padding:data.portrait?'60px 48px':'56px 64px',display:'flex',flexDirection:data.portrait?'column':'row',alignItems:'center',justifyContent:'center',gap:36,opacity}}>
|
|
82
|
+
<div style={{flex:1,minWidth:0,width:'100%'}}><div style={{color:'#F26445',fontSize:18,letterSpacing:4,fontWeight:700,marginBottom:18}}>{data.template.toUpperCase()}</div><h1 style={{fontSize:data.titleSize,lineHeight:1.15,margin:0,overflowWrap:'anywhere'}}>{data.title}</h1><p style={{fontSize:data.bodySize,lineHeight:1.6,marginTop:22,whiteSpace:'pre-line',overflowWrap:'anywhere'}}>{body}</p></div>
|
|
83
|
+
{photos.length>0&&<Img src={staticFile('assets/'+photos[page%photos.length].file)} style={{width:data.portrait?'100%':'45%',height:data.portrait?'40%':'80%',flexShrink:0,objectFit:'contain',borderRadius:18}}/>}</div>
|
|
84
|
+
<div style={{position:'absolute',left:0,top:0,width:14,height:'100%',background:'#F26445'}}/><div style={{position:'absolute',right:40,bottom:22,fontSize:18,opacity:.65}}>{page+1} / {count}</div>{music&&<Audio src={staticFile('assets/'+music.file)} volume={.4}/>}</AbsoluteFill>}
|
|
85
|
+
const Root=()=> <Composition id="ManagedVideo" component={Video} durationInFrames={data.duration*24} fps={24} width={data.width} height={data.height}/>;
|
|
86
|
+
registerRoot(Root);
|
|
87
|
+
`;
|
|
88
|
+
sources['package.json'] = JSON.stringify({ private: true, dependencies: { '@remotion/cli': MEDIA_VERSIONS.remotion, remotion: MEDIA_VERSIONS.remotion, react: '18.3.1', 'react-dom': '18.3.1' }, scripts: { preview: 'remotion studio src/index.jsx --no-open', render: 'remotion render src/index.jsx ManagedVideo final.mp4 --concurrency 1 --codec h264' } }, null, 2);
|
|
89
|
+
}
|
|
90
|
+
return sources;
|
|
91
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { MediaStore } from './media-store.js';
|
|
2
|
+
import { type MediaEngine } from './media-templates.js';
|
|
3
|
+
interface MediaJob {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: 'prepare' | 'preview' | 'render';
|
|
6
|
+
projectId?: string;
|
|
7
|
+
revision?: number;
|
|
8
|
+
state: 'running' | 'done' | 'failed' | 'cancelled';
|
|
9
|
+
stage: string;
|
|
10
|
+
lines: string[];
|
|
11
|
+
url?: string;
|
|
12
|
+
output?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class MediaWorkbench {
|
|
15
|
+
readonly engine: MediaEngine;
|
|
16
|
+
readonly store: MediaStore;
|
|
17
|
+
readonly route: string;
|
|
18
|
+
readonly runtime: string;
|
|
19
|
+
private jobs;
|
|
20
|
+
private disposed;
|
|
21
|
+
constructor(engine: MediaEngine, root?: string);
|
|
22
|
+
private browserFile;
|
|
23
|
+
environment(): Promise<{
|
|
24
|
+
ready: boolean;
|
|
25
|
+
version: string;
|
|
26
|
+
node: string;
|
|
27
|
+
hint: string;
|
|
28
|
+
}>;
|
|
29
|
+
job(id: string): MediaJob;
|
|
30
|
+
private log;
|
|
31
|
+
private stopChild;
|
|
32
|
+
private run;
|
|
33
|
+
private cli;
|
|
34
|
+
start(kind: MediaJob['kind'], id?: string, revision?: unknown): Promise<MediaJob>;
|
|
35
|
+
private execute;
|
|
36
|
+
cancel(id: string): MediaJob;
|
|
37
|
+
dispose(): void;
|
|
38
|
+
private guard;
|
|
39
|
+
fetch(request: Request): Promise<Response>;
|
|
40
|
+
}
|
|
41
|
+
export declare function installMediaWorkbench(ctx: any, engine: MediaEngine): MediaWorkbench;
|
|
42
|
+
export {};
|