dsh-remotion 0.3.3 → 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/CHANGELOG.md +21 -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 +22 -0
- package/lib/client.js +90 -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
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn, execFile } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { mkdir, readFile, writeFile, stat, symlink, copyFile, open, unlink, realpath } from 'node:fs/promises';
|
|
5
|
+
import { basename, dirname, join, delimiter, toNamespacedPath } from 'node:path';
|
|
6
|
+
import { createServer } from 'node:net';
|
|
7
|
+
import lockfile from 'proper-lockfile';
|
|
8
|
+
import { zipSync, strToU8 } from 'fflate';
|
|
9
|
+
import { MediaStore, mediaId, MediaConflict } from './media-store.js';
|
|
10
|
+
import { MEDIA_VERSIONS, MEDIA_TEMPLATES, projectSources } from './media-templates.js';
|
|
11
|
+
const mime = { png: 'image/png', jpg: 'image/jpeg', mp3: 'audio/mpeg', wav: 'audio/wav', mp4: 'video/mp4' };
|
|
12
|
+
const clean = (text) => text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[redacted]@').replace(/(token|password|authorization)(\s*[=:]\s*)[^\s,;]+/gi, '$1$2[redacted]').slice(0, 1600);
|
|
13
|
+
const errorMessage = (e) => clean(e instanceof Error ? e.message : String(e));
|
|
14
|
+
function installedChromePaths() {
|
|
15
|
+
if (process.platform === 'darwin')
|
|
16
|
+
return ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'];
|
|
17
|
+
if (process.platform !== 'win32')
|
|
18
|
+
return ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser'];
|
|
19
|
+
return [join(process.env.ProgramFiles || 'C:/Program Files', 'Google/Chrome/Application/chrome.exe'), join(process.env['ProgramFiles(x86)'] || 'C:/Program Files (x86)', 'Google/Chrome/Application/chrome.exe'), ...(process.env.LOCALAPPDATA ? [join(process.env.LOCALAPPDATA, 'Google/Chrome/Application/chrome.exe')] : [])];
|
|
20
|
+
}
|
|
21
|
+
function npmCli() {
|
|
22
|
+
const paths = [join(dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'), join(dirname(process.execPath), '../lib/node_modules/npm/bin/npm-cli.js')];
|
|
23
|
+
for (const path of (process.env.PATH || '').split(delimiter))
|
|
24
|
+
paths.push(join(path, 'node_modules/npm/bin/npm-cli.js'), join(path, '../node_modules/npm/bin/npm-cli.js'));
|
|
25
|
+
if (process.env.APPDATA)
|
|
26
|
+
paths.push(join(process.env.APPDATA, 'npm/node_modules/npm/bin/npm-cli.js'));
|
|
27
|
+
const file = paths.find(existsSync);
|
|
28
|
+
if (!file)
|
|
29
|
+
throw new Error('没有找到 npm。请安装 Node.js 22.19+ 或 24+,重启 DSH 后再准备环境。');
|
|
30
|
+
return file;
|
|
31
|
+
}
|
|
32
|
+
function childEnv(root) {
|
|
33
|
+
const result = {};
|
|
34
|
+
for (const [key, value] of Object.entries(process.env))
|
|
35
|
+
if (/^(path|pathext|systemroot|windir|userprofile|home|appdata|localappdata|https?_proxy|all_proxy|no_proxy|comspec|programfiles|programfiles\(x86\))$/i.test(key))
|
|
36
|
+
result[key] = value;
|
|
37
|
+
return { ...result, ELECTRON_RUN_AS_NODE: '1', DSH_DESKTOP_NODE_EXECUTABLE: process.execPath, HOME: join(root, 'user-home'), USERPROFILE: join(root, 'user-home'), APPDATA: join(root, 'user-home', 'AppData/Roaming'), LOCALAPPDATA: join(root, 'user-home', 'AppData/Local'), CI: '1', NO_COLOR: '1', BROWSER: 'none', TEMP: join(root, 'tmp'), TMP: join(root, 'tmp'), TMPDIR: join(root, 'tmp'), npm_config_userconfig: join(root, 'local.npmrc'), npm_config_cache: join(root, 'npm-cache'), PUPPETEER_CACHE_DIR: join(root, 'browser'), HYPERFRAMES_NO_UPDATE_CHECK: '1', HYPERFRAMES_SKIP_SKILLS: '1', HYPERFRAMES_TELEMETRY_DISABLED: '1', DO_NOT_TRACK: '1', HYPERFRAMES_EXTRACT_CACHE_DIR: join(root, 'frames') };
|
|
38
|
+
}
|
|
39
|
+
async function freePort() {
|
|
40
|
+
return new Promise((resolve, reject) => { const server = createServer(); server.once('error', reject); server.listen(0, '127.0.0.1', () => { const address = server.address(); const port = typeof address === 'object' && address ? address.port : 0; server.close(() => resolve(port)); }); });
|
|
41
|
+
}
|
|
42
|
+
/** Restrict all servers in our renderer child to loopback using Node's public API. */
|
|
43
|
+
const LOOPBACK = `const net=require('node:net');const listen=net.Server.prototype.listen;net.Server.prototype.listen=function(...args){if(args[0]&&typeof args[0]==='object'&&'port'in args[0])args[0]={...args[0],host:'127.0.0.1'};else if(typeof args[0]==='number'){if(typeof args[1]==='string')args[1]='127.0.0.1';else args.splice(1,0,'127.0.0.1')}return listen.apply(this,args)};\n`;
|
|
44
|
+
export class MediaWorkbench {
|
|
45
|
+
engine;
|
|
46
|
+
store;
|
|
47
|
+
route;
|
|
48
|
+
runtime;
|
|
49
|
+
jobs = new Map();
|
|
50
|
+
disposed = false;
|
|
51
|
+
constructor(engine, root) {
|
|
52
|
+
this.engine = engine;
|
|
53
|
+
this.store = new MediaStore(engine, root);
|
|
54
|
+
this.route = '/api/dsh-' + engine + '/workbench';
|
|
55
|
+
this.runtime = join(this.store.root, 'runtime');
|
|
56
|
+
}
|
|
57
|
+
async browserFile(value) {
|
|
58
|
+
if (typeof value !== 'string')
|
|
59
|
+
throw new Error('请重新准备渲染环境,浏览器路径尚未验证。');
|
|
60
|
+
const file = await realpath(value), normalize = (p) => process.platform === 'win32' ? p.toLowerCase() : p;
|
|
61
|
+
const known = await Promise.all(installedChromePaths().map(p => realpath(p).catch(() => '')));
|
|
62
|
+
if (!known.some(p => normalize(p) === normalize(file)))
|
|
63
|
+
await this.store.checkPath(file);
|
|
64
|
+
if (!(await stat(file)).isFile())
|
|
65
|
+
throw new Error('浏览器文件不存在,请重新准备环境。');
|
|
66
|
+
return file;
|
|
67
|
+
}
|
|
68
|
+
async environment() {
|
|
69
|
+
const version = MEDIA_VERSIONS[this.engine];
|
|
70
|
+
let ready = false;
|
|
71
|
+
try {
|
|
72
|
+
const pkg = JSON.parse(await readFile(join(this.runtime, 'node_modules', this.engine === 'hyperframes' ? 'hyperframes' : '@remotion/cli', 'package.json'), 'utf8'));
|
|
73
|
+
const stamp = JSON.parse(await readFile(join(this.runtime, 'ready.json'), 'utf8'));
|
|
74
|
+
const file = await this.browserFile(stamp.browserPath);
|
|
75
|
+
ready = pkg.version === version && stamp.version === version && !(this.engine === 'hyperframes' && process.platform === 'win32' && !/^chrome-headless-shell\.exe$/i.test(basename(file)));
|
|
76
|
+
}
|
|
77
|
+
catch { }
|
|
78
|
+
return { ready, version, node: process.version, hint: this.engine === 'hyperframes' ? '首次下载官方 CLI;Windows 使用官方专用渲染浏览器,其他系统优先复用 Chrome。还需 PATH 中的 FFmpeg。' : '首次下载官方 CLI。优先使用已安装的 Chrome,没有时下载浏览器。Remotion 使用官方许可证:https://www.remotion.dev/license' };
|
|
79
|
+
}
|
|
80
|
+
job(id) { const job = this.jobs.get(mediaId(id)); if (!job)
|
|
81
|
+
throw new Error('任务不存在或 DSH 已重启。工程仍保留,请检查成品后重新操作。'); return structuredClone(job.view); }
|
|
82
|
+
log(job, text) {
|
|
83
|
+
const normalize = (line) => line.replace(/^[◓◑◒◐]\s*/, '').replace(/[. ]+$/, '');
|
|
84
|
+
for (const line of text.split(/[\r\n]+/).filter(Boolean).map(clean)) {
|
|
85
|
+
if (normalize(line) === normalize(job.view.lines.at(-1) || ''))
|
|
86
|
+
continue;
|
|
87
|
+
job.view.lines.push(line);
|
|
88
|
+
if (job.view.lines.length > 40)
|
|
89
|
+
job.view.lines.shift();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
stopChild(job) {
|
|
93
|
+
const child = job.child;
|
|
94
|
+
if (!child?.pid || child.exitCode !== null || child.signalCode !== null)
|
|
95
|
+
return;
|
|
96
|
+
if (process.platform === 'win32')
|
|
97
|
+
execFile(join(process.env.SystemRoot || 'C:/Windows', 'System32/taskkill.exe'), ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true }, () => { });
|
|
98
|
+
else {
|
|
99
|
+
try {
|
|
100
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
child.kill('SIGKILL');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async run(job, file, args, cwd, environment = {}) {
|
|
108
|
+
job.controller.signal.throwIfAborted();
|
|
109
|
+
await mkdir(join(this.store.root, 'tmp'), { recursive: true });
|
|
110
|
+
await mkdir(join(this.store.root, 'user-home/AppData/Roaming'), { recursive: true });
|
|
111
|
+
await mkdir(join(this.store.root, 'user-home/AppData/Local'), { recursive: true });
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const child = spawn(file, args, { cwd, env: { ...childEnv(this.store.root), ...environment }, windowsHide: true, shell: false, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
114
|
+
job.child = child;
|
|
115
|
+
const abort = () => this.stopChild(job);
|
|
116
|
+
job.controller.signal.addEventListener('abort', abort, { once: true });
|
|
117
|
+
child.stdout?.on('data', data => this.log(job, data.toString()));
|
|
118
|
+
child.stderr?.on('data', data => this.log(job, data.toString()));
|
|
119
|
+
const finish = () => { job.controller.signal.removeEventListener('abort', abort); if (job.child === child)
|
|
120
|
+
job.child = undefined; };
|
|
121
|
+
child.once('error', e => { finish(); reject(e); });
|
|
122
|
+
child.once('close', code => { finish(); if (job.controller.signal.aborted)
|
|
123
|
+
reject(new Error('操作已取消。'));
|
|
124
|
+
else if (code !== 0) {
|
|
125
|
+
const useful = job.view.lines.filter(line => /^(?:\w*Error:|npm error)|\b(?:ENOENT|EACCES|ECONNRESET)\b|Unknown flag|Failed to |DevTools remote debugging/.test(line));
|
|
126
|
+
reject(new Error(`官方 ${this.engine} 命令未完成(退出码 ${code})。\n` + (useful.length ? useful.slice(0, 3) : job.view.lines.slice(-5)).join('\n')));
|
|
127
|
+
}
|
|
128
|
+
else
|
|
129
|
+
resolve(); });
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
cli() { return join(this.runtime, 'node_modules', this.engine === 'hyperframes' ? 'hyperframes/bin/hyperframes.mjs' : '@remotion/cli/remotion-cli.js'); }
|
|
133
|
+
async start(kind, id, revision) {
|
|
134
|
+
if (this.disposed)
|
|
135
|
+
throw new Error('插件已卸载,请重新加载。');
|
|
136
|
+
if ([...this.jobs.values()].some(j => j.view.state === 'running' && (j.view.kind === kind || j.view.kind === 'prepare' || kind === 'prepare')))
|
|
137
|
+
throw new Error('已有同类任务运行,请等待完成或明确取消。');
|
|
138
|
+
if (kind !== 'prepare' && !(await this.environment()).ready)
|
|
139
|
+
throw new Error('请先点击“准备渲染环境”,完成首次下载。');
|
|
140
|
+
const snapshot = kind === 'prepare' ? undefined : await this.store.snapshot(mediaId(id), revision);
|
|
141
|
+
if (snapshot)
|
|
142
|
+
projectSources(snapshot.project, this.engine);
|
|
143
|
+
const job = { view: { id: randomUUID(), kind, projectId: snapshot?.project.id, revision: snapshot?.project.revision, state: 'running', stage: kind === 'prepare' ? '下载环境' : '准备修订快照', lines: [] }, controller: new AbortController() };
|
|
144
|
+
if (this.jobs.size >= 30) {
|
|
145
|
+
const old = [...this.jobs.values()].find(j => j.view.state !== 'running');
|
|
146
|
+
if (old)
|
|
147
|
+
this.jobs.delete(old.view.id);
|
|
148
|
+
else
|
|
149
|
+
throw new Error('运行任务过多。');
|
|
150
|
+
}
|
|
151
|
+
this.jobs.set(job.view.id, job);
|
|
152
|
+
void this.execute(job, snapshot);
|
|
153
|
+
return structuredClone(job.view);
|
|
154
|
+
}
|
|
155
|
+
async execute(job, snapshot) {
|
|
156
|
+
let unlock;
|
|
157
|
+
const timeout = setTimeout(() => job.controller.abort(), job.view.kind === 'preview' ? 30 * 60000 : 20 * 60000);
|
|
158
|
+
timeout.unref();
|
|
159
|
+
try {
|
|
160
|
+
await this.store.init();
|
|
161
|
+
await mkdir(this.runtime, { recursive: true });
|
|
162
|
+
const slot = join(this.store.root, 'workbench-slot');
|
|
163
|
+
await mkdir(slot, { recursive: true });
|
|
164
|
+
unlock = await lockfile.lock(slot, { retries: 0, stale: 30000, update: 10000, onCompromised: () => job.controller.abort() });
|
|
165
|
+
if (job.view.kind === 'prepare') {
|
|
166
|
+
await unlink(join(this.runtime, 'ready.json')).catch(() => { });
|
|
167
|
+
if (this.engine === 'hyperframes') {
|
|
168
|
+
job.view.stage = '检查 FFmpeg';
|
|
169
|
+
await this.run(job, 'ffmpeg', ['-version'], this.runtime);
|
|
170
|
+
}
|
|
171
|
+
const dependencies = this.engine === 'hyperframes' ? { hyperframes: MEDIA_VERSIONS.hyperframes, gsap: MEDIA_VERSIONS.gsap } : { '@remotion/cli': MEDIA_VERSIONS.remotion, remotion: MEDIA_VERSIONS.remotion, react: '18.3.1', 'react-dom': '18.3.1' };
|
|
172
|
+
await writeFile(join(this.runtime, 'package.json'), JSON.stringify({ private: true, dependencies }));
|
|
173
|
+
job.view.stage = '下载官方依赖';
|
|
174
|
+
await this.run(job, process.execPath, [npmCli(), 'install', '--ignore-scripts', '--no-audit', '--no-fund', '--registry=https://registry.npmjs.org'], this.runtime);
|
|
175
|
+
job.view.stage = '检查官方环境';
|
|
176
|
+
await this.run(job, process.execPath, [this.cli(), ...(this.engine === 'hyperframes' ? ['--version'] : ['versions'])], this.runtime);
|
|
177
|
+
job.view.stage = '检查或下载 Chrome';
|
|
178
|
+
if (this.engine === 'hyperframes') {
|
|
179
|
+
if (process.platform === 'win32') {
|
|
180
|
+
job.view.stage = '下载专用渲染浏览器';
|
|
181
|
+
// ensure prefers the managed shell; path alone would fall back to desktop Chrome.
|
|
182
|
+
// Avoid --force: the pinned CLI leaves its first spinner running after a forced install.
|
|
183
|
+
await this.run(job, process.execPath, [this.cli(), 'browser', 'ensure'], this.runtime);
|
|
184
|
+
}
|
|
185
|
+
await this.run(job, process.execPath, [this.cli(), 'browser', 'path'], this.runtime);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const installed = installedChromePaths().find(existsSync);
|
|
189
|
+
if (!installed)
|
|
190
|
+
await this.run(job, process.execPath, [this.cli(), 'browser', 'ensure'], this.runtime);
|
|
191
|
+
await this.run(job, process.execPath, ['-e', "require('@remotion/renderer').ensureBrowser({logLevel:'error',browserExecutable:process.argv[1]||null}).then(v=>console.log(require('node:fs').realpathSync(v.path))).catch(e=>{console.error(e.message);process.exitCode=1})", installed || ''], this.runtime);
|
|
192
|
+
}
|
|
193
|
+
const browserPath = await this.browserFile(job.view.lines.at(-1).trim());
|
|
194
|
+
await writeFile(join(this.runtime, 'ready.json'), JSON.stringify({ version: MEDIA_VERSIONS[this.engine], browserPath }));
|
|
195
|
+
}
|
|
196
|
+
else if (snapshot) {
|
|
197
|
+
const p = snapshot.project, dir = join(await this.store.path(p.id), 'runs', job.view.id);
|
|
198
|
+
await mkdir(dirname(dir), { recursive: true });
|
|
199
|
+
if (this.engine === 'hyperframes')
|
|
200
|
+
await this.run(job, process.execPath, [this.cli(), 'init', dir, '--example', 'blank', '--non-interactive', '--skip-transcribe'], dirname(dir));
|
|
201
|
+
await mkdir(join(dir, 'public/assets'), { recursive: true });
|
|
202
|
+
for (const [file, bytes] of snapshot.assets)
|
|
203
|
+
await writeFile(join(dir, 'public/assets', file), bytes, { flag: 'wx' });
|
|
204
|
+
for (const [file, content] of Object.entries(projectSources(p, this.engine))) {
|
|
205
|
+
await mkdir(dirname(join(dir, file)), { recursive: true });
|
|
206
|
+
await writeFile(join(dir, file), content);
|
|
207
|
+
}
|
|
208
|
+
if (this.engine === 'hyperframes')
|
|
209
|
+
await writeFile(join(dir, 'meta.json'), JSON.stringify({ id: job.view.id, name: p.title, createdAt: p.updated }));
|
|
210
|
+
await writeFile(join(dir, 'loopback.cjs'), LOOPBACK);
|
|
211
|
+
await symlink(join(this.runtime, 'node_modules'), join(dir, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
212
|
+
if (this.engine === 'hyperframes') {
|
|
213
|
+
await mkdir(join(dir, 'vendor'), { recursive: true });
|
|
214
|
+
await copyFile(join(this.runtime, 'node_modules/gsap/dist/gsap.min.js'), join(dir, 'vendor/gsap.min.js'));
|
|
215
|
+
}
|
|
216
|
+
if (job.view.kind === 'preview') {
|
|
217
|
+
const port = await freePort(), url = `http://127.0.0.1:${port}/`;
|
|
218
|
+
job.view.stage = '启动官方预览';
|
|
219
|
+
const args = this.engine === 'hyperframes' ? ['preview', '.', '--port', String(port), '--no-open', '--foreground'] : ['studio', 'src/index.jsx', '--port', String(port), '--no-open', '--ipv4', '--disable-ask-ai'];
|
|
220
|
+
let exited = false;
|
|
221
|
+
const running = this.run(job, process.execPath, ['--require', join(dir, 'loopback.cjs'), this.cli(), ...args], dir).finally(() => { exited = true; });
|
|
222
|
+
// Poll the actual server; a chosen port or spawned process is not readiness.
|
|
223
|
+
const poll = async () => {
|
|
224
|
+
for (let i = 0; i < 180; i++) {
|
|
225
|
+
job.controller.signal.throwIfAborted();
|
|
226
|
+
if (exited)
|
|
227
|
+
return;
|
|
228
|
+
try {
|
|
229
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1500) });
|
|
230
|
+
if (response.ok) {
|
|
231
|
+
job.view.url = this.engine === 'hyperframes' ? url + '#project/' + job.view.id : url;
|
|
232
|
+
job.view.stage = '官方预览已就绪';
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch { }
|
|
237
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
238
|
+
}
|
|
239
|
+
job.controller.abort();
|
|
240
|
+
};
|
|
241
|
+
await Promise.all([running, poll()]);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
if (this.engine === 'hyperframes') {
|
|
245
|
+
job.view.stage = '检查合成';
|
|
246
|
+
await this.run(job, process.execPath, [this.cli(), 'lint', '.', '--json'], dir);
|
|
247
|
+
}
|
|
248
|
+
job.view.stage = '渲染 MP4';
|
|
249
|
+
const output = join(dir, 'final.mp4');
|
|
250
|
+
const browserFile = await this.browserFile(JSON.parse(await readFile(join(this.runtime, 'ready.json'), 'utf8')).browserPath);
|
|
251
|
+
const browserPath = browserFile.length >= 240 ? toNamespacedPath(browserFile) : browserFile;
|
|
252
|
+
const chromeMode = /^chrome-headless-shell(?:\.exe)?$/i.test(basename(browserFile)) ? 'headless-shell' : 'chrome-for-testing';
|
|
253
|
+
const args = this.engine === 'hyperframes' ? ['render', '.', '--output', output, '--fps', '24', '--quality', 'draft', '--workers', '1', '--strict', '--no-best-effort', '--no-browser-gpu'] : ['render', 'src/index.jsx', 'ManagedVideo', output, '--codec', 'h264', '--concurrency', '1', '--browser-executable', browserPath, '--chrome-mode', chromeMode];
|
|
254
|
+
await this.run(job, process.execPath, ['--require', join(dir, 'loopback.cjs'), this.cli(), ...args], dir, this.engine === 'hyperframes' ? { HYPERFRAMES_BROWSER_PATH: browserPath } : {});
|
|
255
|
+
const bytes = (await stat(output)).size;
|
|
256
|
+
if (bytes < 100 || bytes > 200 * 1024 * 1024)
|
|
257
|
+
throw new Error('MP4 输出为空或超过 200 MB,请检查运行记录。原工程已保留。');
|
|
258
|
+
const handle = await open(output, 'r'), header = Buffer.alloc(12);
|
|
259
|
+
try {
|
|
260
|
+
await handle.read(header, 0, 12, 0);
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
await handle.close();
|
|
264
|
+
}
|
|
265
|
+
if (header.toString('ascii', 4, 8) !== 'ftyp')
|
|
266
|
+
throw new Error('渲染器输出不是 MP4 文件,原工程已保留。');
|
|
267
|
+
job.controller.signal.throwIfAborted();
|
|
268
|
+
await this.store.edit(p.id, undefined, current => { current.renders = [{ id: job.view.id, revision: p.revision, bytes, created: new Date().toISOString() }, ...current.renders].slice(0, 5); }, false);
|
|
269
|
+
job.view.output = job.view.id;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
job.view.state = job.controller.signal.aborted ? 'cancelled' : 'done';
|
|
273
|
+
job.view.stage = job.view.state === 'done' ? '已完成' : '已取消';
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
const message = e?.code === 'ELOCKED' ? '另一个 DSH 实例正在运行本插件任务,请等待完成或在那个实例中停止任务。' : errorMessage(e);
|
|
277
|
+
job.view.state = job.controller.signal.aborted ? 'cancelled' : 'failed';
|
|
278
|
+
job.view.stage = job.view.state === 'cancelled' ? '已取消;工程已保留' : /^官方 .*命令未完成/.test(message) ? '官方任务未完成,请展开运行记录后重试。工程已保留。' : /spawn ffmpeg ENOENT/.test(message) ? '未找到 FFmpeg,请安装并加入 PATH,重启 DSH 后重试。工程已保留。' : message;
|
|
279
|
+
this.log(job, message);
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
clearTimeout(timeout);
|
|
283
|
+
if (unlock)
|
|
284
|
+
await unlock().catch(() => { });
|
|
285
|
+
job.view.url = job.view.state === 'running' ? job.view.url : undefined;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
cancel(id) { const job = this.jobs.get(mediaId(id)); if (!job)
|
|
289
|
+
throw new Error('任务不存在。'); if (job.view.state === 'running')
|
|
290
|
+
job.controller.abort(); return structuredClone(job.view); }
|
|
291
|
+
dispose() { this.disposed = true; for (const job of this.jobs.values())
|
|
292
|
+
if (job.view.state === 'running')
|
|
293
|
+
job.controller.abort(); }
|
|
294
|
+
guard(request) {
|
|
295
|
+
const url = new URL(request.url), origin = request.headers.get('origin'), site = request.headers.get('sec-fetch-site');
|
|
296
|
+
// The official carrier keeps Host but rewrites Request.url to its internal URL.
|
|
297
|
+
if (site && !['same-origin', 'none'].includes(site))
|
|
298
|
+
throw new Error('请从 DSH 页面操作。');
|
|
299
|
+
if (origin) {
|
|
300
|
+
const source = new URL(origin);
|
|
301
|
+
if (!['http:', 'https:'].includes(source.protocol) || source.host !== (request.headers.get('host') || url.host))
|
|
302
|
+
throw new Error('请从 DSH 页面操作。');
|
|
303
|
+
}
|
|
304
|
+
if (request.method === 'POST' && request.headers.get('x-dsh-media') !== '1')
|
|
305
|
+
throw new Error('操作来源无效。');
|
|
306
|
+
}
|
|
307
|
+
async fetch(request) {
|
|
308
|
+
try {
|
|
309
|
+
this.guard(request);
|
|
310
|
+
const url = new URL(request.url);
|
|
311
|
+
if (request.method === 'GET') {
|
|
312
|
+
const id = mediaId(url.searchParams.get('id')), p = await this.store.get(id), kind = url.searchParams.get('kind');
|
|
313
|
+
if (kind === 'source') {
|
|
314
|
+
const snapshot = await this.store.snapshot(id, p.revision), files = {};
|
|
315
|
+
for (const [file, content] of Object.entries(projectSources(p, this.engine)))
|
|
316
|
+
files[file] = strToU8(content);
|
|
317
|
+
for (const [file, bytes] of snapshot.assets)
|
|
318
|
+
files['public/assets/' + file] = bytes;
|
|
319
|
+
const bytes = zipSync(files, { level: 1 });
|
|
320
|
+
return new Response(bytes, { headers: { 'content-type': 'application/zip', 'content-disposition': `attachment; filename="${this.engine}-${id}.zip"`, 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' } });
|
|
321
|
+
}
|
|
322
|
+
let file, type;
|
|
323
|
+
if (kind === 'asset') {
|
|
324
|
+
const a = p.assets.find(a => a.id === mediaId(url.searchParams.get('asset')));
|
|
325
|
+
if (!a)
|
|
326
|
+
throw new Error('素材不存在。');
|
|
327
|
+
file = join(await this.store.path(id), 'assets', a.file);
|
|
328
|
+
type = mime[a.file.split('.').at(-1)];
|
|
329
|
+
}
|
|
330
|
+
else if (kind === 'mp4') {
|
|
331
|
+
const r = p.renders.find(r => r.id === mediaId(url.searchParams.get('render')));
|
|
332
|
+
if (!r)
|
|
333
|
+
throw new Error('成品不存在。');
|
|
334
|
+
file = join(await this.store.path(id), 'runs', r.id, 'final.mp4');
|
|
335
|
+
type = mime.mp4;
|
|
336
|
+
}
|
|
337
|
+
else
|
|
338
|
+
throw new Error('下载类型无效。');
|
|
339
|
+
await this.store.checkPath(file);
|
|
340
|
+
const size = (await stat(file)).size;
|
|
341
|
+
if (size > 200 * 1024 * 1024)
|
|
342
|
+
throw new Error('成品过大,请从本地工程目录取回。');
|
|
343
|
+
const bytes = await readFile(file), range = request.headers.get('range'), headers = { 'content-type': type, 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', 'accept-ranges': 'bytes' };
|
|
344
|
+
if (range) {
|
|
345
|
+
const match = /^bytes=(\d+)-(\d*)$/.exec(range);
|
|
346
|
+
if (!match)
|
|
347
|
+
return new Response(null, { status: 416, headers: { 'content-range': `bytes */${size}` } });
|
|
348
|
+
const start = Number(match[1]), end = match[2] ? Math.min(Number(match[2]), size - 1) : size - 1;
|
|
349
|
+
if (start > end || start >= size)
|
|
350
|
+
return new Response(null, { status: 416, headers: { 'content-range': `bytes */${size}` } });
|
|
351
|
+
return new Response(bytes.subarray(start, end + 1), { status: 206, headers: { ...headers, 'content-range': `bytes ${start}-${end}/${size}`, 'content-length': String(end - start + 1) } });
|
|
352
|
+
}
|
|
353
|
+
if (url.searchParams.get('download') === '1')
|
|
354
|
+
headers['content-disposition'] = `attachment; filename="${this.engine}-${id}.${kind === 'mp4' ? 'mp4' : file.split('.').at(-1)}"`;
|
|
355
|
+
return new Response(bytes, { headers: { ...headers, 'content-length': String(size) } });
|
|
356
|
+
}
|
|
357
|
+
if (request.method !== 'POST')
|
|
358
|
+
return new Response(null, { status: 405 });
|
|
359
|
+
let value;
|
|
360
|
+
if (url.pathname.endsWith('/upload')) {
|
|
361
|
+
const reader = request.body?.getReader(), chunks = [];
|
|
362
|
+
let size = 0;
|
|
363
|
+
if (!reader)
|
|
364
|
+
throw new Error('请选择素材。');
|
|
365
|
+
try {
|
|
366
|
+
while (true) {
|
|
367
|
+
const chunk = await reader.read();
|
|
368
|
+
if (chunk.done)
|
|
369
|
+
break;
|
|
370
|
+
size += chunk.value.length;
|
|
371
|
+
if (size > 20 * 1024 * 1024)
|
|
372
|
+
throw new Error('单个素材不能超过 20 MB。');
|
|
373
|
+
chunks.push(chunk.value);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
await reader.cancel().catch(() => { });
|
|
378
|
+
}
|
|
379
|
+
value = await this.store.upload(mediaId(request.headers.get('x-project-id')), Number(request.headers.get('x-project-revision')), decodeURIComponent(request.headers.get('x-file-name') || ''), Buffer.concat(chunks));
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
if (!request.headers.get('content-type')?.startsWith('application/json'))
|
|
383
|
+
throw new Error('请求格式无效。');
|
|
384
|
+
const text = await request.text();
|
|
385
|
+
if (Buffer.byteLength(text) > 16384)
|
|
386
|
+
throw new Error('请求过大。');
|
|
387
|
+
const v = JSON.parse(text);
|
|
388
|
+
switch (v.action) {
|
|
389
|
+
case 'list':
|
|
390
|
+
value = { projects: await this.store.list(), templates: MEDIA_TEMPLATES, environment: await this.environment(), jobs: [...this.jobs.values()].map(j => structuredClone(j.view)) };
|
|
391
|
+
break;
|
|
392
|
+
case 'create':
|
|
393
|
+
value = await this.store.create(v.fields);
|
|
394
|
+
break;
|
|
395
|
+
case 'get':
|
|
396
|
+
value = await this.store.get(mediaId(v.id));
|
|
397
|
+
break;
|
|
398
|
+
case 'update':
|
|
399
|
+
value = await this.store.update(mediaId(v.id), v.revision, v.fields);
|
|
400
|
+
break;
|
|
401
|
+
case 'removeAsset':
|
|
402
|
+
value = await this.store.removeAsset(mediaId(v.id), v.revision, mediaId(v.asset));
|
|
403
|
+
break;
|
|
404
|
+
case 'prepare':
|
|
405
|
+
value = await this.start('prepare');
|
|
406
|
+
break;
|
|
407
|
+
case 'preview':
|
|
408
|
+
case 'render':
|
|
409
|
+
value = await this.start(v.action, mediaId(v.id), v.revision);
|
|
410
|
+
break;
|
|
411
|
+
case 'job':
|
|
412
|
+
value = this.job(mediaId(v.job));
|
|
413
|
+
break;
|
|
414
|
+
case 'cancel':
|
|
415
|
+
value = this.cancel(mediaId(v.job));
|
|
416
|
+
break;
|
|
417
|
+
default: throw new Error('操作无效。');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return Response.json({ ok: true, value }, { headers: { 'cache-control': 'no-store' } });
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
return Response.json({ ok: false, message: errorMessage(e) }, { status: e instanceof MediaConflict ? 409 : 400, headers: { 'cache-control': 'no-store' } });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
export function installMediaWorkbench(ctx, engine) {
|
|
428
|
+
const workbench = new MediaWorkbench(engine);
|
|
429
|
+
ctx.inject?.(['connection'], (host) => {
|
|
430
|
+
host.connection?.fetch?.register({ path: workbench.route, methods: ['GET', 'POST'], requestBody: 'buffered', fetch: (request) => workbench.fetch(request) });
|
|
431
|
+
host.connection?.fetch?.register({ path: workbench.route + '/upload', methods: ['POST'], requestBody: 'streaming', fetch: (request) => workbench.fetch(request) });
|
|
432
|
+
});
|
|
433
|
+
ctx.on?.('dispose', () => workbench.dispose());
|
|
434
|
+
for (const action of ['project', 'render']) {
|
|
435
|
+
const dispose = ctx.tools?.register({
|
|
436
|
+
name: engine + '_' + action,
|
|
437
|
+
description: action === 'project' ? '创建或读取本插件受管理视频工程,列出模板与环境。不会自动安装依赖或渲染。' : '准备官方渲染环境、启动预览/MP4任务、查询进度或取消。工程保存在 DSH 本地数据目录;首次准备环境会下载依赖。',
|
|
438
|
+
parameters: { type: 'object', properties: { action: { type: 'string', enum: action === 'project' ? ['list', 'create', 'get', 'update'] : ['prepare', 'preview', 'render', 'job', 'cancel'] }, id: { type: 'string' }, revision: { type: 'number' }, job: { type: 'string' }, fields: { type: 'object', properties: { template: { type: 'string', enum: ['title', 'product', 'slideshow'] }, title: { type: 'string' }, body: { type: 'string' }, format: { type: 'string', enum: ['landscape', 'portrait', 'square'] }, duration: { type: 'number' } }, required: ['template', 'title', 'body', 'format', 'duration'] } }, required: ['action'] },
|
|
439
|
+
output: {
|
|
440
|
+
schema: { type: 'object', additionalProperties: true },
|
|
441
|
+
render(_args, value) {
|
|
442
|
+
const lines = [engine === 'hyperframes' ? 'HyperFrames 视频工作台' : 'Remotion 视频工作台'];
|
|
443
|
+
if (Array.isArray(value.projects)) {
|
|
444
|
+
lines.push('已保存工程:' + value.projects.length);
|
|
445
|
+
for (const project of value.projects)
|
|
446
|
+
lines.push('- ' + project.title + '(版本 ' + project.revision + ')');
|
|
447
|
+
lines.push(value.environment?.ready ? '渲染环境已就绪。' : '首次预览或导出前,请显式准备渲染环境。');
|
|
448
|
+
}
|
|
449
|
+
else if (value.title)
|
|
450
|
+
lines.push('工程:' + value.title + '(版本 ' + value.revision + ')');
|
|
451
|
+
else if (value.state) {
|
|
452
|
+
lines.push('任务状态:' + value.state, value.stage || '');
|
|
453
|
+
if (value.url)
|
|
454
|
+
lines.push('预览:' + value.url);
|
|
455
|
+
if (value.message)
|
|
456
|
+
lines.push(value.message);
|
|
457
|
+
}
|
|
458
|
+
return [{ type: 'text', text: lines.filter(Boolean).join('\n') }];
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
async execute(args) {
|
|
462
|
+
const allowed = action === 'project' ? ['list', 'create', 'get', 'update'] : ['prepare', 'preview', 'render', 'job', 'cancel'];
|
|
463
|
+
if (!allowed.includes(args.action))
|
|
464
|
+
throw new Error('操作无效。');
|
|
465
|
+
const response = await workbench.fetch(new Request('http://dsh.internal' + workbench.route, { method: 'POST', headers: { 'content-type': 'application/json', 'x-dsh-media': '1' }, body: JSON.stringify(args) }));
|
|
466
|
+
const result = await response.json();
|
|
467
|
+
if (!result.ok)
|
|
468
|
+
throw new Error(result.message);
|
|
469
|
+
return result.value;
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
if (typeof dispose === 'function')
|
|
473
|
+
ctx.on?.('dispose', dispose);
|
|
474
|
+
}
|
|
475
|
+
return workbench;
|
|
476
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remotion",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "DSH 视频插件:Remotion 技能、模板与素材工作台、官方 Studio 预览和本地 MP4 导出。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"default": "./lib/index.js"
|
|
12
12
|
},
|
|
13
13
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
14
|
-
"./package.json": "./package.json"
|
|
14
|
+
"./package.json": "./package.json",
|
|
15
|
+
"./client": "./lib/client.js"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"lib",
|
|
@@ -19,10 +20,13 @@
|
|
|
19
20
|
"cordis.patch.yml",
|
|
20
21
|
"README.md",
|
|
21
22
|
"README.en.md",
|
|
22
|
-
"scripts"
|
|
23
|
+
"scripts",
|
|
24
|
+
"docs",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"templates/DESIGN.md"
|
|
23
27
|
],
|
|
24
28
|
"scripts": {
|
|
25
|
-
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"build": "tsc -p tsconfig.json && node scripts/build-client.mjs",
|
|
26
30
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
27
31
|
"test": "pnpm run build && node --test \"test/*.test.mjs\"",
|
|
28
32
|
"prepublishOnly": "pnpm run build"
|
|
@@ -30,6 +34,12 @@
|
|
|
30
34
|
"dsh": {
|
|
31
35
|
"bundle": {
|
|
32
36
|
"patch": "./cordis.patch.yml"
|
|
37
|
+
},
|
|
38
|
+
"client": {
|
|
39
|
+
"inject": [
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
41
|
+
],
|
|
42
|
+
"platform": "web"
|
|
33
43
|
}
|
|
34
44
|
},
|
|
35
45
|
"keywords": [
|
|
@@ -45,10 +55,13 @@
|
|
|
45
55
|
"node": "^22.19.0 || >=24.0.0"
|
|
46
56
|
},
|
|
47
57
|
"dependencies": {
|
|
58
|
+
"fflate": "0.8.3",
|
|
59
|
+
"proper-lockfile": "4.1.2",
|
|
48
60
|
"yaml": "^2.9.0"
|
|
49
61
|
},
|
|
50
62
|
"devDependencies": {
|
|
51
63
|
"@types/node": "^24.0.0",
|
|
64
|
+
"@types/proper-lockfile": "4.1.4",
|
|
52
65
|
"typescript": "^5.6.0"
|
|
53
66
|
},
|
|
54
67
|
"repository": {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
const engine = JSON.parse(readFileSync('package.json', 'utf8')).name.slice(4)
|
|
3
|
+
if (!['hyperframes','remotion'].includes(engine)) throw new Error('Unexpected package')
|
|
4
|
+
writeFileSync('lib/client.js', readFileSync('templates/client.js', 'utf8').replaceAll('__ENGINE__', engine))
|