luckiest-co 1.0.32 → 1.0.34

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 (478) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/commands/plan.md +1 -0
  4. package/package.json +1 -1
  5. package/skills/luckiest-plan/SKILL.md +1 -0
  6. package/skills/luckiest-research/CHANGELOG.md +5 -0
  7. package/skills/luckiest-research/SKILL.md +8 -4
  8. package/skills/luckiest-video-studio/ATTRIBUTION.md +11 -0
  9. package/skills/luckiest-video-studio/CHANGELOG.md +56 -0
  10. package/skills/luckiest-video-studio/SKILL.md +35 -7
  11. package/skills/luckiest-video-studio/references/audio.md +61 -10
  12. package/skills/luckiest-video-studio/references/lessons.md +44 -0
  13. package/skills/luckiest-video-studio/references/step-2-plan.md +32 -1
  14. package/skills/luckiest-video-studio/references/step-3-compose.md +28 -0
  15. package/skills/luckiest-video-studio/references/step-4-deliver.md +24 -1
  16. package/skills/luckiest-video-studio/references/storyboard.md +24 -3
  17. package/skills/luckiest-video-studio/scripts/qa.mjs +167 -5
  18. package/skills/luckiest-video-studio/scripts/qa.test.mjs +99 -1
  19. package/skills/luckiest-video-studio/scripts/render-scenes.mjs +196 -71
  20. package/skills/luckiest-video-studio/scripts/render-scenes.test.mjs +109 -0
  21. package/skills/luckiest-video-studio/scripts/sfx-cues.mjs +116 -0
  22. package/skills/luckiest-video-studio/scripts/sfx-cues.test.mjs +76 -0
  23. package/skills/luckiest-video-studio/scripts/stamp-format.mjs +26 -0
  24. package/skills/luckiest-video-studio/scripts/storyboard.schema.json +23 -0
  25. package/skills/luckiest-video-studio/templates/logo-line-draw.html +12 -5
  26. package/skills/luckiest-video-studio-art-director/ATTRIBUTION.md +1 -0
  27. package/skills/luckiest-video-studio-art-director/CHANGELOG.md +8 -0
  28. package/skills/luckiest-video-studio-art-director/SKILL.md +18 -5
  29. package/skills/luckiest-video-studio-cards/CHANGELOG.md +18 -0
  30. package/skills/luckiest-video-studio-cards/SKILL.md +22 -7
  31. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-agenda.html +17 -6
  32. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-bigtype.html +15 -5
  33. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-board.html +20 -6
  34. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-checklist.html +27 -6
  35. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-compare.html +40 -9
  36. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-deck.html +26 -6
  37. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-dossier.html +28 -9
  38. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-filmstrip.html +36 -5
  39. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-grid6.html +24 -6
  40. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-indexset.html +34 -6
  41. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-maplegend.html +39 -8
  42. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-matrix.html +35 -7
  43. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-numlist.html +26 -7
  44. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-outline.html +26 -6
  45. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-phases.html +34 -7
  46. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-quad.html +41 -10
  47. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-roadmap.html +66 -17
  48. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-stepper.html +42 -7
  49. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-toc.html +26 -8
  50. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-vtimeline.html +35 -9
  51. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-bracket.html +16 -4
  52. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-circle.html +21 -5
  53. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-classified.html +25 -7
  54. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-collage.html +27 -9
  55. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-correction.html +19 -7
  56. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-crt.html +15 -4
  57. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-cutout.html +18 -8
  58. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-fullbleed.html +18 -5
  59. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-highlight.html +15 -5
  60. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-indexcard.html +18 -5
  61. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-map.html +20 -8
  62. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-markerbox.html +12 -5
  63. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-numeral.html +21 -7
  64. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-split.html +25 -9
  65. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-stacked.html +26 -10
  66. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-stamp.html +14 -5
  67. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-ticker.html +26 -9
  68. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-tornstrip.html +13 -4
  69. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-annotated.html +29 -9
  70. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-arc.html +25 -7
  71. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-axis.html +34 -14
  72. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-bar.html +17 -8
  73. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-bracket.html +24 -8
  74. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-calendar.html +24 -9
  75. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-callout.html +24 -5
  76. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-columns.html +27 -9
  77. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-corner.html +22 -9
  78. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-dots.html +27 -8
  79. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-engraving.html +30 -8
  80. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-footnote.html +20 -8
  81. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-frac.html +24 -8
  82. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-heat.html +22 -8
  83. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-isotype.html +29 -8
  84. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-ledger.html +18 -6
  85. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-left.html +27 -10
  86. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-magnifier.html +21 -8
  87. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-map.html +21 -9
  88. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-meter.html +26 -9
  89. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-multiplier.html +27 -12
  90. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-odo.html +18 -5
  91. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-pack.html +41 -17
  92. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-photo.html +18 -7
  93. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-pie.html +25 -8
  94. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-postage.html +24 -8
  95. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-pricetag.html +25 -11
  96. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-quote.html +21 -8
  97. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-ranking.html +25 -10
  98. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-receipt.html +24 -12
  99. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-ribbon.html +34 -12
  100. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-semi.html +22 -7
  101. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-spotlight.html +23 -9
  102. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-stamp.html +24 -7
  103. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-steps.html +30 -9
  104. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-tape.html +33 -12
  105. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-torn.html +22 -9
  106. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-trend.html +23 -9
  107. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-vs.html +30 -10
  108. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-window.html +38 -25
  109. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat.html +23 -4
  110. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-timeline.html +41 -6
  111. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-label.html +10 -3
  112. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-bracket.html +9 -2
  113. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-caption.html +12 -5
  114. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-circle.html +10 -3
  115. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-definition.html +10 -3
  116. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-footnote.html +11 -4
  117. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-magnify.html +9 -2
  118. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-pin.html +9 -2
  119. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-pullquote.html +10 -3
  120. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-spotlight.html +15 -2
  121. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-sticky.html +9 -2
  122. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-tag.html +10 -3
  123. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-underline.html +9 -2
  124. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lower-third.html +9 -2
  125. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-badge.html +10 -3
  126. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-barslab.html +26 -7
  127. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-bracket.html +9 -2
  128. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-clapper.html +9 -2
  129. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-highlight.html +9 -2
  130. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-indextab.html +10 -3
  131. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-rail.html +9 -2
  132. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-ribbon.html +9 -2
  133. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-stacked.html +9 -2
  134. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-stamp.html +9 -2
  135. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-sticky.html +9 -2
  136. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-typecard.html +9 -2
  137. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/style.json +1 -1
  138. package/skills/luckiest-video-studio-cards/packs/01-paper-collage/tokens.css +18 -8
  139. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-agenda.html +23 -5
  140. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-beforeafter.html +30 -6
  141. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-bento.html +34 -8
  142. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-bigleft.html +28 -6
  143. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-callouts.html +36 -7
  144. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-cardrow5.html +17 -4
  145. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-checklist.html +11 -3
  146. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-cycle.html +30 -6
  147. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-dashboard.html +32 -5
  148. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-deck2.html +21 -5
  149. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-deck3.html +18 -4
  150. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-duallist.html +24 -4
  151. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-featurecompare.html +19 -5
  152. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-featuregrid.html +25 -4
  153. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-flashcards.html +22 -5
  154. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-flow.html +23 -4
  155. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-funnel.html +19 -5
  156. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-gantt.html +22 -6
  157. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-grid6.html +17 -4
  158. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-grid9.html +15 -4
  159. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-iconlist.html +18 -3
  160. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-journey.html +41 -6
  161. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-kpirow.html +24 -6
  162. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-layers.html +20 -5
  163. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-legend.html +19 -4
  164. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-levels.html +19 -5
  165. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-logowall.html +18 -6
  166. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-mappoints.html +18 -5
  167. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-matrix.html +24 -6
  168. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-milestones.html +38 -5
  169. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-mindmap.html +35 -12
  170. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-nested.html +13 -5
  171. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-network.html +76 -31
  172. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-notifs.html +21 -3
  173. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-numlist.html +16 -4
  174. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-orgchart.html +36 -16
  175. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-outline.html +17 -3
  176. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-phases.html +23 -5
  177. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-pipeline.html +25 -4
  178. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-pricing.html +36 -8
  179. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-profiles.html +20 -5
  180. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-proscons.html +21 -6
  181. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-pyramid.html +17 -4
  182. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-quad.html +13 -5
  183. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-quotegrid.html +19 -5
  184. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-ranked.html +19 -5
  185. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-roadmap.html +27 -5
  186. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-spectrum.html +29 -6
  187. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-split3.html +24 -5
  188. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-spotlightcard.html +34 -8
  189. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-statgrid.html +17 -5
  190. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-steps.html +25 -4
  191. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-summary.html +22 -5
  192. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-tiercompare.html +29 -6
  193. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-timeline.html +26 -5
  194. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-toc.html +15 -5
  195. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-tree.html +40 -15
  196. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-tricards.html +22 -4
  197. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-venn.html +34 -9
  198. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-vstable.html +24 -5
  199. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-ascend.html +10 -4
  200. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-aurora-bars.html +12 -4
  201. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-beam.html +11 -4
  202. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-boxed.html +26 -13
  203. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-breath.html +14 -3
  204. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-chaptercard.html +13 -4
  205. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-column-light.html +17 -9
  206. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-constellation.html +15 -6
  207. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-countdown.html +10 -4
  208. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-curtain.html +9 -3
  209. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-dawn.html +13 -4
  210. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-depth.html +18 -12
  211. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-divider.html +10 -3
  212. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-drift-grid.html +11 -4
  213. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-duotone.html +16 -4
  214. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-eclipse.html +11 -4
  215. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-emblem.html +12 -5
  216. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-flare-sweep.html +14 -6
  217. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-glow-rule.html +22 -13
  218. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-gradient-word.html +15 -5
  219. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-gridfloor.html +16 -2
  220. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-halo.html +16 -5
  221. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-horizon.html +16 -4
  222. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-index.html +25 -9
  223. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-kinetic.html +10 -3
  224. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-lensflare.html +10 -3
  225. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-liquid.html +12 -5
  226. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-marquee.html +34 -25
  227. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-mesh.html +13 -4
  228. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-monolith.html +9 -3
  229. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-node-title.html +17 -1
  230. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-numeral.html +13 -4
  231. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-orb.html +12 -2
  232. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-orbit.html +9 -1
  233. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-particles.html +7 -1
  234. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-pill.html +7 -1
  235. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-portal.html +14 -2
  236. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-prism.html +15 -2
  237. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-question.html +14 -2
  238. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-quoteopen.html +11 -2
  239. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-reveal-mask.html +6 -1
  240. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-ribbon.html +12 -1
  241. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-ripple.html +8 -1
  242. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-searchbar.html +15 -2
  243. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-slab.html +9 -2
  244. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-sphere-top.html +12 -3
  245. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-split-reveal.html +24 -7
  246. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-splitpanel.html +27 -4
  247. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-spotlight-cone.html +16 -3
  248. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-spotlight.html +10 -2
  249. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-stack.html +17 -4
  250. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-stacked-words.html +14 -3
  251. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-starfield.html +7 -1
  252. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-sweep.html +11 -2
  253. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-tunnel.html +14 -4
  254. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-type-scale.html +9 -2
  255. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-typed.html +9 -2
  256. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-underline-grow.html +13 -1
  257. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-waveform.html +11 -2
  258. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-wordmark.html +11 -3
  259. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-annot.html +22 -7
  260. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-beforeafter.html +23 -6
  261. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-bigpercent.html +10 -3
  262. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-bracketstat.html +13 -3
  263. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-bubble.html +7 -3
  264. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-cbars.html +8 -3
  265. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-clockface.html +8 -4
  266. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-column.html +12 -4
  267. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-compass.html +7 -3
  268. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-counterpill.html +11 -3
  269. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-curve.html +22 -10
  270. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-delta.html +13 -4
  271. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-dial.html +9 -3
  272. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-donut.html +15 -4
  273. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-dotgrid.html +14 -4
  274. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-figure.html +9 -3
  275. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-fillshape.html +11 -3
  276. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-fraction.html +11 -5
  277. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-funnel.html +24 -8
  278. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-growtharrow.html +18 -4
  279. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-heatcell.html +8 -3
  280. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-isotype.html +16 -4
  281. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-layered.html +10 -3
  282. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-mega.html +9 -3
  283. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-meter.html +9 -4
  284. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-milestone.html +9 -3
  285. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-money.html +16 -4
  286. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-multiplier.html +12 -4
  287. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-multiring.html +10 -3
  288. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-needle.html +10 -3
  289. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-nodes.html +50 -9
  290. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-odometer.html +9 -4
  291. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-orbitmoons.html +21 -8
  292. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-orbitstat.html +9 -3
  293. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-pair.html +18 -3
  294. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-percentile.html +11 -3
  295. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-pictobar.html +11 -4
  296. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-polar.html +12 -2
  297. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-quotestat.html +15 -5
  298. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-radialbars.html +18 -6
  299. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-range.html +12 -5
  300. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-rank.html +13 -5
  301. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-ratio.html +10 -5
  302. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-ring.html +10 -3
  303. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-risewall.html +18 -2
  304. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-score.html +11 -4
  305. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-segbar.html +9 -3
  306. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-semigauge.html +9 -4
  307. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-sparkline.html +9 -3
  308. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-spectrum.html +9 -2
  309. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-split.html +14 -4
  310. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-stackedbars.html +9 -4
  311. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-tickerstat.html +18 -5
  312. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-tiles.html +18 -4
  313. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-timer.html +14 -4
  314. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-trendline.html +18 -4
  315. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-underscore.html +9 -3
  316. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-versus.html +18 -5
  317. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-waffle.html +18 -4
  318. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-waterfall.html +9 -4
  319. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-badge.html +13 -4
  320. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-brace.html +13 -4
  321. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-bracket.html +13 -4
  322. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-bubble.html +16 -4
  323. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-byline.html +17 -5
  324. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-callout.html +13 -4
  325. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-category.html +14 -5
  326. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-checkmark.html +13 -4
  327. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-chipstat.html +13 -4
  328. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-comparemini.html +14 -5
  329. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-coord.html +26 -8
  330. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-counter.html +17 -6
  331. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-crosshair.html +18 -6
  332. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-dashed.html +19 -5
  333. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-definition.html +14 -5
  334. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-deltachip.html +15 -6
  335. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-eyebrow.html +14 -5
  336. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-fact.html +14 -5
  337. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-flag.html +20 -7
  338. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-formula.html +17 -7
  339. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-hash.html +17 -5
  340. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-highlightbox.html +13 -4
  341. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-iconchip.html +13 -4
  342. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-info.html +14 -5
  343. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-infocard.html +13 -4
  344. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-keycap.html +13 -4
  345. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-keyword.html +12 -3
  346. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-leader.html +16 -5
  347. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-legend.html +14 -5
  348. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-measure.html +14 -5
  349. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-meterchip.html +16 -7
  350. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-metric.html +15 -6
  351. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-micropie.html +15 -6
  352. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-minibar.html +15 -6
  353. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-node.html +28 -18
  354. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-note.html +14 -5
  355. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-numbercallout.html +18 -6
  356. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-percent.html +15 -6
  357. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-pointer.html +31 -22
  358. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-price.html +20 -8
  359. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-progress.html +15 -6
  360. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-pulse.html +14 -5
  361. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-qmark.html +15 -6
  362. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-quote.html +16 -7
  363. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-rating.html +14 -5
  364. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-ribbon.html +19 -6
  365. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-ringmark.html +27 -17
  366. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-search.html +14 -5
  367. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-sparkchip.html +15 -6
  368. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-spec.html +25 -13
  369. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-status.html +15 -6
  370. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-step.html +16 -7
  371. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-tag.html +14 -5
  372. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-takeaway.html +13 -4
  373. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-tcode.html +18 -6
  374. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-timestamp.html +15 -6
  375. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-toast.html +15 -6
  376. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-tooltip.html +14 -5
  377. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-underline.html +14 -5
  378. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-warning.html +16 -7
  379. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-avatar.html +12 -5
  380. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-badgecard.html +12 -6
  381. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-barslide.html +11 -5
  382. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-bracket.html +20 -7
  383. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-brandbar.html +13 -7
  384. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-bug.html +12 -5
  385. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-capsule.html +14 -5
  386. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-chapterbar.html +12 -5
  387. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-chip.html +11 -5
  388. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-cinemastrip.html +20 -8
  389. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-cite.html +12 -6
  390. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-clockname.html +11 -5
  391. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-coordbar.html +16 -6
  392. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-corner.html +11 -5
  393. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-cornerframe.html +16 -6
  394. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-countbar.html +13 -7
  395. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-dividername.html +26 -6
  396. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-dotline.html +11 -5
  397. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-doublecard.html +12 -6
  398. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-dual.html +12 -6
  399. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-edgebar.html +11 -5
  400. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-flipcard.html +15 -5
  401. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-geopin.html +11 -5
  402. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-glasscard.html +11 -5
  403. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-gradient.html +11 -5
  404. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-gradientbar.html +12 -4
  405. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-handlebar.html +11 -5
  406. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-infochips.html +11 -5
  407. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-lensbar.html +25 -12
  408. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-live.html +12 -6
  409. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-location.html +14 -6
  410. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-logolock.html +14 -6
  411. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-micname.html +14 -6
  412. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-minicard.html +13 -5
  413. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-minimal.html +13 -5
  414. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-nameplate3d.html +13 -5
  415. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-neonframe.html +14 -5
  416. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-neonpill.html +13 -6
  417. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-nowplaying.html +15 -7
  418. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-orbitbadge.html +13 -5
  419. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-pill.html +14 -6
  420. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-portraitframe.html +14 -6
  421. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-progressname.html +13 -5
  422. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-quote.html +15 -7
  423. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-ratingname.html +14 -6
  424. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-ribbon.html +13 -5
  425. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-sidebracket.html +13 -5
  426. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-slidetab.html +15 -6
  427. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-social.html +14 -6
  428. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-spotlightname.html +14 -6
  429. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-stacked.html +13 -5
  430. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-statline.html +13 -5
  431. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-tagrow.html +14 -5
  432. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-thinstrip.html +18 -8
  433. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-ticker.html +29 -7
  434. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-titlebar.html +19 -5
  435. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-trust.html +16 -8
  436. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-typedname.html +13 -5
  437. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-underbar.html +13 -5
  438. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-waveband.html +14 -6
  439. package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/tokens.css +18 -8
  440. package/skills/luckiest-video-studio-cards/packs/_blueprint/cards/tier1/t1-stat.html +4 -2
  441. package/skills/luckiest-video-studio-cards/packs/_blueprint/cards/tier2/t2-list.html +5 -2
  442. package/skills/luckiest-video-studio-cards/packs/_blueprint/tokens.css +12 -4
  443. package/skills/luckiest-video-studio-cards/references/card-contract.md +15 -1
  444. package/skills/luckiest-video-studio-cards/scripts/qa-cards.mjs +14 -8
  445. package/skills/luckiest-video-studio-cards/templates/glass-popout/index.html +35 -11
  446. package/skills/luckiest-video-studio-cards/templates/glass-popout/meta.json +6 -1
  447. package/skills/luckiest-video-studio-cards/templates/graph-paper/index.html +15 -10
  448. package/skills/luckiest-video-studio-cards/templates/graph-paper/meta.json +7 -2
  449. package/skills/luckiest-video-studio-motion/ATTRIBUTION.md +4 -0
  450. package/skills/luckiest-video-studio-motion/CHANGELOG.md +9 -0
  451. package/skills/luckiest-video-studio-motion/SKILL.md +7 -3
  452. package/skills/luckiest-video-studio-motion/references/vocabulary.md +21 -0
  453. package/skills/luckiest-video-studio-motion/templates/spring.js +50 -0
  454. package/skills/luckiest-video-studio-motion/templates/spring.test.mjs +51 -0
  455. package/skills/luckiest-video-studio-recreate/ATTRIBUTION.md +2 -0
  456. package/skills/luckiest-video-studio-recreate/CHANGELOG.md +11 -0
  457. package/skills/luckiest-video-studio-recreate/SKILL.md +46 -7
  458. package/skills/luckiest-video-studio-reel/CHANGELOG.md +6 -0
  459. package/skills/luckiest-video-studio-reel/SKILL.md +9 -4
  460. package/skills/luckiest-video-studio-reel/templates/reel.html +36 -18
  461. package/skills/luckiest-video-studio-reference/CHANGELOG.md +4 -0
  462. package/skills/luckiest-video-studio-reference/SKILL.md +10 -3
  463. package/skills/luckiest-video-studio-review/ATTRIBUTION.md +4 -0
  464. package/skills/luckiest-video-watcher/ATTRIBUTION.md +37 -0
  465. package/skills/luckiest-video-watcher/CHANGELOG.md +90 -0
  466. package/skills/luckiest-video-watcher/LICENSE-claude-video +21 -0
  467. package/skills/luckiest-video-watcher/SKILL.md +201 -0
  468. package/skills/luckiest-video-watcher/references/watch-guide.md +159 -0
  469. package/skills/luckiest-video-watcher/scripts/config.py +174 -0
  470. package/skills/luckiest-video-watcher/scripts/download.py +207 -0
  471. package/skills/luckiest-video-watcher/scripts/frames.py +758 -0
  472. package/skills/luckiest-video-watcher/scripts/gemini.py +162 -0
  473. package/skills/luckiest-video-watcher/scripts/local_whisperx.py +147 -0
  474. package/skills/luckiest-video-watcher/scripts/runtime.py +35 -0
  475. package/skills/luckiest-video-watcher/scripts/setup.py +338 -0
  476. package/skills/luckiest-video-watcher/scripts/transcribe.py +113 -0
  477. package/skills/luckiest-video-watcher/scripts/watch.py +430 -0
  478. package/skills/luckiest-video-watcher/scripts/whisper.py +425 -0
