dsh-drawai 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.
Files changed (538) hide show
  1. package/LICENSE +4154 -0
  2. package/README.md +49 -11
  3. package/lib/audio-media.js +150 -0
  4. package/lib/audio-service.js +66 -0
  5. package/lib/canvas-format.js +134 -0
  6. package/lib/client.js +5090 -184
  7. package/lib/comfyui-default-workflow.mjs +1825 -0
  8. package/lib/comfyui-workflow.mjs +426 -0
  9. package/lib/document-commands.js +12 -0
  10. package/lib/document-core.js +29 -0
  11. package/lib/drawing-runner.js +6 -4
  12. package/lib/drawing-tasks.js +26 -0
  13. package/lib/ffmpeg-manager.js +1056 -0
  14. package/lib/imagegen.mjs +596 -0
  15. package/lib/index.js +1762 -129
  16. package/lib/media.js +26 -13
  17. package/lib/mgstyle-core.js +75 -0
  18. package/lib/motion/apply.js +181 -0
  19. package/lib/motion/asset-pack-lock.js +10 -0
  20. package/lib/motion/asset-pack.js +335 -0
  21. package/lib/motion/assets.js +291 -0
  22. package/lib/motion/audio.js +103 -0
  23. package/lib/motion/audiolevel.js +63 -0
  24. package/lib/motion/audiomix.js +144 -0
  25. package/lib/motion/beats.js +338 -0
  26. package/lib/motion/builtin-assets.js +62 -0
  27. package/lib/motion/canvas-spec.js +442 -0
  28. package/lib/motion/capture.js +95 -0
  29. package/lib/motion/cards.js +80 -0
  30. package/lib/motion/collect.js +449 -0
  31. package/lib/motion/delivery.js +140 -0
  32. package/lib/motion/edge-speech.py +90 -0
  33. package/lib/motion/entry.jsx +73 -0
  34. package/lib/motion/export.js +200 -0
  35. package/lib/motion/ffmpeg.js +43 -0
  36. package/lib/motion/fonts.js +71 -0
  37. package/lib/motion/jobs.js +117 -0
  38. package/lib/motion/json.js +4 -0
  39. package/lib/motion/media-catalog.js +59 -0
  40. package/lib/motion/mgstyles/BRIEF-TEMPLATE.md +63 -0
  41. package/lib/motion/mgstyles/briefs/01-flat-vector.md +104 -0
  42. package/lib/motion/mgstyles/briefs/02-line-art.md +97 -0
  43. package/lib/motion/mgstyles/briefs/03-isometric.md +108 -0
  44. package/lib/motion/mgstyles/briefs/05-cel-boil.md +145 -0
  45. package/lib/motion/mgstyles/briefs/06-collage.md +149 -0
  46. package/lib/motion/mgstyles/briefs/07-liquid.md +106 -0
  47. package/lib/motion/mgstyles/briefs/08-morph.md +111 -0
  48. package/lib/motion/mgstyles/briefs/09-bauhaus.md +150 -0
  49. package/lib/motion/mgstyles/briefs/10-synthwave.md +151 -0
  50. package/lib/motion/mgstyles/briefs/12-aurora-glass.md +147 -0
  51. package/lib/motion/mgstyles/briefs/18-hanazi.md +140 -0
  52. package/lib/motion/mgstyles/briefs/19-paperclip.md +156 -0
  53. package/lib/motion/mgstyles/briefs/20-pixel.md +118 -0
  54. package/lib/motion/mgstyles/briefs/22-hud.md +118 -0
  55. package/lib/motion/mgstyles/catalog.d/01-flat-vector.json +34 -0
  56. package/lib/motion/mgstyles/catalog.d/02-line-art.json +34 -0
  57. package/lib/motion/mgstyles/catalog.d/03-isometric.json +35 -0
  58. package/lib/motion/mgstyles/catalog.d/05-cel-boil.json +28 -0
  59. package/lib/motion/mgstyles/catalog.d/06-collage.json +28 -0
  60. package/lib/motion/mgstyles/catalog.d/07-liquid.json +28 -0
  61. package/lib/motion/mgstyles/catalog.d/08-morph.json +28 -0
  62. package/lib/motion/mgstyles/catalog.d/09-bauhaus.json +28 -0
  63. package/lib/motion/mgstyles/catalog.d/10-synthwave.json +28 -0
  64. package/lib/motion/mgstyles/catalog.d/12-aurora-glass.json +28 -0
  65. package/lib/motion/mgstyles/catalog.d/18-hanazi.json +28 -0
  66. package/lib/motion/mgstyles/catalog.d/19-paperclip.json +28 -0
  67. package/lib/motion/mgstyles/catalog.d/20-pixel.json +28 -0
  68. package/lib/motion/mgstyles/catalog.d/22-hud.json +28 -0
  69. package/lib/motion/mgstyles/catalog.json +1356 -0
  70. package/lib/motion/mgstyles/upstream/README.md +147 -0
  71. package/lib/motion/mgstyles/upstream/README.zh-CN.md +147 -0
  72. package/lib/motion/mgstyles/upstream/demos/01-flat-vector/cues.json +1 -0
  73. package/lib/motion/mgstyles/upstream/demos/01-flat-vector/index.html +712 -0
  74. package/lib/motion/mgstyles/upstream/demos/02-line-art/cues.json +331 -0
  75. package/lib/motion/mgstyles/upstream/demos/02-line-art/film.js +676 -0
  76. package/lib/motion/mgstyles/upstream/demos/02-line-art/index.html +18 -0
  77. package/lib/motion/mgstyles/upstream/demos/02-line-art/tools/dump_timing.mjs +26 -0
  78. package/lib/motion/mgstyles/upstream/demos/03-isometric/cues.json +109 -0
  79. package/lib/motion/mgstyles/upstream/demos/03-isometric/index.html +25 -0
  80. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/anim.js +82 -0
  81. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/main.js +219 -0
  82. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/night5.js +180 -0
  83. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/render.js +202 -0
  84. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/title.js +167 -0
  85. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/title5.js +294 -0
  86. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/ui.js +141 -0
  87. package/lib/motion/mgstyles/upstream/demos/03-isometric/js/world.js +1399 -0
  88. package/lib/motion/mgstyles/upstream/demos/05-cel-boil/cues.json +157 -0
  89. package/lib/motion/mgstyles/upstream/demos/05-cel-boil/index.html +34 -0
  90. package/lib/motion/mgstyles/upstream/demos/05-cel-boil/js/engine.js +326 -0
  91. package/lib/motion/mgstyles/upstream/demos/05-cel-boil/js/scene.js +2036 -0
  92. package/lib/motion/mgstyles/upstream/demos/06-collage/assets/gen/layout.json +157 -0
  93. package/lib/motion/mgstyles/upstream/demos/06-collage/cues.json +80 -0
  94. package/lib/motion/mgstyles/upstream/demos/06-collage/index.html +889 -0
  95. package/lib/motion/mgstyles/upstream/demos/07-liquid/cues.json +100 -0
  96. package/lib/motion/mgstyles/upstream/demos/07-liquid/index.html +108 -0
  97. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/engine.js +153 -0
  98. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/links.js +64 -0
  99. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/lookdev.js +46 -0
  100. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/scene.js +963 -0
  101. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/shaders.js +647 -0
  102. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/textsdf.js +114 -0
  103. package/lib/motion/mgstyles/upstream/demos/07-liquid/js/type.js +50 -0
  104. package/lib/motion/mgstyles/upstream/demos/08-morph/cues.json +131 -0
  105. package/lib/motion/mgstyles/upstream/demos/08-morph/glyphs.json +1 -0
  106. package/lib/motion/mgstyles/upstream/demos/08-morph/index.html +901 -0
  107. package/lib/motion/mgstyles/upstream/demos/09-bauhaus/cues.json +270 -0
  108. package/lib/motion/mgstyles/upstream/demos/09-bauhaus/film.js +697 -0
  109. package/lib/motion/mgstyles/upstream/demos/09-bauhaus/index.html +12 -0
  110. package/lib/motion/mgstyles/upstream/demos/10-synthwave/cues.json +44 -0
  111. package/lib/motion/mgstyles/upstream/demos/10-synthwave/index.html +1028 -0
  112. package/lib/motion/mgstyles/upstream/demos/12-aurora-glass/cues.json +67 -0
  113. package/lib/motion/mgstyles/upstream/demos/12-aurora-glass/index.html +16 -0
  114. package/lib/motion/mgstyles/upstream/demos/12-aurora-glass/main.js +967 -0
  115. package/lib/motion/mgstyles/upstream/demos/12-aurora-glass/shaders.js +468 -0
  116. package/lib/motion/mgstyles/upstream/demos/18-hanazi/cues.json +117 -0
  117. package/lib/motion/mgstyles/upstream/demos/18-hanazi/index.html +541 -0
  118. package/lib/motion/mgstyles/upstream/demos/19-paperclip/assets/countries-110m.json +1 -0
  119. package/lib/motion/mgstyles/upstream/demos/19-paperclip/assets/stickers/meta.json +34 -0
  120. package/lib/motion/mgstyles/upstream/demos/19-paperclip/cues.json +285 -0
  121. package/lib/motion/mgstyles/upstream/demos/19-paperclip/index.html +599 -0
  122. package/lib/motion/mgstyles/upstream/demos/20-pixel/cues.json +42 -0
  123. package/lib/motion/mgstyles/upstream/demos/20-pixel/film.js +914 -0
  124. package/lib/motion/mgstyles/upstream/demos/20-pixel/index.html +18 -0
  125. package/lib/motion/mgstyles/upstream/demos/22-hud/cues.json +108 -0
  126. package/lib/motion/mgstyles/upstream/demos/22-hud/index.html +125 -0
  127. package/lib/motion/mgstyles/upstream/demos/22-hud/js/holo.js +275 -0
  128. package/lib/motion/mgstyles/upstream/demos/22-hud/js/hud.js +855 -0
  129. package/lib/motion/mgstyles/upstream/demos/22-hud/js/post.js +150 -0
  130. package/lib/motion/mgstyles/upstream/demos/22-hud/js/util.js +65 -0
  131. package/lib/motion/mgstyles/upstream/demos/22-hud/js/v5.js +142 -0
  132. package/lib/motion/mgstyles/upstream/harness/render.mjs +319 -0
  133. package/lib/motion/mgstyles/upstream/manifest.json +269 -0
  134. package/lib/motion/mgstyles/upstream/prompts/01-flat-vector.md +98 -0
  135. package/lib/motion/mgstyles/upstream/prompts/02-line-art.md +95 -0
  136. package/lib/motion/mgstyles/upstream/prompts/03-isometric.md +97 -0
  137. package/lib/motion/mgstyles/upstream/prompts/05-cel-boil.md +95 -0
  138. package/lib/motion/mgstyles/upstream/prompts/06-collage.md +95 -0
  139. package/lib/motion/mgstyles/upstream/prompts/07-liquid.md +98 -0
  140. package/lib/motion/mgstyles/upstream/prompts/08-morph.md +95 -0
  141. package/lib/motion/mgstyles/upstream/prompts/09-bauhaus.md +96 -0
  142. package/lib/motion/mgstyles/upstream/prompts/10-synthwave.md +95 -0
  143. package/lib/motion/mgstyles/upstream/prompts/12-aurora-glass.md +96 -0
  144. package/lib/motion/mgstyles/upstream/prompts/18-hanazi.md +95 -0
  145. package/lib/motion/mgstyles/upstream/prompts/19-paperclip.md +96 -0
  146. package/lib/motion/mgstyles/upstream/prompts/20-pixel.md +96 -0
  147. package/lib/motion/mgstyles/upstream/prompts/22-hud.md +95 -0
  148. package/lib/motion/mgstyles/upstream/rubric.md +51 -0
  149. package/lib/motion/mgstyles/upstream/template.md +36 -0
  150. package/lib/motion/pixelqa.js +198 -0
  151. package/lib/motion/preview.js +97 -0
  152. package/lib/motion/project.js +395 -0
  153. package/lib/motion/qa.js +290 -0
  154. package/lib/motion/reference-media.jsx +10 -0
  155. package/lib/motion/render-settings.js +112 -0
  156. package/lib/motion/render.js +161 -0
  157. package/lib/motion/runtime/package-lock.json +3058 -0
  158. package/lib/motion/runtime/package.json +16 -0
  159. package/lib/motion/scene-kit.js +183 -0
  160. package/lib/motion/sfx.js +232 -0
  161. package/lib/motion/shotcraft/ClipCard.tsx +204 -0
  162. package/lib/motion/shotcraft/catalog.json +2461 -0
  163. package/lib/motion/shotcraft/upstream/SKILL.md +286 -0
  164. package/lib/motion/shotcraft/upstream/assets/lib/Caption.tsx +54 -0
  165. package/lib/motion/shotcraft/upstream/assets/lib/ClipCard.tsx +200 -0
  166. package/lib/motion/shotcraft/upstream/assets/lib/DigitRoll.tsx +45 -0
  167. package/lib/motion/shotcraft/upstream/assets/lib/FlashCut.tsx +20 -0
  168. package/lib/motion/shotcraft/upstream/assets/lib/FlatPanel.tsx +65 -0
  169. package/lib/motion/shotcraft/upstream/assets/lib/PageCam.tsx +151 -0
  170. package/lib/motion/shotcraft/upstream/assets/lib/VerticalTicker.tsx +129 -0
  171. package/lib/motion/shotcraft/upstream/assets/lib/helpers/camera.tsx +75 -0
  172. package/lib/motion/shotcraft/upstream/assets/lib/helpers/motion.ts +40 -0
  173. package/lib/motion/shotcraft/upstream/assets/lib/helpers/rand.ts +11 -0
  174. package/lib/motion/shotcraft/upstream/assets/lib/helpers/shake.ts +10 -0
  175. package/lib/motion/shotcraft/upstream/assets/scripts/__pycache__/sfx-source.cpython-313.pyc +0 -0
  176. package/lib/motion/shotcraft/upstream/assets/scripts/build-readme-cover.mjs +49 -0
  177. package/lib/motion/shotcraft/upstream/assets/scripts/capture-template.mjs +210 -0
  178. package/lib/motion/shotcraft/upstream/assets/scripts/sfx-source.py +423 -0
  179. package/lib/motion/shotcraft/upstream/assets/scripts/smoke-render-demos.py +170 -0
  180. package/lib/motion/shotcraft/upstream/assets/scripts/test_sfx_source.py +157 -0
  181. package/lib/motion/shotcraft/upstream/demos/README.md +120 -0
  182. package/lib/motion/shotcraft/upstream/demos/_fixtures/Brand.tsx +112 -0
  183. package/lib/motion/shotcraft/upstream/demos/_fixtures/Fixtures.tsx +647 -0
  184. package/lib/motion/shotcraft/upstream/demos/_fixtures/Look.tsx +316 -0
  185. package/lib/motion/shotcraft/upstream/demos/_fixtures/Motion.tsx +82 -0
  186. package/lib/motion/shotcraft/upstream/demos/_fixtures/PageCam2D.tsx +115 -0
  187. package/lib/motion/shotcraft/upstream/demos/_fixtures/Polish.tsx +243 -0
  188. package/lib/motion/shotcraft/upstream/demos/_textures/live-layout.json +389 -0
  189. package/lib/motion/shotcraft/upstream/demos/camera/basic-3d-scene/Basic3DScene.tsx +250 -0
  190. package/lib/motion/shotcraft/upstream/demos/camera/crash-zoom-punch/CrashImpactReal.tsx +305 -0
  191. package/lib/motion/shotcraft/upstream/demos/camera/crash-zoom-punch/CrashZoomReal.tsx +246 -0
  192. package/lib/motion/shotcraft/upstream/demos/camera/cursor-flyover/CursorFlyover.tsx +390 -0
  193. package/lib/motion/shotcraft/upstream/demos/camera/depth-layer-moves/MultiplaneReal.tsx +138 -0
  194. package/lib/motion/shotcraft/upstream/demos/camera/graze-face-tour/GrazeFaceTour.tsx +542 -0
  195. package/lib/motion/shotcraft/upstream/demos/camera/overhead-camera-moves/OverheadTabletopDrop.tsx +315 -0
  196. package/lib/motion/shotcraft/upstream/demos/camera/overhead-camera-moves/TiltReveal.tsx +299 -0
  197. package/lib/motion/shotcraft/upstream/demos/camera/space-camera-moves/DroneDiveLanding.tsx +173 -0
  198. package/lib/motion/shotcraft/upstream/demos/camera/space-camera-moves/ExplodedView.tsx +348 -0
  199. package/lib/motion/shotcraft/upstream/demos/camera/steep-tilt-glide/SteepTiltGlide.tsx +366 -0
  200. package/lib/motion/shotcraft/upstream/demos/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx +279 -0
  201. package/lib/motion/shotcraft/upstream/demos/camera/tension-camera-moves/PullBackIsolation.tsx +184 -0
  202. package/lib/motion/shotcraft/upstream/demos/camera/terminal-3d/Terminal3D.tsx +390 -0
  203. package/lib/motion/shotcraft/upstream/demos/data/before-after-slider-scrub/BeforeAfterSliderScrub.tsx +155 -0
  204. package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/AxisRescaleShockV2.tsx +303 -0
  205. package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/OscilloscopeStreamV2.tsx +290 -0
  206. package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/UnitDotSwarmRegroupV2.tsx +278 -0
  207. package/lib/motion/shotcraft/upstream/demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx +307 -0
  208. package/lib/motion/shotcraft/upstream/demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx +215 -0
  209. package/lib/motion/shotcraft/upstream/demos/data/scroll-brake-moves/BrakeReticleLock.tsx +299 -0
  210. package/lib/motion/shotcraft/upstream/demos/data/scroll-brake-moves/ChangelogScrollBrake.tsx +232 -0
  211. package/lib/motion/shotcraft/upstream/demos/data/timeline-travel/TimelineTravel.tsx +290 -0
  212. package/lib/motion/shotcraft/upstream/demos/effects/assemble-then-type-flyin/AssembleThenTypeFlyin.tsx +273 -0
  213. package/lib/motion/shotcraft/upstream/demos/effects/aurora-bloom-bg-flip/AuroraBloomBgFlip.tsx +167 -0
  214. package/lib/motion/shotcraft/upstream/demos/effects/brand-frame-snap/BrandFrameSnap.tsx +536 -0
  215. package/lib/motion/shotcraft/upstream/demos/effects/fui-hud-moves/ReticleLockOn.tsx +164 -0
  216. package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/FlylineArc.tsx +175 -0
  217. package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/GlowOrbAmbient.tsx +197 -0
  218. package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/OrbFlylineRelay.tsx +241 -0
  219. package/lib/motion/shotcraft/upstream/demos/effects/icon-performance-moves/AttentionBounce.tsx +279 -0
  220. package/lib/motion/shotcraft/upstream/demos/effects/impact-feedback/AnimeImpact.tsx +230 -0
  221. package/lib/motion/shotcraft/upstream/demos/effects/impact-feedback/HitCounter.tsx +345 -0
  222. package/lib/motion/shotcraft/upstream/demos/effects/light-play-moves/SheenSweepRetry.tsx +275 -0
  223. package/lib/motion/shotcraft/upstream/demos/effects/light-play-moves/SpotlightSweepReveal.tsx +229 -0
  224. package/lib/motion/shotcraft/upstream/demos/effects/radial-ripple-phone-chips/RadialRipplePhoneChips.tsx +312 -0
  225. package/lib/motion/shotcraft/upstream/demos/effects/riso-print-hits/RisoMisregistrationHit.tsx +222 -0
  226. package/lib/motion/shotcraft/upstream/demos/effects/scan-bracket-sweep/ScanBracketSweep.tsx +295 -0
  227. package/lib/motion/shotcraft/upstream/demos/effects/scanline-annotate-focus/ScanlineAnnotateFocus.tsx +329 -0
  228. package/lib/motion/shotcraft/upstream/demos/effects/scanline-assemble-flyin/ScanlineAssembleFlyin.tsx +315 -0
  229. package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/ImpactBurstKit.tsx +264 -0
  230. package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/KanadaPerspectiveSnap.tsx +260 -0
  231. package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/ScoreSlam.tsx +263 -0
  232. package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/CornerSpotlightReveal.tsx +123 -0
  233. package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/GlowWakeSleepPanel.tsx +187 -0
  234. package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/SlideSpotlightPan.tsx +175 -0
  235. package/lib/motion/shotcraft/upstream/demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx +305 -0
  236. package/lib/motion/shotcraft/upstream/demos/interaction/canvas-materialize-moves/DiagramCascadeBuild.tsx +350 -0
  237. package/lib/motion/shotcraft/upstream/demos/interaction/canvas-materialize-moves/PanelToCanvasMaterialize.tsx +352 -0
  238. package/lib/motion/shotcraft/upstream/demos/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.tsx +218 -0
  239. package/lib/motion/shotcraft/upstream/demos/interaction/collab-cursor-moves/CursorCastEnsemble.tsx +297 -0
  240. package/lib/motion/shotcraft/upstream/demos/interaction/glass-pill-dictation-typing/GlassPillDictationTyping.tsx +157 -0
  241. package/lib/motion/shotcraft/upstream/demos/interaction/hashtag-to-pill-materialize/HashtagToPillMaterialize.tsx +218 -0
  242. package/lib/motion/shotcraft/upstream/demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx +202 -0
  243. package/lib/motion/shotcraft/upstream/demos/interaction/segmented-thumb-hero/SegmentedThumbHero.tsx +265 -0
  244. package/lib/motion/shotcraft/upstream/demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx +457 -0
  245. package/lib/motion/shotcraft/upstream/demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx +446 -0
  246. package/lib/motion/shotcraft/upstream/demos/interaction/type-and-filter/TypeAndFilter.tsx +249 -0
  247. package/lib/motion/shotcraft/upstream/demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx +278 -0
  248. package/lib/motion/shotcraft/upstream/demos/opening/crane-rise-reveal/CraneRiseReveal.tsx +332 -0
  249. package/lib/motion/shotcraft/upstream/demos/opening/dataviz-landscape-open/DatavizLandscapeOpen.tsx +358 -0
  250. package/lib/motion/shotcraft/upstream/demos/opening/fracture/Fracture.tsx +201 -0
  251. package/lib/motion/shotcraft/upstream/demos/opening/icon-field-colorize/IconFieldColorize.tsx +200 -0
  252. package/lib/motion/shotcraft/upstream/demos/opening/letterspace-materialize/LetterspaceMaterialize.tsx +208 -0
  253. package/lib/motion/shotcraft/upstream/demos/opening/magician-card-flourish/MagicianCardFlourish.tsx +341 -0
  254. package/lib/motion/shotcraft/upstream/demos/opening/orbit-ring-title-open/OrbitRingTitleOpen.tsx +389 -0
  255. package/lib/motion/shotcraft/upstream/demos/opening/spotlight-hero-card/SpotlightHeroCard.tsx +293 -0
  256. package/lib/motion/shotcraft/upstream/demos/opening/stroke-segment-build/StrokeSegmentBuild.tsx +183 -0
  257. package/lib/motion/shotcraft/upstream/demos/opening/text-as-mask/TextAsMask.tsx +245 -0
  258. package/lib/motion/shotcraft/upstream/demos/outro/grain-dissolve/GrainDissolve.tsx +222 -0
  259. package/lib/motion/shotcraft/upstream/demos/outro/logo-shrink-wordmark-lockup/LogoShrinkWordmarkLockup.tsx +223 -0
  260. package/lib/motion/shotcraft/upstream/demos/outro/neon-triple-marquee/NeonTripleMarquee.tsx +217 -0
  261. package/lib/motion/shotcraft/upstream/demos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx +343 -0
  262. package/lib/motion/shotcraft/upstream/demos/outro/ui-strip-away-outro/UiStripAwayOutro.tsx +357 -0
  263. package/lib/motion/shotcraft/upstream/demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx +187 -0
  264. package/lib/motion/shotcraft/upstream/demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx +333 -0
  265. package/lib/motion/shotcraft/upstream/demos/rhythm/beat-cut-moves/PaparazziFlash.tsx +274 -0
  266. package/lib/motion/shotcraft/upstream/demos/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.tsx +270 -0
  267. package/lib/motion/shotcraft/upstream/demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx +430 -0
  268. package/lib/motion/shotcraft/upstream/demos/rhythm/quad-split-parallel-scenes/QuadSplitParallelScenes.tsx +252 -0
  269. package/lib/motion/shotcraft/upstream/demos/rhythm/rhythm-interrupt-moves/JumpCutPunchIn.tsx +106 -0
  270. package/lib/motion/shotcraft/upstream/demos/rhythm/sakuga-timing-shift/SakugaTimingShift.tsx +288 -0
  271. package/lib/motion/shotcraft/upstream/demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx +362 -0
  272. package/lib/motion/shotcraft/upstream/demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx +168 -0
  273. package/lib/motion/shotcraft/upstream/demos/transition/bottom-push-stack-wipe/BottomPushStackWipe.tsx +310 -0
  274. package/lib/motion/shotcraft/upstream/demos/transition/card-flip-reveal/CardFlipReveal.tsx +261 -0
  275. package/lib/motion/shotcraft/upstream/demos/transition/card-flock-tumble/CardFlockTumble.tsx +459 -0
  276. package/lib/motion/shotcraft/upstream/demos/transition/circle-match-iris/CircleMatchIris.tsx +294 -0
  277. package/lib/motion/shotcraft/upstream/demos/transition/gradient-transition/GradientTransition.tsx +161 -0
  278. package/lib/motion/shotcraft/upstream/demos/transition/line-carry-transition/LineCarryTransition.tsx +248 -0
  279. package/lib/motion/shotcraft/upstream/demos/transition/mosaic-reframe/MosaicReframe.tsx +359 -0
  280. package/lib/motion/shotcraft/upstream/demos/transition/page-turn-transitions/BarnDoorSplit.tsx +227 -0
  281. package/lib/motion/shotcraft/upstream/demos/transition/print-texture-transitions/InkBleedReveal.tsx +162 -0
  282. package/lib/motion/shotcraft/upstream/demos/transition/shot-transitions/PortalWipeV2.tsx +248 -0
  283. package/lib/motion/shotcraft/upstream/demos/transition/shot-transitions/WhipBrakeReal.tsx +186 -0
  284. package/lib/motion/shotcraft/upstream/demos/transition/tear-streak-transitions/GlitchDisplace.tsx +355 -0
  285. package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/InvisibleCut.tsx +272 -0
  286. package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/LightLeakBurn.tsx +262 -0
  287. package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/VersusSlam.tsx +217 -0
  288. package/lib/motion/shotcraft/upstream/demos/transition/transition-travel/LetterformZoom.tsx +285 -0
  289. package/lib/motion/shotcraft/upstream/demos/transition/white-flash-logo-simplify-cut/WhiteFlashLogoSimplifyCut.tsx +205 -0
  290. package/lib/motion/shotcraft/upstream/demos/transition/wipe-transitions/BlindsSlice.tsx +178 -0
  291. package/lib/motion/shotcraft/upstream/demos/typography/blur-slide/BlurSlide.tsx +147 -0
  292. package/lib/motion/shotcraft/upstream/demos/typography/brace-expand/BraceExpand.tsx +143 -0
  293. package/lib/motion/shotcraft/upstream/demos/typography/brand-ink-open/BrandInkOpen.tsx +134 -0
  294. package/lib/motion/shotcraft/upstream/demos/typography/countdown-arc-scatter/CountdownArcScatter.tsx +217 -0
  295. package/lib/motion/shotcraft/upstream/demos/typography/flying-words/FlyingWords.tsx +198 -0
  296. package/lib/motion/shotcraft/upstream/demos/typography/glitch-cycle/GlitchCycle.tsx +221 -0
  297. package/lib/motion/shotcraft/upstream/demos/typography/gradient-word-sweep/GradientWordSweep.tsx +312 -0
  298. package/lib/motion/shotcraft/upstream/demos/typography/lead-word-zoom-assemble/LeadWordZoomAssemble.tsx +261 -0
  299. package/lib/motion/shotcraft/upstream/demos/typography/marker-underline-title/MarkerUnderlineTitle.tsx +121 -0
  300. package/lib/motion/shotcraft/upstream/demos/typography/outline-word-fill/OutlineWordFill.tsx +153 -0
  301. package/lib/motion/shotcraft/upstream/demos/typography/paper-title-card/PaperTitleCard.tsx +179 -0
  302. package/lib/motion/shotcraft/upstream/demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx +140 -0
  303. package/lib/motion/shotcraft/upstream/demos/typography/pill-slot-cycle/PillSlotCycle.tsx +309 -0
  304. package/lib/motion/shotcraft/upstream/demos/typography/text-column-converge/TextColumnConverge.tsx +191 -0
  305. package/lib/motion/shotcraft/upstream/demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx +292 -0
  306. package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx +135 -0
  307. package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/SplitTextStagger.tsx +124 -0
  308. package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/TextOnPath.tsx +255 -0
  309. package/lib/motion/shotcraft/upstream/demos/typography/type-entrance-moves/LetterDropPhysics.tsx +173 -0
  310. package/lib/motion/shotcraft/upstream/demos/typography/type-rhythm-sync/FontWeightPump.tsx +225 -0
  311. package/lib/motion/shotcraft/upstream/demos/typography/type-rhythm-sync/KaraokeFillSync.tsx +141 -0
  312. package/lib/motion/shotcraft/upstream/demos/typography/typewriter-moves/TypewriterErrorRetype.tsx +137 -0
  313. package/lib/motion/shotcraft/upstream/demos/typography/vertical-word-roll-blur-cycle/VerticalWordRollBlurCycle.tsx +147 -0
  314. package/lib/motion/shotcraft/upstream/demos/typography/word-relay-filmstrip/WordRelayFilmstrip.tsx +409 -0
  315. package/lib/motion/shotcraft/upstream/demos/typography/word-relay-geometry/WordRelayGeometry.tsx +280 -0
  316. package/lib/motion/shotcraft/upstream/demos/ui-entrance/avatar-bracket-carousel/AvatarBracketCarousel.tsx +271 -0
  317. package/lib/motion/shotcraft/upstream/demos/ui-entrance/bezier-source-converge-merge/BezierSourceConvergeMerge.tsx +385 -0
  318. package/lib/motion/shotcraft/upstream/demos/ui-entrance/card-stack/CardStack.tsx +312 -0
  319. package/lib/motion/shotcraft/upstream/demos/ui-entrance/carousel-3d/Carousel3D.tsx +303 -0
  320. package/lib/motion/shotcraft/upstream/demos/ui-entrance/clipcard-looping/ClipCardLooping.tsx +77 -0
  321. package/lib/motion/shotcraft/upstream/demos/ui-entrance/cloner-depth-echo/ClonerDepthEcho.tsx +311 -0
  322. package/lib/motion/shotcraft/upstream/demos/ui-entrance/deck-deal-flyin/DeckDealFlyin.tsx +307 -0
  323. package/lib/motion/shotcraft/upstream/demos/ui-entrance/doc-park-left-pill-deal/DocParkLeftPillDeal.tsx +349 -0
  324. package/lib/motion/shotcraft/upstream/demos/ui-entrance/document-typewriter-reveal/DocumentTypewriterReveal.tsx +255 -0
  325. package/lib/motion/shotcraft/upstream/demos/ui-entrance/draw-svg-trace/DrawSvgTrace.tsx +244 -0
  326. package/lib/motion/shotcraft/upstream/demos/ui-entrance/element-body-moves/ContactShadowLift.tsx +217 -0
  327. package/lib/motion/shotcraft/upstream/demos/ui-entrance/integration-hub-map/IntegrationHubMap.tsx +652 -0
  328. package/lib/motion/shotcraft/upstream/demos/ui-entrance/list-reveal/ListReveal.tsx +153 -0
  329. package/lib/motion/shotcraft/upstream/demos/ui-entrance/list-stack-press/ListStackPress.tsx +238 -0
  330. package/lib/motion/shotcraft/upstream/demos/ui-entrance/neon-frame-forerun/NeonFrameForerun.tsx +324 -0
  331. package/lib/motion/shotcraft/upstream/demos/ui-entrance/neon-frame-orbit-drop/NeonFrameForerunOrbit.tsx +354 -0
  332. package/lib/motion/shotcraft/upstream/demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx +101 -0
  333. package/lib/motion/shotcraft/upstream/demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx +129 -0
  334. package/lib/motion/shotcraft/upstream/demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.tsx +297 -0
  335. package/lib/motion/shotcraft/upstream/demos/ui-entrance/radial-wave/RadialWave.tsx +57 -0
  336. package/lib/motion/shotcraft/upstream/demos/ui-entrance/research-card-stack-scroll/ResearchCardStackScroll.tsx +155 -0
  337. package/lib/motion/shotcraft/upstream/demos/ui-entrance/row-embed/RowEmbed.tsx +175 -0
  338. package/lib/motion/shotcraft/upstream/demos/ui-entrance/value-stagger-gradient/ValueStaggerGradient.tsx +153 -0
  339. package/lib/motion/shotcraft/upstream/demos/ui-entrance/wall-reveal-moves/BentoLightUp.tsx +146 -0
  340. package/lib/motion/shotcraft/upstream/demos/ui-entrance/wall-reveal-moves/WireframeDrawOn.tsx +226 -0
  341. package/lib/motion/shotcraft/upstream/gallery/api/library.json +1 -0
  342. package/lib/motion/shotcraft/upstream/manifest.json +2715 -0
  343. package/lib/motion/shotcraft/upstream/references/aesthetic-rules.md +193 -0
  344. package/lib/motion/shotcraft/upstream/references/final-review.md +121 -0
  345. package/lib/motion/shotcraft/upstream/references/guided-free-creation.md +199 -0
  346. package/lib/motion/shotcraft/upstream/references/jianying-export.md +183 -0
  347. package/lib/motion/shotcraft/upstream/references/music-beat-sync.md +257 -0
  348. package/lib/motion/shotcraft/upstream/references/pipeline.md +382 -0
  349. package/lib/motion/shotcraft/upstream/references/publish-upload-post.md +111 -0
  350. package/lib/motion/shotcraft/upstream/references/sequences/promo-energy-arc.md +50 -0
  351. package/lib/motion/shotcraft/upstream/references/sfx-sourcing.md +95 -0
  352. package/lib/motion/shotcraft/upstream/references/shots/camera/basic-3d-scene.md +41 -0
  353. package/lib/motion/shotcraft/upstream/references/shots/camera/crash-zoom-punch.md +52 -0
  354. package/lib/motion/shotcraft/upstream/references/shots/camera/cursor-flyover.md +39 -0
  355. package/lib/motion/shotcraft/upstream/references/shots/camera/depth-layer-moves.md +39 -0
  356. package/lib/motion/shotcraft/upstream/references/shots/camera/graze-face-tour.md +57 -0
  357. package/lib/motion/shotcraft/upstream/references/shots/camera/overhead-camera-moves.md +51 -0
  358. package/lib/motion/shotcraft/upstream/references/shots/camera/space-camera-moves.md +46 -0
  359. package/lib/motion/shotcraft/upstream/references/shots/camera/steep-tilt-glide.md +52 -0
  360. package/lib/motion/shotcraft/upstream/references/shots/camera/tension-camera-moves.md +43 -0
  361. package/lib/motion/shotcraft/upstream/references/shots/camera/terminal-3d.md +42 -0
  362. package/lib/motion/shotcraft/upstream/references/shots/data/before-after-slider-scrub.md +48 -0
  363. package/lib/motion/shotcraft/upstream/references/shots/data/chart-live-moves.md +52 -0
  364. package/lib/motion/shotcraft/upstream/references/shots/data/gauge-readout-moves.md +45 -0
  365. package/lib/motion/shotcraft/upstream/references/shots/data/scroll-brake-moves.md +46 -0
  366. package/lib/motion/shotcraft/upstream/references/shots/data/timeline-travel.md +44 -0
  367. package/lib/motion/shotcraft/upstream/references/shots/effects/assemble-then-type-flyin.md +63 -0
  368. package/lib/motion/shotcraft/upstream/references/shots/effects/aurora-bloom-bg-flip.md +54 -0
  369. package/lib/motion/shotcraft/upstream/references/shots/effects/brand-frame-snap.md +58 -0
  370. package/lib/motion/shotcraft/upstream/references/shots/effects/fui-hud-moves.md +35 -0
  371. package/lib/motion/shotcraft/upstream/references/shots/effects/glow-flyline-moves.md +53 -0
  372. package/lib/motion/shotcraft/upstream/references/shots/effects/icon-performance-moves.md +42 -0
  373. package/lib/motion/shotcraft/upstream/references/shots/effects/impact-feedback.md +50 -0
  374. package/lib/motion/shotcraft/upstream/references/shots/effects/light-play-moves.md +46 -0
  375. package/lib/motion/shotcraft/upstream/references/shots/effects/radial-ripple-phone-chips.md +56 -0
  376. package/lib/motion/shotcraft/upstream/references/shots/effects/riso-print-hits.md +49 -0
  377. package/lib/motion/shotcraft/upstream/references/shots/effects/scan-bracket-sweep.md +50 -0
  378. package/lib/motion/shotcraft/upstream/references/shots/effects/scanline-annotate-focus.md +53 -0
  379. package/lib/motion/shotcraft/upstream/references/shots/effects/scanline-assemble-flyin.md +53 -0
  380. package/lib/motion/shotcraft/upstream/references/shots/effects/slam-entrance-moves.md +55 -0
  381. package/lib/motion/shotcraft/upstream/references/shots/effects/spotlight-sweep-moves.md +52 -0
  382. package/lib/motion/shotcraft/upstream/references/shots/interaction/autolayout-gap-dial.md +60 -0
  383. package/lib/motion/shotcraft/upstream/references/shots/interaction/canvas-materialize-moves.md +57 -0
  384. package/lib/motion/shotcraft/upstream/references/shots/interaction/chip-grid-single-select-blackout.md +51 -0
  385. package/lib/motion/shotcraft/upstream/references/shots/interaction/collab-cursor-moves.md +41 -0
  386. package/lib/motion/shotcraft/upstream/references/shots/interaction/glass-pill-dictation-typing.md +53 -0
  387. package/lib/motion/shotcraft/upstream/references/shots/interaction/hashtag-to-pill-materialize.md +60 -0
  388. package/lib/motion/shotcraft/upstream/references/shots/interaction/picker-carousel-feature-cycle.md +55 -0
  389. package/lib/motion/shotcraft/upstream/references/shots/interaction/segmented-thumb-hero.md +57 -0
  390. package/lib/motion/shotcraft/upstream/references/shots/interaction/theme-switch-moves.md +47 -0
  391. package/lib/motion/shotcraft/upstream/references/shots/interaction/type-and-filter.md +37 -0
  392. package/lib/motion/shotcraft/upstream/references/shots/interaction/voice-waveform-live.md +54 -0
  393. package/lib/motion/shotcraft/upstream/references/shots/opening/brand-ink-open.md +36 -0
  394. package/lib/motion/shotcraft/upstream/references/shots/opening/crane-rise-reveal.md +56 -0
  395. package/lib/motion/shotcraft/upstream/references/shots/opening/dataviz-landscape-open.md +56 -0
  396. package/lib/motion/shotcraft/upstream/references/shots/opening/fracture.md +40 -0
  397. package/lib/motion/shotcraft/upstream/references/shots/opening/icon-field-colorize.md +60 -0
  398. package/lib/motion/shotcraft/upstream/references/shots/opening/letterspace-materialize.md +57 -0
  399. package/lib/motion/shotcraft/upstream/references/shots/opening/magician-card-flourish.md +73 -0
  400. package/lib/motion/shotcraft/upstream/references/shots/opening/orbit-ring-title-open.md +87 -0
  401. package/lib/motion/shotcraft/upstream/references/shots/opening/spotlight-hero-card.md +55 -0
  402. package/lib/motion/shotcraft/upstream/references/shots/opening/stroke-segment-build.md +50 -0
  403. package/lib/motion/shotcraft/upstream/references/shots/opening/text-as-mask.md +59 -0
  404. package/lib/motion/shotcraft/upstream/references/shots/outro/grain-dissolve.md +38 -0
  405. package/lib/motion/shotcraft/upstream/references/shots/outro/logo-shrink-wordmark-lockup.md +38 -0
  406. package/lib/motion/shotcraft/upstream/references/shots/outro/neon-triple-marquee.md +59 -0
  407. package/lib/motion/shotcraft/upstream/references/shots/outro/outro-group-photo-launch.md +39 -0
  408. package/lib/motion/shotcraft/upstream/references/shots/outro/ui-strip-away-outro.md +61 -0
  409. package/lib/motion/shotcraft/upstream/references/shots/outro/ui-to-brand-morph.md +52 -0
  410. package/lib/motion/shotcraft/upstream/references/shots/rhythm/beat-cut-moves.md +50 -0
  411. package/lib/motion/shotcraft/upstream/references/shots/rhythm/beat-step-list-theme-cycle.md +66 -0
  412. package/lib/motion/shotcraft/upstream/references/shots/rhythm/panel-grid-moves.md +43 -0
  413. package/lib/motion/shotcraft/upstream/references/shots/rhythm/quad-split-parallel-scenes.md +38 -0
  414. package/lib/motion/shotcraft/upstream/references/shots/rhythm/rhythm-interrupt-moves.md +38 -0
  415. package/lib/motion/shotcraft/upstream/references/shots/rhythm/sakuga-timing-shift.md +56 -0
  416. package/lib/motion/shotcraft/upstream/references/shots/rhythm/speed-ramp-freeze.md +44 -0
  417. package/lib/motion/shotcraft/upstream/references/shots/rhythm/trailer-grammar-moves.md +38 -0
  418. package/lib/motion/shotcraft/upstream/references/shots/transition/bottom-push-stack-wipe.md +63 -0
  419. package/lib/motion/shotcraft/upstream/references/shots/transition/card-flip-reveal.md +57 -0
  420. package/lib/motion/shotcraft/upstream/references/shots/transition/card-flock-tumble.md +63 -0
  421. package/lib/motion/shotcraft/upstream/references/shots/transition/circle-match-iris.md +58 -0
  422. package/lib/motion/shotcraft/upstream/references/shots/transition/gradient-transition.md +45 -0
  423. package/lib/motion/shotcraft/upstream/references/shots/transition/line-carry-transition.md +55 -0
  424. package/lib/motion/shotcraft/upstream/references/shots/transition/mosaic-reframe.md +49 -0
  425. package/lib/motion/shotcraft/upstream/references/shots/transition/page-turn-transitions.md +42 -0
  426. package/lib/motion/shotcraft/upstream/references/shots/transition/print-texture-transitions.md +42 -0
  427. package/lib/motion/shotcraft/upstream/references/shots/transition/shot-transitions.md +45 -0
  428. package/lib/motion/shotcraft/upstream/references/shots/transition/tear-streak-transitions.md +48 -0
  429. package/lib/motion/shotcraft/upstream/references/shots/transition/transition-hidden-cut.md +55 -0
  430. package/lib/motion/shotcraft/upstream/references/shots/transition/transition-travel.md +43 -0
  431. package/lib/motion/shotcraft/upstream/references/shots/transition/white-flash-logo-simplify-cut.md +41 -0
  432. package/lib/motion/shotcraft/upstream/references/shots/transition/wipe-transitions.md +42 -0
  433. package/lib/motion/shotcraft/upstream/references/shots/typography/blur-slide.md +59 -0
  434. package/lib/motion/shotcraft/upstream/references/shots/typography/brace-expand.md +55 -0
  435. package/lib/motion/shotcraft/upstream/references/shots/typography/countdown-arc-scatter.md +65 -0
  436. package/lib/motion/shotcraft/upstream/references/shots/typography/document-typewriter-reveal.md +42 -0
  437. package/lib/motion/shotcraft/upstream/references/shots/typography/flying-words.md +64 -0
  438. package/lib/motion/shotcraft/upstream/references/shots/typography/glitch-cycle.md +64 -0
  439. package/lib/motion/shotcraft/upstream/references/shots/typography/gradient-word-sweep.md +54 -0
  440. package/lib/motion/shotcraft/upstream/references/shots/typography/lead-word-zoom-assemble.md +91 -0
  441. package/lib/motion/shotcraft/upstream/references/shots/typography/marker-underline-title.md +51 -0
  442. package/lib/motion/shotcraft/upstream/references/shots/typography/outline-word-fill.md +66 -0
  443. package/lib/motion/shotcraft/upstream/references/shots/typography/paper-title-card.md +38 -0
  444. package/lib/motion/shotcraft/upstream/references/shots/typography/pill-chip-slot-cycle-handled.md +66 -0
  445. package/lib/motion/shotcraft/upstream/references/shots/typography/pill-slot-cycle.md +58 -0
  446. package/lib/motion/shotcraft/upstream/references/shots/typography/text-column-converge.md +61 -0
  447. package/lib/motion/shotcraft/upstream/references/shots/typography/title-demote-to-label.md +64 -0
  448. package/lib/motion/shotcraft/upstream/references/shots/typography/type-assembly-moves.md +45 -0
  449. package/lib/motion/shotcraft/upstream/references/shots/typography/type-entrance-moves.md +41 -0
  450. package/lib/motion/shotcraft/upstream/references/shots/typography/type-rhythm-sync.md +51 -0
  451. package/lib/motion/shotcraft/upstream/references/shots/typography/typewriter-moves.md +42 -0
  452. package/lib/motion/shotcraft/upstream/references/shots/typography/vertical-word-roll-blur-cycle.md +63 -0
  453. package/lib/motion/shotcraft/upstream/references/shots/typography/word-relay-filmstrip.md +57 -0
  454. package/lib/motion/shotcraft/upstream/references/shots/typography/word-relay-geometry.md +75 -0
  455. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/avatar-bracket-carousel.md +59 -0
  456. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/bezier-source-converge-merge.md +67 -0
  457. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/card-stack.md +60 -0
  458. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/carousel-3d.md +66 -0
  459. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/cloner-depth-echo.md +40 -0
  460. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/deck-deal-flyin.md +45 -0
  461. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/doc-park-left-pill-deal.md +62 -0
  462. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/draw-svg-trace.md +55 -0
  463. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/element-body-moves.md +37 -0
  464. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/integration-hub-map.md +55 -0
  465. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/list-reveal.md +50 -0
  466. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/list-stack-press.md +43 -0
  467. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/neon-frame-forerun.md +55 -0
  468. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/neon-frame-orbit-drop.md +51 -0
  469. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/page-waterfall-wall.md +51 -0
  470. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/product-card-progressive-assemble.md +56 -0
  471. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/radial-wave.md +49 -0
  472. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/research-card-stack-scroll.md +57 -0
  473. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/row-embed.md +36 -0
  474. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/value-stagger-gradient.md +64 -0
  475. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/wall-reveal-moves.md +47 -0
  476. package/lib/motion/shotcraft/upstream/references/sound-design.md +170 -0
  477. package/lib/motion/shotcraft/upstream/references/workbench.md +157 -0
  478. package/lib/motion/shotcraft/upstream/template/TEMPLATE.md +103 -0
  479. package/lib/motion/shotcraft/upstream/template/THEMES.md +44 -0
  480. package/lib/motion/shotcraft/upstream/template/package.json +20 -0
  481. package/lib/motion/shotcraft/upstream/template/remotion.config.ts +6 -0
  482. package/lib/motion/shotcraft/upstream/template/src/Root.tsx +17 -0
  483. package/lib/motion/shotcraft/upstream/template/src/aifl/Caption.tsx +65 -0
  484. package/lib/motion/shotcraft/upstream/template/src/aifl/DigitRoll.tsx +48 -0
  485. package/lib/motion/shotcraft/upstream/template/src/aifl/FlashCut.tsx +24 -0
  486. package/lib/motion/shotcraft/upstream/template/src/aifl/Main.tsx +192 -0
  487. package/lib/motion/shotcraft/upstream/template/src/aifl/PaperTitleCard.tsx +111 -0
  488. package/lib/motion/shotcraft/upstream/template/src/aifl/live/PageCam.tsx +150 -0
  489. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneDetail.tsx +158 -0
  490. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneFlyIn.tsx +468 -0
  491. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneOpen.tsx +552 -0
  492. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneOutroLive.tsx +411 -0
  493. package/lib/motion/shotcraft/upstream/template/src/aifl/live/ScenePapers.tsx +200 -0
  494. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneWbr.tsx +324 -0
  495. package/lib/motion/shotcraft/upstream/template/src/aifl/live-layout.json +389 -0
  496. package/lib/motion/shotcraft/upstream/template/src/index.ts +4 -0
  497. package/lib/motion/shotcraft/upstream/template/src/themes/ThemedFilm.tsx +7 -0
  498. package/lib/motion/shotcraft/upstream/template/src/themes/manifest.tsx +68 -0
  499. package/lib/motion/shotcraft/upstream/template/src/themes/palette-assets.json +1 -0
  500. package/lib/motion/shotcraft/upstream/template/src/themes/palette-assets.ts +24 -0
  501. package/lib/motion/shotcraft/upstream/template/src/themes/palettes.json +74 -0
  502. package/lib/motion/shotcraft/upstream/template/src/themes/visual-theme.tsx +91 -0
  503. package/lib/motion/shotcraft/upstream/template/src/workbench.ts +213 -0
  504. package/lib/motion/shotcraft/upstream/template/tsconfig.json +15 -0
  505. package/lib/motion/spec.js +113 -0
  506. package/lib/motion/style-engine.js +153 -0
  507. package/lib/motion/style-frames.js +271 -0
  508. package/lib/motion/sync.js +214 -0
  509. package/lib/motion/theme.js +121 -0
  510. package/lib/motion/video-runtime.js +192 -0
  511. package/lib/motion/voice.js +417 -0
  512. package/lib/motion-agent-files.js +82 -0
  513. package/lib/motion-agent-protocol.js +66 -0
  514. package/lib/motion-agent-tools.js +196 -0
  515. package/lib/motion-agent.js +171 -0
  516. package/lib/motion-capture.js +93 -0
  517. package/lib/motion-core.js +238 -0
  518. package/lib/motion-film-review.js +71 -0
  519. package/lib/motion-library.js +210 -0
  520. package/lib/motion-planner.js +254 -0
  521. package/lib/motion-project.js +87 -0
  522. package/lib/motion-render-worker.js +153 -0
  523. package/lib/motion-session.js +32 -0
  524. package/lib/motion-source.js +49 -0
  525. package/lib/motion-tasks.js +452 -0
  526. package/lib/motion-vision.js +44 -0
  527. package/lib/mxfile.js +27 -7
  528. package/lib/routing/node-runtime.js +14 -0
  529. package/lib/routing/router.js +211 -0
  530. package/lib/routing/vendor/README.md +96 -0
  531. package/lib/routing/vendor/libavoid-routing.js +753 -0
  532. package/lib/routing/vendor/libavoid.min.js +59 -0
  533. package/lib/shotcard-core.js +39 -0
  534. package/lib/shotcard-reference.js +56 -0
  535. package/lib/style-kernel.js +37 -3
  536. package/lib/task-patches.js +26 -2
  537. package/lib/video-export.js +49 -22
  538. package/package.json +34 -4
