dsh-drawai 0.4.1 → 0.4.3

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 (365) hide show
  1. package/README.md +96 -253
  2. package/lib/client.js +28 -7
  3. package/lib/index.js +474 -97
  4. package/lib/motion/asset-pack.js +109 -50
  5. package/lib/motion/assets.js +291 -291
  6. package/lib/motion/demo-assets-lock.js +10 -0
  7. package/lib/motion/mgstyles/vendor/README.md +28 -0
  8. package/lib/motion/mgstyles/vendor/modules/d3/LICENSE +13 -0
  9. package/lib/motion/mgstyles/vendor/modules/d3/dist/d3.min.js +2 -0
  10. package/lib/motion/mgstyles/vendor/modules/opentype.js/LICENSE +20 -0
  11. package/lib/motion/mgstyles/vendor/modules/opentype.js/dist/opentype.mjs +14460 -0
  12. package/lib/motion/mgstyles/vendor/modules/simplex-noise/LICENSE +21 -0
  13. package/lib/motion/mgstyles/vendor/modules/simplex-noise/dist/esm/simplex-noise.js +466 -0
  14. package/lib/motion/mgstyles/vendor/modules/topojson-client/LICENSE +13 -0
  15. package/lib/motion/mgstyles/vendor/modules/topojson-client/dist/topojson-client.min.js +2 -0
  16. package/lib/motion/preview.js +10 -3
  17. package/lib/motion/render.js +19 -1
  18. package/lib/motion/shotcraft/upstream/SKILL.md +286 -286
  19. package/lib/motion/shotcraft/upstream/assets/lib/Caption.tsx +54 -54
  20. package/lib/motion/shotcraft/upstream/assets/lib/ClipCard.tsx +200 -200
  21. package/lib/motion/shotcraft/upstream/assets/lib/DigitRoll.tsx +45 -45
  22. package/lib/motion/shotcraft/upstream/assets/lib/FlashCut.tsx +20 -20
  23. package/lib/motion/shotcraft/upstream/assets/lib/FlatPanel.tsx +65 -65
  24. package/lib/motion/shotcraft/upstream/assets/lib/PageCam.tsx +151 -151
  25. package/lib/motion/shotcraft/upstream/assets/lib/VerticalTicker.tsx +129 -129
  26. package/lib/motion/shotcraft/upstream/assets/lib/helpers/camera.tsx +75 -75
  27. package/lib/motion/shotcraft/upstream/assets/lib/helpers/motion.ts +40 -40
  28. package/lib/motion/shotcraft/upstream/assets/lib/helpers/rand.ts +11 -11
  29. package/lib/motion/shotcraft/upstream/assets/lib/helpers/shake.ts +10 -10
  30. package/lib/motion/shotcraft/upstream/assets/scripts/build-readme-cover.mjs +49 -49
  31. package/lib/motion/shotcraft/upstream/assets/scripts/capture-template.mjs +210 -210
  32. package/lib/motion/shotcraft/upstream/assets/scripts/sfx-source.py +423 -423
  33. package/lib/motion/shotcraft/upstream/assets/scripts/smoke-render-demos.py +170 -170
  34. package/lib/motion/shotcraft/upstream/assets/scripts/test_sfx_source.py +157 -157
  35. package/lib/motion/shotcraft/upstream/demos/README.md +120 -120
  36. package/lib/motion/shotcraft/upstream/demos/_fixtures/Brand.tsx +112 -112
  37. package/lib/motion/shotcraft/upstream/demos/_fixtures/Fixtures.tsx +647 -647
  38. package/lib/motion/shotcraft/upstream/demos/_fixtures/Look.tsx +316 -316
  39. package/lib/motion/shotcraft/upstream/demos/_fixtures/Motion.tsx +82 -82
  40. package/lib/motion/shotcraft/upstream/demos/_fixtures/PageCam2D.tsx +115 -115
  41. package/lib/motion/shotcraft/upstream/demos/_fixtures/Polish.tsx +243 -243
  42. package/lib/motion/shotcraft/upstream/demos/_textures/live-layout.json +388 -388
  43. package/lib/motion/shotcraft/upstream/demos/camera/basic-3d-scene/Basic3DScene.tsx +250 -250
  44. package/lib/motion/shotcraft/upstream/demos/camera/crash-zoom-punch/CrashImpactReal.tsx +305 -305
  45. package/lib/motion/shotcraft/upstream/demos/camera/crash-zoom-punch/CrashZoomReal.tsx +246 -246
  46. package/lib/motion/shotcraft/upstream/demos/camera/cursor-flyover/CursorFlyover.tsx +390 -390
  47. package/lib/motion/shotcraft/upstream/demos/camera/depth-layer-moves/MultiplaneReal.tsx +138 -138
  48. package/lib/motion/shotcraft/upstream/demos/camera/graze-face-tour/GrazeFaceTour.tsx +542 -542
  49. package/lib/motion/shotcraft/upstream/demos/camera/overhead-camera-moves/OverheadTabletopDrop.tsx +315 -315
  50. package/lib/motion/shotcraft/upstream/demos/camera/overhead-camera-moves/TiltReveal.tsx +299 -299
  51. package/lib/motion/shotcraft/upstream/demos/camera/space-camera-moves/DroneDiveLanding.tsx +173 -173
  52. package/lib/motion/shotcraft/upstream/demos/camera/space-camera-moves/ExplodedView.tsx +348 -348
  53. package/lib/motion/shotcraft/upstream/demos/camera/steep-tilt-glide/SteepTiltGlide.tsx +366 -366
  54. package/lib/motion/shotcraft/upstream/demos/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx +279 -279
  55. package/lib/motion/shotcraft/upstream/demos/camera/tension-camera-moves/PullBackIsolation.tsx +184 -184
  56. package/lib/motion/shotcraft/upstream/demos/camera/terminal-3d/Terminal3D.tsx +390 -390
  57. package/lib/motion/shotcraft/upstream/demos/data/before-after-slider-scrub/BeforeAfterSliderScrub.tsx +155 -155
  58. package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/AxisRescaleShockV2.tsx +303 -303
  59. package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/OscilloscopeStreamV2.tsx +290 -290
  60. package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/UnitDotSwarmRegroupV2.tsx +278 -278
  61. package/lib/motion/shotcraft/upstream/demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx +307 -307
  62. package/lib/motion/shotcraft/upstream/demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx +215 -215
  63. package/lib/motion/shotcraft/upstream/demos/data/scroll-brake-moves/BrakeReticleLock.tsx +299 -299
  64. package/lib/motion/shotcraft/upstream/demos/data/scroll-brake-moves/ChangelogScrollBrake.tsx +232 -232
  65. package/lib/motion/shotcraft/upstream/demos/data/timeline-travel/TimelineTravel.tsx +290 -290
  66. package/lib/motion/shotcraft/upstream/demos/effects/assemble-then-type-flyin/AssembleThenTypeFlyin.tsx +273 -273
  67. package/lib/motion/shotcraft/upstream/demos/effects/aurora-bloom-bg-flip/AuroraBloomBgFlip.tsx +167 -167
  68. package/lib/motion/shotcraft/upstream/demos/effects/brand-frame-snap/BrandFrameSnap.tsx +536 -536
  69. package/lib/motion/shotcraft/upstream/demos/effects/fui-hud-moves/ReticleLockOn.tsx +164 -164
  70. package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/FlylineArc.tsx +175 -175
  71. package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/GlowOrbAmbient.tsx +197 -197
  72. package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/OrbFlylineRelay.tsx +241 -241
  73. package/lib/motion/shotcraft/upstream/demos/effects/icon-performance-moves/AttentionBounce.tsx +279 -279
  74. package/lib/motion/shotcraft/upstream/demos/effects/impact-feedback/AnimeImpact.tsx +230 -230
  75. package/lib/motion/shotcraft/upstream/demos/effects/impact-feedback/HitCounter.tsx +345 -345
  76. package/lib/motion/shotcraft/upstream/demos/effects/light-play-moves/SheenSweepRetry.tsx +275 -275
  77. package/lib/motion/shotcraft/upstream/demos/effects/light-play-moves/SpotlightSweepReveal.tsx +229 -229
  78. package/lib/motion/shotcraft/upstream/demos/effects/radial-ripple-phone-chips/RadialRipplePhoneChips.tsx +312 -312
  79. package/lib/motion/shotcraft/upstream/demos/effects/riso-print-hits/RisoMisregistrationHit.tsx +222 -222
  80. package/lib/motion/shotcraft/upstream/demos/effects/scan-bracket-sweep/ScanBracketSweep.tsx +295 -295
  81. package/lib/motion/shotcraft/upstream/demos/effects/scanline-annotate-focus/ScanlineAnnotateFocus.tsx +329 -329
  82. package/lib/motion/shotcraft/upstream/demos/effects/scanline-assemble-flyin/ScanlineAssembleFlyin.tsx +315 -315
  83. package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/ImpactBurstKit.tsx +264 -264
  84. package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/KanadaPerspectiveSnap.tsx +260 -260
  85. package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/ScoreSlam.tsx +263 -263
  86. package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/CornerSpotlightReveal.tsx +123 -123
  87. package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/GlowWakeSleepPanel.tsx +187 -187
  88. package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/SlideSpotlightPan.tsx +175 -175
  89. package/lib/motion/shotcraft/upstream/demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx +305 -305
  90. package/lib/motion/shotcraft/upstream/demos/interaction/canvas-materialize-moves/DiagramCascadeBuild.tsx +350 -350
  91. package/lib/motion/shotcraft/upstream/demos/interaction/canvas-materialize-moves/PanelToCanvasMaterialize.tsx +352 -352
  92. package/lib/motion/shotcraft/upstream/demos/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.tsx +218 -218
  93. package/lib/motion/shotcraft/upstream/demos/interaction/collab-cursor-moves/CursorCastEnsemble.tsx +297 -297
  94. package/lib/motion/shotcraft/upstream/demos/interaction/glass-pill-dictation-typing/GlassPillDictationTyping.tsx +157 -157
  95. package/lib/motion/shotcraft/upstream/demos/interaction/hashtag-to-pill-materialize/HashtagToPillMaterialize.tsx +218 -218
  96. package/lib/motion/shotcraft/upstream/demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx +202 -202
  97. package/lib/motion/shotcraft/upstream/demos/interaction/segmented-thumb-hero/SegmentedThumbHero.tsx +265 -265
  98. package/lib/motion/shotcraft/upstream/demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx +457 -457
  99. package/lib/motion/shotcraft/upstream/demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx +446 -446
  100. package/lib/motion/shotcraft/upstream/demos/interaction/type-and-filter/TypeAndFilter.tsx +249 -249
  101. package/lib/motion/shotcraft/upstream/demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx +278 -278
  102. package/lib/motion/shotcraft/upstream/demos/opening/crane-rise-reveal/CraneRiseReveal.tsx +332 -332
  103. package/lib/motion/shotcraft/upstream/demos/opening/dataviz-landscape-open/DatavizLandscapeOpen.tsx +358 -358
  104. package/lib/motion/shotcraft/upstream/demos/opening/fracture/Fracture.tsx +201 -201
  105. package/lib/motion/shotcraft/upstream/demos/opening/icon-field-colorize/IconFieldColorize.tsx +200 -200
  106. package/lib/motion/shotcraft/upstream/demos/opening/letterspace-materialize/LetterspaceMaterialize.tsx +208 -208
  107. package/lib/motion/shotcraft/upstream/demos/opening/magician-card-flourish/MagicianCardFlourish.tsx +341 -341
  108. package/lib/motion/shotcraft/upstream/demos/opening/orbit-ring-title-open/OrbitRingTitleOpen.tsx +389 -389
  109. package/lib/motion/shotcraft/upstream/demos/opening/spotlight-hero-card/SpotlightHeroCard.tsx +293 -293
  110. package/lib/motion/shotcraft/upstream/demos/opening/stroke-segment-build/StrokeSegmentBuild.tsx +183 -183
  111. package/lib/motion/shotcraft/upstream/demos/opening/text-as-mask/TextAsMask.tsx +245 -245
  112. package/lib/motion/shotcraft/upstream/demos/outro/grain-dissolve/GrainDissolve.tsx +222 -222
  113. package/lib/motion/shotcraft/upstream/demos/outro/logo-shrink-wordmark-lockup/LogoShrinkWordmarkLockup.tsx +223 -223
  114. package/lib/motion/shotcraft/upstream/demos/outro/neon-triple-marquee/NeonTripleMarquee.tsx +217 -217
  115. package/lib/motion/shotcraft/upstream/demos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx +343 -343
  116. package/lib/motion/shotcraft/upstream/demos/outro/ui-strip-away-outro/UiStripAwayOutro.tsx +357 -357
  117. package/lib/motion/shotcraft/upstream/demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx +187 -187
  118. package/lib/motion/shotcraft/upstream/demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx +333 -333
  119. package/lib/motion/shotcraft/upstream/demos/rhythm/beat-cut-moves/PaparazziFlash.tsx +274 -274
  120. package/lib/motion/shotcraft/upstream/demos/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.tsx +270 -270
  121. package/lib/motion/shotcraft/upstream/demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx +430 -430
  122. package/lib/motion/shotcraft/upstream/demos/rhythm/quad-split-parallel-scenes/QuadSplitParallelScenes.tsx +252 -252
  123. package/lib/motion/shotcraft/upstream/demos/rhythm/rhythm-interrupt-moves/JumpCutPunchIn.tsx +106 -106
  124. package/lib/motion/shotcraft/upstream/demos/rhythm/sakuga-timing-shift/SakugaTimingShift.tsx +288 -288
  125. package/lib/motion/shotcraft/upstream/demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx +362 -362
  126. package/lib/motion/shotcraft/upstream/demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx +168 -168
  127. package/lib/motion/shotcraft/upstream/demos/transition/bottom-push-stack-wipe/BottomPushStackWipe.tsx +310 -310
  128. package/lib/motion/shotcraft/upstream/demos/transition/card-flip-reveal/CardFlipReveal.tsx +261 -261
  129. package/lib/motion/shotcraft/upstream/demos/transition/card-flock-tumble/CardFlockTumble.tsx +459 -459
  130. package/lib/motion/shotcraft/upstream/demos/transition/circle-match-iris/CircleMatchIris.tsx +294 -294
  131. package/lib/motion/shotcraft/upstream/demos/transition/gradient-transition/GradientTransition.tsx +161 -161
  132. package/lib/motion/shotcraft/upstream/demos/transition/line-carry-transition/LineCarryTransition.tsx +248 -248
  133. package/lib/motion/shotcraft/upstream/demos/transition/mosaic-reframe/MosaicReframe.tsx +359 -359
  134. package/lib/motion/shotcraft/upstream/demos/transition/page-turn-transitions/BarnDoorSplit.tsx +227 -227
  135. package/lib/motion/shotcraft/upstream/demos/transition/print-texture-transitions/InkBleedReveal.tsx +162 -162
  136. package/lib/motion/shotcraft/upstream/demos/transition/shot-transitions/PortalWipeV2.tsx +248 -248
  137. package/lib/motion/shotcraft/upstream/demos/transition/shot-transitions/WhipBrakeReal.tsx +186 -186
  138. package/lib/motion/shotcraft/upstream/demos/transition/tear-streak-transitions/GlitchDisplace.tsx +355 -355
  139. package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/InvisibleCut.tsx +272 -272
  140. package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/LightLeakBurn.tsx +262 -262
  141. package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/VersusSlam.tsx +217 -217
  142. package/lib/motion/shotcraft/upstream/demos/transition/transition-travel/LetterformZoom.tsx +285 -285
  143. package/lib/motion/shotcraft/upstream/demos/transition/white-flash-logo-simplify-cut/WhiteFlashLogoSimplifyCut.tsx +205 -205
  144. package/lib/motion/shotcraft/upstream/demos/transition/wipe-transitions/BlindsSlice.tsx +178 -178
  145. package/lib/motion/shotcraft/upstream/demos/typography/blur-slide/BlurSlide.tsx +147 -147
  146. package/lib/motion/shotcraft/upstream/demos/typography/brace-expand/BraceExpand.tsx +143 -143
  147. package/lib/motion/shotcraft/upstream/demos/typography/brand-ink-open/BrandInkOpen.tsx +134 -134
  148. package/lib/motion/shotcraft/upstream/demos/typography/countdown-arc-scatter/CountdownArcScatter.tsx +217 -217
  149. package/lib/motion/shotcraft/upstream/demos/typography/flying-words/FlyingWords.tsx +198 -198
  150. package/lib/motion/shotcraft/upstream/demos/typography/glitch-cycle/GlitchCycle.tsx +221 -221
  151. package/lib/motion/shotcraft/upstream/demos/typography/gradient-word-sweep/GradientWordSweep.tsx +312 -312
  152. package/lib/motion/shotcraft/upstream/demos/typography/lead-word-zoom-assemble/LeadWordZoomAssemble.tsx +261 -261
  153. package/lib/motion/shotcraft/upstream/demos/typography/marker-underline-title/MarkerUnderlineTitle.tsx +121 -121
  154. package/lib/motion/shotcraft/upstream/demos/typography/outline-word-fill/OutlineWordFill.tsx +153 -153
  155. package/lib/motion/shotcraft/upstream/demos/typography/paper-title-card/PaperTitleCard.tsx +179 -179
  156. package/lib/motion/shotcraft/upstream/demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx +140 -140
  157. package/lib/motion/shotcraft/upstream/demos/typography/pill-slot-cycle/PillSlotCycle.tsx +309 -309
  158. package/lib/motion/shotcraft/upstream/demos/typography/text-column-converge/TextColumnConverge.tsx +191 -191
  159. package/lib/motion/shotcraft/upstream/demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx +292 -292
  160. package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx +135 -135
  161. package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/SplitTextStagger.tsx +124 -124
  162. package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/TextOnPath.tsx +255 -255
  163. package/lib/motion/shotcraft/upstream/demos/typography/type-entrance-moves/LetterDropPhysics.tsx +173 -173
  164. package/lib/motion/shotcraft/upstream/demos/typography/type-rhythm-sync/FontWeightPump.tsx +225 -225
  165. package/lib/motion/shotcraft/upstream/demos/typography/type-rhythm-sync/KaraokeFillSync.tsx +141 -141
  166. package/lib/motion/shotcraft/upstream/demos/typography/typewriter-moves/TypewriterErrorRetype.tsx +137 -137
  167. package/lib/motion/shotcraft/upstream/demos/typography/vertical-word-roll-blur-cycle/VerticalWordRollBlurCycle.tsx +147 -147
  168. package/lib/motion/shotcraft/upstream/demos/typography/word-relay-filmstrip/WordRelayFilmstrip.tsx +409 -409
  169. package/lib/motion/shotcraft/upstream/demos/typography/word-relay-geometry/WordRelayGeometry.tsx +280 -280
  170. package/lib/motion/shotcraft/upstream/demos/ui-entrance/avatar-bracket-carousel/AvatarBracketCarousel.tsx +271 -271
  171. package/lib/motion/shotcraft/upstream/demos/ui-entrance/bezier-source-converge-merge/BezierSourceConvergeMerge.tsx +385 -385
  172. package/lib/motion/shotcraft/upstream/demos/ui-entrance/card-stack/CardStack.tsx +312 -312
  173. package/lib/motion/shotcraft/upstream/demos/ui-entrance/carousel-3d/Carousel3D.tsx +303 -303
  174. package/lib/motion/shotcraft/upstream/demos/ui-entrance/clipcard-looping/ClipCardLooping.tsx +77 -77
  175. package/lib/motion/shotcraft/upstream/demos/ui-entrance/cloner-depth-echo/ClonerDepthEcho.tsx +311 -311
  176. package/lib/motion/shotcraft/upstream/demos/ui-entrance/deck-deal-flyin/DeckDealFlyin.tsx +307 -307
  177. package/lib/motion/shotcraft/upstream/demos/ui-entrance/doc-park-left-pill-deal/DocParkLeftPillDeal.tsx +349 -349
  178. package/lib/motion/shotcraft/upstream/demos/ui-entrance/document-typewriter-reveal/DocumentTypewriterReveal.tsx +255 -255
  179. package/lib/motion/shotcraft/upstream/demos/ui-entrance/draw-svg-trace/DrawSvgTrace.tsx +244 -244
  180. package/lib/motion/shotcraft/upstream/demos/ui-entrance/element-body-moves/ContactShadowLift.tsx +217 -217
  181. package/lib/motion/shotcraft/upstream/demos/ui-entrance/integration-hub-map/IntegrationHubMap.tsx +652 -652
  182. package/lib/motion/shotcraft/upstream/demos/ui-entrance/list-reveal/ListReveal.tsx +153 -153
  183. package/lib/motion/shotcraft/upstream/demos/ui-entrance/list-stack-press/ListStackPress.tsx +238 -238
  184. package/lib/motion/shotcraft/upstream/demos/ui-entrance/neon-frame-forerun/NeonFrameForerun.tsx +324 -324
  185. package/lib/motion/shotcraft/upstream/demos/ui-entrance/neon-frame-orbit-drop/NeonFrameForerunOrbit.tsx +354 -354
  186. package/lib/motion/shotcraft/upstream/demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx +101 -101
  187. package/lib/motion/shotcraft/upstream/demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx +129 -129
  188. package/lib/motion/shotcraft/upstream/demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.tsx +297 -297
  189. package/lib/motion/shotcraft/upstream/demos/ui-entrance/radial-wave/RadialWave.tsx +57 -57
  190. package/lib/motion/shotcraft/upstream/demos/ui-entrance/research-card-stack-scroll/ResearchCardStackScroll.tsx +155 -155
  191. package/lib/motion/shotcraft/upstream/demos/ui-entrance/row-embed/RowEmbed.tsx +175 -175
  192. package/lib/motion/shotcraft/upstream/demos/ui-entrance/value-stagger-gradient/ValueStaggerGradient.tsx +153 -153
  193. package/lib/motion/shotcraft/upstream/demos/ui-entrance/wall-reveal-moves/BentoLightUp.tsx +146 -146
  194. package/lib/motion/shotcraft/upstream/demos/ui-entrance/wall-reveal-moves/WireframeDrawOn.tsx +226 -226
  195. package/lib/motion/shotcraft/upstream/gallery/api/library.json +1 -1
  196. package/lib/motion/shotcraft/upstream/references/aesthetic-rules.md +193 -193
  197. package/lib/motion/shotcraft/upstream/references/final-review.md +121 -121
  198. package/lib/motion/shotcraft/upstream/references/guided-free-creation.md +199 -199
  199. package/lib/motion/shotcraft/upstream/references/jianying-export.md +183 -183
  200. package/lib/motion/shotcraft/upstream/references/music-beat-sync.md +257 -257
  201. package/lib/motion/shotcraft/upstream/references/pipeline.md +382 -382
  202. package/lib/motion/shotcraft/upstream/references/publish-upload-post.md +111 -111
  203. package/lib/motion/shotcraft/upstream/references/sequences/promo-energy-arc.md +50 -50
  204. package/lib/motion/shotcraft/upstream/references/sfx-sourcing.md +95 -95
  205. package/lib/motion/shotcraft/upstream/references/shots/camera/basic-3d-scene.md +41 -41
  206. package/lib/motion/shotcraft/upstream/references/shots/camera/crash-zoom-punch.md +52 -52
  207. package/lib/motion/shotcraft/upstream/references/shots/camera/cursor-flyover.md +39 -39
  208. package/lib/motion/shotcraft/upstream/references/shots/camera/depth-layer-moves.md +39 -39
  209. package/lib/motion/shotcraft/upstream/references/shots/camera/graze-face-tour.md +57 -57
  210. package/lib/motion/shotcraft/upstream/references/shots/camera/overhead-camera-moves.md +51 -51
  211. package/lib/motion/shotcraft/upstream/references/shots/camera/space-camera-moves.md +46 -46
  212. package/lib/motion/shotcraft/upstream/references/shots/camera/steep-tilt-glide.md +52 -52
  213. package/lib/motion/shotcraft/upstream/references/shots/camera/tension-camera-moves.md +43 -43
  214. package/lib/motion/shotcraft/upstream/references/shots/camera/terminal-3d.md +42 -42
  215. package/lib/motion/shotcraft/upstream/references/shots/data/before-after-slider-scrub.md +48 -48
  216. package/lib/motion/shotcraft/upstream/references/shots/data/chart-live-moves.md +52 -52
  217. package/lib/motion/shotcraft/upstream/references/shots/data/gauge-readout-moves.md +45 -45
  218. package/lib/motion/shotcraft/upstream/references/shots/data/scroll-brake-moves.md +46 -46
  219. package/lib/motion/shotcraft/upstream/references/shots/data/timeline-travel.md +44 -44
  220. package/lib/motion/shotcraft/upstream/references/shots/effects/assemble-then-type-flyin.md +63 -63
  221. package/lib/motion/shotcraft/upstream/references/shots/effects/aurora-bloom-bg-flip.md +54 -54
  222. package/lib/motion/shotcraft/upstream/references/shots/effects/brand-frame-snap.md +58 -58
  223. package/lib/motion/shotcraft/upstream/references/shots/effects/fui-hud-moves.md +35 -35
  224. package/lib/motion/shotcraft/upstream/references/shots/effects/glow-flyline-moves.md +53 -53
  225. package/lib/motion/shotcraft/upstream/references/shots/effects/icon-performance-moves.md +42 -42
  226. package/lib/motion/shotcraft/upstream/references/shots/effects/impact-feedback.md +50 -50
  227. package/lib/motion/shotcraft/upstream/references/shots/effects/light-play-moves.md +46 -46
  228. package/lib/motion/shotcraft/upstream/references/shots/effects/radial-ripple-phone-chips.md +56 -56
  229. package/lib/motion/shotcraft/upstream/references/shots/effects/riso-print-hits.md +49 -49
  230. package/lib/motion/shotcraft/upstream/references/shots/effects/scan-bracket-sweep.md +50 -50
  231. package/lib/motion/shotcraft/upstream/references/shots/effects/scanline-annotate-focus.md +53 -53
  232. package/lib/motion/shotcraft/upstream/references/shots/effects/scanline-assemble-flyin.md +53 -53
  233. package/lib/motion/shotcraft/upstream/references/shots/effects/slam-entrance-moves.md +55 -55
  234. package/lib/motion/shotcraft/upstream/references/shots/effects/spotlight-sweep-moves.md +52 -52
  235. package/lib/motion/shotcraft/upstream/references/shots/interaction/autolayout-gap-dial.md +60 -60
  236. package/lib/motion/shotcraft/upstream/references/shots/interaction/canvas-materialize-moves.md +57 -57
  237. package/lib/motion/shotcraft/upstream/references/shots/interaction/chip-grid-single-select-blackout.md +51 -51
  238. package/lib/motion/shotcraft/upstream/references/shots/interaction/collab-cursor-moves.md +41 -41
  239. package/lib/motion/shotcraft/upstream/references/shots/interaction/glass-pill-dictation-typing.md +53 -53
  240. package/lib/motion/shotcraft/upstream/references/shots/interaction/hashtag-to-pill-materialize.md +60 -60
  241. package/lib/motion/shotcraft/upstream/references/shots/interaction/picker-carousel-feature-cycle.md +55 -55
  242. package/lib/motion/shotcraft/upstream/references/shots/interaction/segmented-thumb-hero.md +57 -57
  243. package/lib/motion/shotcraft/upstream/references/shots/interaction/theme-switch-moves.md +47 -47
  244. package/lib/motion/shotcraft/upstream/references/shots/interaction/type-and-filter.md +37 -37
  245. package/lib/motion/shotcraft/upstream/references/shots/interaction/voice-waveform-live.md +54 -54
  246. package/lib/motion/shotcraft/upstream/references/shots/opening/brand-ink-open.md +36 -36
  247. package/lib/motion/shotcraft/upstream/references/shots/opening/crane-rise-reveal.md +56 -56
  248. package/lib/motion/shotcraft/upstream/references/shots/opening/dataviz-landscape-open.md +56 -56
  249. package/lib/motion/shotcraft/upstream/references/shots/opening/fracture.md +40 -40
  250. package/lib/motion/shotcraft/upstream/references/shots/opening/icon-field-colorize.md +60 -60
  251. package/lib/motion/shotcraft/upstream/references/shots/opening/letterspace-materialize.md +57 -57
  252. package/lib/motion/shotcraft/upstream/references/shots/opening/magician-card-flourish.md +73 -73
  253. package/lib/motion/shotcraft/upstream/references/shots/opening/orbit-ring-title-open.md +87 -87
  254. package/lib/motion/shotcraft/upstream/references/shots/opening/spotlight-hero-card.md +55 -55
  255. package/lib/motion/shotcraft/upstream/references/shots/opening/stroke-segment-build.md +50 -50
  256. package/lib/motion/shotcraft/upstream/references/shots/opening/text-as-mask.md +59 -59
  257. package/lib/motion/shotcraft/upstream/references/shots/outro/grain-dissolve.md +38 -38
  258. package/lib/motion/shotcraft/upstream/references/shots/outro/logo-shrink-wordmark-lockup.md +38 -38
  259. package/lib/motion/shotcraft/upstream/references/shots/outro/neon-triple-marquee.md +59 -59
  260. package/lib/motion/shotcraft/upstream/references/shots/outro/outro-group-photo-launch.md +39 -39
  261. package/lib/motion/shotcraft/upstream/references/shots/outro/ui-strip-away-outro.md +61 -61
  262. package/lib/motion/shotcraft/upstream/references/shots/outro/ui-to-brand-morph.md +52 -52
  263. package/lib/motion/shotcraft/upstream/references/shots/rhythm/beat-cut-moves.md +50 -50
  264. package/lib/motion/shotcraft/upstream/references/shots/rhythm/beat-step-list-theme-cycle.md +66 -66
  265. package/lib/motion/shotcraft/upstream/references/shots/rhythm/panel-grid-moves.md +43 -43
  266. package/lib/motion/shotcraft/upstream/references/shots/rhythm/quad-split-parallel-scenes.md +38 -38
  267. package/lib/motion/shotcraft/upstream/references/shots/rhythm/rhythm-interrupt-moves.md +38 -38
  268. package/lib/motion/shotcraft/upstream/references/shots/rhythm/sakuga-timing-shift.md +56 -56
  269. package/lib/motion/shotcraft/upstream/references/shots/rhythm/speed-ramp-freeze.md +44 -44
  270. package/lib/motion/shotcraft/upstream/references/shots/rhythm/trailer-grammar-moves.md +38 -38
  271. package/lib/motion/shotcraft/upstream/references/shots/transition/bottom-push-stack-wipe.md +63 -63
  272. package/lib/motion/shotcraft/upstream/references/shots/transition/card-flip-reveal.md +57 -57
  273. package/lib/motion/shotcraft/upstream/references/shots/transition/card-flock-tumble.md +63 -63
  274. package/lib/motion/shotcraft/upstream/references/shots/transition/circle-match-iris.md +58 -58
  275. package/lib/motion/shotcraft/upstream/references/shots/transition/gradient-transition.md +45 -45
  276. package/lib/motion/shotcraft/upstream/references/shots/transition/line-carry-transition.md +55 -55
  277. package/lib/motion/shotcraft/upstream/references/shots/transition/mosaic-reframe.md +49 -49
  278. package/lib/motion/shotcraft/upstream/references/shots/transition/page-turn-transitions.md +42 -42
  279. package/lib/motion/shotcraft/upstream/references/shots/transition/print-texture-transitions.md +42 -42
  280. package/lib/motion/shotcraft/upstream/references/shots/transition/shot-transitions.md +45 -45
  281. package/lib/motion/shotcraft/upstream/references/shots/transition/tear-streak-transitions.md +48 -48
  282. package/lib/motion/shotcraft/upstream/references/shots/transition/transition-hidden-cut.md +55 -55
  283. package/lib/motion/shotcraft/upstream/references/shots/transition/transition-travel.md +43 -43
  284. package/lib/motion/shotcraft/upstream/references/shots/transition/white-flash-logo-simplify-cut.md +41 -41
  285. package/lib/motion/shotcraft/upstream/references/shots/transition/wipe-transitions.md +42 -42
  286. package/lib/motion/shotcraft/upstream/references/shots/typography/blur-slide.md +59 -59
  287. package/lib/motion/shotcraft/upstream/references/shots/typography/brace-expand.md +55 -55
  288. package/lib/motion/shotcraft/upstream/references/shots/typography/countdown-arc-scatter.md +65 -65
  289. package/lib/motion/shotcraft/upstream/references/shots/typography/document-typewriter-reveal.md +42 -42
  290. package/lib/motion/shotcraft/upstream/references/shots/typography/flying-words.md +64 -64
  291. package/lib/motion/shotcraft/upstream/references/shots/typography/glitch-cycle.md +64 -64
  292. package/lib/motion/shotcraft/upstream/references/shots/typography/gradient-word-sweep.md +54 -54
  293. package/lib/motion/shotcraft/upstream/references/shots/typography/lead-word-zoom-assemble.md +91 -91
  294. package/lib/motion/shotcraft/upstream/references/shots/typography/marker-underline-title.md +51 -51
  295. package/lib/motion/shotcraft/upstream/references/shots/typography/outline-word-fill.md +66 -66
  296. package/lib/motion/shotcraft/upstream/references/shots/typography/paper-title-card.md +38 -38
  297. package/lib/motion/shotcraft/upstream/references/shots/typography/pill-chip-slot-cycle-handled.md +66 -66
  298. package/lib/motion/shotcraft/upstream/references/shots/typography/pill-slot-cycle.md +58 -58
  299. package/lib/motion/shotcraft/upstream/references/shots/typography/text-column-converge.md +61 -61
  300. package/lib/motion/shotcraft/upstream/references/shots/typography/title-demote-to-label.md +64 -64
  301. package/lib/motion/shotcraft/upstream/references/shots/typography/type-assembly-moves.md +45 -45
  302. package/lib/motion/shotcraft/upstream/references/shots/typography/type-entrance-moves.md +41 -41
  303. package/lib/motion/shotcraft/upstream/references/shots/typography/type-rhythm-sync.md +51 -51
  304. package/lib/motion/shotcraft/upstream/references/shots/typography/typewriter-moves.md +42 -42
  305. package/lib/motion/shotcraft/upstream/references/shots/typography/vertical-word-roll-blur-cycle.md +63 -63
  306. package/lib/motion/shotcraft/upstream/references/shots/typography/word-relay-filmstrip.md +57 -57
  307. package/lib/motion/shotcraft/upstream/references/shots/typography/word-relay-geometry.md +75 -75
  308. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/avatar-bracket-carousel.md +59 -59
  309. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/bezier-source-converge-merge.md +67 -67
  310. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/card-stack.md +60 -60
  311. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/carousel-3d.md +66 -66
  312. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/cloner-depth-echo.md +40 -40
  313. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/deck-deal-flyin.md +45 -45
  314. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/doc-park-left-pill-deal.md +62 -62
  315. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/draw-svg-trace.md +55 -55
  316. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/element-body-moves.md +37 -37
  317. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/integration-hub-map.md +55 -55
  318. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/list-reveal.md +50 -50
  319. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/list-stack-press.md +43 -43
  320. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/neon-frame-forerun.md +55 -55
  321. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/neon-frame-orbit-drop.md +51 -51
  322. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/page-waterfall-wall.md +51 -51
  323. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/product-card-progressive-assemble.md +56 -56
  324. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/radial-wave.md +49 -49
  325. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/research-card-stack-scroll.md +57 -57
  326. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/row-embed.md +36 -36
  327. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/value-stagger-gradient.md +64 -64
  328. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/wall-reveal-moves.md +47 -47
  329. package/lib/motion/shotcraft/upstream/references/sound-design.md +170 -170
  330. package/lib/motion/shotcraft/upstream/references/workbench.md +157 -157
  331. package/lib/motion/shotcraft/upstream/template/TEMPLATE.md +103 -103
  332. package/lib/motion/shotcraft/upstream/template/THEMES.md +44 -44
  333. package/lib/motion/shotcraft/upstream/template/package.json +20 -20
  334. package/lib/motion/shotcraft/upstream/template/remotion.config.ts +6 -6
  335. package/lib/motion/shotcraft/upstream/template/src/Root.tsx +17 -17
  336. package/lib/motion/shotcraft/upstream/template/src/aifl/Caption.tsx +65 -65
  337. package/lib/motion/shotcraft/upstream/template/src/aifl/DigitRoll.tsx +48 -48
  338. package/lib/motion/shotcraft/upstream/template/src/aifl/FlashCut.tsx +24 -24
  339. package/lib/motion/shotcraft/upstream/template/src/aifl/Main.tsx +192 -192
  340. package/lib/motion/shotcraft/upstream/template/src/aifl/PaperTitleCard.tsx +111 -111
  341. package/lib/motion/shotcraft/upstream/template/src/aifl/live/PageCam.tsx +150 -150
  342. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneDetail.tsx +158 -158
  343. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneFlyIn.tsx +468 -468
  344. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneOpen.tsx +552 -552
  345. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneOutroLive.tsx +411 -411
  346. package/lib/motion/shotcraft/upstream/template/src/aifl/live/ScenePapers.tsx +200 -200
  347. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneWbr.tsx +324 -324
  348. package/lib/motion/shotcraft/upstream/template/src/aifl/live-layout.json +388 -388
  349. package/lib/motion/shotcraft/upstream/template/src/index.ts +4 -4
  350. package/lib/motion/shotcraft/upstream/template/src/themes/ThemedFilm.tsx +7 -7
  351. package/lib/motion/shotcraft/upstream/template/src/themes/manifest.tsx +68 -68
  352. package/lib/motion/shotcraft/upstream/template/src/themes/palette-assets.json +1 -1
  353. package/lib/motion/shotcraft/upstream/template/src/themes/palette-assets.ts +24 -24
  354. package/lib/motion/shotcraft/upstream/template/src/themes/palettes.json +74 -74
  355. package/lib/motion/shotcraft/upstream/template/src/themes/visual-theme.tsx +91 -91
  356. package/lib/motion/shotcraft/upstream/template/src/workbench.ts +213 -213
  357. package/lib/motion/shotcraft/upstream/template/tsconfig.json +15 -15
  358. package/lib/motion/style-frames.js +47 -12
  359. package/lib/motion-project.js +22 -3
  360. package/lib/motion-render-worker.js +31 -9
  361. package/lib/routing/router.js +12 -1
  362. package/lib/style-kernel.js +16 -6
  363. package/lib/video-trace-pool.js +128 -128
  364. package/package.json +1 -1
  365. package/LICENSE +0 -4175