@@ -1,24 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  // Render each storyboard scene to clips/NN-id.mp4, skip unchanged scenes, then join final.mp4 and write TIMING.md.
3
- // Usage: node render-scenes.mjs <run-dir> [--scene <id>]... [--quality draft|looks|delivery] [--no-join]
4
- import { spawnSync } from "node:child_process";
3
+ // With storyboard "formats", each format renders into clips/<format>/ and joins final-<format>.mp4.
4
+ // Landscape reuses the clips of a finished single-format 16:9 render, so adding formats renders only the new ones.
5
+ // --stills skips the render: one hold-frame PNG per scene per format in stills/[<format>/], plus a sheet.png each.
6
+ // Usage: node render-scenes.mjs <run-dir> [--scene <id>]... [--quality draft|looks|delivery] [--no-join] [--stills [--jobs 2]]
7
+ import { spawn, spawnSync } from "node:child_process";
5
8
  import { createHash } from "node:crypto";
6
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
9
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
7
11
  import { join, resolve } from "node:path";
12
+ import { FORMATS, stampFormat } from "./stamp-format.mjs";
8
13
 
9
14
  const args = process.argv.slice(2);
10
- const runDir = resolve(args.find((a, i) => !a.startsWith("--") && !["--scene", "--quality"].includes(args[i - 1])) ?? ".");
15
+ const runDir = resolve(args.find((a, i) => !a.startsWith("--") && !["--scene", "--quality", "--jobs"].includes(args[i - 1])) ?? ".");
11
16
  const forced = args.flatMap((a, i) => (a === "--scene" ? [args[i + 1]] : []));