@@ -0,0 +1,1056 @@
1
+ /* 由 tools/build.mjs 生成 —— 请勿直接编辑;改 src/ 下的源文件。 */
2
+ /* 由 tools/build.mjs 生成 —— 请勿直接编辑;改 src/ 下的源文件。 */
3
+ /**
4
+ * 独立的 FFmpeg 管理器。
5
+ *
6
+ * 目标:新用户第一次用到音频 / 素材 / 视频功能时,自动拿到一个可用的 FFmpeg,
7
+ * 而不是先看到「未找到 FFmpeg」再自己去装。查找顺序固定:
8
+ *
9
+ * 1. DRAWAI_FFMPEG_PATH(显式指定;无效就直接报错,不回退,免得用户以为设置生效了)
10
+ * 2. 系统 PATH
11
+ * 3. 开发工作区的 _local/bin(只有源码 checkout 里才有)
12
+ * 4. 已验证的独立缓存 ~/.dsh/drawai-ffmpeg/<platform>-<arch>/
13
+ * 5. 按平台下载 FFmpeg 官方页面列出的预编译构建,校验后放进缓存
14
+ *
15
+ * 缓存写入走 staging + 跨进程锁 + 原子 rename,失败或取消会清掉临时文件,
16
+ * 下一次操作可以直接重试。下载只在真正需要 FFmpeg 时发生,模块 import 本身不做任何 IO。
17
+ */
18
+ import { spawn } from 'node:child_process'
19
+ import { createHash, randomUUID } from 'node:crypto'
20
+ import {
21
+ accessSync,
22
+ chmodSync,
23
+ closeSync,
24
+ constants as fsConstants,
25
+ copyFileSync,
26
+ cpSync,
27
+ createReadStream,
28
+ createWriteStream,
29
+ existsSync,
30
+ mkdirSync,
31
+ mkdtempSync,
32
+ openSync,
33
+ readFileSync,
34
+ readdirSync,
35
+ readSync,
36
+ renameSync,
37
+ rmSync,
38
+ statSync,
39
+ writeFileSync,
40
+ } from 'node:fs'
41
+ import { homedir, tmpdir } from 'node:os'
42
+ import { basename, delimiter, dirname, join, resolve } from 'node:path'
43
+ import { Readable, Transform } from 'node:stream'
44
+ import { pipeline } from 'node:stream/promises'
45
+ import { fileURLToPath } from 'node:url'
46
+ import { inflateRawSync } from 'node:zlib'
47
+ import zlib from 'node:zlib'
48
+
49
+ const READY_FILE = '.ready.json'
50
+ const READY_SCHEMA = 1
51
+ const MAX_DOWNLOAD_BYTES = 400 * 1024 * 1024
52
+ const DOWNLOAD_IDLE_MS = 90_000
53
+ const LOCK_STALE_MS = 10 * 60 * 1000
54
+ const LOCK_WAIT_MS = 3 * 60 * 1000
55
+ /** 项目用到 -fps_mode(FFmpeg 5.1 起)等较新选项,更老的构建会在真实任务里失败。 */
56
+ const FFMPEG_MIN_VERSION = [5, 1]
57
+ const FFMPEG_MIN_VERSION_TEXT = '5.1'
58
+
59
+ const BTBN_RELEASE = 'https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-10-31-12-59/'
60
+ const BTBN_VERSION = '8.1.3'
61
+
62
+ /**
63
+ * 从 FFmpeg 官方下载页(https://ffmpeg.org/download.html)列出的预编译来源里
64
+ * 固定下来的具体版本。所有条目都带 SHA-256:
65
+ * - gyan.dev / BtbN 校验压缩包;
66
+ * - osxexperts 只公布解压后 FFmpeg 文件的哈希,所以校验二进制本身。
67
+ * 构建均为 GPL(含 libx264 / libx265),本项目只在运行时下载并作为独立进程调用。
68
+ */
69
+ export const FFMPEG_REGISTRY = Object.freeze({
70
+ 'win32-x64': Object.freeze({
71
+ version: '8.1.2',
72
+ archive: 'zip',
73
+ url: 'https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-8.1.2-essentials_build.zip',
74
+ size: 109728040,
75
+ sha256: 'db580001caa24ac104c8cb856cd113a87b0a443f7bdf47d8c12b1d740584a2ec',
76
+ license: 'GPL-3.0-or-later',
77
+ credit: 'gyan.dev(FFmpeg 官方下载页列出的 Windows 构建)',
78
+ homepage: 'https://www.gyan.dev/ffmpeg/builds/',
79
+ }),
80
+ 'win32-arm64': Object.freeze({
81
+ version: BTBN_VERSION,
82
+ archive: 'zip',
83
+ url: BTBN_RELEASE + 'ffmpeg-n8.1.3-27-g8dca1586f4-winarm64-gpl-8.1.zip',
84
+ size: 134963677,
85
+ sha256: '40029da82e02abf0d12491e23666e710b7a8943d13501594cf38e6135b95efba',
86
+ license: 'GPL-3.0-or-later',
87
+ credit: 'BtbN/FFmpeg-Builds(FFmpeg 官方下载页列出的 Windows 构建)',
88
+ homepage: 'https://github.com/BtbN/FFmpeg-Builds',
89
+ }),
90
+ 'linux-x64': Object.freeze({
91
+ version: BTBN_VERSION,
92
+ archive: 'tar.xz',
93
+ url: BTBN_RELEASE + 'ffmpeg-n8.1.3-27-g8dca1586f4-linux64-gpl-8.1.tar.xz',
94
+ size: 152246308,
95
+ sha256: '41e9c565594c390622cf968d82fa4135d9e9a11d06c1515644288cbc844e3380',
96
+ license: 'GPL-3.0-or-later',
97
+ credit: 'BtbN/FFmpeg-Builds(FFmpeg 官方下载页列出的 Linux 构建)',
98
+ homepage: 'https://github.com/BtbN/FFmpeg-Builds',
99
+ }),
100
+ 'linux-arm64': Object.freeze({
101
+ version: BTBN_VERSION,
102
+ archive: 'tar.xz',
103
+ url: BTBN_RELEASE + 'ffmpeg-n8.1.3-27-g8dca1586f4-linuxarm64-gpl-8.1.tar.xz',
104
+ size: 127720576,
105
+ sha256: 'a08c0ee6e1c4e02139344b303002dcc7c37cf125d639f1fbd15dabee40fcb525',
106
+ license: 'GPL-3.0-or-later',
107
+ credit: 'BtbN/FFmpeg-Builds(FFmpeg 官方下载页列出的 Linux 构建)',
108
+ homepage: 'https://github.com/BtbN/FFmpeg-Builds',
109
+ }),
110
+ 'darwin-arm64': Object.freeze({
111
+ version: '9.0.2',
112
+ archive: 'zip',
113
+ url: 'https://www.osxexperts.net/ffmpeg902arm.zip',
114
+ // 官网只公布解压后 FFmpeg 文件的 SHA-256,没有压缩包哈希。
115
+ binarySha256: '762f16b2913b8d3b8fdb8092df12b691053b035b13ae9b88205513c55cb8e71e',
116
+ license: 'GPL-3.0-or-later',
117
+ credit: 'OSXExperts(FFmpeg 官方下载页列出的 Apple Silicon 构建)',
118
+ homepage: 'https://www.osxexperts.net/',
119
+ }),
120
+ 'darwin-x64': Object.freeze({
121
+ version: '8.0',
122
+ archive: 'zip',
123
+ url: 'https://www.osxexperts.net/ffmpeg80intel.zip',
124
+ binarySha256: 'df3f1e3facdc1ae0ad0bd898cdfb072fbc9641bf47b11f172844525a05db8d11',
125
+ license: 'GPL-3.0-or-later',
126
+ credit: 'OSXExperts(FFmpeg 官方下载页列出的 macOS Intel 构建)',
127
+ homepage: 'https://www.osxexperts.net/',
128
+ }),
129
+ })
130
+
131
+ /**
132
+ * 项目实际用到的全部 FFmpeg 能力。来源是 src 里所有 ffmpeg 调用:
133
+ * - 音频导入(mp3/wav/m4a/aac/ogg/flac/webm)与 48kHz 转码、响度/峰值分析
134
+ * - 混音(裁切、循环、增益、淡入淡出、延迟、amix、限幅)
135
+ * - 视频探测、抽帧、帧检(黑场/静帧/信号统计)、接触表
136
+ * - 逐帧导出与 GIF/MP4/WebM 编码、调色板两遍法
137
+ * - 剪映交付包(ProRes 母版、竖版合成)
138
+ * 这些名字同时用于校验环境变量、PATH、缓存和下载的构建;缺一项都视为不可用。
139
+ */
140
+ export const FFMPEG_REQUIRED_FEATURES = Object.freeze({
141
+ encoders: Object.freeze(['libx264', 'libvpx-vp9', 'aac', 'libopus', 'gif', 'png', 'mjpeg', 'prores_ks', 'ffv1', 'pcm_s16le', 'pcm_s24le', 'rawvideo']),
142
+ decoders: Object.freeze(['h264', 'hevc', 'vp9', 'png', 'mjpeg', 'gif', 'aac', 'opus', 'mp3', 'flac', 'vorbis', 'pcm_s16le', 'pcm_s24le']),
143
+ filters: Object.freeze([
144
+ 'scale', 'crop', 'pad', 'overlay', 'split', 'lutrgb', 'palettegen', 'paletteuse', 'tile',
145
+ 'fps', 'format', 'tpad', 'trim', 'setpts', 'setsar', 'select', 'signalstats', 'metadata', 'gblur', 'color',
146
+ 'amix', 'alimiter', 'afade', 'apad', 'atrim', 'adelay', 'loudnorm', 'ebur128', 'volumedetect',
147
+ 'blackdetect', 'freezedetect', 'silencedetect', 'volume', 'aloop', 'asetpts',
148
+ ]),
149
+ muxers: Object.freeze(['mp4', 'mov', 'webm', 'gif', 'matroska', 'image2', 'image2pipe', 'wav', 'null', 'rawvideo', 's16le', 'ipod']),
150
+ demuxers: Object.freeze(['mp4', 'mov', 'm4a', 'matroska', 'webm', 'ogg', 'wav', 'mp3', 'flac', 'aac', 'gif', 'image2', 'lavfi']),
151
+ })
152
+
153
+ const REQUIRED_FEATURES = FFMPEG_REQUIRED_FEATURES
154
+
155
+ const FEATURE_LABELS = Object.freeze({ encoders: '编码器', decoders: '解码器', filters: '滤镜', muxers: '封装器', demuxers: '解封装器' })
156
+ const FEATURE_LISTS = Object.freeze({
157
+ encoders: ['-encoders'],
158
+ decoders: ['-decoders'],
159
+ filters: ['-filters'],
160
+ muxers: ['-muxers'],
161
+ demuxers: ['-demuxers'],
162
+ })
163
+
164
+ let memo = null
165
+ let state = {
166
+ status: 'unknown',
167
+ path: null,
168
+ source: null,
169
+ version: null,
170
+ cacheDirectory: null,
171
+ error: null,
172
+ progress: null,
173
+ }
174
+
175
+ /** 缓存目录:默认 ~/.dsh/drawai-ffmpeg,可用 DRAWAI_FFMPEG_DIR 覆盖。 */
176
+ export function ffmpegCacheDirectory() {
177
+ const configured = (process.env.DRAWAI_FFMPEG_DIR || '').trim()
178
+ return configured ? resolve(configured) : join(homedir(), '.dsh', 'drawai-ffmpeg')
179
+ }
180
+
181
+ /** 只读状态,给诊断/界面用。progress 是最近一次准备进度。 */
182
+ export function ffmpegState() {
183
+ return { ...state, progress: state.progress ? { ...state.progress } : null }
184
+ }
185
+
186
+ /** 没有异步准备过时的同步近似值:env → PATH → 开发目录 → 缓存 → 'ffmpeg'。 */
187
+ export function ffmpegPathSync() {
188
+ if (state.path) return state.path
189
+ const explicit = explicitBinaryPath()
190
+ if (explicit) return explicit
191
+ const onPath = findOnPathSync()
192
+ if (onPath) return onPath
193
+ const development = developmentBinaryPath()
194
+ if (development) return development
195
+ const cached = cachedBinarySync()
196
+ if (cached) return cached
197
+ return process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'
198
+ }
199
+
200
+ /** 明确指定并已经验证过的路径;找不到时给 null。 */
201
+ export function resolvedFfmpegPath() {
202
+ return state.status === 'ready' ? state.path : null
203
+ }
204
+
205
+ /**
206
+ * 确保 FFmpeg 可用。同一次准备在进程内共享下载/校验/解压工作,但每个调用者
207
+ * 有自己的取消信号与进度回调:某个调用者取消不会把别的调用者一起取消;
208
+ * 只有所有等待者都取消时才中止后台下载,之后可以重新调用重试。
209
+ */
210
+ export function ensureFfmpeg(options = {}) {
211
+ const platform = options.platform || process.platform
212
+ const arch = options.arch || process.arch
213
+ const cacheDir = options.cacheDir || ffmpegCacheDirectory()
214
+ if (options.signal?.aborted) return Promise.reject(abortedError())
215
+ // PATH 与显式路径也进 key:环境变了就重新解析,避免沿用旧结论。
216
+ const key = [
217
+ `${platform}-${arch}`,
218
+ cacheDir,
219
+ options.registry ? 'custom' : 'default',
220
+ explicitBinaryPath() || '',
221
+ process.env.PATH || '',
222
+ ].join('|')
223
+ let record = memo && memo.key === key ? memo : null
224
+ // 已经完成时 refresh 才需要重新解析;正在安装时 refresh 直接加入,避免并发下载。
225
+ if (record?.settled && options.refresh) record = null
226
+ if (!record) {
227
+ record = { key, controller: new AbortController(), listeners: new Set(), waiters: 0, snapshot: null, settled: false, promise: null }
228
+ memo = record
229
+ record.promise = resolveFfmpeg({
230
+ ...options,
231
+ platform,
232
+ arch,
233
+ cacheDir,
234
+ signal: record.controller.signal,
235
+ onProgress: (event) => publish(record, event),
236
+ })
237
+ record.promise.then(
238
+ () => { record.settled = true },
239
+ () => { record.settled = true; if (memo === record) memo = null },
240
+ )
241
+ }
242
+ return attachCaller(record, options)
243
+ }
244
+
245
+ /** 把进度事件发给当前所有调用者;晚加入的调用者会先收到最近一次快照。 */
246
+ function publish(record, event) {
247
+ record.snapshot = event
248
+ state = { ...state, progress: event }
249
+ for (const listener of [...record.listeners]) {
250
+ try {
251
+ listener(event)
252
+ } catch {
253
+ // 单个调用者的回调异常不能中断准备
254
+ }
255
+ }
256
+ }
257
+
258
+ function attachCaller(record, { signal, onProgress }) {
259
+ const listener = typeof onProgress === 'function'
260
+ ? (event) => { try { onProgress(event) } catch { /* 忽略 */ } }
261
+ : null
262
+ record.waiters += 1
263
+ if (listener) {
264
+ record.listeners.add(listener)
265
+ if (record.snapshot) queueMicrotask(() => { if (record.listeners.has(listener)) listener(record.snapshot) })
266
+ }
267
+ let done = false
268
+ return new Promise((resolvePromise, reject) => {
269
+ const cleanup = () => {
270
+ if (listener) record.listeners.delete(listener)
271
+ record.waiters = Math.max(0, record.waiters - 1)
272
+ signal?.removeEventListener('abort', onAbort)
273
+ }
274
+ const finish = (settle, value) => {
275
+ if (done) return
276
+ done = true
277
+ cleanup()
278
+ settle(value)
279
+ }
280
+ const onAbort = () => {
281
+ finish(reject, abortedError())
282
+ if (record.waiters === 0 && !record.settled) record.controller.abort()
283
+ }
284
+ record.promise.then((value) => finish(resolvePromise, value), (error) => finish(reject, error))
285
+ if (signal) {
286
+ if (signal.aborted) onAbort()
287
+ else signal.addEventListener('abort', onAbort, { once: true })
288
+ }
289
+ })
290
+ }
291
+
292
+ /**
293
+ * 删掉**本平台**的独立缓存并忘记本次进程内的解析结果。
294
+ * 只删除 `<cache>/<platform>-<arch>`、该平台的 staging 和锁目录;
295
+ * 缓存目录里其他平台的文件、用户自己放的文件都不会动。
296
+ */
297
+ export async function resetFfmpegCache({ cacheDir = ffmpegCacheDirectory(), platform = process.platform, arch = process.arch } = {}) {
298
+ memo?.controller.abort()
299
+ memo = null
300
+ state = { status: 'unknown', path: null, source: null, version: null, cacheDirectory: null, error: null, progress: null }
301
+ const suffix = `${platform}-${arch}`
302
+ let entries
303
+ try {
304
+ entries = readdirSync(cacheDir)
305
+ } catch {
306
+ return
307
+ }
308
+ for (const name of entries) {
309
+ if (name === suffix || name === `.lock-${suffix}` || name.startsWith(`.staging-${suffix}-`)) {
310
+ rmSync(join(cacheDir, name), { recursive: true, force: true })
311
+ }
312
+ }
313
+ }
314
+
315
+ async function resolveFfmpeg({ platform, arch, cacheDir, signal, onProgress, registry, probe = probeFfmpeg, readFeatures = readFfmpegFeatures, smoke = smokeFfmpeg, allowDownload = true, allowDevelopment = true }) {
316
+ throwIfAborted(signal)
317
+ setState({ status: 'resolving', cacheDirectory: cacheDir, path: null, error: null })
318
+ emit(onProgress, { phase: 'resolve', message: '正在查找 FFmpeg…' })
319
+
320
+ const rejected = []
321
+ const explicit = explicitBinaryPath()
322
+ if (explicit) {
323
+ const inspection = await inspectBinary(explicit, { probe, readFeatures, smoke, signal })
324
+ if (!inspection.ok) {
325
+ const detail = inspection.missing.length ? `缺少项目需要的组件:${inspection.missing.join('、')}` : inspection.reason
326
+ const error = ffmpegError(`DRAWAI_FFMPEG_PATH 指定的 FFmpeg 不可用:${explicit}(${detail})。这个路径不会被自动替换成其他 FFmpeg;请修正路径,或删除这个环境变量,让 DrawAI 自动准备 FFmpeg。`, 'FFMPEG_INVALID_PATH')
327
+ setState({ status: 'invalid', path: null, source: 'environment', error: error.message })
328
+ throw error
329
+ }
330
+ return ready(explicit, 'environment', inspection.version)
331
+ }
332
+
333
+ for (const candidate of findOnPath(platform)) {
334
+ const inspection = await inspectBinary(candidate, { probe, readFeatures, smoke, signal })
335
+ if (inspection.ok) return ready(candidate, 'path', inspection.version)
336
+ rejected.push(`${candidate}:${inspection.missing.length ? '缺少 ' + inspection.missing.join('、') : inspection.reason}`)
337
+ }
338
+
339
+ if (allowDevelopment) {
340
+ const development = developmentBinaryPath(platform)
341
+ if (development) {
342
+ const inspection = await inspectBinary(development, { probe, readFeatures, smoke, signal })
343
+ if (inspection.ok) return ready(development, 'development', inspection.version)
344
+ rejected.push(`${development}:${inspection.missing.length ? '缺少 ' + inspection.missing.join('、') : inspection.reason}`)
345
+ }
346
+ }
347
+
348
+ const table = registry || FFMPEG_REGISTRY
349
+ const entry = table[`${platform}-${arch}`]
350
+ // 缓存复用也要核对版本、二进制哈希和功能,而不是只要能跑 `-version` 就算数。
351
+ const cached = await inspectCache(join(cacheDir, `${platform}-${arch}`), { platform, arch, entry, probe, readFeatures, smoke, signal })
352
+ if (cached) return ready(cached.path, 'cache', cached.ready.version)
353
+
354
+ if (!entry) {
355
+ const suffix = rejected.length ? ` 已找到但不能用的 FFmpeg:${rejected.join(';')}。` : ''
356
+ const error = ffmpegError(`当前平台(${platform}-${arch})没有可自动下载的 FFmpeg 构建。请安装 FFmpeg 并加入系统 PATH,或把 DRAWAI_FFMPEG_PATH 设为 ffmpeg 可执行文件的路径;也可以把 DRAWAI_FFMPEG_DIR 指向已有的缓存目录。${suffix}`, 'FFMPEG_UNSUPPORTED')
357
+ setState({ status: 'unsupported', path: null, source: null, error: error.message })
358
+ throw error
359
+ }
360
+ if (!allowDownload) {
361
+ const suffix = rejected.length ? ` 已找到但不能用的 FFmpeg:${rejected.join(';')}。` : ''
362
+ const error = ffmpegError(`没有找到可用的 FFmpeg,而且当前操作不允许自动下载。请安装 FFmpeg 并加入 PATH,或把 DRAWAI_FFMPEG_PATH 设为可执行文件路径。${suffix}`, 'FFMPEG_MISSING')
363
+ setState({ status: 'missing', path: null, error: error.message })
364
+ throw error
365
+ }
366
+
367
+ let binary
368
+ try {
369
+ binary = await install(entry, { platform, arch, cacheDir, signal, onProgress, probe, readFeatures, smoke })
370
+ } catch (error) {
371
+ if (rejected.length && !isAbort(error)) {
372
+ error.message += ` 另外,PATH 或开发目录中的 FFmpeg 也不可用:${rejected.join(';')}。`
373
+ }
374
+ throw error
375
+ }
376
+ return ready(binary, 'download', entry.version)
377
+ }
378
+
379
+ /** 取出 `8.1.3` / `7.1-essentials_build` 这类版本号的主次版本;`N-92722-…` 取不到就返回 null。 */
380
+ function numericVersion(version) {
381
+ const match = /^(\d+)\.(\d+)/.exec(String(version || ''))
382
+ return match ? [Number(match[1]), Number(match[2])] : null
383
+ }
384
+
385
+ /** 探测 + 读取能力清单,判断一个 FFmpeg 是否覆盖项目需要。 */
386
+ async function inspectBinary(binary, { probe, readFeatures, smoke = smokeFfmpeg, signal }) {
387
+ const info = await probe(binary, { signal })
388
+ if (!info.ok) return { ok: false, reason: info.reason || '无法运行', version: null, features: null, missing: [] }
389
+ const version = numericVersion(info.version)
390
+ if (version && (version[0] < FFMPEG_MIN_VERSION[0] || (version[0] === FFMPEG_MIN_VERSION[0] && version[1] < FFMPEG_MIN_VERSION[1]))) {
391
+ return {
392
+ ok: false,
393
+ reason: `版本过旧(${info.version}),需要 FFmpeg ${FFMPEG_MIN_VERSION_TEXT} 或更新版本`,
394
+ version: info.version,
395
+ features: null,
396
+ missing: [],
397
+ }
398
+ }
399
+ let features
400
+ try {
401
+ features = await readFeatures(binary, { signal })
402
+ } catch (error) {
403
+ if (isAbort(error)) throw error
404
+ return { ok: false, reason: `读取功能清单失败:${error.message}`, version: info.version, features: null, missing: [] }
405
+ }
406
+ const missing = missingFeatures(features)
407
+ if (missing.length) {
408
+ return { ok: false, reason: `缺少项目需要的组件:${missing.join('、')}`, version: info.version, features, missing }
409
+ }
410
+ const check = await smoke(binary, { signal })
411
+ if (!check.ok) {
412
+ return { ok: false, reason: `实际编码自检失败:${check.reason}`, version: info.version, features, missing: [] }
413
+ }
414
+ return { ok: true, reason: '', version: info.version, features, missing: [] }
415
+ }
416
+
417
+ function ready(path, source, version) {
418
+ setState({ status: 'ready', path, source, version: version || null, error: null })
419
+ return path
420
+ }
421
+
422
+ function setState(patch) {
423
+ state = { ...state, ...patch }
424
+ }
425
+
426
+ function explicitBinaryPath() {
427
+ const configured = (process.env.DRAWAI_FFMPEG_PATH || '').trim()
428
+ return configured ? resolve(configured) : null
429
+ }
430
+
431
+ function findOnPathSync(platform = process.platform) {
432
+ return findOnPath(platform)[0] || null
433
+ }
434
+
435
+ /** PATH 里所有名为 ffmpeg 的候选;逐个验证功能,不能只看第一个。 */
436
+ function findOnPath(platform = process.platform) {
437
+ const names = platform === 'win32' ? ['ffmpeg.exe'] : ['ffmpeg']
438
+ const found = []
439
+ const seen = new Set()
440
+ for (const directory of (process.env.PATH || '').split(delimiter)) {
441
+ if (!directory) continue
442
+ for (const name of names) {
443
+ const candidate = join(directory, name)
444
+ if (seen.has(candidate)) continue
445
+ seen.add(candidate)
446
+ try {
447
+ accessSync(candidate, fsConstants.F_OK)
448
+ found.push(candidate)
449
+ } catch {
450
+ // 继续看下一个目录
451
+ }
452
+ }
453
+ }
454
+ return found
455
+ }
456
+
457
+ /** 开发 checkout 里自带的二进制:打包后的插件不会有这个目录。 */
458
+ function developmentBinaryPath(platform = process.platform) {
459
+ const name = platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'
460
+ const candidate = fileURLToPath(new URL(`../_local/bin/${name}`, import.meta.url))
461
+ return existsSync(candidate) ? candidate : null
462
+ }
463
+
464
+ function cachedBinarySync(platform = process.platform, arch = process.arch) {
465
+ const binary = join(ffmpegCacheDirectory(), `${platform}-${arch}`, platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg')
466
+ return existsSync(binary) ? binary : null
467
+ }
468
+
469
+ async function inspectCache(target, { platform, arch, entry, probe, readFeatures, smoke, signal }) {
470
+ const readyPath = join(target, READY_FILE)
471
+ if (!existsSync(readyPath)) return null
472
+ let readyFile
473
+ try {
474
+ readyFile = JSON.parse(readFileSync(readyPath, 'utf8'))
475
+ } catch {
476
+ return null
477
+ }
478
+ if (readyFile?.schema !== READY_SCHEMA) return null
479
+ if (readyFile.platform && readyFile.platform !== platform) return null
480
+ if (readyFile.arch && readyFile.arch !== arch) return null
481
+ // 缓存必须对应注册表里固定的版本;版本变了就重新下载,而不是默默沿用旧版。
482
+ if (entry && readyFile.version !== entry.version) return null
483
+ const binary = join(target, readyFile.binary || (platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'))
484
+ if (!existsSync(binary)) return null
485
+ if (readyFile.sha256) {
486
+ const digest = await sha256File(binary)
487
+ if (digest !== readyFile.sha256) return null
488
+ }
489
+ const inspection = await inspectBinary(binary, { probe, readFeatures, smoke, signal })
490
+ if (!inspection.ok) return null
491
+ return { path: binary, ready: { ...readyFile, version: readyFile.version || inspection.version }, inspection }
492
+ }
493
+
494
+ async function install(entry, { platform, arch, cacheDir, signal, onProgress, probe, readFeatures, smoke }) {
495
+ mkdirSync(cacheDir, { recursive: true })
496
+ const target = join(cacheDir, `${platform}-${arch}`)
497
+ const binaryName = platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'
498
+ const release = await acquireLock(join(cacheDir, `.lock-${platform}-${arch}`), { signal, onProgress })
499
+ try {
500
+ throwIfAborted(signal)
501
+ // 等锁期间可能已经被另一个进程装好了。
502
+ const existing = await inspectCache(target, { platform, arch, entry, probe, readFeatures, smoke, signal })
503
+ if (existing) {
504
+ emit(onProgress, { phase: 'ready', message: 'FFmpeg 已经准备好' })
505
+ return existing.path
506
+ }
507
+
508
+ const staging = mkdtempSync(join(cacheDir, `.staging-${platform}-${arch}-`))
509
+ try {
510
+ const extension = entry.archive === 'tar.xz' ? '.tar.xz' : '.zip'
511
+ const archivePath = join(staging, `download${extension}`)
512
+ const download = await downloadArchive(entry, archivePath, { signal, onProgress })
513
+
514
+ emit(onProgress, { phase: 'verify', message: '正在校验下载的 FFmpeg…' })
515
+ if (entry.sha256 && download.sha256 !== entry.sha256) {
516
+ throw ffmpegError(`FFmpeg 下载校验失败:期望 SHA-256 ${entry.sha256},实际 ${download.sha256}。已清理临时文件;请重试,若持续出现请检查网络或代理。`, 'FFMPEG_CHECKSUM')
517
+ }
518
+ if (entry.size && download.bytes !== entry.size) {
519
+ throw ffmpegError(`FFmpeg 下载不完整:期望 ${entry.size} 字节,实际 ${download.bytes} 字节。已清理临时文件,请重试。`, 'FFMPEG_CHECKSUM')
520
+ }
521
+
522
+ emit(onProgress, { phase: 'extract', message: '正在解压 FFmpeg…' })
523
+ const packageDir = join(staging, 'package')
524
+ mkdirSync(packageDir, { recursive: true })
525
+ const binaryPath = join(packageDir, binaryName)
526
+ if (entry.archive === 'tar.xz') {
527
+ const extracted = join(staging, 'extracted')
528
+ mkdirSync(extracted, { recursive: true })
529
+ await extractTarXz(archivePath, extracted, { signal })
530
+ const found = await findBinary(extracted, binaryName)
531
+ if (!found) throw new Error(`解压后的压缩包里没有 ${binaryName}`)
532
+ copyFileSync(found, binaryPath)
533
+ rmSync(extracted, { recursive: true, force: true })
534
+ } else {
535
+ extractZipBinary(archivePath, binaryPath, binaryName)
536
+ }
537
+ chmodSync(binaryPath, 0o755)
538
+ const binarySha256 = await sha256File(binaryPath)
539
+ if (entry.binarySha256 && binarySha256 !== entry.binarySha256) {
540
+ throw ffmpegError(`FFmpeg 校验失败:期望 SHA-256 ${entry.binarySha256},实际 ${binarySha256}。已清理临时文件;请重试,若持续出现请检查下载源是否更新。`, 'FFMPEG_CHECKSUM')
541
+ }
542
+
543
+ emit(onProgress, { phase: 'check', message: '正在检查 FFmpeg 是否包含所需功能…' })
544
+ const inspection = await inspectBinary(binaryPath, { probe, readFeatures, smoke, signal })
545
+ if (!inspection.ok) {
546
+ const detail = inspection.missing.length
547
+ ? `缺少项目需要的组件:${inspection.missing.join('、')}`
548
+ : inspection.reason || '无法运行'
549
+ throw ffmpegError(`下载的 FFmpeg 不可用:${detail}。已清理临时文件,请重试;若仍然出现,请设置 DRAWAI_FFMPEG_PATH 指向完整的 FFmpeg ${FFMPEG_MIN_VERSION_TEXT}+ 构建。`, 'FFMPEG_INCOMPLETE')
550
+ }
551
+
552
+ writeFileSync(join(packageDir, READY_FILE), JSON.stringify({
553
+ schema: READY_SCHEMA,
554
+ platform,
555
+ arch,
556
+ version: entry.version,
557
+ binary: binaryName,
558
+ binarySize: statSync(binaryPath).size,
559
+ sha256: binarySha256,
560
+ source: {
561
+ url: entry.url,
562
+ sha256: entry.sha256 || null,
563
+ license: entry.license,
564
+ credit: entry.credit,
565
+ homepage: entry.homepage,
566
+ },
567
+ features: Object.fromEntries(Object.entries(inspection.features).map(([key, names]) => [key, [...names].filter((name) => REQUIRED_FEATURES[key]?.includes(name))])),
568
+ installedAt: new Date().toISOString(),
569
+ }, null, 2))
570
+
571
+ await replaceDirectory(packageDir, target)
572
+ emit(onProgress, { phase: 'ready', message: `FFmpeg ${entry.version} 已准备好(仅首次需要下载)` })
573
+ return join(target, binaryName)
574
+ } finally {
575
+ rmSync(staging, { recursive: true, force: true })
576
+ }
577
+ } finally {
578
+ release()
579
+ }
580
+ }
581
+
582
+ /**
583
+ * 把 staging/package 放进缓存目录。
584
+ * Windows 上刚执行过的 ffmpeg.exe 可能还被 Defender/索引器短暂持有,rename 会报 EPERM;
585
+ * 这里重试几次,仍不行就退回「复制 + 删 staging」,复制完成前不删源,避免半成品进入缓存。
586
+ */
587
+ async function replaceDirectory(source, target) {
588
+ rmSync(target, { recursive: true, force: true })
589
+ let lastError = null
590
+ for (let attempt = 0; attempt < 8; attempt += 1) {
591
+ try {
592
+ renameSync(source, target)
593
+ return
594
+ } catch (error) {
595
+ if (!['EPERM', 'EBUSY', 'EACCES', 'ENOTEMPTY'].includes(error.code)) throw error
596
+ lastError = error
597
+ await new Promise((resolve) => setTimeout(resolve, 150 * (attempt + 1)))
598
+ }
599
+ }
600
+ try {
601
+ cpSync(source, target, { recursive: true })
602
+ rmSync(source, { recursive: true, force: true })
603
+ } catch (error) {
604
+ rmSync(target, { recursive: true, force: true })
605
+ throw lastError || error
606
+ }
607
+ }
608
+
609
+ async function downloadArchive(entry, destination, { signal, onProgress }) {
610
+ const url = downloadUrl(entry)
611
+ emit(onProgress, {
612
+ phase: 'download',
613
+ message: `正在下载 FFmpeg ${entry.version}${entry.size ? `(约 ${formatBytes(entry.size)},仅首次)` : '(仅首次)'}…`,
614
+ received: 0,
615
+ total: entry.size || 0,
616
+ })
617
+ const controller = new AbortController()
618
+ let idleTimedOut = false
619
+ let idleTimer = null
620
+ let received = 0
621
+ let total = entry.size || 0
622
+ let lastEmit = 0
623
+ let response = null
624
+ const hash = createHash('sha256')
625
+ const resetIdle = () => {
626
+ clearTimeout(idleTimer)
627
+ idleTimer = setTimeout(() => {
628
+ idleTimedOut = true
629
+ controller.abort()
630
+ }, DOWNLOAD_IDLE_MS)
631
+ }
632
+ const onAbort = () => controller.abort()
633
+ if (signal) {
634
+ if (signal.aborted) throw abortedError()
635
+ signal.addEventListener('abort', onAbort, { once: true })
636
+ }
637
+ resetIdle()
638
+ try {
639
+ response = await fetch(url, {
640
+ signal: controller.signal,
641
+ redirect: 'follow',
642
+ headers: { 'user-agent': 'drawai-ffmpeg/1' },
643
+ })
644
+ if (!response.ok || !response.body) throw new Error(`HTTP ${response.status} ${response.statusText}`.trim())
645
+ total = Number(response.headers.get('content-length')) || total
646
+ if (total > MAX_DOWNLOAD_BYTES) throw new Error(`下载文件超过 ${formatBytes(MAX_DOWNLOAD_BYTES)}`)
647
+ const meter = new Transform({
648
+ transform(chunk, _encoding, callback) {
649
+ received += chunk.length
650
+ if (received > MAX_DOWNLOAD_BYTES) {
651
+ callback(new Error(`下载文件超过 ${formatBytes(MAX_DOWNLOAD_BYTES)}`))
652
+ return
653
+ }
654
+ hash.update(chunk)
655
+ resetIdle()
656
+ const now = Date.now()
657
+ if (now - lastEmit > 250) {
658
+ lastEmit = now
659
+ emit(onProgress, {
660
+ phase: 'download',
661
+ message: `正在下载 FFmpeg ${entry.version}(${formatBytes(received)}${total ? ` / ${formatBytes(total)}` : ''})…`,
662
+ received,
663
+ total,
664
+ })
665
+ }
666
+ callback(null, chunk)
667
+ },
668
+ })
669
+ await pipeline(Readable.fromWeb(response.body), meter, createWriteStream(destination, { flags: 'wx' }))
670
+ emit(onProgress, {
671
+ phase: 'download',
672
+ message: `FFmpeg ${entry.version} 下载完成(${formatBytes(received)})`,
673
+ received,
674
+ total,
675
+ })
676
+ return { bytes: received, sha256: hash.digest('hex') }
677
+ } catch (error) {
678
+ await response?.body?.cancel?.().catch(() => {})
679
+ if (idleTimedOut) {
680
+ throw ffmpegError(`下载 FFmpeg 超时(${Math.round(DOWNLOAD_IDLE_MS / 1000)} 秒没有收到数据)。已清理临时文件,请检查网络后重试当前操作。${proxyHint()}`, 'FFMPEG_DOWNLOAD', { cause: error })
681
+ }
682
+ if (signal?.aborted || error?.name === 'AbortError') throw abortedError()
683
+ throw ffmpegError(`下载 FFmpeg 失败:${describeError(error)}(${url})。已清理临时文件,联网后重试当前操作即可。${proxyHint()}`, 'FFMPEG_DOWNLOAD', { cause: error })
684
+ } finally {
685
+ clearTimeout(idleTimer)
686
+ signal?.removeEventListener('abort', onAbort)
687
+ }
688
+ }
689
+
690
+ /**
691
+ * 下载地址。可用 DRAWAI_FFMPEG_MIRROR 指向镜像前缀(例如公司内网镜像):
692
+ * 实际地址为 `<mirror>/<原文件名>`。镜像只改下载位置,哈希/大小仍然按注册表校验。
693
+ */
694
+ function downloadUrl(entry) {
695
+ const mirror = (process.env.DRAWAI_FFMPEG_MIRROR || '').trim()
696
+ if (!mirror) return entry.url
697
+ const name = basename(new URL(entry.url).pathname)
698
+ if (!mirror.endsWith('/')) return `${mirror}/${name}`
699
+ return `${mirror}${name}`
700
+ }
701
+
702
+ /** Node 的 fetch 不会自动使用 HTTP(S)_PROXY;检测到代理时给出可操作的替代方案。 */
703
+ function proxyHint() {
704
+ const proxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || process.env.ALL_PROXY || process.env.all_proxy
705
+ if (!proxy) return ''
706
+ return ` 检测到代理 ${proxy},但程序内置下载不会自动使用它;可设置 DRAWAI_FFMPEG_MIRROR 指向可直连的镜像,或手动安装后把 DRAWAI_FFMPEG_PATH 设为 ffmpeg 可执行文件路径。`
707
+ }
708
+
709
+ async function extractTarXz(archive, directory, { signal }) {
710
+ const { code } = await runLogged('tar', ['-xf', archive, '-C', directory], { signal, timeoutMs: 5 * 60 * 1000 })
711
+ if (code !== 0) throw new Error(`解压 FFmpeg 失败(tar 退出码 ${code})`)
712
+ }
713
+
714
+ async function findBinary(root, binaryName, maxDepth = 4) {
715
+ const queue = [[root, 0]]
716
+ const candidates = []
717
+ while (queue.length) {
718
+ const [directory, depth] = queue.shift()
719
+ let entries
720
+ try {
721
+ entries = readdirSync(directory, { withFileTypes: true })
722
+ } catch {
723
+ continue
724
+ }
725
+ for (const entry of entries) {
726
+ const full = join(directory, entry.name)
727
+ if (entry.isDirectory()) {
728
+ if (depth < maxDepth) queue.push([full, depth + 1])
729
+ } else if (entry.name === binaryName) {
730
+ candidates.push(full)
731
+ }
732
+ }
733
+ }
734
+ return candidates.find((candidate) => candidate.split(/[\\/]/).includes('bin')) || candidates[0] || null
735
+ }
736
+
737
+ function extractZipBinary(archive, destination, binaryName) {
738
+ const size = statSync(archive).size
739
+ const fd = openSync(archive, 'r')
740
+ try {
741
+ const tailLength = Math.min(size, 65_557)
742
+ const tail = Buffer.alloc(tailLength)
743
+ readSync(fd, tail, 0, tailLength, size - tailLength)
744
+ let eocd = -1
745
+ for (let index = tailLength - 22; index >= 0; index -= 1) {
746
+ if (tail.readUInt32LE(index) === 0x06054b50) {
747
+ eocd = index
748
+ break
749
+ }
750
+ }
751
+ if (eocd < 0) throw new Error('压缩包结构无效(找不到中央目录)')
752
+ const count = tail.readUInt16LE(eocd + 10)
753
+ const directorySize = tail.readUInt32LE(eocd + 12)
754
+ const directoryOffset = tail.readUInt32LE(eocd + 16)
755
+ if (count === 0xffff || directorySize === 0xffffffff || directoryOffset === 0xffffffff) throw new Error('暂不支持 ZIP64 压缩包')
756
+ const directory = Buffer.alloc(directorySize)
757
+ readSync(fd, directory, 0, directorySize, directoryOffset)
758
+
759
+ let pointer = 0
760
+ let best = null
761
+ for (let index = 0; index < count; index += 1) {
762
+ if (pointer + 46 > directory.length || directory.readUInt32LE(pointer) !== 0x02014b50) break
763
+ const flags = directory.readUInt16LE(pointer + 8)
764
+ const method = directory.readUInt16LE(pointer + 10)
765
+ const checksum = directory.readUInt32LE(pointer + 16)
766
+ const compressedSize = directory.readUInt32LE(pointer + 20)
767
+ const uncompressedSize = directory.readUInt32LE(pointer + 24)
768
+ const nameLength = directory.readUInt16LE(pointer + 28)
769
+ const extraLength = directory.readUInt16LE(pointer + 30)
770
+ const commentLength = directory.readUInt16LE(pointer + 32)
771
+ const localOffset = directory.readUInt32LE(pointer + 42)
772
+ const name = directory.toString('utf8', pointer + 46, pointer + 46 + nameLength)
773
+ pointer += 46 + nameLength + extraLength + commentLength
774
+ const normalized = name.replace(/\\/g, '/')
775
+ if (basename(normalized) !== binaryName) continue
776
+ const candidate = { name: normalized, flags, method, checksum, compressedSize, uncompressedSize, localOffset }
777
+ if (!best || (normalized.includes('/bin/') && !best.name.includes('/bin/'))) best = candidate
778
+ }
779
+ if (!best) throw new Error(`压缩包里没有 ${binaryName}`)
780
+ if (best.flags & 0x1) throw new Error('压缩包已加密,无法解压')
781
+ if (best.compressedSize > 200 * 1024 * 1024) throw new Error('压缩包里的 FFmpeg 文件过大')
782
+
783
+ const header = Buffer.alloc(30)
784
+ readSync(fd, header, 0, 30, best.localOffset)
785
+ if (header.readUInt32LE(0) !== 0x04034b50) throw new Error('压缩包条目损坏')
786
+ const dataOffset = best.localOffset + 30 + header.readUInt16LE(26) + header.readUInt16LE(28)
787
+ const compressed = Buffer.alloc(best.compressedSize)
788
+ readSync(fd, compressed, 0, compressed.length, dataOffset)
789
+ let data
790
+ if (best.method === 0) data = compressed
791
+ else if (best.method === 8) data = inflateRawSync(compressed)
792
+ else throw new Error('压缩包使用了不支持的压缩方式')
793
+ if (data.length !== best.uncompressedSize) throw new Error('解压后的大小不符')
794
+ if (crc32(data) !== (best.checksum >>> 0)) throw new Error('解压校验失败(CRC 不匹配)')
795
+ mkdirSync(dirname(destination), { recursive: true })
796
+ writeFileSync(destination, data)
797
+ } finally {
798
+ closeSync(fd)
799
+ }
800
+ }
801
+
802
+ async function acquireLock(lockPath, { signal, onProgress }) {
803
+ const startedAt = Date.now()
804
+ while (true) {
805
+ throwIfAborted(signal)
806
+ try {
807
+ mkdirSync(lockPath)
808
+ return () => rmSync(lockPath, { recursive: true, force: true })
809
+ } catch (error) {
810
+ if (error.code !== 'EEXIST') throw error
811
+ }
812
+ try {
813
+ if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
814
+ rmSync(lockPath, { recursive: true, force: true })
815
+ continue
816
+ }
817
+ } catch {
818
+ continue
819
+ }
820
+ if (Date.now() - startedAt > LOCK_WAIT_MS) {
821
+ throw ffmpegError('另一个 DrawAI 任务正在准备 FFmpeg,等待超时。请稍后重试当前操作。', 'FFMPEG_BUSY')
822
+ }
823
+ emit(onProgress, { phase: 'waiting', message: '另一个任务正在准备 FFmpeg,等待中…' })
824
+ await delay(500, signal)
825
+ }
826
+ }
827
+
828
+ /** 运行一个子进程,把 stdout/stderr 写进临时文件(受限环境里管道会 EPERM)。 */
829
+ function runLogged(command, args, { signal, timeoutMs = 20_000 } = {}) {
830
+ return new Promise((resolvePromise, reject) => {
831
+ const logPath = join(tmpdir(), `drawai-${randomUUID()}.log`)
832
+ const fd = openSync(logPath, 'w')
833
+ let child = null
834
+ let settled = false
835
+ let timer = null
836
+ const cleanup = () => {
837
+ clearTimeout(timer)
838
+ signal?.removeEventListener('abort', onAbort)
839
+ try {
840
+ closeSync(fd)
841
+ } catch {
842
+ // 已经关掉了
843
+ }
844
+ }
845
+ const settle = (error, value) => {
846
+ if (settled) return
847
+ settled = true
848
+ cleanup()
849
+ let text = ''
850
+ try {
851
+ text = readFileSync(logPath, 'utf8')
852
+ } catch {
853
+ // 日志读不到就用空字符串
854
+ }
855
+ try {
856
+ rmSync(logPath, { force: true })
857
+ } catch {
858
+ // Windows 上偶尔还会被占用,留给系统临时目录清理
859
+ }
860
+ if (error) reject(error)
861
+ else resolvePromise({ code: value.code, text })
862
+ }
863
+ const failSpawn = (error) => settle(error.code === 'ENOENT' ? new Error(`系统里没有 ${command} 命令`) : error)
864
+ const onAbort = () => {
865
+ child?.kill('SIGKILL')
866
+ settle(abortedError())
867
+ }
868
+ timer = setTimeout(() => {
869
+ child?.kill('SIGKILL')
870
+ settle(ffmpegError(`${basename(command)} 执行超时`, 'FFMPEG_TIMEOUT'))
871
+ }, timeoutMs)
872
+ try {
873
+ child = spawn(command, args, { windowsHide: true, stdio: ['ignore', fd, fd] })
874
+ } catch (error) {
875
+ failSpawn(error)
876
+ return
877
+ }
878
+ signal?.addEventListener('abort', onAbort, { once: true })
879
+ child.on('error', failSpawn)
880
+ child.on('close', (code) => settle(null, { code }))
881
+ })
882
+ }
883
+
884
+ /**
885
+ * 探测一个可执行文件是不是 FFmpeg:`-hide_banner -version` 必须成功并打印版本号。
886
+ * 返回 { ok, version?, reason? },不抛异常(取消除外)。
887
+ */
888
+ export async function probeFfmpeg(binary, { signal, timeoutMs = 20_000 } = {}) {
889
+ if (!binary) return { ok: false, reason: '没有提供可执行文件路径' }
890
+ try {
891
+ accessSync(binary, process.platform === 'win32' ? fsConstants.F_OK : fsConstants.F_OK | fsConstants.X_OK)
892
+ } catch (error) {
893
+ return { ok: false, reason: error.code === 'EACCES' ? '文件不可执行' : '文件不存在' }
894
+ }
895
+ try {
896
+ const { code, text } = await runLogged(binary, ['-hide_banner', '-version'], { signal, timeoutMs })
897
+ if (code !== 0) return { ok: false, reason: `运行失败(退出码 ${code})` }
898
+ const match = /ffmpeg version\s+(\S+)/i.exec(text)
899
+ if (!match) return { ok: false, reason: '不是可用的 FFmpeg 可执行文件' }
900
+ return { ok: true, version: match[1].replace(/^v/i, '') }
901
+ } catch (error) {
902
+ if (isAbort(error)) throw error
903
+ return { ok: false, reason: error.message }
904
+ }
905
+ }
906
+
907
+ /**
908
+ * 实际跑一次极短的编码,验证 `-fps_mode` 这类选项真的被这个构建接受。
909
+ * 功能清单只能证明编码器/滤镜存在,不能证明命令行选项存在(FFmpeg 4.x 能通过清单,
910
+ * 但在 `-fps_mode` 上直接报 Unrecognized option)。
911
+ */
912
+ export async function smokeFfmpeg(binary, { signal } = {}) {
913
+ try {
914
+ const { code, text } = await runLogged(binary, [
915
+ '-hide_banner', '-loglevel', 'error',
916
+ '-f', 'lavfi', '-i', 'color=c=black:s=64x64:r=30:d=0.1',
917
+ '-frames:v', '2', '-fps_mode', 'passthrough',
918
+ '-c:v', 'libx264', '-f', 'null', '-',
919
+ ], { signal, timeoutMs: 60_000 })
920
+ if (code === 0) return { ok: true }
921
+ const unsupported = /Unrecognized option '([^']+)'/.exec(text)
922
+ if (unsupported) return { ok: false, reason: `不支持选项 ${unsupported[1]}(需要 FFmpeg 5.1 或更新版本)` }
923
+ const line = text.trim().split(/\r?\n/).filter(Boolean).at(-1) || `退出码 ${code}`
924
+ return { ok: false, reason: line }
925
+ } catch (error) {
926
+ if (isAbort(error)) throw error
927
+ return { ok: false, reason: error.message }
928
+ }
929
+ }
930
+
931
+ /** 读取编码器/解码器/滤镜/封装器/解封装器清单,用来确认构建覆盖项目需要的能力。 */
932
+ export async function readFfmpegFeatures(binary, { signal } = {}) {
933
+ const features = {}
934
+ for (const [key, args] of Object.entries(FEATURE_LISTS)) {
935
+ const { code, text } = await runLogged(binary, ['-hide_banner', ...args], { signal, timeoutMs: 30_000 })
936
+ if (code !== 0) throw new Error(`读取 FFmpeg ${FEATURE_LABELS[key]}清单失败(退出码 ${code})`)
937
+ features[key] = parseListing(text)
938
+ }
939
+ return features
940
+ }
941
+
942
+ function parseListing(text) {
943
+ const names = new Set()
944
+ for (const line of text.split(/\r?\n/)) {
945
+ const tokens = line.trim().split(/\s+/)
946
+ if (tokens.length < 2) continue
947
+ // 列表首列是能力标记(V/A/S/D/T/.);demuxer 的输入设备还会多一个 `d` 列。
948
+ let name = tokens[1]
949
+ if (name === 'd' && tokens.length >= 3) name = tokens[2]
950
+ if (!name || name.includes('=') || !/^[A-Za-z0-9_.,-]+$/.test(name)) continue
951
+ // demuxers 会把 mov,mp4,m4a 写成同一行,拆开才能按名字查。
952
+ for (const item of name.split(',')) if (item) names.add(item)
953
+ }
954
+ return names
955
+ }
956
+
957
+ function missingFeatures(features) {
958
+ const missing = []
959
+ for (const key of Object.keys(REQUIRED_FEATURES)) {
960
+ for (const name of REQUIRED_FEATURES[key]) {
961
+ if (!features[key]?.has(name)) missing.push(`${FEATURE_LABELS[key]} ${name}`)
962
+ }
963
+ }
964
+ return missing
965
+ }
966
+
967
+ async function sha256File(file) {
968
+ const hash = createHash('sha256')
969
+ for await (const chunk of createReadStream(file)) hash.update(chunk)
970
+ return hash.digest('hex')
971
+ }
972
+
973
+ const CRC32_TABLE = (() => {
974
+ const table = new Int32Array(256)
975
+ for (let index = 0; index < 256; index += 1) {
976
+ let value = index
977
+ for (let bit = 0; bit < 8; bit += 1) value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1
978
+ table[index] = value
979
+ }
980
+ return table
981
+ })()
982
+
983
+ /** 纯 JS 的 CRC32(IEEE),供 Node 20 等没有 zlib.crc32 的运行时使用。 */
984
+ export function crc32Fallback(buffer) {
985
+ let value = 0xffffffff
986
+ for (let index = 0; index < buffer.length; index += 1) {
987
+ value = CRC32_TABLE[(value ^ buffer[index]) & 0xff] ^ (value >>> 8)
988
+ }
989
+ return (value ^ 0xffffffff) >>> 0
990
+ }
991
+
992
+ /** 优先用 zlib 的原生实现;没有时退回纯 JS,保证 Node 20 也能解压 ZIP。 */
993
+ export function crc32(buffer) {
994
+ return typeof zlib.crc32 === 'function' ? zlib.crc32(buffer) >>> 0 : crc32Fallback(buffer)
995
+ }
996
+
997
+ function formatBytes(bytes) {
998
+ if (!bytes) return '未知大小'
999
+ if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`
1000
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`
1001
+ }
1002
+
1003
+ function describeError(error) {
1004
+ const code = error?.cause?.code || error?.code || ''
1005
+ if (code === 'ENOTFOUND' || code === 'EAI_AGAIN') return '无法解析下载地址(域名解析失败)'
1006
+ if (code === 'ECONNREFUSED') return '连接被拒绝'
1007
+ if (code === 'ECONNRESET') return '连接被重置'
1008
+ if (code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT') return '连接超时'
1009
+ if (code.startsWith('CERT_') || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') return 'HTTPS 证书校验失败'
1010
+ if (error?.name === 'TypeError' && /fetch failed/i.test(error.message)) return '网络请求失败'
1011
+ return error?.message || String(error)
1012
+ }
1013
+
1014
+ function emit(onProgress, event) {
1015
+ if (typeof onProgress !== 'function') return
1016
+ try {
1017
+ onProgress(event)
1018
+ } catch {
1019
+ // 进度回调不能影响安装本身
1020
+ }
1021
+ }
1022
+
1023
+ function delay(ms, signal) {
1024
+ return new Promise((resolvePromise, reject) => {
1025
+ const timer = setTimeout(() => {
1026
+ signal?.removeEventListener('abort', onAbort)
1027
+ resolvePromise()
1028
+ }, ms)
1029
+ const onAbort = () => {
1030
+ clearTimeout(timer)
1031
+ reject(abortedError())
1032
+ }
1033
+ signal?.addEventListener('abort', onAbort, { once: true })
1034
+ })
1035
+ }
1036
+
1037
+ function throwIfAborted(signal) {
1038
+ if (signal?.aborted) throw abortedError()
1039
+ }
1040
+
1041
+ function isAbort(error) {
1042
+ return error?.name === 'AbortError' || error?.code === 'FFMPEG_CANCELLED'
1043
+ }
1044
+
1045
+ function abortedError() {
1046
+ const error = new Error('已取消准备 FFmpeg')
1047
+ error.name = 'AbortError'
1048
+ error.code = 'FFMPEG_CANCELLED'
1049
+ return error
1050
+ }
1051
+
1052
+ function ffmpegError(message, code, options) {
1053
+ const error = new Error(message, options)
1054
+ error.code = code
1055
+ return error
1056
+ }