@@ -1,423 +1,423 @@
1
- #!/usr/bin/env python3
2
- """Find Freesound SFX and freeze licensed MP3 previews in a video project.
3
-
4
- Python standard library; get/analyze also require ffmpeg and ffprobe.
5
- No API key, global cache, login, or proxy configuration changes.
6
- """
7
-
8
- import argparse
9
- import array
10
- from concurrent.futures import ThreadPoolExecutor
11
- from datetime import datetime, timezone
12
- import hashlib
13
- from html.parser import HTMLParser
14
- import json
15
- import math
16
- from pathlib import Path
17
- import re
18
- import shutil
19
- import subprocess
20
- import sys
21
- import tempfile
22
- from urllib.parse import unquote, urlencode, urljoin, urlparse
23
- from urllib.request import Request, urlopen
24
-
25
-
26
- SITE = "https://freesound.org"
27
- MANIFEST = "audio-manifest.json"
28
- CREDITS = "FREESOUND-CREDITS.md"
29
- MAX_BYTES = 32 * 1024 * 1024
30
- VOID = set("area base br col embed hr img input link meta param source track wbr".split())
31
-
32
-
33
- class Node:
34
- def __init__(self, tag="root", attrs=()):
35
- self.tag, self.attrs, self.children = tag, dict(attrs), []
36
-
37
- def nodes(self):
38
- yield self
39
- for child in self.children:
40
- if isinstance(child, Node):
41
- yield from child.nodes()
42
-
43
- def text(self):
44
- return " ".join(c.text() if isinstance(c, Node) else c for c in self.children).strip()
45
-
46
- def has_class(self, name):
47
- return name in self.attrs.get("class", "").split()
48
-
49
-
50
- class Document(HTMLParser):
51
- def __init__(self, content):
52
- super().__init__(convert_charrefs=True)
53
- self.root = Node()
54
- self.stack = [self.root]
55
- self.feed(content)
56
- self.close()
57
-
58
- def handle_starttag(self, tag, attrs):
59
- node = Node(tag, attrs)
60
- self.stack[-1].children.append(node)
61
- if tag not in VOID:
62
- self.stack.append(node)
63
-
64
- def handle_startendtag(self, tag, attrs):
65
- self.handle_starttag(tag, attrs)
66
- if tag not in VOID:
67
- self.handle_endtag(tag)
68
-
69
- def handle_endtag(self, tag):
70
- for i in range(len(self.stack) - 1, 0, -1):
71
- if self.stack[i].tag == tag:
72
- del self.stack[i:]
73
- break
74
-
75
- def handle_data(self, data):
76
- self.stack[-1].children.append(data)
77
-
78
-
79
- def request(url):
80
- req = Request(url, headers={"User-Agent": "video-shotcraft/1.0 (Freesound SFX sourcing)"})
81
- with urlopen(req, timeout=30) as response:
82
- data = response.read(MAX_BYTES + 1)
83
- if len(data) > MAX_BYTES:
84
- raise ValueError("Response exceeds 32 MiB; select a shorter sound or download the original manually")
85
- return data, response.geturl(), response.headers.get_content_type()
86
-
87
-
88
- def web_page(url):
89
- data, final_url, content_type = request(url)
90
- if urlparse(final_url).hostname != "freesound.org" or content_type != "text/html":
91
- raise ValueError("Expected a Freesound HTML page")
92
- return Document(data.decode("utf-8")), final_url
93
-
94
-
95
- def sound_id(value):
96
- value = value.removeprefix("fs:")
97
- if not re.fullmatch(r"[1-9][0-9]*", value):
98
- raise ValueError(f"Invalid Freesound ID: {value!r}; use fs:12345")
99
- return value
100
-
101
-
102
- def license_code(url):
103
- parsed = urlparse(url)
104
- if parsed.scheme not in {"http", "https"} or parsed.hostname not in {"creativecommons.org", "www.creativecommons.org"}:
105
- return None
106
- path = parsed.path.rstrip("/")
107
- if path == "/publicdomain/zero/1.0":
108
- return "CC0-1.0"
109
- match = re.fullmatch(r"/licenses/by/(1\.0|2\.0|2\.5|3\.0|4\.0)", path)
110
- return f"CC-BY-{match[1]}" if match else None
111
-
112
-
113
- def check_license(code, policy):
114
- if policy == "any":
115
- return
116
- if code == "CC0-1.0" or (policy == "by" and code and code.startswith("CC-BY-")):
117
- return
118
- raise ValueError(f"License {code or 'unknown'} is outside --license {policy}; CC BY requires --license by")
119
-
120
-
121
- def player_data(node, page_author=None):
122
- attrs = node.attrs
123
- sid = sound_id(attrs["data-sound-id"])
124
- title, author = attrs.get("data-title"), page_author or attrs.get("data-username")
125
- duration = float(attrs["data-duration"])
126
- if not title or not author or not math.isfinite(duration) or duration <= 0:
127
- raise ValueError(f"Incomplete Freesound metadata for fs:{sid}")
128
- preview = attrs.get("data-mp3", "")
129
- parsed = urlparse(preview)
130
- if (parsed.scheme != "https" or parsed.hostname != "cdn.freesound.org"
131
- or not re.fullmatch(rf"/previews/[0-9]+/{sid}_[0-9]+-(lq|hq)\.mp3", parsed.path)):
132
- raise ValueError(f"Missing or unexpected preview URL for fs:{sid}")
133
- return {"id": f"fs:{sid}", "title": title, "author": author,
134
- "duration_seconds": duration, "preview_url": preview.replace("-lq.mp3", "-hq.mp3"),
135
- "downloads": int(attrs.get("data-num-downloads") or 0)}
136
-
137
-
138
- def stats(nodes):
139
- rating, count = None, None
140
- for node in nodes:
141
- label = node.attrs.get("aria-label", "")
142
- if label.startswith("Average rating of "):
143
- rating = float(label.removeprefix("Average rating of "))
144
- if node.has_class("bw-rating__count"):
145
- found = re.search(r"\(([0-9]+)(?: ratings?)?\)", node.text())
146
- if found:
147
- count = int(found[1])
148
- return {"rating": rating, "rating_count": count}
149
-
150
-
151
- def parse_search(doc):
152
- rows = []
153
- for block in doc.root.nodes():
154
- if not block.has_class("bw-search__result"):
155
- continue
156
- nodes = list(block.nodes())
157
- player = next((n for n in nodes if n.attrs.get("data-sound-id")), None)
158
- if player is None:
159
- raise ValueError("Freesound search markup changed: result has no sound metadata")
160
- row = player_data(player)
161
- links = [urljoin(SITE, n.attrs.get("href", "")) for n in nodes if n.tag == "a"]
162
- row["page_url"] = next((u for u in links if u.endswith(f"/sounds/{sound_id(row['id'])}/")), None)
163
- row["license_label"] = next((n.attrs["title"][9:] for n in nodes
164
- if n.attrs.get("title", "").startswith("License: ")), "unknown")
165
- desc = next((n for n in nodes if any(c.startswith("max-h-60px")
166
- for c in n.attrs.get("class", "").split())), None)
167
- row["description"] = " ".join((desc.attrs.get("title") or desc.text()).split()) if desc else ""
168
- row.update(stats(nodes))
169
- rows.append(row)
170
- if not rows and not re.search(r"no (?:sounds|results)|0 sounds|couldn't find|did not match", doc.root.text(), re.I):
171
- raise ValueError("No recognizable search results; check Freesound availability or update the parser")
172
- return rows
173
-
174
-
175
- def search(args):
176
- if args.license == "any":
177
- labels = None
178
- filters = []
179
- else:
180
- labels = {"Creative Commons 0"} | ({"Attribution"} if args.license == "by" else set())
181
- filters = ['license:("Creative Commons 0" OR "Attribution")' if args.license == "by"
182
- else 'license:"Creative Commons 0"']
183
- filters.append(f"duration:[{args.min_duration} TO {args.max_duration if args.max_duration is not None else '*'}]")
184
- found = {}
185
- for page in range(1, args.pages + 1):
186
- query = urlencode({"q": args.query, "f": " ".join(filters), "page": page, "s": "Automatic by relevance"})
187
- doc, _ = web_page(f"{SITE}/search/?{query}")
188
- for row in parse_search(doc):
189
- # Enforce locally too; never trust a server-side filter alone.
190
- if labels is not None and row["license_label"] not in labels:
191
- continue
192
- if row["duration_seconds"] < args.min_duration:
193
- continue
194
- if args.max_duration is not None and row["duration_seconds"] > args.max_duration:
195
- continue
196
- found.setdefault(row["id"], row)
197
- result = {"provider": "freesound", "query": args.query, "license_policy": args.license,
198
- "ordering": "relevance; popularity is supporting evidence only",
199
- "results": list(found.values())[:args.limit]}
200
- emit(result, args.output)
201
- return 0
202
-
203
-
204
- def detail(sid):
205
- doc, final_url = web_page(f"{SITE}/s/{sid}/")
206
- nodes = list(doc.root.nodes())
207
- player = next((n for n in nodes if n.attrs.get("data-sound-id") == sid and n.attrs.get("data-mp3")), None)
208
- canonical = re.fullmatch(rf"/people/([^/]+)/sounds/{sid}/", urlparse(final_url).path)
209
- if player is None or canonical is None:
210
- raise ValueError(f"Missing canonical sound page for fs:{sid}")
211
- # Large players omit data-username; the canonical page supplies the creator.
212
- row = player_data(player, unquote(canonical[1]))
213
- row["page_url"] = final_url
214
- licenses = {n.attrs["href"] for n in nodes if n.tag == "a"
215
- and urlparse(n.attrs.get("href", "")).hostname in {"creativecommons.org", "www.creativecommons.org"}}
216
- if len(licenses) != 1:
217
- raise ValueError(f"Missing or ambiguous license on fs:{sid}")
218
- row["license_url"] = licenses.pop()
219
- row["license"] = license_code(row["license_url"])
220
- row["description"] = next((n.attrs.get("content", "") for n in nodes
221
- if n.attrs.get("name") == "twitter:description"), "")
222
- row.update(stats(nodes))
223
- return row
224
-
225
-
226
- def require_audio_tools():
227
- if not all(shutil.which(name) for name in ("ffmpeg", "ffprobe")):
228
- raise ValueError("get/analyze require ffmpeg and ffprobe on PATH")
229
-
230
-
231
- def analyze(path):
232
- require_audio_tools()
233
- path = Path(path).resolve()
234
- probe = subprocess.run(["ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries",
235
- "stream=codec_name,sample_rate,channels:format=duration", "-of", "json", str(path)],
236
- capture_output=True, text=True, check=True)
237
- info = json.loads(probe.stdout)
238
- stream = info["streams"][0]
239
- channels = int(stream["channels"])
240
- if not 1 <= channels <= 8:
241
- raise ValueError("Expected an audio file with 1–8 channels")
242
- rate, window = 48000, 0.01
243
- block_size = int(rate * window) * channels * 4
244
- energies, peak, squares, samples = [], 0.0, 0.0, 0
245
- # Preserve channels: downmixing first can hide opposite-polarity peaks.
246
- with tempfile.TemporaryFile() as errors:
247
- with subprocess.Popen(["ffmpeg", "-v", "error", "-i", str(path), "-map", "0:a:0", "-ar", str(rate),
248
- "-f", "f32le", "-acodec", "pcm_f32le", "pipe:1"],
249
- stdout=subprocess.PIPE, stderr=errors) as process:
250
- while True:
251
- raw = process.stdout.read(block_size)
252
- if not raw:
253
- break
254
- values = array.array("f")
255
- values.frombytes(raw)
256
- if sys.byteorder != "little":
257
- values.byteswap()
258
- energy = sum(v * v for v in values)
259
- energies.append(math.sqrt(energy / len(values)))
260
- peak = max(peak, max(abs(v) for v in values))
261
- squares += energy
262
- samples += len(values)
263
- if process.wait() != 0:
264
- errors.seek(0)
265
- raise ValueError(errors.read().decode("utf-8", errors="replace"))
266
- if not samples or not peak:
267
- raise ValueError("Audio is empty or silent")
268
- loudest = max(energies)
269
- active = [i for i, e in enumerate(energies) if e >= loudest * 0.01]
270
- duration = samples / (rate * channels)
271
- return {"codec": stream["codec_name"], "sample_rate": int(stream["sample_rate"]), "channels": channels,
272
- "duration_seconds": round(duration, 6), "sample_peak_dbfs": round(20 * math.log10(peak), 3),
273
- "rms_dbfs": round(20 * math.log10(math.sqrt(squares / samples)), 3),
274
- "energy_window_seconds": window,
275
- "energy_peak_seconds": round(min(duration, (energies.index(loudest) + 0.5) * window), 6),
276
- "active_start_seconds": round(active[0] * window, 6),
277
- "active_end_seconds": round(min(duration, (active[-1] + 1) * window), 6),
278
- "activity_threshold_db_below_peak_rms": -40}
279
-
280
-
281
- def write_json(path, data):
282
- path = Path(path)
283
- path.parent.mkdir(parents=True, exist_ok=True)
284
- with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent, delete=False) as tmp:
285
- json.dump(data, tmp, ensure_ascii=False, indent=2, allow_nan=False)
286
- tmp.write("\n")
287
- Path(tmp.name).replace(path)
288
-
289
-
290
- def emit(data, output=None):
291
- if output:
292
- write_json(output, data)
293
- print(json.dumps(data, ensure_ascii=False, indent=2, allow_nan=False))
294
-
295
-
296
- def digest(path):
297
- return hashlib.sha256(Path(path).read_bytes()).hexdigest()
298
-
299
-
300
- def write_credits(out, rows):
301
- # Plain text inside a fence keeps uploaded titles from becoming Markdown links/instructions.
302
- lines = ["# Freesound audio credits", "", "Generated from audio-manifest.json; preserve with the video.", "",
303
- "CC BY entries require attribution in the published video's credits/description.", "",
304
- "Downloaded files are lossy MP3 previews, not uploader originals.", ""]
305
- for row in rows:
306
- lines += ["~~~text", f"{row['file']} — {row['title']} by {row['author']}".replace("~", "\\~"),
307
- row["page_url"], f"{row['license']}: {row['license_url']}",
308
- "Edits: " + ("; ".join(row.get("changes", [])) or "None recorded; update if edited."), "~~~", ""]
309
- (out / CREDITS).write_text("\n".join(lines), encoding="utf-8")
310
-
311
-
312
- def download_one(sid, out, policy, existing):
313
- row = detail(sid) # Refresh the actual license, even when search/cache metadata is stale.
314
- check_license(row["license"], policy)
315
- dst = out / f"fs-{sid}.mp3"
316
- old = next((r for r in existing if r["id"] == row["id"]), None)
317
- if dst.exists():
318
- if old is None or old.get("file") != dst.name or digest(dst) != old.get("sha256"):
319
- raise ValueError(f"Refusing to overwrite untracked/modified file: {dst}")
320
- return {**old, **row, "analysis": analyze(dst), "license_checked_at": datetime.now(timezone.utc).isoformat()}
321
- payload, final_url, mime = request(row["preview_url"])
322
- if urlparse(final_url).hostname != "cdn.freesound.org" or mime not in {"audio/mpeg", "audio/mp3", "application/octet-stream"}:
323
- raise ValueError("Expected a Freesound MP3 preview, not an HTML error page")
324
- with tempfile.NamedTemporaryFile(dir=out, suffix=".mp3", delete=False) as tmp:
325
- tmp.write(payload)
326
- tmp_path = Path(tmp.name)
327
- try:
328
- measurements = analyze(tmp_path) # Decode successfully before admitting the file.
329
- if measurements["codec"] != "mp3":
330
- raise ValueError("Preview is not MP3 audio")
331
- row.update({"provider": "freesound", "file": dst.name, "sha256": digest(tmp_path),
332
- "download_url": final_url, "download_kind": "mp3-preview-hq", "is_preview": True,
333
- "downloaded_at": datetime.now(timezone.utc).isoformat(),
334
- "license_checked_at": datetime.now(timezone.utc).isoformat(), "changes": [], "analysis": measurements})
335
- # Exclusive creation: another process/user's file is never replaced.
336
- with dst.open("xb") as target:
337
- target.write(payload)
338
- return row
339
- finally:
340
- tmp_path.unlink(missing_ok=True)
341
-
342
-
343
- def get(args):
344
- require_audio_tools()
345
- ids = list(dict.fromkeys(sound_id(value) for value in args.ids))
346
- out = Path(args.out_dir).resolve()
347
- out.mkdir(parents=True, exist_ok=True)
348
- lock = out / ".freesound-download.lock"
349
- try:
350
- handle = lock.open("x")
351
- except FileExistsError:
352
- raise ValueError(f"Download already active in {out}; remove {lock.name} only if a previous run crashed")
353
- try:
354
- with handle:
355
- return get_locked(args, ids, out)
356
- finally:
357
- lock.unlink(missing_ok=True)
358
-
359
-
360
- def get_locked(args, ids, out):
361
- manifest_path = out / MANIFEST
362
- manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if manifest_path.exists() else {"version": 1, "assets": []}
363
- if manifest.get("version") != 1 or not isinstance(manifest.get("assets"), list):
364
- raise ValueError("Unsupported audio manifest; use a separate output directory")
365
- existing = manifest["assets"]
366
-
367
- def run(sid):
368
- try:
369
- return {"ok": True, "asset": download_one(sid, out, args.license, existing)}
370
- except (OSError, ValueError, KeyError, IndexError, subprocess.SubprocessError) as error:
371
- return {"ok": False, "id": f"fs:{sid}", "error": str(error)}
372
-
373
- results = []
374
- with ThreadPoolExecutor(max_workers=args.workers) as pool:
375
- for result in pool.map(run, ids):
376
- results.append(result)
377
- if result["ok"]:
378
- row = result["asset"]
379
- manifest["assets"] = [r for r in manifest["assets"] if r["id"] != row["id"]] + [row]
380
- write_json(manifest_path, manifest)
381
- write_credits(out, manifest["assets"])
382
- emit({"out_dir": str(out), "results": results})
383
- return 0 if all(r["ok"] for r in results) else 1
384
-
385
-
386
- def main(argv=None):
387
- parser = argparse.ArgumentParser(description=__doc__)
388
- commands = parser.add_subparsers(dest="command", required=True)
389
- find = commands.add_parser("search", help="Search public Freesound pages; emit JSON candidates")
390
- find.add_argument("query")
391
- find.add_argument("--license", choices=("cc0", "by", "any"), default="any", help="any does not filter by license; by permits CC0 + CC BY")
392
- find.add_argument("--min-duration", type=float, default=0)
393
- find.add_argument("--max-duration", type=float)
394
- find.add_argument("--limit", type=int, default=8)
395
- find.add_argument("--pages", type=int, choices=range(1, 4), default=1)
396
- find.add_argument("--output", help="Optional project-local JSON path")
397
- fetch = commands.add_parser("get", help="Download IDs, verify licenses/audio, save manifest and credits")
398
- fetch.add_argument("ids", nargs="+")
399
- fetch.add_argument("--out-dir", required=True, help="Video project's public/audio/sfx directory")
400
- fetch.add_argument("--license", choices=("cc0", "by", "any"), default="any")
401
- fetch.add_argument("--workers", type=int, choices=range(1, 5), default=3)
402
- measure = commands.add_parser("analyze", help="Measure decoded audio (10 ms RMS peak; not LUFS/true peak)")
403
- measure.add_argument("file")
404
- measure.add_argument("--output")
405
- args = parser.parse_args(argv)
406
- try:
407
- if args.command == "search":
408
- if not 1 <= args.limit <= 45 or not math.isfinite(args.min_duration) or args.min_duration < 0:
409
- raise ValueError("Use --limit 1–45 and a nonnegative finite --min-duration")
410
- if args.max_duration is not None and (not math.isfinite(args.max_duration) or args.max_duration <= args.min_duration):
411
- raise ValueError("--max-duration must be finite and greater than --min-duration")
412
- return search(args)
413
- if args.command == "get":
414
- return get(args)
415
- emit(analyze(args.file), args.output)
416
- return 0
417
- except (OSError, ValueError, KeyError, IndexError, subprocess.SubprocessError) as error:
418
- print(f"sfx-source: {error}", file=sys.stderr)
419
- return 1
420
-
421
-
422
- if __name__ == "__main__":
423
- sys.exit(main())
1
+ #!/usr/bin/env python3
2
+ """Find Freesound SFX and freeze licensed MP3 previews in a video project.
3
+
4
+ Python standard library; get/analyze also require ffmpeg and ffprobe.
5
+ No API key, global cache, login, or proxy configuration changes.
6
+ """
7
+
8
+ import argparse
9
+ import array
10
+ from concurrent.futures import ThreadPoolExecutor
11
+ from datetime import datetime, timezone
12
+ import hashlib
13
+ from html.parser import HTMLParser
14
+ import json
15
+ import math
16
+ from pathlib import Path
17
+ import re
18
+ import shutil
19
+ import subprocess
20
+ import sys
21
+ import tempfile
22
+ from urllib.parse import unquote, urlencode, urljoin, urlparse
23
+ from urllib.request import Request, urlopen
24
+
25
+
26
+ SITE = "https://freesound.org"
27
+ MANIFEST = "audio-manifest.json"
28
+ CREDITS = "FREESOUND-CREDITS.md"
29
+ MAX_BYTES = 32 * 1024 * 1024
30
+ VOID = set("area base br col embed hr img input link meta param source track wbr".split())
31
+
32
+
33
+ class Node:
34
+ def __init__(self, tag="root", attrs=()):
35
+ self.tag, self.attrs, self.children = tag, dict(attrs), []
36
+
37
+ def nodes(self):
38
+ yield self
39
+ for child in self.children:
40
+ if isinstance(child, Node):
41
+ yield from child.nodes()
42
+
43
+ def text(self):
44
+ return " ".join(c.text() if isinstance(c, Node) else c for c in self.children).strip()
45
+
46
+ def has_class(self, name):
47
+ return name in self.attrs.get("class", "").split()
48
+
49
+
50
+ class Document(HTMLParser):
51
+ def __init__(self, content):
52
+ super().__init__(convert_charrefs=True)
53
+ self.root = Node()
54
+ self.stack = [self.root]
55
+ self.feed(content)
56
+ self.close()
57
+
58
+ def handle_starttag(self, tag, attrs):
59
+ node = Node(tag, attrs)
60
+ self.stack[-1].children.append(node)
61
+ if tag not in VOID:
62
+ self.stack.append(node)
63
+
64
+ def handle_startendtag(self, tag, attrs):
65
+ self.handle_starttag(tag, attrs)
66
+ if tag not in VOID:
67
+ self.handle_endtag(tag)
68
+
69
+ def handle_endtag(self, tag):
70
+ for i in range(len(self.stack) - 1, 0, -1):
71
+ if self.stack[i].tag == tag:
72
+ del self.stack[i:]
73
+ break
74
+
75
+ def handle_data(self, data):
76
+ self.stack[-1].children.append(data)
77
+
78
+
79
+ def request(url):
80
+ req = Request(url, headers={"User-Agent": "video-shotcraft/1.0 (Freesound SFX sourcing)"})
81
+ with urlopen(req, timeout=30) as response:
82
+ data = response.read(MAX_BYTES + 1)
83
+ if len(data) > MAX_BYTES:
84
+ raise ValueError("Response exceeds 32 MiB; select a shorter sound or download the original manually")
85
+ return data, response.geturl(), response.headers.get_content_type()
86
+
87
+
88
+ def web_page(url):
89
+ data, final_url, content_type = request(url)
90
+ if urlparse(final_url).hostname != "freesound.org" or content_type != "text/html":
91
+ raise ValueError("Expected a Freesound HTML page")
92
+ return Document(data.decode("utf-8")), final_url
93
+
94
+
95
+ def sound_id(value):
96
+ value = value.removeprefix("fs:")
97
+ if not re.fullmatch(r"[1-9][0-9]*", value):
98
+ raise ValueError(f"Invalid Freesound ID: {value!r}; use fs:12345")
99
+ return value
100
+
101
+
102
+ def license_code(url):
103
+ parsed = urlparse(url)
104
+ if parsed.scheme not in {"http", "https"} or parsed.hostname not in {"creativecommons.org", "www.creativecommons.org"}:
105
+ return None
106
+ path = parsed.path.rstrip("/")
107
+ if path == "/publicdomain/zero/1.0":
108
+ return "CC0-1.0"
109
+ match = re.fullmatch(r"/licenses/by/(1\.0|2\.0|2\.5|3\.0|4\.0)", path)
110
+ return f"CC-BY-{match[1]}" if match else None
111
+
112
+
113
+ def check_license(code, policy):
114
+ if policy == "any":
115
+ return
116
+ if code == "CC0-1.0" or (policy == "by" and code and code.startswith("CC-BY-")):
117
+ return
118
+ raise ValueError(f"License {code or 'unknown'} is outside --license {policy}; CC BY requires --license by")
119
+
120
+
121
+ def player_data(node, page_author=None):
122
+ attrs = node.attrs
123
+ sid = sound_id(attrs["data-sound-id"])
124
+ title, author = attrs.get("data-title"), page_author or attrs.get("data-username")
125
+ duration = float(attrs["data-duration"])
126
+ if not title or not author or not math.isfinite(duration) or duration <= 0:
127
+ raise ValueError(f"Incomplete Freesound metadata for fs:{sid}")
128
+ preview = attrs.get("data-mp3", "")
129
+ parsed = urlparse(preview)
130
+ if (parsed.scheme != "https" or parsed.hostname != "cdn.freesound.org"
131
+ or not re.fullmatch(rf"/previews/[0-9]+/{sid}_[0-9]+-(lq|hq)\.mp3", parsed.path)):
132
+ raise ValueError(f"Missing or unexpected preview URL for fs:{sid}")
133
+ return {"id": f"fs:{sid}", "title": title, "author": author,
134
+ "duration_seconds": duration, "preview_url": preview.replace("-lq.mp3", "-hq.mp3"),
135
+ "downloads": int(attrs.get("data-num-downloads") or 0)}
136
+
137
+
138
+ def stats(nodes):
139
+ rating, count = None, None
140
+ for node in nodes:
141
+ label = node.attrs.get("aria-label", "")
142
+ if label.startswith("Average rating of "):
143
+ rating = float(label.removeprefix("Average rating of "))
144
+ if node.has_class("bw-rating__count"):
145
+ found = re.search(r"\(([0-9]+)(?: ratings?)?\)", node.text())
146
+ if found:
147
+ count = int(found[1])
148
+ return {"rating": rating, "rating_count": count}
149
+
150
+
151
+ def parse_search(doc):
152
+ rows = []
153
+ for block in doc.root.nodes():
154
+ if not block.has_class("bw-search__result"):
155
+ continue
156
+ nodes = list(block.nodes())
157
+ player = next((n for n in nodes if n.attrs.get("data-sound-id")), None)
158
+ if player is None:
159
+ raise ValueError("Freesound search markup changed: result has no sound metadata")
160
+ row = player_data(player)
161
+ links = [urljoin(SITE, n.attrs.get("href", "")) for n in nodes if n.tag == "a"]
162
+ row["page_url"] = next((u for u in links if u.endswith(f"/sounds/{sound_id(row['id'])}/")), None)
163
+ row["license_label"] = next((n.attrs["title"][9:] for n in nodes
164
+ if n.attrs.get("title", "").startswith("License: ")), "unknown")
165
+ desc = next((n for n in nodes if any(c.startswith("max-h-60px")
166
+ for c in n.attrs.get("class", "").split())), None)
167
+ row["description"] = " ".join((desc.attrs.get("title") or desc.text()).split()) if desc else ""
168
+ row.update(stats(nodes))
169
+ rows.append(row)
170
+ if not rows and not re.search(r"no (?:sounds|results)|0 sounds|couldn't find|did not match", doc.root.text(), re.I):
171
+ raise ValueError("No recognizable search results; check Freesound availability or update the parser")
172
+ return rows
173
+
174
+
175
+ def search(args):
176
+ if args.license == "any":
177
+ labels = None
178
+ filters = []
179
+ else:
180
+ labels = {"Creative Commons 0"} | ({"Attribution"} if args.license == "by" else set())
181
+ filters = ['license:("Creative Commons 0" OR "Attribution")' if args.license == "by"
182
+ else 'license:"Creative Commons 0"']
183
+ filters.append(f"duration:[{args.min_duration} TO {args.max_duration if args.max_duration is not None else '*'}]")
184
+ found = {}
185
+ for page in range(1, args.pages + 1):
186
+ query = urlencode({"q": args.query, "f": " ".join(filters), "page": page, "s": "Automatic by relevance"})
187
+ doc, _ = web_page(f"{SITE}/search/?{query}")
188
+ for row in parse_search(doc):
189
+ # Enforce locally too; never trust a server-side filter alone.
190
+ if labels is not None and row["license_label"] not in labels:
191
+ continue
192
+ if row["duration_seconds"] < args.min_duration:
193
+ continue
194
+ if args.max_duration is not None and row["duration_seconds"] > args.max_duration:
195
+ continue
196
+ found.setdefault(row["id"], row)
197
+ result = {"provider": "freesound", "query": args.query, "license_policy": args.license,
198
+ "ordering": "relevance; popularity is supporting evidence only",
199
+ "results": list(found.values())[:args.limit]}
200
+ emit(result, args.output)
201
+ return 0
202
+
203
+
204
+ def detail(sid):
205
+ doc, final_url = web_page(f"{SITE}/s/{sid}/")
206
+ nodes = list(doc.root.nodes())
207
+ player = next((n for n in nodes if n.attrs.get("data-sound-id") == sid and n.attrs.get("data-mp3")), None)
208
+ canonical = re.fullmatch(rf"/people/([^/]+)/sounds/{sid}/", urlparse(final_url).path)
209
+ if player is None or canonical is None:
210
+ raise ValueError(f"Missing canonical sound page for fs:{sid}")
211
+ # Large players omit data-username; the canonical page supplies the creator.
212
+ row = player_data(player, unquote(canonical[1]))
213
+ row["page_url"] = final_url
214
+ licenses = {n.attrs["href"] for n in nodes if n.tag == "a"
215
+ and urlparse(n.attrs.get("href", "")).hostname in {"creativecommons.org", "www.creativecommons.org"}}
216
+ if len(licenses) != 1:
217
+ raise ValueError(f"Missing or ambiguous license on fs:{sid}")
218
+ row["license_url"] = licenses.pop()
219
+ row["license"] = license_code(row["license_url"])
220
+ row["description"] = next((n.attrs.get("content", "") for n in nodes
221
+ if n.attrs.get("name") == "twitter:description"), "")
222
+ row.update(stats(nodes))
223
+ return row
224
+
225
+
226
+ def require_audio_tools():
227
+ if not all(shutil.which(name) for name in ("ffmpeg", "ffprobe")):
228
+ raise ValueError("get/analyze require ffmpeg and ffprobe on PATH")
229
+
230
+
231
+ def analyze(path):
232
+ require_audio_tools()
233
+ path = Path(path).resolve()
234
+ probe = subprocess.run(["ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries",
235
+ "stream=codec_name,sample_rate,channels:format=duration", "-of", "json", str(path)],
236
+ capture_output=True, text=True, check=True)
237
+ info = json.loads(probe.stdout)
238
+ stream = info["streams"][0]
239
+ channels = int(stream["channels"])
240
+ if not 1 <= channels <= 8:
241
+ raise ValueError("Expected an audio file with 1–8 channels")
242
+ rate, window = 48000, 0.01
243
+ block_size = int(rate * window) * channels * 4
244
+ energies, peak, squares, samples = [], 0.0, 0.0, 0
245
+ # Preserve channels: downmixing first can hide opposite-polarity peaks.
246
+ with tempfile.TemporaryFile() as errors:
247
+ with subprocess.Popen(["ffmpeg", "-v", "error", "-i", str(path), "-map", "0:a:0", "-ar", str(rate),
248
+ "-f", "f32le", "-acodec", "pcm_f32le", "pipe:1"],
249
+ stdout=subprocess.PIPE, stderr=errors) as process:
250
+ while True:
251
+ raw = process.stdout.read(block_size)
252
+ if not raw:
253
+ break
254
+ values = array.array("f")
255
+ values.frombytes(raw)
256
+ if sys.byteorder != "little":
257
+ values.byteswap()
258
+ energy = sum(v * v for v in values)
259
+ energies.append(math.sqrt(energy / len(values)))
260
+ peak = max(peak, max(abs(v) for v in values))
261
+ squares += energy
262
+ samples += len(values)
263
+ if process.wait() != 0:
264
+ errors.seek(0)
265
+ raise ValueError(errors.read().decode("utf-8", errors="replace"))
266
+ if not samples or not peak:
267
+ raise ValueError("Audio is empty or silent")
268
+ loudest = max(energies)
269
+ active = [i for i, e in enumerate(energies) if e >= loudest * 0.01]
270
+ duration = samples / (rate * channels)
271
+ return {"codec": stream["codec_name"], "sample_rate": int(stream["sample_rate"]), "channels": channels,
272
+ "duration_seconds": round(duration, 6), "sample_peak_dbfs": round(20 * math.log10(peak), 3),
273
+ "rms_dbfs": round(20 * math.log10(math.sqrt(squares / samples)), 3),
274
+ "energy_window_seconds": window,
275
+ "energy_peak_seconds": round(min(duration, (energies.index(loudest) + 0.5) * window), 6),
276
+ "active_start_seconds": round(active[0] * window, 6),
277
+ "active_end_seconds": round(min(duration, (active[-1] + 1) * window), 6),
278
+ "activity_threshold_db_below_peak_rms": -40}
279
+
280
+
281
+ def write_json(path, data):
282
+ path = Path(path)
283
+ path.parent.mkdir(parents=True, exist_ok=True)
284
+ with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent, delete=False) as tmp:
285
+ json.dump(data, tmp, ensure_ascii=False, indent=2, allow_nan=False)
286
+ tmp.write("\n")
287
+ Path(tmp.name).replace(path)
288
+
289
+
290
+ def emit(data, output=None):
291
+ if output:
292
+ write_json(output, data)
293
+ print(json.dumps(data, ensure_ascii=False, indent=2, allow_nan=False))
294
+
295
+
296
+ def digest(path):
297
+ return hashlib.sha256(Path(path).read_bytes()).hexdigest()
298
+
299
+
300
+ def write_credits(out, rows):
301
+ # Plain text inside a fence keeps uploaded titles from becoming Markdown links/instructions.
302
+ lines = ["# Freesound audio credits", "", "Generated from audio-manifest.json; preserve with the video.", "",
303
+ "CC BY entries require attribution in the published video's credits/description.", "",
304
+ "Downloaded files are lossy MP3 previews, not uploader originals.", ""]
305
+ for row in rows:
306
+ lines += ["~~~text", f"{row['file']} — {row['title']} by {row['author']}".replace("~", "\\~"),
307
+ row["page_url"], f"{row['license']}: {row['license_url']}",
308
+ "Edits: " + ("; ".join(row.get("changes", [])) or "None recorded; update if edited."), "~~~", ""]
309
+ (out / CREDITS).write_text("\n".join(lines), encoding="utf-8")
310
+
311
+
312
+ def download_one(sid, out, policy, existing):
313
+ row = detail(sid) # Refresh the actual license, even when search/cache metadata is stale.
314
+ check_license(row["license"], policy)
315
+ dst = out / f"fs-{sid}.mp3"
316
+ old = next((r for r in existing if r["id"] == row["id"]), None)
317
+ if dst.exists():
318
+ if old is None or old.get("file") != dst.name or digest(dst) != old.get("sha256"):
319
+ raise ValueError(f"Refusing to overwrite untracked/modified file: {dst}")
320
+ return {**old, **row, "analysis": analyze(dst), "license_checked_at": datetime.now(timezone.utc).isoformat()}
321
+ payload, final_url, mime = request(row["preview_url"])
322
+ if urlparse(final_url).hostname != "cdn.freesound.org" or mime not in {"audio/mpeg", "audio/mp3", "application/octet-stream"}:
323
+ raise ValueError("Expected a Freesound MP3 preview, not an HTML error page")
324
+ with tempfile.NamedTemporaryFile(dir=out, suffix=".mp3", delete=False) as tmp:
325
+ tmp.write(payload)
326
+ tmp_path = Path(tmp.name)
327
+ try:
328
+ measurements = analyze(tmp_path) # Decode successfully before admitting the file.
329
+ if measurements["codec"] != "mp3":
330
+ raise ValueError("Preview is not MP3 audio")
331
+ row.update({"provider": "freesound", "file": dst.name, "sha256": digest(tmp_path),
332
+ "download_url": final_url, "download_kind": "mp3-preview-hq", "is_preview": True,
333
+ "downloaded_at": datetime.now(timezone.utc).isoformat(),
334
+ "license_checked_at": datetime.now(timezone.utc).isoformat(), "changes": [], "analysis": measurements})
335
+ # Exclusive creation: another process/user's file is never replaced.
336
+ with dst.open("xb") as target:
337
+ target.write(payload)
338
+ return row
339
+ finally:
340
+ tmp_path.unlink(missing_ok=True)
341
+
342
+
343
+ def get(args):
344
+ require_audio_tools()
345
+ ids = list(dict.fromkeys(sound_id(value) for value in args.ids))
346
+ out = Path(args.out_dir).resolve()
347
+ out.mkdir(parents=True, exist_ok=True)
348
+ lock = out / ".freesound-download.lock"
349
+ try:
350
+ handle = lock.open("x")
351
+ except FileExistsError:
352
+ raise ValueError(f"Download already active in {out}; remove {lock.name} only if a previous run crashed")
353
+ try:
354
+ with handle:
355
+ return get_locked(args, ids, out)
356
+ finally:
357
+ lock.unlink(missing_ok=True)
358
+
359
+
360
+ def get_locked(args, ids, out):
361
+ manifest_path = out / MANIFEST
362
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if manifest_path.exists() else {"version": 1, "assets": []}
363
+ if manifest.get("version") != 1 or not isinstance(manifest.get("assets"), list):
364
+ raise ValueError("Unsupported audio manifest; use a separate output directory")
365
+ existing = manifest["assets"]
366
+
367
+ def run(sid):
368
+ try:
369
+ return {"ok": True, "asset": download_one(sid, out, args.license, existing)}
370
+ except (OSError, ValueError, KeyError, IndexError, subprocess.SubprocessError) as error:
371
+ return {"ok": False, "id": f"fs:{sid}", "error": str(error)}
372
+
373
+ results = []
374
+ with ThreadPoolExecutor(max_workers=args.workers) as pool:
375
+ for result in pool.map(run, ids):
376
+ results.append(result)
377
+ if result["ok"]:
378
+ row = result["asset"]
379
+ manifest["assets"] = [r for r in manifest["assets"] if r["id"] != row["id"]] + [row]
380
+ write_json(manifest_path, manifest)
381
+ write_credits(out, manifest["assets"])
382
+ emit({"out_dir": str(out), "results": results})
383
+ return 0 if all(r["ok"] for r in results) else 1
384
+
385
+
386
+ def main(argv=None):
387
+ parser = argparse.ArgumentParser(description=__doc__)
388
+ commands = parser.add_subparsers(dest="command", required=True)
389
+ find = commands.add_parser("search", help="Search public Freesound pages; emit JSON candidates")
390
+ find.add_argument("query")
391
+ find.add_argument("--license", choices=("cc0", "by", "any"), default="any", help="any does not filter by license; by permits CC0 + CC BY")
392
+ find.add_argument("--min-duration", type=float, default=0)
393
+ find.add_argument("--max-duration", type=float)
394
+ find.add_argument("--limit", type=int, default=8)
395
+ find.add_argument("--pages", type=int, choices=range(1, 4), default=1)
396
+ find.add_argument("--output", help="Optional project-local JSON path")
397
+ fetch = commands.add_parser("get", help="Download IDs, verify licenses/audio, save manifest and credits")
398
+ fetch.add_argument("ids", nargs="+")
399
+ fetch.add_argument("--out-dir", required=True, help="Video project's public/audio/sfx directory")
400
+ fetch.add_argument("--license", choices=("cc0", "by", "any"), default="any")
401
+ fetch.add_argument("--workers", type=int, choices=range(1, 5), default=3)
402
+ measure = commands.add_parser("analyze", help="Measure decoded audio (10 ms RMS peak; not LUFS/true peak)")
403
+ measure.add_argument("file")
404
+ measure.add_argument("--output")
405
+ args = parser.parse_args(argv)
406
+ try:
407
+ if args.command == "search":
408
+ if not 1 <= args.limit <= 45 or not math.isfinite(args.min_duration) or args.min_duration < 0:
409
+ raise ValueError("Use --limit 1–45 and a nonnegative finite --min-duration")
410
+ if args.max_duration is not None and (not math.isfinite(args.max_duration) or args.max_duration <= args.min_duration):
411
+ raise ValueError("--max-duration must be finite and greater than --min-duration")
412
+ return search(args)
413
+ if args.command == "get":
414
+ return get(args)
415
+ emit(analyze(args.file), args.output)
416
+ return 0
417
+ except (OSError, ValueError, KeyError, IndexError, subprocess.SubprocessError) as error:
418
+ print(f"sfx-source: {error}", file=sys.stderr)
419
+ return 1
420
+
421
+
422
+ if __name__ == "__main__":
423
+ sys.exit(main())