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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/commands/plan.md +1 -0
- package/package.json +1 -1
- package/skills/luckiest-plan/SKILL.md +1 -0
- package/skills/luckiest-research/CHANGELOG.md +5 -0
- package/skills/luckiest-research/SKILL.md +8 -4
- package/skills/luckiest-video-studio/ATTRIBUTION.md +11 -0
- package/skills/luckiest-video-studio/CHANGELOG.md +56 -0
- package/skills/luckiest-video-studio/SKILL.md +35 -7
- package/skills/luckiest-video-studio/references/audio.md +61 -10
- package/skills/luckiest-video-studio/references/lessons.md +44 -0
- package/skills/luckiest-video-studio/references/step-2-plan.md +32 -1
- package/skills/luckiest-video-studio/references/step-3-compose.md +28 -0
- package/skills/luckiest-video-studio/references/step-4-deliver.md +24 -1
- package/skills/luckiest-video-studio/references/storyboard.md +24 -3
- package/skills/luckiest-video-studio/scripts/qa.mjs +167 -5
- package/skills/luckiest-video-studio/scripts/qa.test.mjs +99 -1
- package/skills/luckiest-video-studio/scripts/render-scenes.mjs +196 -71
- package/skills/luckiest-video-studio/scripts/render-scenes.test.mjs +109 -0
- package/skills/luckiest-video-studio/scripts/sfx-cues.mjs +116 -0
- package/skills/luckiest-video-studio/scripts/sfx-cues.test.mjs +76 -0
- package/skills/luckiest-video-studio/scripts/stamp-format.mjs +26 -0
- package/skills/luckiest-video-studio/scripts/storyboard.schema.json +23 -0
- package/skills/luckiest-video-studio/templates/logo-line-draw.html +12 -5
- package/skills/luckiest-video-studio-art-director/ATTRIBUTION.md +1 -0
- package/skills/luckiest-video-studio-art-director/CHANGELOG.md +8 -0
- package/skills/luckiest-video-studio-art-director/SKILL.md +18 -5
- package/skills/luckiest-video-studio-cards/CHANGELOG.md +18 -0
- package/skills/luckiest-video-studio-cards/SKILL.md +22 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-agenda.html +17 -6
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-bigtype.html +15 -5
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-board.html +20 -6
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-checklist.html +27 -6
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-compare.html +40 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-deck.html +26 -6
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-dossier.html +28 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-filmstrip.html +36 -5
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-grid6.html +24 -6
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-indexset.html +34 -6
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-maplegend.html +39 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-matrix.html +35 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-numlist.html +26 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-outline.html +26 -6
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-phases.html +34 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-quad.html +41 -10
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-roadmap.html +66 -17
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-stepper.html +42 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-toc.html +26 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-overview-vtimeline.html +35 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-bracket.html +16 -4
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-circle.html +21 -5
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-classified.html +25 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-collage.html +27 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-correction.html +19 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-crt.html +15 -4
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-cutout.html +18 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-fullbleed.html +18 -5
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-highlight.html +15 -5
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-indexcard.html +18 -5
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-map.html +20 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-markerbox.html +12 -5
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-numeral.html +21 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-split.html +25 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-stacked.html +26 -10
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-stamp.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-ticker.html +26 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-section-tornstrip.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-annotated.html +29 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-arc.html +25 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-axis.html +34 -14
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-bar.html +17 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-bracket.html +24 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-calendar.html +24 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-callout.html +24 -5
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-columns.html +27 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-corner.html +22 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-dots.html +27 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-engraving.html +30 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-footnote.html +20 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-frac.html +24 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-heat.html +22 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-isotype.html +29 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-ledger.html +18 -6
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-left.html +27 -10
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-magnifier.html +21 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-map.html +21 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-meter.html +26 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-multiplier.html +27 -12
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-odo.html +18 -5
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-pack.html +41 -17
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-photo.html +18 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-pie.html +25 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-postage.html +24 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-pricetag.html +25 -11
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-quote.html +21 -8
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-ranking.html +25 -10
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-receipt.html +24 -12
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-ribbon.html +34 -12
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-semi.html +22 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-spotlight.html +23 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-stamp.html +24 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-steps.html +30 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-tape.html +33 -12
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-torn.html +22 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-trend.html +23 -9
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-vs.html +30 -10
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat-window.html +38 -25
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-stat.html +23 -4
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier1/t1-timeline.html +41 -6
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-label.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-bracket.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-caption.html +12 -5
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-circle.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-definition.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-footnote.html +11 -4
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-magnify.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-pin.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-pullquote.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-spotlight.html +15 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-sticky.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-tag.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lb-underline.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lower-third.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-badge.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-barslab.html +26 -7
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-bracket.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-clapper.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-highlight.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-indextab.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-rail.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-ribbon.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-stacked.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-stamp.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-sticky.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/cards/tier2/t2-lt-typecard.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/style.json +1 -1
- package/skills/luckiest-video-studio-cards/packs/01-paper-collage/tokens.css +18 -8
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-agenda.html +23 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-beforeafter.html +30 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-bento.html +34 -8
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-bigleft.html +28 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-callouts.html +36 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-cardrow5.html +17 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-checklist.html +11 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-cycle.html +30 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-dashboard.html +32 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-deck2.html +21 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-deck3.html +18 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-duallist.html +24 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-featurecompare.html +19 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-featuregrid.html +25 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-flashcards.html +22 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-flow.html +23 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-funnel.html +19 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-gantt.html +22 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-grid6.html +17 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-grid9.html +15 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-iconlist.html +18 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-journey.html +41 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-kpirow.html +24 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-layers.html +20 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-legend.html +19 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-levels.html +19 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-logowall.html +18 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-mappoints.html +18 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-matrix.html +24 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-milestones.html +38 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-mindmap.html +35 -12
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-nested.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-network.html +76 -31
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-notifs.html +21 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-numlist.html +16 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-orgchart.html +36 -16
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-outline.html +17 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-phases.html +23 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-pipeline.html +25 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-pricing.html +36 -8
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-profiles.html +20 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-proscons.html +21 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-pyramid.html +17 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-quad.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-quotegrid.html +19 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-ranked.html +19 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-roadmap.html +27 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-spectrum.html +29 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-split3.html +24 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-spotlightcard.html +34 -8
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-statgrid.html +17 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-steps.html +25 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-summary.html +22 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-tiercompare.html +29 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-timeline.html +26 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-toc.html +15 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-tree.html +40 -15
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-tricards.html +22 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-venn.html +34 -9
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-overview-vstable.html +24 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-ascend.html +10 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-aurora-bars.html +12 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-beam.html +11 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-boxed.html +26 -13
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-breath.html +14 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-chaptercard.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-column-light.html +17 -9
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-constellation.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-countdown.html +10 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-curtain.html +9 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-dawn.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-depth.html +18 -12
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-divider.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-drift-grid.html +11 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-duotone.html +16 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-eclipse.html +11 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-emblem.html +12 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-flare-sweep.html +14 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-glow-rule.html +22 -13
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-gradient-word.html +15 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-gridfloor.html +16 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-halo.html +16 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-horizon.html +16 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-index.html +25 -9
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-kinetic.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-lensflare.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-liquid.html +12 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-marquee.html +34 -25
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-mesh.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-monolith.html +9 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-node-title.html +17 -1
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-numeral.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-orb.html +12 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-orbit.html +9 -1
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-particles.html +7 -1
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-pill.html +7 -1
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-portal.html +14 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-prism.html +15 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-question.html +14 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-quoteopen.html +11 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-reveal-mask.html +6 -1
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-ribbon.html +12 -1
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-ripple.html +8 -1
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-searchbar.html +15 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-slab.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-sphere-top.html +12 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-split-reveal.html +24 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-splitpanel.html +27 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-spotlight-cone.html +16 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-spotlight.html +10 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-stack.html +17 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-stacked-words.html +14 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-starfield.html +7 -1
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-sweep.html +11 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-tunnel.html +14 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-type-scale.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-typed.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-underline-grow.html +13 -1
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-waveform.html +11 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-section-wordmark.html +11 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-annot.html +22 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-beforeafter.html +23 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-bigpercent.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-bracketstat.html +13 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-bubble.html +7 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-cbars.html +8 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-clockface.html +8 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-column.html +12 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-compass.html +7 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-counterpill.html +11 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-curve.html +22 -10
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-delta.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-dial.html +9 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-donut.html +15 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-dotgrid.html +14 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-figure.html +9 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-fillshape.html +11 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-fraction.html +11 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-funnel.html +24 -8
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-growtharrow.html +18 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-heatcell.html +8 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-isotype.html +16 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-layered.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-mega.html +9 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-meter.html +9 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-milestone.html +9 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-money.html +16 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-multiplier.html +12 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-multiring.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-needle.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-nodes.html +50 -9
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-odometer.html +9 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-orbitmoons.html +21 -8
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-orbitstat.html +9 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-pair.html +18 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-percentile.html +11 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-pictobar.html +11 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-polar.html +12 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-quotestat.html +15 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-radialbars.html +18 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-range.html +12 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-rank.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-ratio.html +10 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-ring.html +10 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-risewall.html +18 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-score.html +11 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-segbar.html +9 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-semigauge.html +9 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-sparkline.html +9 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-spectrum.html +9 -2
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-split.html +14 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-stackedbars.html +9 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-tickerstat.html +18 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-tiles.html +18 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-timer.html +14 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-trendline.html +18 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-underscore.html +9 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-versus.html +18 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-waffle.html +18 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier1/t1-stat-waterfall.html +9 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-badge.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-brace.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-bracket.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-bubble.html +16 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-byline.html +17 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-callout.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-category.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-checkmark.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-chipstat.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-comparemini.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-coord.html +26 -8
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-counter.html +17 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-crosshair.html +18 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-dashed.html +19 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-definition.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-deltachip.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-eyebrow.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-fact.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-flag.html +20 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-formula.html +17 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-hash.html +17 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-highlightbox.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-iconchip.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-info.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-infocard.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-keycap.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-keyword.html +12 -3
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-leader.html +16 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-legend.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-measure.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-meterchip.html +16 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-metric.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-micropie.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-minibar.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-node.html +28 -18
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-note.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-numbercallout.html +18 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-percent.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-pointer.html +31 -22
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-price.html +20 -8
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-progress.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-pulse.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-qmark.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-quote.html +16 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-rating.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-ribbon.html +19 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-ringmark.html +27 -17
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-search.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-sparkchip.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-spec.html +25 -13
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-status.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-step.html +16 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-tag.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-takeaway.html +13 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-tcode.html +18 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-timestamp.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-toast.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-tooltip.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-underline.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lb-warning.html +16 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-avatar.html +12 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-badgecard.html +12 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-barslide.html +11 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-bracket.html +20 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-brandbar.html +13 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-bug.html +12 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-capsule.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-chapterbar.html +12 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-chip.html +11 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-cinemastrip.html +20 -8
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-cite.html +12 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-clockname.html +11 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-coordbar.html +16 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-corner.html +11 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-cornerframe.html +16 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-countbar.html +13 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-dividername.html +26 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-dotline.html +11 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-doublecard.html +12 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-dual.html +12 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-edgebar.html +11 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-flipcard.html +15 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-geopin.html +11 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-glasscard.html +11 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-gradient.html +11 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-gradientbar.html +12 -4
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-handlebar.html +11 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-infochips.html +11 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-lensbar.html +25 -12
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-live.html +12 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-location.html +14 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-logolock.html +14 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-micname.html +14 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-minicard.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-minimal.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-nameplate3d.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-neonframe.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-neonpill.html +13 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-nowplaying.html +15 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-orbitbadge.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-pill.html +14 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-portraitframe.html +14 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-progressname.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-quote.html +15 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-ratingname.html +14 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-ribbon.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-sidebracket.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-slidetab.html +15 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-social.html +14 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-spotlightname.html +14 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-stacked.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-statline.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-tagrow.html +14 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-thinstrip.html +18 -8
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-ticker.html +29 -7
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-titlebar.html +19 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-trust.html +16 -8
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-typedname.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-underbar.html +13 -5
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/cards/tier2/t2-lt-waveband.html +14 -6
- package/skills/luckiest-video-studio-cards/packs/02-aurora-glass/tokens.css +18 -8
- package/skills/luckiest-video-studio-cards/packs/_blueprint/cards/tier1/t1-stat.html +4 -2
- package/skills/luckiest-video-studio-cards/packs/_blueprint/cards/tier2/t2-list.html +5 -2
- package/skills/luckiest-video-studio-cards/packs/_blueprint/tokens.css +12 -4
- package/skills/luckiest-video-studio-cards/references/card-contract.md +15 -1
- package/skills/luckiest-video-studio-cards/scripts/qa-cards.mjs +14 -8
- package/skills/luckiest-video-studio-cards/templates/glass-popout/index.html +35 -11
- package/skills/luckiest-video-studio-cards/templates/glass-popout/meta.json +6 -1
- package/skills/luckiest-video-studio-cards/templates/graph-paper/index.html +15 -10
- package/skills/luckiest-video-studio-cards/templates/graph-paper/meta.json +7 -2
- package/skills/luckiest-video-studio-motion/ATTRIBUTION.md +4 -0
- package/skills/luckiest-video-studio-motion/CHANGELOG.md +9 -0
- package/skills/luckiest-video-studio-motion/SKILL.md +7 -3
- package/skills/luckiest-video-studio-motion/references/vocabulary.md +21 -0
- package/skills/luckiest-video-studio-motion/templates/spring.js +50 -0
- package/skills/luckiest-video-studio-motion/templates/spring.test.mjs +51 -0
- package/skills/luckiest-video-studio-recreate/ATTRIBUTION.md +2 -0
- package/skills/luckiest-video-studio-recreate/CHANGELOG.md +11 -0
- package/skills/luckiest-video-studio-recreate/SKILL.md +46 -7
- package/skills/luckiest-video-studio-reel/CHANGELOG.md +6 -0
- package/skills/luckiest-video-studio-reel/SKILL.md +9 -4
- package/skills/luckiest-video-studio-reel/templates/reel.html +36 -18
- package/skills/luckiest-video-studio-reference/CHANGELOG.md +4 -0
- package/skills/luckiest-video-studio-reference/SKILL.md +10 -3
- package/skills/luckiest-video-studio-review/ATTRIBUTION.md +4 -0
- package/skills/luckiest-video-watcher/ATTRIBUTION.md +37 -0
- package/skills/luckiest-video-watcher/CHANGELOG.md +90 -0
- package/skills/luckiest-video-watcher/LICENSE-claude-video +21 -0
- package/skills/luckiest-video-watcher/SKILL.md +201 -0
- package/skills/luckiest-video-watcher/references/watch-guide.md +159 -0
- package/skills/luckiest-video-watcher/scripts/config.py +174 -0
- package/skills/luckiest-video-watcher/scripts/download.py +207 -0
- package/skills/luckiest-video-watcher/scripts/frames.py +758 -0
- package/skills/luckiest-video-watcher/scripts/gemini.py +162 -0
- package/skills/luckiest-video-watcher/scripts/local_whisperx.py +147 -0
- package/skills/luckiest-video-watcher/scripts/runtime.py +35 -0
- package/skills/luckiest-video-watcher/scripts/setup.py +338 -0
- package/skills/luckiest-video-watcher/scripts/transcribe.py +113 -0
- package/skills/luckiest-video-watcher/scripts/watch.py +430 -0
- 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
|
-
//
|
|
4
|
-
|
|
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 {
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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 {
|
|
97
|
-
}
|
|
189
|
+
return { clips, t, tag };
|
|
190
|
+
}
|
|
98
191
|
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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} | ${
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
});
|