12
17
  const quality = args.includes("--quality") ? args[args.indexOf("--quality") + 1] : "looks";
13
18
  const join_ = !args.includes("--no-join");
19
+ const stills = args.includes("--stills");
20
+ const jobs = args.includes("--jobs") ? Number(args[args.indexOf("--jobs") + 1]) : 2; // ponytail: 4 parallel snapshots hit the 10s navigation timeout here
14
21
 
15
22
  const sb = JSON.parse(readFileSync(join(runDir, "storyboard.json"), "utf8"));
16
- const { width: W, height: H, fps } = sb.format;
17
- const clipsDir = join(runDir, "clips");
18
- const rawDir = join(clipsDir, ".raw");
19
- mkdirSync(rawDir, { recursive: true });
20
- const cachePath = join(clipsDir, ".cache.json");
21
- const cache = existsSync(cachePath) ? JSON.parse(readFileSync(cachePath, "utf8")) : {};
23
+ const { fps } = sb.format;
22
24
 
23
25
  const fail = (msg) => { console.error(`error: ${msg}`); process.exit(1); };
24
26
  const run = (cmd, argv, opts = {}) => {
@@ -37,6 +39,17 @@ for (const s of sb.scenes) {
37
39
  }
38
40
  for (const id of forced) if (!ids.has(id)) fail(`--scene ${id}: no such scene`);
39
41
 
42
+ // No "formats": one target with today's paths. With "formats": one target per format, each in its own folder.
43
+ const multi = Array.isArray(sb.formats);
44
+ const clipsRoot = join(runDir, "clips");
45
+ const targets = multi
46
+ ? sb.formats.map((fmt) => {
47
+ if (!FORMATS[fmt]) fail(`formats: unknown format "${fmt}" (landscape, vertical, square)`);
48
+ const [W, H] = FORMATS[fmt];
49
+ return { fmt, W, H, dir: join(clipsRoot, fmt), final: `final-${fmt}.mp4` };
50
+ })
51
+ : [{ fmt: null, W: sb.format.width, H: sb.format.height, dir: clipsRoot, final: "final.mp4" }];
52
+
40
53
  const inputsOf = (s) => {
41
54
  const files = [...(s.assets ?? [])];
42
55
  if (s.kind === "demo") {
@@ -45,81 +58,193 @@ const inputsOf = (s) => {
45
58
  } else files.push(join("composition", s.composition ?? `compositions/${s.id}.html`));
46
59
  return files;
47
60
  };
48
- const hashOf = (s) => {
49
- const h = createHash("sha256").update(JSON.stringify({ s, W, H, fps, quality }));
61
+
62
+ // Cache key for one scene at one target. fmt null is a single-format run with today's paths.
63
+ function sceneHash(s, { W, H, fmt }) {
64
+ const h = createHash("sha256").update(JSON.stringify({ s, W, H, fps, quality, fmt }));
50
65
  for (const f of inputsOf(s)) {
51
66
  const p = join(runDir, f);
52
67
  if (!existsSync(p)) fail(`scene ${s.id}: missing ${f}`);
53
68
  h.update(f).update(readFileSync(p));
54
69
  }
55
70
  return h.digest("hex").slice(0, 16);
56
- };
71
+ }
57
72
 
58
- // Every clip gets the same codec, size, fps and a stereo audio track so the join is a lossless concat.
59
- const normalize = (input, out, s, seek = 0) => {
60
- const probe = spawnSync("ffprobe", ["-v", "error", "-select_streams", "a", "-show_entries", "stream=index", "-of", "csv=p=0", input], { encoding: "utf8" });
61
- const hasAudio = probe.stdout.trim().length > 0;
62
- const vf = `scale=${W}:${H}:force_original_aspect_ratio=decrease,pad=${W}:${H}:(ow-iw)/2:(oh-ih)/2,fps=${fps},format=yuv420p`;
63
- const a = ["-y", "-v", "error", "-ss", String(seek), "-t", String(s.duration), "-i", input];
64
- if (!hasAudio) a.push("-f", "lavfi", "-t", String(s.duration), "-i", "anullsrc=r=48000:cl=stereo");
65
- a.push("-map", "0:v:0", "-map", hasAudio ? "0:a:0" : "1:a:0", "-vf", vf, "-c:v", "libx264", "-crf", "18", "-preset", "medium",
66
- "-c:a", "aac", "-ar", "48000", "-ac", "2", "-shortest", "-movflags", "+faststart", out);
67
- run("ffmpeg", a);
68
- };
73
+ // Footage is cropped to fill around scenes[].focus when rendering formats or when focus is set;
74
+ // otherwise it is letterboxed as before, so a single-format run keeps its old framing.
75
+ function fitFilter(s, W, H) {
76
+ const { x = 0.5, y = 0.5 } = s.focus ?? {};
77
+ return s.kind === "demo" && (multi || s.focus)
78
+ ? `scale=${W}:${H}:force_original_aspect_ratio=increase,crop=${W}:${H}:'max(0,min(iw-ow,${x}*iw-ow/2))':'max(0,min(ih-oh,${y}*ih-oh/2))'`
79
+ : `scale=${W}:${H}:force_original_aspect_ratio=decrease,pad=${W}:${H}:(ow-iw)/2:(oh-ih)/2`;
80
+ }
69
81
 
70
82
  const hf = hasBin("hyperframes") ? ["hyperframes"] : ["npx", `hyperframes@${sb.hyperframes ?? "latest"}`];
71
83
  let rendered = 0;
72
- const clips = sb.scenes.map((s, i) => {
73
- const name = `${String(i + 1).padStart(2, "0")}-${s.id}.mp4`;
74
- const out = join(clipsDir, name);
75
- const hash = hashOf(s);
76
- const prev = cache[s.id];
77
- const force = forced.includes(s.id);
78
- if (!force && prev?.hash === hash && existsSync(join(clipsDir, prev.file))) {
79
- if (prev.file !== name) renameSync(join(clipsDir, prev.file), out);
80
- console.log(`skip ${name} (unchanged)`);
81
- } else if (forced.length && !force && prev && existsSync(join(clipsDir, prev.file))) {
82
- if (prev.file !== name) renameSync(join(clipsDir, prev.file), out);
83
- console.log(`keep ${name} (not selected)`);
84
- return { s, name, hash: prev.hash };
85
- } else {
86
- console.log(`render ${name}`);
87
- if (s.kind === "demo") normalize(join(runDir, s.source), out, s, s.sourceStart ?? 0);
88
- else {
89
- const raw = join(rawDir, name);
90
- const comp = s.composition ?? `compositions/${s.id}.html`;
91
- run(hf[0], [...hf.slice(1), "render", "-c", comp, "-o", raw, "--fps", String(fps), "--quality", quality], { cwd: join(runDir, "composition") });
92
- normalize(raw, out, s);
84
+
85
+ function renderTarget({ fmt, W, H, dir, final: finalName }) {
86
+ const rawDir = join(dir, ".raw");
87
+ mkdirSync(rawDir, { recursive: true });
88
+ const cachePath = join(dir, ".cache.json");
89
+ const cache = existsSync(cachePath) ? JSON.parse(readFileSync(cachePath, "utf8")) : {};
90
+ const tag = (name) => (fmt ? `${fmt}/${name}` : name);
91
+
92
+ const hashOf = (s) => sceneHash(s, { W, H, fmt });
93
+ // A finished single-format 16:9 render (clips/.cache.json) stands in for the landscape format: the landscape
94
+ // stamp only restates the master defaults, so the pixels match. Footage is framed differently, so demos re-run.
95
+ const single = fmt === "landscape" && sb.format.width === W && sb.format.height === H && existsSync(join(clipsRoot, ".cache.json"))
96
+ ? JSON.parse(readFileSync(join(clipsRoot, ".cache.json"), "utf8")) : {};
97
+
98
+ // Every clip gets the same codec, size, fps and a stereo audio track so the join is a lossless concat.
99
+ const normalize = (input, out, s, seek = 0) => {
100
+ const probe = spawnSync("ffprobe", ["-v", "error", "-select_streams", "a", "-show_entries", "stream=index", "-of", "csv=p=0", input], { encoding: "utf8" });
101
+ const hasAudio = probe.stdout.trim().length > 0;
102
+ const vf = `${fitFilter(s, W, H)},fps=${fps},format=yuv420p`;
103
+ const a = ["-y", "-v", "error", "-ss", String(seek), "-t", String(s.duration), "-i", input];
104
+ if (!hasAudio) a.push("-f", "lavfi", "-t", String(s.duration), "-i", "anullsrc=r=48000:cl=stereo");
105
+ a.push("-map", "0:v:0", "-map", hasAudio ? "0:a:0" : "1:a:0", "-vf", vf, "-c:v", "libx264", "-crf", "18", "-preset", "medium",
106
+ "-c:a", "aac", "-ar", "48000", "-ac", "2", "-shortest", "-movflags", "+faststart", out);
107
+ run("ffmpeg", a);
108
+ };
109
+
110
+ const clips = sb.scenes.map((s, i) => {
111
+ const name = `${String(i + 1).padStart(2, "0")}-${s.id}.mp4`;
112
+ const out = join(dir, name);
113
+ const hash = hashOf(s);
114
+ const prev = cache[s.id];
115
+ const force = forced.includes(s.id);
116
+ const legacy = single[s.id];
117
+ if (!force && prev?.hash === hash && existsSync(join(dir, prev.file))) {
118
+ if (prev.file !== name) renameSync(join(dir, prev.file), out);
119
+ console.log(`skip ${tag(name)} (unchanged)`);
120
+ } else if (!force && s.kind !== "demo" && legacy?.hash === sceneHash(s, { W, H, fmt: null }) && existsSync(join(clipsRoot, legacy.file))) {
121
+ copyFileSync(join(clipsRoot, legacy.file), out);
122
+ console.log(`reuse ${tag(name)} (from the 16:9 render)`);
123
+ } else if (forced.length && !force && prev && existsSync(join(dir, prev.file))) {
124
+ if (prev.file !== name) renameSync(join(dir, prev.file), out);
125
+ console.log(`keep ${tag(name)} (not selected)`);
126
+ return { s, name, hash: prev.hash };
127
+ } else {
128
+ console.log(`render ${tag(name)}`);
129
+ if (s.kind === "demo") normalize(join(runDir, s.source), out, s, s.sourceStart ?? 0);
130
+ else {
131
+ const raw = join(rawDir, name);
132
+ const compDir = join(runDir, "composition");
133
+ let comp = s.composition ?? `compositions/${s.id}.html`;
134
+ // A stamped copy next to the original, so relative asset paths still resolve.
135
+ if (fmt) {
136
+ const stamped = comp.replace(/\.html$/, `.${fmt}.html`);
137
+ writeFileSync(join(compDir, stamped), stampFormat(readFileSync(join(compDir, comp), "utf8"), fmt));
138
+ comp = stamped;
139
+ }
140
+ run(hf[0], [...hf.slice(1), "render", "-c", comp, "-o", raw, "--fps", String(fps), "--quality", quality], { cwd: compDir });
141
+ if (fmt) rmSync(join(compDir, comp));
142
+ normalize(raw, out, s);
143
+ }
144
+ rendered++;
145
+ }
146
+ return { s, name, hash };
147
+ });
148
+
149
+ writeFileSync(cachePath, JSON.stringify(Object.fromEntries(clips.map((c) => [c.s.id, { hash: c.hash, file: c.name }])), null, 2));
150
+ const t = clips.reduce((sum, c) => sum + c.s.duration, 0);
151
+
152
+ if (join_) {
153
+ const list = join(dir, ".concat.txt");
154
+ writeFileSync(list, clips.map((c) => `file '${join(dir, c.name).replace(/'/g, "'\\''")}'`).join("\n") + "\n");
155
+ const joined = join(dir, ".joined.mp4");
156
+ run("ffmpeg", ["-y", "-v", "error", "-f", "concat", "-safe", "0", "-i", list, "-c", "copy", joined]);
157
+ const final = join(runDir, finalName);
158
+ // Mix the clips' own audio with the music bed and the global SFX track from sfx-cues.mjs.
159
+ // music.offset skips into the track, music.at starts it in the video, so a chosen kick lands on a chosen moment.
160
+ const ins = ["-i", joined], legs = [], tags = ["[0:a]"];
161
+ if (sb.music?.file) {
162
+ const at = sb.music.at ?? 0;
163
+ ins.push("-stream_loop", "-1", "-i", join(runDir, sb.music.file));
164
+ legs.push(`[1:a]atrim=start=${sb.music.offset ?? 0},asetpts=PTS-STARTPTS,volume=${sb.music.volume ?? 0.35},afade=t=out:st=${Math.max(0, t - at - 1.5)}:d=1.5,adelay=${Math.round(at * 1000)}:all=1[m]`);
165
+ tags.push("[m]");
166
+ }
167
+ if (sb.sfx?.file) {
168
+ ins.push("-i", join(runDir, sb.sfx.file));
169
+ legs.push(`[${tags.length}:a]volume=${sb.sfx.volume ?? 1}[x]`);
170
+ tags.push("[x]");
171
+ }
172
+ const mixed = join(dir, ".mix.wav");
173
+ const graph = tags.length > 1 ? `${legs.join(";")};${tags.join("")}amix=inputs=${tags.length}:duration=first:normalize=0[a]` : "[0:a]anull[a]";
174
+ run("ffmpeg", ["-y", "-v", "error", ...ins, "-filter_complex", graph, "-map", "[a]", "-c:a", "pcm_s16le", "-ar", "48000", "-t", String(t), mixed]);
175
+ // Two-pass loudness to sb.loudness LUFS (default -14, the streaming norm). false skips it; silence is left alone.
176
+ const target = sb.loudness ?? -14;
177
+ let af = "anull";
178
+ if (target !== false) {
179
+ const m = spawnSync("ffmpeg", ["-hide_banner", "-nostats", "-i", mixed, "-af", `loudnorm=I=${target}:TP=-1.2:LRA=11:print_format=json`, "-f", "null", "-"], { encoding: "utf8" });
180
+ const js = JSON.parse(m.stderr.match(/\{[^{}]*"input_i"[^{}]*\}/)?.[0] ?? "{}");
181
+ if (Number(js.input_i) > -70) af = `loudnorm=I=${target}:TP=-1.2:LRA=11:measured_I=${js.input_i}:measured_TP=${js.input_tp}:measured_LRA=${js.input_lra}:measured_thresh=${js.input_thresh}:offset=${js.target_offset}:linear=true,aresample=48000`;
93
182
  }
94
- rendered++;
183
+ run("ffmpeg", ["-y", "-v", "error", "-i", joined, "-i", mixed, "-map", "0:v", "-map", "1:a", "-af", af,
184
+ "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-t", String(t), "-movflags", "+faststart", final]);
185
+ rmSync(joined);
186
+ rmSync(mixed);
187
+ console.log(`joined ${finalName} (${t.toFixed(2)}s)`);
95
188
  }
96
- return { s, name, hash };
97
- });
189
+ return { clips, t, tag };
190
+ }
98
191
 
99
- const nextCache = Object.fromEntries(clips.map((c) => [c.s.id, { hash: c.hash, file: c.name }]));
100
- writeFileSync(cachePath, JSON.stringify(nextCache, null, 2));
192
+ // One PNG per scene at its hold frame (scenes[].hold, else 0.5s before the cut), no video encode.
193
+ // Each motion or logo scene is snapshotted as the index.html of a temp project whose other entries link
194
+ // back to composition/, the same stamped copy the render uses, so relative asset paths still resolve.
195
+ async function captureStills({ fmt, W, H }) {
196
+ const dir = join(runDir, "stills", fmt ?? "");
197
+ rmSync(dir, { recursive: true, force: true });
198
+ mkdirSync(dir, { recursive: true });
199
+ const compDir = join(runDir, "composition");
200
+ const queue = sb.scenes.map((s, i) => ({ s, name: `${String(i + 1).padStart(2, "0")}-${s.id}.png` }));
201
+ const worker = async () => {
202
+ for (let job = queue.shift(); job; job = queue.shift()) {
203
+ const { s, name } = job, at = s.hold ?? Math.max(0, s.duration - 0.5), out = join(dir, name);
204
+ if (s.kind === "demo") {
205
+ run("ffmpeg", ["-y", "-v", "error", "-ss", String((s.sourceStart ?? 0) + at), "-i", join(runDir, s.source), "-frames:v", "1", "-vf", fitFilter(s, W, H), out]);
206
+ continue;
207
+ }
208
+ const proj = mkdtempSync(join(tmpdir(), "stills-"));
209
+ for (const e of readdirSync(compDir)) if (e !== "index.html") symlinkSync(join(compDir, e), join(proj, e));
210
+ const html = readFileSync(join(compDir, s.composition ?? `compositions/${s.id}.html`), "utf8");
211
+ writeFileSync(join(proj, "index.html"), fmt ? stampFormat(html, fmt) : html);
212
+ let err = "";
213
+ const code = await new Promise((done) => {
214
+ const p = spawn(hf[0], [...hf.slice(1), "snapshot", proj, "--at", String(at), "--no-end", "-o", join(proj, ".snap"), "--describe", "false"]);
215
+ p.stdout.resume();
216
+ p.stderr.on("data", (b) => (err += b));
217
+ p.on("close", done);
218
+ });
219
+ const png = existsSync(join(proj, ".snap")) && readdirSync(join(proj, ".snap")).find((f) => f.startsWith("frame-"));
220
+ if (code !== 0 || !png) fail(`snapshot failed for scene ${s.id}${fmt ? ` (${fmt})` : ""}: ${err.trim().split("\n").pop()}`);
221
+ copyFileSync(join(proj, ".snap", png), out);
222
+ rmSync(proj, { recursive: true, force: true });
223
+ }
224
+ };
225
+ await Promise.all(Array.from({ length: jobs }, worker));
226
+ // ponytail: one sheet per format, 6 across at 320px wide; read it instead of every PNG.
227
+ const cols = Math.min(6, sb.scenes.length), rows = Math.ceil(sb.scenes.length / cols);
228
+ run("ffmpeg", ["-y", "-v", "error", "-pattern_type", "glob", "-i", join(dir, "[0-9]*.png"),
229
+ "-vf", `scale=320:-2,tile=${cols}x${rows}:padding=6:color=white`, "-frames:v", "1", join(dir, "sheet.png")]);
230
+ console.log(`stills ${join("stills", fmt ?? "", "sheet.png")} (${sb.scenes.length} scenes, ${W}x${H})`);
231
+ }
232
+
233
+ if (stills) {
234
+ for (const t of targets) await captureStills(t);
235
+ process.exit(0);
236
+ }
237
+ const results = targets.map(renderTarget);
101
238
 
102
- let t = 0;
239
+ // Timing is the same in every format, so TIMING.md lists the first format's clips.
240
+ const { clips, t, tag } = results[0];
241
+ let at = 0;
103
242
  const rows = clips.map(({ s, name }, i) => {
104
- const row = `| ${i + 1} | ${s.id} | ${s.kind} | ${t.toFixed(2)} | ${(t + s.duration).toFixed(2)} | \`clips/${name}\` | ${(s.line ?? "").replace(/\|/g, "/")} |`;
105
- t += s.duration;
243
+ const row = `| ${i + 1} | ${s.id} | ${s.kind} | ${at.toFixed(2)} | ${(at + s.duration).toFixed(2)} | \`clips/${tag(name)}\` | ${(s.line ?? "").replace(/\|/g, "/")} |`;
244
+ at += s.duration;
106
245
  return row;
107
246
  });
108
- writeFileSync(join(runDir, "TIMING.md"), `# Timing: ${sb.title}\n\nTotal ${t.toFixed(2)}s at ${W}x${H}, ${fps} fps.\n\n| # | Scene | Kind | In (s) | Out (s) | Clip | Line |\n|---|---|---|---|---|---|---|\n${rows.join("\n")}\n`);
109
-
110
- if (join_) {
111
- const list = join(clipsDir, ".concat.txt");
112
- writeFileSync(list, clips.map((c) => `file '${join(clipsDir, c.name).replace(/'/g, "'\\''")}'`).join("\n") + "\n");
113
- const joined = join(clipsDir, ".joined.mp4");
114
- run("ffmpeg", ["-y", "-v", "error", "-f", "concat", "-safe", "0", "-i", list, "-c", "copy", joined]);
115
- const final = join(runDir, "final.mp4");
116
- if (sb.music?.file) {
117
- const vol = String(sb.music.volume ?? 0.35);
118
- run("ffmpeg", ["-y", "-v", "error", "-i", joined, "-stream_loop", "-1", "-i", join(runDir, sb.music.file),
119
- "-filter_complex", `[1:a]volume=${vol},afade=t=out:st=${Math.max(0, t - 1.5)}:d=1.5[m];[0:a][m]amix=inputs=2:duration=first:normalize=0[a]`,
120
- "-map", "0:v", "-map", "[a]", "-c:v", "copy", "-c:a", "aac", "-t", String(t), "-movflags", "+faststart", final]);
121
- rmSync(joined);
122
- } else renameSync(joined, final);
123
- console.log(`joined final.mp4 (${t.toFixed(2)}s)`);
124
- }
125
- console.log(`done: ${rendered} rendered, ${clips.length - rendered} reused`);
247
+ const size = multi ? targets.map((x) => `${x.fmt} ${x.W}x${x.H} (${x.final})`).join(", ") : `${targets[0].W}x${targets[0].H}`;
248
+ writeFileSync(join(runDir, "TIMING.md"), `# Timing: ${sb.title}\n\nTotal ${t.toFixed(2)}s at ${size}, ${fps} fps.\n\n| # | Scene | Kind | In (s) | Out (s) | Clip | Line |\n|---|---|---|---|---|---|---|\n${rows.join("\n")}\n`);
249
+ const total = clips.length * targets.length;
250
+ console.log(`done: ${rendered} rendered, ${total - rendered} reused`);
@@ -0,0 +1,109 @@
1
+ // The join places the music where storyboard.json says and lands the mix at the target loudness.
2
+ // Demo scenes only, so nothing but ffmpeg runs; fixtures are lavfi clips in a temp folder.
3
+ import test from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { createHash } from "node:crypto";
6
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { spawnSync } from "node:child_process";
10
+ import { audioOnsets } from "./qa.mjs";
11
+ import { stampFormat } from "./stamp-format.mjs";
12
+
13
+ const ff = (...a) => { const r = spawnSync("ffmpeg", ["-v", "error", "-y", ...a]); assert.equal(r.status, 0, r.stderr?.toString()); };
14
+ const lufs = (f) => Number(spawnSync("ffmpeg", ["-hide_banner", "-nostats", "-i", f, "-af", "ebur128", "-f", "null", "-"], { encoding: "utf8" }).stderr.match(/I:\s+(-?[\d.]+) LUFS/g).pop().match(/-?[\d.]+/)[0]);
15
+
16
+ function run(music, { burst = true } = {}) {
17
+ const dir = mkdtempSync(join(tmpdir(), "render-scenes-"));
18
+ mkdirSync(join(dir, "demo"));
19
+ mkdirSync(join(dir, "audio"));
20
+ ff("-f", "lavfi", "-i", "testsrc2=size=320x180:rate=30:duration=4", "-c:v", "libx264", join(dir, "demo/a.mp4"));
21
+ // A quiet noise bed, with one loud burst 1.0 s into the track for the placement test.
22
+ ff("-f", "lavfi", "-i", `aevalsrc='(0.05+${burst ? 0.8 : 0}*between(t,1,1.1))*(random(0)-0.5)':s=48000:d=6`, join(dir, "audio/music.wav"));
23
+ writeFileSync(join(dir, "storyboard.json"), JSON.stringify({
24
+ version: 1, title: "t", mode: "demo", format: { width: 320, height: 180, fps: 30 },
25
+ music: { file: "audio/music.wav", volume: 0.5, ...music },
26
+ scenes: [{ id: "a", kind: "demo", duration: 2, source: "demo/a.mp4" }, { id: "b", kind: "demo", duration: 2, source: "demo/a.mp4", sourceStart: 2 }],
27
+ }));
28
+ const r = spawnSync(process.execPath, [join(import.meta.dirname, "render-scenes.mjs"), dir], { encoding: "utf8" });
29
+ assert.equal(r.status, 0, r.stderr);
30
+ return join(dir, "final.mp4");
31
+ }
32
+
33
+ test("music.offset and music.at land a moment in the track on a moment in the video", () => {
34
+ // Burst at 1.0 s in the track; skip 0.5 s of it and start it 1.0 s in: the burst lands at 1.5 s.
35
+ const on = audioOnsets(run({ offset: 0.5, at: 1 }));
36
+ assert.ok(on[0] >= 0.95, `music starts at ${on[0]}, not before 1.0`);
37
+ assert.ok(on.some((t) => Math.abs(t - 1.5) < 1 / 30), JSON.stringify(on));
38
+ });
39
+
40
+ test("the final mix is normalized to -14 LUFS by default", () => {
41
+ const quiet = run({}, { burst: false });
42
+ assert.ok(Math.abs(lufs(quiet) + 14) <= 1, `measured ${lufs(quiet)} LUFS`);
43
+ });
44
+
45
+ test("formats render one final per format, cropping footage around focus", () => {
46
+ const dir = mkdtempSync(join(tmpdir(), "render-formats-"));
47
+ mkdirSync(join(dir, "demo"));
48
+ // Left half red, right half blue, so the crop side is visible in one pixel.
49
+ ff("-f", "lavfi", "-i", "color=red:size=320x180:rate=30:duration=2", "-f", "lavfi", "-i", "color=blue:size=160x180:rate=30:duration=2",
50
+ "-filter_complex", "[0][1]overlay=x=160", "-c:v", "libx264", join(dir, "demo/a.mp4"));
51
+ writeFileSync(join(dir, "storyboard.json"), JSON.stringify({
52
+ version: 1, title: "t", mode: "demo", format: { width: 1920, height: 1080, fps: 30 }, formats: ["landscape", "vertical"], loudness: false,
53
+ scenes: [{ id: "a", kind: "demo", duration: 1, source: "demo/a.mp4", focus: { x: 0.9, y: 0.5 } }],
54
+ }));
55
+ const r = spawnSync(process.execPath, [join(import.meta.dirname, "render-scenes.mjs"), dir], { encoding: "utf8" });
56
+ assert.equal(r.status, 0, r.stderr);
57
+ const size = (f) => spawnSync("ffprobe", ["-v", "error", "-select_streams", "v", "-show_entries", "stream=width,height", "-of", "csv=p=0", join(dir, f)], { encoding: "utf8" }).stdout.trim();
58
+ assert.equal(size("final-landscape.mp4"), "1920,1080");
59
+ assert.equal(size("final-vertical.mp4"), "1080,1920");
60
+ assert.ok(existsSync(join(dir, "clips/vertical/01-a.mp4")) && !existsSync(join(dir, "final.mp4")));
61
+ // focus x 0.9 keeps the blue right side in the vertical crop.
62
+ const px = spawnSync("ffmpeg", ["-v", "error", "-i", join(dir, "final-vertical.mp4"), "-frames:v", "1", "-vf", "crop=2:2:540:960", "-f", "rawvideo", "-pix_fmt", "rgb24", "-"]).stdout;
63
+ assert.ok(px[2] > 150 && px[0] < 100, `center pixel ${[...px]}`);
64
+ });
65
+
66
+ test("stampFormat sizes the root, tags the format, and injects size tokens", () => {
67
+ const html = `<html lang="en" data-resolution="landscape"><head><style>:root{--w:1920px}</style></head><body><div id="root" data-composition-id="main" data-width="1920" data-height="1080"></div></body></html>`;
68
+ const out = stampFormat(html, "vertical");
69
+ assert.match(out, /<html lang="en" data-resolution="portrait" data-format="vertical">/);
70
+ assert.match(out, /data-composition-id="main" data-width="1080" data-height="1920" data-format="vertical">/);
71
+ assert.match(out, /--w:1920px\}<\/style><style>:root\{--w:1080px;--h:1920px;--u:10\.8px;--safe-t:288px;--safe-b:672px;--safe-l:119px;--safe-r:194px\}<\/style><\/head>/);
72
+ });
73
+
74
+ test("formats reuse a finished 16:9 render for landscape instead of rendering it again", () => {
75
+ const dir = mkdtempSync(join(tmpdir(), "render-reuse-"));
76
+ mkdirSync(join(dir, "composition/compositions"), { recursive: true });
77
+ mkdirSync(join(dir, "clips"));
78
+ writeFileSync(join(dir, "composition/compositions/m.html"), "<html><body>m</body></html>");
79
+ const scene = { id: "m", kind: "motion", duration: 1 };
80
+ const sb = { version: 1, title: "t", mode: "launch", format: { width: 1920, height: 1080, fps: 30 }, scenes: [scene] };
81
+ // The cache entry a single-format render writes: same key the coordinator computes for fmt null.
82
+ const comp = join("composition", "compositions/m.html");
83
+ const hash = createHash("sha256").update(JSON.stringify({ s: scene, W: 1920, H: 1080, fps: 30, quality: "looks", fmt: null }))
84
+ .update(comp).update(readFileSync(join(dir, comp))).digest("hex").slice(0, 16);
85
+ writeFileSync(join(dir, "clips/.cache.json"), JSON.stringify({ m: { hash, file: "01-m.mp4" } }));
86
+ ff("-f", "lavfi", "-i", "color=red:size=1920x1080:rate=30:duration=1", "-c:v", "libx264", join(dir, "clips/01-m.mp4"));
87
+ writeFileSync(join(dir, "storyboard.json"), JSON.stringify({ ...sb, formats: ["landscape"] }));
88
+ const r = spawnSync(process.execPath, [join(import.meta.dirname, "render-scenes.mjs"), dir, "--no-join"], { encoding: "utf8" });
89
+ assert.equal(r.status, 0, r.stderr);
90
+ assert.match(r.stdout, /reuse {2}landscape\/01-m\.mp4/);
91
+ assert.match(r.stdout, /done: 0 rendered, 1 reused/);
92
+ assert.ok(existsSync(join(dir, "clips/landscape/01-m.mp4")));
93
+ });
94
+
95
+ test("--stills writes one hold frame per scene per format and a sheet, without rendering", () => {
96
+ const dir = mkdtempSync(join(tmpdir(), "render-stills-"));
97
+ mkdirSync(join(dir, "demo"));
98
+ ff("-f", "lavfi", "-i", "testsrc2=size=320x180:rate=30:duration=2", "-c:v", "libx264", join(dir, "demo/a.mp4"));
99
+ writeFileSync(join(dir, "storyboard.json"), JSON.stringify({
100
+ version: 1, title: "t", mode: "demo", format: { width: 1920, height: 1080, fps: 30 }, formats: ["landscape", "vertical"],
101
+ scenes: [{ id: "a", kind: "demo", duration: 1.5, source: "demo/a.mp4" }],
102
+ }));
103
+ const r = spawnSync(process.execPath, [join(import.meta.dirname, "render-scenes.mjs"), dir, "--stills"], { encoding: "utf8" });
104
+ assert.equal(r.status, 0, r.stderr);
105
+ const size = (f) => spawnSync("ffprobe", ["-v", "error", "-show_entries", "stream=width,height", "-of", "csv=p=0", join(dir, f)], { encoding: "utf8" }).stdout.trim();
106
+ assert.equal(size("stills/landscape/01-a.png"), "1920,1080");
107
+ assert.equal(size("stills/vertical/01-a.png"), "1080,1920");
108
+ assert.ok(existsSync(join(dir, "stills/vertical/sheet.png")) && !existsSync(join(dir, "clips")));
109
+ });
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ // Mix a cue file of sound effects into one sfx.wav, each sound placed by its anchor, plus a cue sheet.
3
+ // Needs Node 22+ and ffmpeg/ffprobe; nothing else is installed or downloaded.
4
+ //
5
+ // node sfx-cues.mjs <sfx_cues.json> [--sfx-dir <skill-dir>/assets/sfx] [--events <composition-dir>] [--out <dir>]
6
+ //
7
+ // sfx_cues.json:
8
+ // { "duration": 20,
9
+ // "cues": [{ "t": 4.0, "file": "impact/impactSoft_medium_001.ogg", "align": "start", "volume": 0.8, "label": "reveal" },
10
+ // { "t": 6.0, "file": "<sourced>/whoosh.wav", "align": "peak" }, // peak on the cut
11
+ // { "t": 9.5, "kind": "impact" }], // file from kinds
12
+ // "slots": { "02-reveal": 3.5 }, // scene id -> global start, for scene events
13
+ // "kinds": { "card": "casino/card-slide-1.ogg" } } // extra or overriding event kinds
14
+ //
15
+ // Scene events: <events-dir>/<scene-id>.motion.json = { "events": [{ "t": 0.25, "kind": "key", "note": "typed H" }] }
16
+ // with times local to the scene. Each one becomes a cue at slot start + t.
17
+ // align: start (default) puts the file start on t, peak its loudest 10 ms, end its last sample.
18
+
19
+ import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
20
+ import { join, resolve, dirname, isAbsolute } from "node:path";
21
+ import { spawnSync } from "node:child_process";
22
+ import { fileURLToPath } from "node:url";
23
+
24
+ const KEYS = Array.from({ length: 32 }, (_, i) => `keyboard/keypress-${String(i + 1).padStart(3, "0")}.wav`);
25
+ // [file, volume] per event kind; volumes follow the gain ladder in references/audio.md.
26
+ export const DEFAULT_KINDS = {
27
+ key: [KEYS, 0.3], click: ["interface/click_001.ogg", 0.35], switch: ["interface/switch_001.ogg", 0.35],
28
+ pop: ["interface/drop_001.ogg", 0.4], card: ["casino/card-place-1.ogg", 0.4],
29
+ glitch: ["interface/glitch_002.ogg", 0.6], error: ["interface/error_005.ogg", 0.6],
30
+ impact: ["impact/impactSoft_medium_000.ogg", 0.8], success: ["impact/impactBell_heavy_000.ogg", 0.8],
31
+ };
32
+
33
+ const anchors = new Map();
34
+ export function anchorOffset(file, align = "start") {
35
+ if (align === "start") return 0;
36
+ const key = `${file}|${align}`;
37
+ if (anchors.has(key)) return anchors.get(key);
38
+ const r = spawnSync("ffmpeg", ["-v", "error", "-i", file, "-ac", "1", "-ar", "8000", "-f", "s16le", "-"], { maxBuffer: 1 << 28 });
39
+ if (r.status !== 0) throw new Error(`ffmpeg failed on ${file}: ${r.stderr}`);
40
+ const pcm = new Int16Array(r.stdout.buffer, r.stdout.byteOffset, r.stdout.length >> 1);
41
+ let off;
42
+ if (align === "end") off = pcm.length / 8000;
43
+ else if (align === "peak") {
44
+ let best = -1, at = 0;
45
+ for (let o = 0; o + 80 <= pcm.length; o += 80) {
46
+ let s = 0; for (let i = o; i < o + 80; i++) s += (pcm[i] / 32768) ** 2;
47
+ if (s > best) { best = s; at = o + 40; }
48
+ }
49
+ off = at / 8000;
50
+ } else throw new Error(`unknown align "${align}" (start, peak, end)`);
51
+ anchors.set(key, off);
52
+ return off;
53
+ }
54
+
55
+ // Cue file + scene events -> [{ t, file, align, volume, label, start, trim }], sorted by t.
56
+ export function buildCues(spec, { sfxDir, eventsDir } = {}) {
57
+ const kinds = { ...DEFAULT_KINDS };
58
+ for (const [k, f] of Object.entries(spec.kinds ?? {})) kinds[k] = [f, kinds[k]?.[1] ?? 0.6];
59
+ const at = (f) => (isAbsolute(f) ? f : join(sfxDir, f));
60
+ const fromKind = (kind, i) => { const [f, v] = kinds[kind]; return [Array.isArray(f) ? f[i % f.length] : f, v]; };
61
+ const cues = [];
62
+ (spec.cues ?? []).forEach((c, i) => {
63
+ if (!c.file && !kinds[c.kind]) return console.error(`skip cue at ${c.t}s: no file and unknown kind "${c.kind}"`);
64
+ const [f, v] = c.file ? [c.file, 0.6] : fromKind(c.kind, i);
65
+ cues.push({ t: c.t, file: at(f), align: c.align ?? "start", volume: c.volume ?? v, label: c.label ?? c.kind ?? "" });
66
+ });
67
+ for (const [id, start] of Object.entries(spec.slots ?? {})) {
68
+ const p = eventsDir && join(eventsDir, `${id}.motion.json`);
69
+ if (!p || !existsSync(p)) continue;
70
+ JSON.parse(readFileSync(p, "utf8")).events?.forEach((e, i) => {
71
+ if (!kinds[e.kind]) return console.error(`skip ${id} event at ${e.t}s: unknown kind "${e.kind}"`);
72
+ const [f, v] = fromKind(e.kind, i);
73
+ cues.push({ t: Number((start + e.t).toFixed(3)), file: at(f), align: "start", volume: v, label: `${id}: ${e.note ?? e.kind}` });
74
+ });
75
+ }
76
+ for (const c of cues) {
77
+ if (!existsSync(c.file)) throw new Error(`missing sound file: ${c.file}`);
78
+ const s = c.t - anchorOffset(c.file, c.align);
79
+ c.start = Math.max(0, s);
80
+ c.trim = Math.max(0, -s); // a sound whose lead-in falls before 0 loses that part
81
+ }
82
+ return cues.sort((a, b) => a.t - b.t);
83
+ }
84
+
85
+ export function mixCues(cues, { duration, outDir }) {
86
+ mkdirSync(outDir, { recursive: true });
87
+ const out = join(outDir, "sfx.wav");
88
+ const args = ["-v", "error", "-y"];
89
+ let graph;
90
+ if (!cues.length) {
91
+ args.push("-f", "lavfi", "-i", `anullsrc=r=48000:cl=stereo:d=${duration}`);
92
+ graph = "[0:a]anull[out]";
93
+ } else {
94
+ // ponytail: one ffmpeg input per cue; hundreds are fine, thousands would want pre-mixed buses.
95
+ cues.forEach((c) => args.push("-i", c.file));
96
+ const legs = cues.map((c, i) => `[${i}:a]aresample=48000,aformat=channel_layouts=stereo,atrim=start=${c.trim.toFixed(4)},asetpts=PTS-STARTPTS,volume=${c.volume},adelay=${Math.round(c.start * 1000)}:all=1[a${i}]`);
97
+ graph = `${legs.join(";")};${cues.map((_, i) => `[a${i}]`).join("")}amix=inputs=${cues.length}:normalize=0:duration=longest,apad=whole_dur=${duration},atrim=0:${duration}[out]`;
98
+ }
99
+ const r = spawnSync("ffmpeg", [...args, "-filter_complex", graph, "-map", "[out]", "-c:a", "pcm_s16le", out], { encoding: "utf8" });
100
+ if (r.status !== 0) throw new Error(`ffmpeg mix failed: ${r.stderr}`);
101
+ const rows = cues.map((c) => `| ${c.t.toFixed(3)} | ${c.align} | ${c.start.toFixed(3)} | ${c.volume} | ${c.file.split("/").slice(-2).join("/")} | ${c.label} |`);
102
+ writeFileSync(join(outDir, "sfx-cuesheet.md"), `# SFX cue sheet\n\n${cues.length} cues, ${duration}s.\n\n| t | anchor | file starts | volume | sound | label |\n|---|---|---|---|---|---|\n${rows.join("\n")}\n`);
103
+ return out;
104
+ }
105
+
106
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
107
+ const [file, ...rest] = process.argv.slice(2);
108
+ const opt = (n, d) => { const i = rest.indexOf(n); return i >= 0 ? rest[i + 1] : d; };
109
+ if (!file) { console.error("Usage: sfx-cues.mjs <sfx_cues.json> [--sfx-dir dir] [--events dir] [--out dir]"); process.exit(1); }
110
+ const spec = JSON.parse(readFileSync(file, "utf8"));
111
+ if (!(spec.duration > 0)) { console.error("sfx_cues.json needs a positive duration"); process.exit(1); }
112
+ const here = dirname(fileURLToPath(import.meta.url));
113
+ const cues = buildCues(spec, { sfxDir: resolve(opt("--sfx-dir", join(here, "../assets/sfx"))), eventsDir: opt("--events") && resolve(opt("--events")) });
114
+ const out = mixCues(cues, { duration: spec.duration, outDir: resolve(opt("--out", dirname(resolve(file)))) });
115
+ console.log(JSON.stringify({ ok: true, out, cues: cues.length }, null, 2));
116
+ }
@@ -0,0 +1,76 @@
1
+ // sfx-cues places each sound by its anchor (start, peak, end) and mixes one sfx.wav.
2
+ // Fixtures are tiny ffmpeg tones in a temp folder; nothing is downloaded.
3
+ import test from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { spawnSync } from "node:child_process";
9
+ import { anchorOffset, buildCues, mixCues } from "./sfx-cues.mjs";
10
+
11
+ const dir = mkdtempSync(join(tmpdir(), "sfx-cues-"));
12
+ const sfx = join(dir, "sfx");
13
+ mkdirSync(sfx);
14
+ const tone = (name, d, env) => {
15
+ const r = spawnSync("ffmpeg", ["-v", "error", "-y", "-f", "lavfi", "-i", `aevalsrc='(${env})*sin(2*PI*800*t)':s=48000:d=${d}`, join(sfx, name)]);
16
+ assert.equal(r.status, 0, r.stderr?.toString());
17
+ };
18
+ tone("click.wav", 0.05, "0.8");
19
+ tone("whoosh.wav", 0.6, "0.8*(1-abs(t-0.3)/0.3)"); // loudest at 0.3 s
20
+ tone("riser.wav", 0.5, "0.8*t/0.5"); // ends at full level
21
+
22
+ // RMS per 10 ms window of a mono decode.
23
+ function energy(file) {
24
+ const r = spawnSync("ffmpeg", ["-v", "error", "-i", file, "-ac", "1", "-ar", "8000", "-f", "s16le", "-"], { maxBuffer: 1 << 28 });
25
+ const pcm = new Int16Array(r.stdout.buffer, r.stdout.byteOffset, r.stdout.length >> 1), n = 80, e = [];
26
+ for (let o = 0; o + n <= pcm.length; o += n) { let s = 0; for (let i = o; i < o + n; i++) s += (pcm[i] / 32768) ** 2; e.push(Math.sqrt(s / n)); }
27
+ return e;
28
+ }
29
+ const win = (e, a, b) => e.slice(Math.round(a * 100), Math.round(b * 100));
30
+
31
+ test("anchorOffset: start is 0, peak is the loudest point, end is the length", () => {
32
+ assert.equal(anchorOffset(join(sfx, "click.wav"), "start"), 0);
33
+ assert.ok(Math.abs(anchorOffset(join(sfx, "whoosh.wav"), "peak") - 0.3) < 0.02);
34
+ assert.ok(Math.abs(anchorOffset(join(sfx, "riser.wav"), "end") - 0.5) < 0.02);
35
+ });
36
+
37
+ test("mix: a click starts, a whoosh peaks, and a riser ends on its cue, within one frame", () => {
38
+ const cues = { duration: 3, cues: [
39
+ { t: 0.5, file: "click.wav", align: "start", label: "pop" },
40
+ { t: 1.5, file: "whoosh.wav", align: "peak", label: "cut" },
41
+ { t: 2.5, file: "riser.wav", align: "end", label: "into the hit" },
42
+ ] };
43
+ const placed = buildCues(cues, { sfxDir: sfx });
44
+ const out = join(dir, "out");
45
+ mixCues(placed, { duration: 3, outDir: out });
46
+ const e = energy(join(out, "sfx.wav")), frame = 1 / 30;
47
+ const firstLoud = win(e, 0.3, 0.8).findIndex((v) => v > 0.05) / 100 + 0.3;
48
+ assert.ok(Math.abs(firstLoud - 0.5) < frame, `click at ${firstLoud}`);
49
+ const w = win(e, 1.0, 2.0), peak = w.indexOf(Math.max(...w)) / 100 + 1.0;
50
+ assert.ok(Math.abs(peak - 1.5) < frame, `whoosh peak at ${peak}`);
51
+ const r = win(e, 2.0, 3.0), lastLoud = (r.length - 1 - [...r].reverse().findIndex((v) => v > 0.05)) / 100 + 2.0;
52
+ assert.ok(Math.abs(lastLoud - 2.5) < frame, `riser ends at ${lastLoud}`);
53
+ const sheet = readFileSync(join(out, "sfx-cuesheet.md"), "utf8");
54
+ assert.match(sheet, /into the hit/);
55
+ assert.equal(spawnSync("ffprobe", ["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", join(out, "sfx.wav")], { encoding: "utf8" }).stdout.trim().slice(0, 3), "3.0");
56
+ });
57
+
58
+ test("scene events: local times shift by the slot start and map kinds to files", () => {
59
+ const comp = join(dir, "composition");
60
+ mkdirSync(comp, { recursive: true });
61
+ writeFileSync(join(comp, "02-reveal.motion.json"), JSON.stringify({ events: [{ t: 0.25, kind: "click", note: "button" }, { t: 0.5, kind: "nope" }] }));
62
+ const placed = buildCues({ duration: 4, slots: { "02-reveal": 2 }, kinds: { click: "click.wav" }, cues: [] }, { sfxDir: sfx, eventsDir: comp });
63
+ assert.equal(placed.length, 1);
64
+ assert.equal(placed[0].t, 2.25);
65
+ assert.equal(placed[0].file, join(sfx, "click.wav"));
66
+ assert.equal(placed[0].skipped, undefined);
67
+ });
68
+
69
+ test("CLI writes sfx.wav and the cue sheet next to the cue file", () => {
70
+ const run = join(dir, "run");
71
+ mkdirSync(run);
72
+ writeFileSync(join(run, "sfx_cues.json"), JSON.stringify({ duration: 1, cues: [{ t: 0.2, file: "click.wav" }] }));
73
+ const r = spawnSync(process.execPath, [join(import.meta.dirname, "sfx-cues.mjs"), join(run, "sfx_cues.json"), "--sfx-dir", sfx], { encoding: "utf8" });
74
+ assert.equal(r.status, 0, r.stderr);
75
+ assert.ok(existsSync(join(run, "sfx.wav")) && existsSync(join(run, "sfx-cuesheet.md")));
76
+ });