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
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Probe video metadata and extract frames at an auto-scaled fps.
|
|
3
|
+
|
|
4
|
+
Auto-fps targets a frame budget, not a fixed rate. Token cost scales with frame
|
|
5
|
+
count, so budget-by-duration keeps short videos dense and long videos capped.
|
|
6
|
+
When a user-specified range is passed, focused-mode budgets denser (they are
|
|
7
|
+
zooming in for detail).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from runtime import configure_stdio, run_text, diagnostic
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
MAX_FPS = 2.0
|
|
24
|
+
SCENE_THRESHOLD = 0.20
|
|
25
|
+
# Keep scene-detection results once we have at least this many distinct shots.
|
|
26
|
+
# Below this the video is effectively static (screen recording, talking head),
|
|
27
|
+
# so we fall back to uniform sampling. Matching the reference fork's behaviour,
|
|
28
|
+
# this is a low floor — NOT the frame budget — so normal videos with cuts use
|
|
29
|
+
# the (single-pass) scene engine instead of paying for a wasted second decode.
|
|
30
|
+
SCENE_MIN_FRAMES = 8
|
|
31
|
+
# Below this many decoded keyframes a clip is too sparse for keyframe coverage
|
|
32
|
+
# (very short or oddly encoded), so the cheap tier falls back to uniform.
|
|
33
|
+
KEYFRAME_MIN = 4
|
|
34
|
+
MAX_READ_DIMENSION = 1998
|
|
35
|
+
# Frame-delta dedup: downscale each frame to a DEDUP_THUMB x DEDUP_THUMB
|
|
36
|
+
# RGB thumbnail and treat two frames as near-identical when their mean
|
|
37
|
+
# per-channel difference (0-255) is at or below DEDUP_THRESHOLD. This catches
|
|
38
|
+
# held slides, but subtle code/text changes can be missed at this thumbnail size. Unlike a within-frame
|
|
39
|
+
# perceptual hash, this distinguishes flat frames (solid slides, fades) by color.
|
|
40
|
+
DEDUP_THUMB = 16
|
|
41
|
+
DEDUP_THRESHOLD = 2.0
|
|
42
|
+
SHOWINFO_TS_RE = re.compile(r"pts_time:([^\s]+)")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _scale_filter(resolution: int) -> str:
|
|
46
|
+
return (
|
|
47
|
+
f"scale=w='min({resolution},iw)':h='min({MAX_READ_DIMENSION},ih)':"
|
|
48
|
+
"force_original_aspect_ratio=decrease:force_divisible_by=2"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _clamp_fps(fps: float, duration_seconds: float, max_frames: int) -> tuple[float, int]:
|
|
53
|
+
fps = min(fps, MAX_FPS)
|
|
54
|
+
target = min(max_frames, max(1, int(round(fps * duration_seconds))))
|
|
55
|
+
return fps, target
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_time(value: str | float | int | None) -> float | None:
|
|
59
|
+
"""Parse a finite non-negative SS, MM:SS, or HH:MM:SS time."""
|
|
60
|
+
if value is None or str(value).strip() == "":
|
|
61
|
+
return None
|
|
62
|
+
try:
|
|
63
|
+
parts = str(value).strip().split(":")
|
|
64
|
+
if len(parts) > 3:
|
|
65
|
+
raise ValueError()
|
|
66
|
+
numbers = [float(part) for part in parts]
|
|
67
|
+
if any(not math.isfinite(n) or n < 0 for n in numbers):
|
|
68
|
+
raise ValueError()
|
|
69
|
+
if len(parts) > 1 and (any(n >= 60 for n in numbers[1:]) or any(n != int(n) for n in numbers[:-1])):
|
|
70
|
+
raise ValueError()
|
|
71
|
+
return sum(n * 60 ** i for i, n in enumerate(reversed(numbers)))
|
|
72
|
+
except ValueError:
|
|
73
|
+
raise SystemExit("Invalid time; expected finite non-negative SS, MM:SS, or HH:MM:SS.") from None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def format_time(seconds: float) -> str:
|
|
77
|
+
total = int(round(seconds))
|
|
78
|
+
hours, rem = divmod(total, 3600)
|
|
79
|
+
minutes, sec = divmod(rem, 60)
|
|
80
|
+
if hours:
|
|
81
|
+
return f"{hours}:{minutes:02d}:{sec:02d}"
|
|
82
|
+
return f"{minutes:02d}:{sec:02d}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_metadata(video_path: str) -> dict:
|
|
86
|
+
result = run_text([
|
|
87
|
+
"ffprobe", "-v", "error", "-print_format", "json", "-show_format", "-show_streams",
|
|
88
|
+
str(Path(video_path).resolve()),
|
|
89
|
+
], timeout=30)
|
|
90
|
+
if result.returncode != 0:
|
|
91
|
+
raise SystemExit(f"ffprobe failed: {diagnostic(result.stderr)}")
|
|
92
|
+
try:
|
|
93
|
+
data = json.loads(result.stdout)
|
|
94
|
+
streams, fmt = data["streams"], data.get("format", {})
|
|
95
|
+
if not isinstance(streams, list) or not streams:
|
|
96
|
+
raise ValueError()
|
|
97
|
+
video = next((s for s in streams if s.get("codec_type") == "video"), {})
|
|
98
|
+
audio = next((s for s in streams if s.get("codec_type") == "audio"), {})
|
|
99
|
+
# Some containers include a nonzero start PTS in format.duration.
|
|
100
|
+
# Stream durations describe elapsed source time and avoid overestimating
|
|
101
|
+
# a three-second clip whose first packet starts at seven seconds.
|
|
102
|
+
elapsed = [float(s['duration']) for s in (video, audio) if s.get('duration') not in (None, 'N/A')]
|
|
103
|
+
duration = max(elapsed) if elapsed else float(fmt.get('duration'))
|
|
104
|
+
if any(not math.isfinite(d) or d <= 0 for d in elapsed) or not math.isfinite(duration) or duration <= 0:
|
|
105
|
+
raise ValueError()
|
|
106
|
+
return {
|
|
107
|
+
"duration_seconds": duration, "width": video.get("width"), "height": video.get("height"),
|
|
108
|
+
"codec": video.get("codec_name"), "size_bytes": int(fmt.get("size") or 0),
|
|
109
|
+
"has_audio": bool(audio), "has_video": bool(video),
|
|
110
|
+
}
|
|
111
|
+
except (ValueError, TypeError, KeyError, AttributeError):
|
|
112
|
+
raise SystemExit(f"ffprobe returned invalid metadata, streams, or duration: {diagnostic(result.stderr)}") from None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@lru_cache(maxsize=8)
|
|
116
|
+
def _sync_option(executable: str) -> tuple[str, str]:
|
|
117
|
+
result = run_text([executable, "-hide_banner", "-h", "full"], timeout=30)
|
|
118
|
+
if result.returncode == 0:
|
|
119
|
+
help_text = result.stdout + result.stderr
|
|
120
|
+
# FFmpeg 9 lists options with stream specifiers, e.g. "-fps_mode[:<stream_spec>]".
|
|
121
|
+
if re.search(r"(?m)^\s*-fps_mode\b", help_text):
|
|
122
|
+
return "-fps_mode", "vfr"
|
|
123
|
+
if re.search(r"(?m)^\s*-vsync\b", help_text):
|
|
124
|
+
return "-vsync", "vfr"
|
|
125
|
+
raise SystemExit(f"{executable} did not advertise -fps_mode or -vsync; check the FFmpeg installation. {diagnostic(result.stderr)}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def sync_args() -> tuple[str, str]:
|
|
129
|
+
return _sync_option(shutil.which("ffmpeg") or "ffmpeg")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def validate_controls(resolution, max_frames, start_seconds, end_seconds, fps=None):
|
|
133
|
+
if not isinstance(resolution, int) or resolution < 2:
|
|
134
|
+
raise SystemExit("--resolution must be an integer of at least 2 pixels")
|
|
135
|
+
if max_frames is not None and (not isinstance(max_frames, int) or max_frames < 1):
|
|
136
|
+
raise SystemExit("--max-frames must be a positive integer")
|
|
137
|
+
start, end = parse_time(start_seconds) or 0.0, parse_time(end_seconds)
|
|
138
|
+
if end is not None and end <= start:
|
|
139
|
+
raise SystemExit("--end must be greater than --start")
|
|
140
|
+
if fps is not None and (not math.isfinite(fps) or fps <= 0):
|
|
141
|
+
raise SystemExit("--fps must be finite and greater than zero")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _emitted_frames(files, stderr, offset, reason):
|
|
145
|
+
try:
|
|
146
|
+
stamps = [float(m.group(1)) for m in SHOWINFO_TS_RE.finditer(stderr)]
|
|
147
|
+
# FFmpeg may log an extra filtered frame before an output cap stops it.
|
|
148
|
+
if len(stamps) < len(files) or any(not math.isfinite(t) for t in stamps):
|
|
149
|
+
raise ValueError()
|
|
150
|
+
if any(b < a for a, b in zip(stamps, stamps[1:])):
|
|
151
|
+
raise ValueError()
|
|
152
|
+
except ValueError:
|
|
153
|
+
raise SystemExit("ffmpeg output is missing valid frame timestamps; refusing to mislabel images.") from None
|
|
154
|
+
return [{"index": i, "timestamp_seconds": round(offset + stamps[i], 6),
|
|
155
|
+
"path": str(path), "reason": reason} for i, path in enumerate(files)]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def auto_fps(duration_seconds: float, max_frames: int = 100) -> tuple[float, int]:
|
|
159
|
+
"""Pick fps that targets a sensible frame budget for full-video scans."""
|
|
160
|
+
if duration_seconds <= 0:
|
|
161
|
+
return 1.0, 1
|
|
162
|
+
|
|
163
|
+
if duration_seconds <= 30:
|
|
164
|
+
target = min(max_frames, max(12, int(round(duration_seconds))))
|
|
165
|
+
elif duration_seconds <= 60:
|
|
166
|
+
target = min(max_frames, 40)
|
|
167
|
+
elif duration_seconds <= 180: # 3 min
|
|
168
|
+
target = min(max_frames, 60)
|
|
169
|
+
elif duration_seconds <= 600: # 10 min
|
|
170
|
+
target = min(max_frames, 80)
|
|
171
|
+
else:
|
|
172
|
+
target = max_frames
|
|
173
|
+
|
|
174
|
+
return _clamp_fps(target / duration_seconds, duration_seconds, max_frames)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def auto_fps_focus(duration_seconds: float, max_frames: int = 100) -> tuple[float, int]:
|
|
178
|
+
"""Denser budget for user-specified ranges — they are zooming in for detail."""
|
|
179
|
+
if duration_seconds <= 0:
|
|
180
|
+
return min(MAX_FPS, 2.0), 2
|
|
181
|
+
|
|
182
|
+
if duration_seconds <= 5:
|
|
183
|
+
target = min(max_frames, max(10, int(round(duration_seconds * 6))))
|
|
184
|
+
elif duration_seconds <= 15:
|
|
185
|
+
target = min(max_frames, max(30, int(round(duration_seconds * 4))))
|
|
186
|
+
elif duration_seconds <= 30:
|
|
187
|
+
target = min(max_frames, 60)
|
|
188
|
+
elif duration_seconds <= 60:
|
|
189
|
+
target = min(max_frames, 80)
|
|
190
|
+
elif duration_seconds <= 180:
|
|
191
|
+
target = max_frames
|
|
192
|
+
else:
|
|
193
|
+
target = max_frames
|
|
194
|
+
|
|
195
|
+
return _clamp_fps(target / duration_seconds, duration_seconds, max_frames)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def extract(
|
|
199
|
+
video_path: str, out_dir: Path, fps: float, resolution: int = 512,
|
|
200
|
+
max_frames: int = 100, start_seconds: float | None = None,
|
|
201
|
+
end_seconds: float | None = None,
|
|
202
|
+
) -> list[dict]:
|
|
203
|
+
validate_controls(resolution, max_frames, start_seconds, end_seconds, fps)
|
|
204
|
+
duration = get_metadata(video_path)["duration_seconds"]
|
|
205
|
+
start = start_seconds or 0.0
|
|
206
|
+
end = min(duration, end_seconds) if end_seconds is not None else duration
|
|
207
|
+
if end <= start:
|
|
208
|
+
raise SystemExit("Requested range is outside the video duration")
|
|
209
|
+
rate = min(fps, MAX_FPS, max_frames / (end - start))
|
|
210
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
for existing in out_dir.glob("frame_*.jpg"):
|
|
212
|
+
existing.unlink()
|
|
213
|
+
# Accurate input seeking resets t relative to the seek point. select keeps
|
|
214
|
+
# the first real frame in each time bucket; fps would shift the pixels.
|
|
215
|
+
vf = (f"select='isnan(prev_selected_t)+gt(floor(t*{rate:.17g}),"
|
|
216
|
+
f"floor(prev_selected_t*{rate:.17g}))',{_scale_filter(resolution)},showinfo")
|
|
217
|
+
result = run_text([
|
|
218
|
+
"ffmpeg", "-hide_banner", "-loglevel", "info", "-y",
|
|
219
|
+
"-ss", f"{start:.6f}", "-to", f"{end:.6f}", "-i", str(Path(video_path).resolve()),
|
|
220
|
+
"-vf", vf, *sync_args(), "-frames:v", str(max_frames), "-q:v", "4",
|
|
221
|
+
str(out_dir / "frame_%04d.jpg"),
|
|
222
|
+
])
|
|
223
|
+
if result.returncode != 0:
|
|
224
|
+
raise SystemExit(f"ffmpeg frame extraction failed: {diagnostic(result.stderr)}")
|
|
225
|
+
files = sorted(out_dir.glob("frame_*.jpg"))
|
|
226
|
+
if not files:
|
|
227
|
+
raise SystemExit("ffmpeg produced no video frames in the requested range")
|
|
228
|
+
return _emitted_frames(files, result.stderr, start, "uniform")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def extract_scene_candidates(
|
|
232
|
+
video_path: str,
|
|
233
|
+
out_dir: Path,
|
|
234
|
+
resolution: int = 512,
|
|
235
|
+
max_frames: int | None = 100,
|
|
236
|
+
start_seconds: float | None = None,
|
|
237
|
+
end_seconds: float | None = None,
|
|
238
|
+
threshold: float = SCENE_THRESHOLD,
|
|
239
|
+
) -> list[dict]:
|
|
240
|
+
"""Extract first frame plus ffmpeg scene-change frames.
|
|
241
|
+
|
|
242
|
+
When ``max_frames`` is set, ``-frames:v`` lets ffmpeg stop decoding once it
|
|
243
|
+
has emitted that many frames (early exit) and avoids writing extras that we
|
|
244
|
+
would only delete afterwards. ``None`` (uncapped "complete" detail) keeps
|
|
245
|
+
every detected shot, as the user explicitly opted in.
|
|
246
|
+
"""
|
|
247
|
+
validate_controls(resolution, max_frames, start_seconds, end_seconds)
|
|
248
|
+
if shutil.which("ffmpeg") is None:
|
|
249
|
+
raise SystemExit("ffmpeg is not installed. Install with: brew install ffmpeg")
|
|
250
|
+
|
|
251
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
for existing in out_dir.glob("frame_*.jpg"):
|
|
253
|
+
existing.unlink()
|
|
254
|
+
|
|
255
|
+
output_pattern = str(out_dir / "frame_%04d.jpg")
|
|
256
|
+
cmd: list[str] = [
|
|
257
|
+
"ffmpeg",
|
|
258
|
+
"-hide_banner",
|
|
259
|
+
"-loglevel", "info",
|
|
260
|
+
"-y",
|
|
261
|
+
]
|
|
262
|
+
if start_seconds is not None:
|
|
263
|
+
cmd += ["-ss", f"{start_seconds:.6f}"]
|
|
264
|
+
if end_seconds is not None:
|
|
265
|
+
cmd += ["-to", f"{end_seconds:.6f}"]
|
|
266
|
+
|
|
267
|
+
vf = f"select='eq(n\\,0)+gt(scene\\,{threshold})',{_scale_filter(resolution)},showinfo"
|
|
268
|
+
cmd += [
|
|
269
|
+
"-i", str(Path(video_path).resolve()),
|
|
270
|
+
"-vf", vf,
|
|
271
|
+
*sync_args(),
|
|
272
|
+
]
|
|
273
|
+
if max_frames is not None:
|
|
274
|
+
cmd += ["-frames:v", str(max_frames)]
|
|
275
|
+
cmd += [
|
|
276
|
+
"-q:v", "4",
|
|
277
|
+
output_pattern,
|
|
278
|
+
]
|
|
279
|
+
result = run_text(cmd)
|
|
280
|
+
if result.returncode != 0:
|
|
281
|
+
raise SystemExit(f"ffmpeg scene extraction failed: {result.stderr.strip()}")
|
|
282
|
+
|
|
283
|
+
out = _emitted_frames(sorted(out_dir.glob("frame_*.jpg")), result.stderr, start_seconds or 0.0, "scene-change")
|
|
284
|
+
if out:
|
|
285
|
+
out[0]["reason"] = "first-frame"
|
|
286
|
+
return out
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _even_indices(count: int, n: int) -> list[int]:
|
|
290
|
+
"""Indices of ``n`` evenly-spaced items out of ``count`` (first + last kept).
|
|
291
|
+
|
|
292
|
+
``n >= count`` returns every index; ``n == 1`` returns just the first.
|
|
293
|
+
"""
|
|
294
|
+
if n >= count:
|
|
295
|
+
return list(range(count))
|
|
296
|
+
if n <= 1:
|
|
297
|
+
return [0]
|
|
298
|
+
return [round(i * (count - 1) / (n - 1)) for i in range(n)]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def parse_timestamps(value: str | None) -> list[float]:
|
|
302
|
+
"""Parse a comma-separated list of times (SS, MM:SS, HH:MM:SS) into a
|
|
303
|
+
sorted, de-duplicated list of seconds. Empty/blank tokens are skipped;
|
|
304
|
+
an unparseable token raises (via :func:`parse_time`)."""
|
|
305
|
+
if not value:
|
|
306
|
+
return []
|
|
307
|
+
out: list[float] = []
|
|
308
|
+
for token in value.split(","):
|
|
309
|
+
token = token.strip()
|
|
310
|
+
if not token:
|
|
311
|
+
continue
|
|
312
|
+
seconds = parse_time(token)
|
|
313
|
+
if seconds is not None:
|
|
314
|
+
out.append(float(seconds))
|
|
315
|
+
return sorted(set(out))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def merge_frames(primary: list[dict], pinned: list[dict]) -> list[dict]:
|
|
319
|
+
"""Combine two frame lists into one chronological list and reindex 0..n-1.
|
|
320
|
+
|
|
321
|
+
``pinned`` frames (transcript cues) are never dropped — this is a plain
|
|
322
|
+
union, so the cap is enforced upstream by reserving budget for the cues.
|
|
323
|
+
"""
|
|
324
|
+
merged = sorted([*primary, *pinned], key=lambda f: f["timestamp_seconds"])
|
|
325
|
+
for i, frame in enumerate(merged):
|
|
326
|
+
frame["index"] = i
|
|
327
|
+
return merged
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def extract_at_timestamps(
|
|
331
|
+
video_path: str,
|
|
332
|
+
out_dir: Path,
|
|
333
|
+
timestamps: list[float],
|
|
334
|
+
resolution: int = 512,
|
|
335
|
+
max_frames: int | None = None,
|
|
336
|
+
start_seconds: float | None = None,
|
|
337
|
+
end_seconds: float | None = None,
|
|
338
|
+
) -> tuple[list[dict], dict]:
|
|
339
|
+
"""Grab exactly one frame at each requested timestamp (transcript cues).
|
|
340
|
+
|
|
341
|
+
Timestamps are absolute source seconds. Any falling outside an active
|
|
342
|
+
``[start, end]`` focus window are dropped. Files use a ``cue_*.jpg`` prefix
|
|
343
|
+
so they sit alongside detail-engine ``frame_*.jpg`` output without either
|
|
344
|
+
clobbering the other. When more cues than ``max_frames`` survive, they are
|
|
345
|
+
even-sampled (first + last kept) before extraction.
|
|
346
|
+
"""
|
|
347
|
+
validate_controls(resolution, max_frames, start_seconds, end_seconds)
|
|
348
|
+
if shutil.which("ffmpeg") is None:
|
|
349
|
+
raise SystemExit("ffmpeg is not installed. Install with: brew install ffmpeg")
|
|
350
|
+
|
|
351
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
352
|
+
for existing in out_dir.glob("cue_*.jpg"):
|
|
353
|
+
existing.unlink()
|
|
354
|
+
|
|
355
|
+
lo = start_seconds or 0.0
|
|
356
|
+
hi = end_seconds if end_seconds is not None else float("inf")
|
|
357
|
+
requested = sorted(set(parse_time(t) for t in timestamps))
|
|
358
|
+
in_window = [t for t in requested if lo <= t <= hi]
|
|
359
|
+
dropped = len(requested) - len(in_window)
|
|
360
|
+
|
|
361
|
+
if max_frames is not None and len(in_window) > max_frames:
|
|
362
|
+
points = [in_window[i] for i in _even_indices(len(in_window), max_frames)]
|
|
363
|
+
else:
|
|
364
|
+
points = in_window
|
|
365
|
+
|
|
366
|
+
out: list[dict] = []
|
|
367
|
+
for t in points:
|
|
368
|
+
path = out_dir / f"cue_{len(out):04d}.jpg"
|
|
369
|
+
cmd = [
|
|
370
|
+
"ffmpeg", "-hide_banner", "-loglevel", "info", "-y",
|
|
371
|
+
"-ss", f"{t:.6f}", "-i", str(Path(video_path).resolve()),
|
|
372
|
+
"-frames:v", "1", "-vf", f"{_scale_filter(resolution)},showinfo",
|
|
373
|
+
*sync_args(), "-q:v", "4", str(path),
|
|
374
|
+
]
|
|
375
|
+
result = run_text(cmd)
|
|
376
|
+
if result.returncode != 0:
|
|
377
|
+
raise SystemExit(f"ffmpeg cue extraction failed: {diagnostic(result.stderr)}")
|
|
378
|
+
if path.exists():
|
|
379
|
+
frame = _emitted_frames([path], result.stderr, t, "transcript-cue")[0]
|
|
380
|
+
frame["requested_timestamp_seconds"] = t
|
|
381
|
+
frame["index"] = len(out)
|
|
382
|
+
if frame["timestamp_seconds"] <= hi:
|
|
383
|
+
out.append(frame)
|
|
384
|
+
else:
|
|
385
|
+
path.unlink()
|
|
386
|
+
|
|
387
|
+
meta = {
|
|
388
|
+
"engine": "timestamps",
|
|
389
|
+
"candidate_count": len(requested),
|
|
390
|
+
"selected_count": len(out),
|
|
391
|
+
"dropped_out_of_window": dropped,
|
|
392
|
+
"fallback": False,
|
|
393
|
+
}
|
|
394
|
+
return out, meta
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _even_sample(candidates: list[dict], n: int) -> list[dict]:
|
|
398
|
+
"""Pick ``n`` evenly-spaced candidates (always including first and last),
|
|
399
|
+
delete the JPEGs we drop, and reindex the survivors 0..len-1.
|
|
400
|
+
|
|
401
|
+
Shared by every capped engine so all detail modes sample the same way:
|
|
402
|
+
detect all candidates across the full range, then thin down to the cap.
|
|
403
|
+
``n >= len(candidates)`` keeps everything (the uncapped / under-cap case).
|
|
404
|
+
"""
|
|
405
|
+
selected = [candidates[i] for i in _even_indices(len(candidates), n)]
|
|
406
|
+
|
|
407
|
+
keep_paths = {sel["path"] for sel in selected}
|
|
408
|
+
for cand in candidates:
|
|
409
|
+
if cand["path"] not in keep_paths:
|
|
410
|
+
try:
|
|
411
|
+
Path(cand["path"]).unlink()
|
|
412
|
+
except OSError:
|
|
413
|
+
pass
|
|
414
|
+
for i, frame in enumerate(selected):
|
|
415
|
+
frame["index"] = i
|
|
416
|
+
return selected
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _frame_delta(a: bytes, b: bytes) -> float:
|
|
420
|
+
"""Mean absolute per-pixel difference (0-255) between two RGB
|
|
421
|
+
thumbnails. Mismatched lengths are treated as maximally different so a
|
|
422
|
+
decode hiccup never collapses distinct frames."""
|
|
423
|
+
if not a or len(a) != len(b):
|
|
424
|
+
return float("inf")
|
|
425
|
+
return sum(abs(x - y) for x, y in zip(a, b)) / len(a)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _thumb_frames(paths: list[Path]) -> list[bytes]:
|
|
429
|
+
"""Decode every frame in ``paths`` to a small RGB thumbnail via one
|
|
430
|
+
ffmpeg pass over the JPEG sequence.
|
|
431
|
+
|
|
432
|
+
ffmpeg does the pixel decode (keeps us pure-stdlib); we slice the raw
|
|
433
|
+
RGB stream into one ``DEDUP_THUMB``-square thumbnail per frame.
|
|
434
|
+
Fail-open: any ffmpeg error, an unrecognized name, or a byte-count mismatch
|
|
435
|
+
returns ``[]`` so the caller skips dedup rather than breaking extraction.
|
|
436
|
+
"""
|
|
437
|
+
if not paths:
|
|
438
|
+
return []
|
|
439
|
+
paths = [Path(p) for p in paths]
|
|
440
|
+
m = re.match(r"(.*?)(\d+)(\.[A-Za-z0-9]+)$", paths[0].name)
|
|
441
|
+
if m is None:
|
|
442
|
+
return []
|
|
443
|
+
prefix, digits, ext = m.group(1), m.group(2), m.group(3)
|
|
444
|
+
pattern = str(paths[0].parent / f"{prefix}%0{len(digits)}d{ext}")
|
|
445
|
+
|
|
446
|
+
cmd = [
|
|
447
|
+
"ffmpeg",
|
|
448
|
+
"-hide_banner",
|
|
449
|
+
"-loglevel", "error",
|
|
450
|
+
"-start_number", str(int(digits)),
|
|
451
|
+
"-i", pattern,
|
|
452
|
+
"-vf", f"scale={DEDUP_THUMB}:{DEDUP_THUMB},format=rgb24",
|
|
453
|
+
"-f", "rawvideo",
|
|
454
|
+
"-",
|
|
455
|
+
]
|
|
456
|
+
try:
|
|
457
|
+
result = subprocess.run(cmd, capture_output=True)
|
|
458
|
+
except OSError:
|
|
459
|
+
return []
|
|
460
|
+
if result.returncode != 0:
|
|
461
|
+
return []
|
|
462
|
+
|
|
463
|
+
chunk = DEDUP_THUMB * DEDUP_THUMB * 3
|
|
464
|
+
data = result.stdout
|
|
465
|
+
if len(data) != chunk * len(paths):
|
|
466
|
+
return []
|
|
467
|
+
return [data[i * chunk:(i + 1) * chunk] for i in range(len(paths))]
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def dedupe_perceptual(
|
|
471
|
+
candidates: list[dict], threshold: float = DEDUP_THRESHOLD
|
|
472
|
+
) -> tuple[list[dict], int]:
|
|
473
|
+
"""Drop near-identical frames from a chronological candidate list.
|
|
474
|
+
|
|
475
|
+
Thumbnails the extracted JPEGs and greedily removes frames whose mean
|
|
476
|
+
per-pixel difference from the last kept one is within ``threshold``. Returns
|
|
477
|
+
``(survivors, dropped_count)``; a no-op (unchanged list) when thumbnails are
|
|
478
|
+
unavailable or there are fewer than two candidates.
|
|
479
|
+
"""
|
|
480
|
+
if len(candidates) <= 1:
|
|
481
|
+
return candidates, 0
|
|
482
|
+
thumbs = _thumb_frames([Path(c["path"]) for c in candidates])
|
|
483
|
+
return _dedupe_by_deltas(candidates, thumbs, threshold)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _dedupe_by_deltas(
|
|
487
|
+
candidates: list[dict], thumbs: list[bytes], threshold: float = DEDUP_THRESHOLD
|
|
488
|
+
) -> tuple[list[dict], int]:
|
|
489
|
+
"""Greedily drop frames within ``threshold`` mean per-pixel difference of the
|
|
490
|
+
last *kept* frame. Deletes dropped JPEGs and reindexes survivors 0..n-1 (same
|
|
491
|
+
cleanup contract as :func:`_even_sample`). Fail-open: if ``thumbs`` does not
|
|
492
|
+
line up 1:1 with ``candidates``, return them unchanged.
|
|
493
|
+
"""
|
|
494
|
+
if len(thumbs) != len(candidates) or len(candidates) <= 1:
|
|
495
|
+
return candidates, 0
|
|
496
|
+
|
|
497
|
+
kept = [candidates[0]]
|
|
498
|
+
last = thumbs[0]
|
|
499
|
+
dropped: list[dict] = []
|
|
500
|
+
for cand, thumb in zip(candidates[1:], thumbs[1:]):
|
|
501
|
+
if _frame_delta(thumb, last) <= threshold:
|
|
502
|
+
dropped.append(cand)
|
|
503
|
+
else:
|
|
504
|
+
kept.append(cand)
|
|
505
|
+
last = thumb
|
|
506
|
+
|
|
507
|
+
for cand in dropped:
|
|
508
|
+
try:
|
|
509
|
+
Path(cand["path"]).unlink()
|
|
510
|
+
except OSError:
|
|
511
|
+
pass
|
|
512
|
+
for i, frame in enumerate(kept):
|
|
513
|
+
frame["index"] = i
|
|
514
|
+
return kept, len(dropped)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def extract_scene_or_uniform(
|
|
518
|
+
video_path: str,
|
|
519
|
+
out_dir: Path,
|
|
520
|
+
fps: float,
|
|
521
|
+
target_frames: int,
|
|
522
|
+
resolution: int = 512,
|
|
523
|
+
max_frames: int | None = 100,
|
|
524
|
+
start_seconds: float | None = None,
|
|
525
|
+
end_seconds: float | None = None,
|
|
526
|
+
dedup: bool = True,
|
|
527
|
+
) -> tuple[list[dict], dict]:
|
|
528
|
+
"""Prefer scene selection, falling back to uniform only when the video is
|
|
529
|
+
effectively static (fewer than ``SCENE_MIN_FRAMES`` detected shots).
|
|
530
|
+
|
|
531
|
+
Scene cuts are detected across the *whole* range (uncapped), near-identical
|
|
532
|
+
frames are dropped (:func:`dedupe_perceptual`, unless ``dedup`` is False),
|
|
533
|
+
and the survivors are even-sampled down to ``max_frames`` via
|
|
534
|
+
:func:`_even_sample`, exactly like the keyframe engine. This costs a full
|
|
535
|
+
decode, but it guarantees coverage spans the entire clip — capping detection
|
|
536
|
+
with ``-frames:v`` instead would keep only the first ``max_frames`` cuts and
|
|
537
|
+
drop the tail of long videos (and could even fall below ``SCENE_MIN_FRAMES``
|
|
538
|
+
and misfire the uniform fallback on a cut-heavy clip).
|
|
539
|
+
"""
|
|
540
|
+
scene_frames = extract_scene_candidates(
|
|
541
|
+
video_path,
|
|
542
|
+
out_dir,
|
|
543
|
+
resolution=resolution,
|
|
544
|
+
max_frames=None,
|
|
545
|
+
start_seconds=start_seconds,
|
|
546
|
+
end_seconds=end_seconds,
|
|
547
|
+
)
|
|
548
|
+
scene_count = len(scene_frames)
|
|
549
|
+
if scene_count >= SCENE_MIN_FRAMES:
|
|
550
|
+
deduped, n_dropped = dedupe_perceptual(scene_frames) if dedup else (scene_frames, 0)
|
|
551
|
+
cap = len(deduped) if max_frames is None else max_frames
|
|
552
|
+
selected = _even_sample(deduped, cap)
|
|
553
|
+
return selected, {
|
|
554
|
+
"engine": "scene",
|
|
555
|
+
"candidate_count": scene_count,
|
|
556
|
+
"deduped_count": n_dropped,
|
|
557
|
+
"selected_count": len(selected),
|
|
558
|
+
"fallback": False,
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
fallback_cap = target_frames if max_frames is None else min(max_frames, target_frames)
|
|
562
|
+
frames = extract(
|
|
563
|
+
video_path,
|
|
564
|
+
out_dir,
|
|
565
|
+
fps=fps,
|
|
566
|
+
resolution=resolution,
|
|
567
|
+
max_frames=fallback_cap,
|
|
568
|
+
start_seconds=start_seconds,
|
|
569
|
+
end_seconds=end_seconds,
|
|
570
|
+
)
|
|
571
|
+
uniform_count = len(frames)
|
|
572
|
+
n_dropped = 0
|
|
573
|
+
if dedup:
|
|
574
|
+
frames, n_dropped = dedupe_perceptual(frames)
|
|
575
|
+
return frames, {
|
|
576
|
+
"engine": "uniform",
|
|
577
|
+
"candidate_count": uniform_count,
|
|
578
|
+
"deduped_count": n_dropped,
|
|
579
|
+
"selected_count": len(frames),
|
|
580
|
+
"fallback": True,
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def extract_keyframes(
|
|
585
|
+
video_path: str,
|
|
586
|
+
out_dir: Path,
|
|
587
|
+
resolution: int = 512,
|
|
588
|
+
max_frames: int | None = 50,
|
|
589
|
+
start_seconds: float | None = None,
|
|
590
|
+
end_seconds: float | None = None,
|
|
591
|
+
dedup: bool = True,
|
|
592
|
+
) -> tuple[list[dict], dict]:
|
|
593
|
+
"""Decode only keyframes (I-frames) — the cheap, near-instant tier.
|
|
594
|
+
|
|
595
|
+
``-skip_frame nokey`` makes ffmpeg reconstruct only keyframes, skipping all
|
|
596
|
+
P/B frames. Encoders emit keyframes at scene cuts, so these already
|
|
597
|
+
approximate "distinct moments". Near-identical frames are dropped
|
|
598
|
+
(:func:`dedupe_perceptual`, unless ``dedup`` is False); over-cap →
|
|
599
|
+
even-sample first→last; too few keyframes → uniform fallback.
|
|
600
|
+
"""
|
|
601
|
+
validate_controls(resolution, max_frames, start_seconds, end_seconds)
|
|
602
|
+
if shutil.which("ffmpeg") is None:
|
|
603
|
+
raise SystemExit("ffmpeg is not installed. Install with: brew install ffmpeg")
|
|
604
|
+
|
|
605
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
606
|
+
for existing in out_dir.glob("frame_*.jpg"):
|
|
607
|
+
existing.unlink()
|
|
608
|
+
|
|
609
|
+
output_pattern = str(out_dir / "frame_%04d.jpg")
|
|
610
|
+
cmd: list[str] = [
|
|
611
|
+
"ffmpeg",
|
|
612
|
+
"-hide_banner",
|
|
613
|
+
"-loglevel", "info",
|
|
614
|
+
"-y",
|
|
615
|
+
]
|
|
616
|
+
if start_seconds is not None:
|
|
617
|
+
cmd += ["-ss", f"{start_seconds:.6f}"]
|
|
618
|
+
if end_seconds is not None:
|
|
619
|
+
cmd += ["-to", f"{end_seconds:.6f}"]
|
|
620
|
+
cmd += [
|
|
621
|
+
"-skip_frame", "nokey",
|
|
622
|
+
"-i", str(Path(video_path).resolve()),
|
|
623
|
+
"-vf", f"{_scale_filter(resolution)},showinfo",
|
|
624
|
+
*sync_args(),
|
|
625
|
+
"-q:v", "4",
|
|
626
|
+
output_pattern,
|
|
627
|
+
]
|
|
628
|
+
result = run_text(cmd)
|
|
629
|
+
files = sorted(out_dir.glob("frame_*.jpg"))
|
|
630
|
+
if result.returncode != 0:
|
|
631
|
+
if files:
|
|
632
|
+
raise SystemExit(f"ffmpeg keyframe extraction failed after partial output: {diagnostic(result.stderr)}")
|
|
633
|
+
print(f"[watch] no keyframe candidates; trying normal decoding. {diagnostic(result.stderr, 700)}", file=sys.stderr)
|
|
634
|
+
candidates = _emitted_frames(files, result.stderr, start_seconds or 0.0, "keyframe")
|
|
635
|
+
|
|
636
|
+
# Too few keyframes → uniform fallback over the same range.
|
|
637
|
+
if len(candidates) < KEYFRAME_MIN:
|
|
638
|
+
for cand in candidates:
|
|
639
|
+
try:
|
|
640
|
+
Path(cand["path"]).unlink()
|
|
641
|
+
except OSError:
|
|
642
|
+
pass
|
|
643
|
+
meta = get_metadata(video_path)
|
|
644
|
+
full_duration = meta["duration_seconds"]
|
|
645
|
+
eff_start = start_seconds or 0.0
|
|
646
|
+
eff_end = min(end_seconds, full_duration) if end_seconds is not None else full_duration
|
|
647
|
+
eff_duration = max(0.0, eff_end - eff_start)
|
|
648
|
+
budget = max_frames if max_frames is not None else 100
|
|
649
|
+
fps, _ = auto_fps(eff_duration, max_frames=budget)
|
|
650
|
+
frames_out = extract(
|
|
651
|
+
video_path,
|
|
652
|
+
out_dir,
|
|
653
|
+
fps=fps,
|
|
654
|
+
resolution=resolution,
|
|
655
|
+
max_frames=budget,
|
|
656
|
+
start_seconds=start_seconds,
|
|
657
|
+
end_seconds=end_seconds,
|
|
658
|
+
)
|
|
659
|
+
uniform_count = len(frames_out)
|
|
660
|
+
n_dropped = 0
|
|
661
|
+
if dedup:
|
|
662
|
+
frames_out, n_dropped = dedupe_perceptual(frames_out)
|
|
663
|
+
return frames_out, {
|
|
664
|
+
"engine": "uniform",
|
|
665
|
+
"candidate_count": uniform_count,
|
|
666
|
+
"deduped_count": n_dropped,
|
|
667
|
+
"selected_count": len(frames_out),
|
|
668
|
+
"fallback": True,
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
# Detect-all, drop near-duplicates, then even-sample down to the cap (first +
|
|
672
|
+
# last always kept). ``max_frames is None`` (uncapped) keeps every keyframe.
|
|
673
|
+
candidate_count = len(candidates)
|
|
674
|
+
deduped, n_dropped = dedupe_perceptual(candidates) if dedup else (candidates, 0)
|
|
675
|
+
cap = len(deduped) if max_frames is None else max_frames
|
|
676
|
+
selected = _even_sample(deduped, cap)
|
|
677
|
+
return selected, {
|
|
678
|
+
"engine": "keyframe",
|
|
679
|
+
"candidate_count": candidate_count,
|
|
680
|
+
"deduped_count": n_dropped,
|
|
681
|
+
"selected_count": len(selected),
|
|
682
|
+
"fallback": False,
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
if __name__ == "__main__":
|
|
687
|
+
configure_stdio()
|
|
688
|
+
if len(sys.argv) < 3:
|
|
689
|
+
print(
|
|
690
|
+
"usage: frames.py <video-path> <out-dir> [--fps F] [--resolution W] "
|
|
691
|
+
"[--max-frames N] [--start T] [--end T] [--no-dedup]",
|
|
692
|
+
file=sys.stderr,
|
|
693
|
+
)
|
|
694
|
+
raise SystemExit(2)
|
|
695
|
+
|
|
696
|
+
video = sys.argv[1]
|
|
697
|
+
out = Path(sys.argv[2])
|
|
698
|
+
args = sys.argv[3:]
|
|
699
|
+
|
|
700
|
+
fps_override = None
|
|
701
|
+
resolution = 512
|
|
702
|
+
max_frames = 100
|
|
703
|
+
start_arg = None
|
|
704
|
+
end_arg = None
|
|
705
|
+
dedup = True
|
|
706
|
+
i = 0
|
|
707
|
+
while i < len(args):
|
|
708
|
+
if args[i] == "--fps":
|
|
709
|
+
fps_override = float(args[i + 1]); i += 2
|
|
710
|
+
elif args[i] == "--resolution":
|
|
711
|
+
resolution = int(args[i + 1]); i += 2
|
|
712
|
+
elif args[i] == "--max-frames":
|
|
713
|
+
max_frames = int(args[i + 1]); i += 2
|
|
714
|
+
elif args[i] == "--start":
|
|
715
|
+
start_arg = args[i + 1]; i += 2
|
|
716
|
+
elif args[i] == "--end":
|
|
717
|
+
end_arg = args[i + 1]; i += 2
|
|
718
|
+
elif args[i] == "--no-dedup":
|
|
719
|
+
dedup = False; i += 1
|
|
720
|
+
else:
|
|
721
|
+
i += 1
|
|
722
|
+
|
|
723
|
+
meta = get_metadata(video)
|
|
724
|
+
start_sec = parse_time(start_arg)
|
|
725
|
+
end_sec = parse_time(end_arg)
|
|
726
|
+
full_duration = meta["duration_seconds"]
|
|
727
|
+
|
|
728
|
+
effective_start = start_sec if start_sec is not None else 0.0
|
|
729
|
+
effective_end = end_sec if end_sec is not None else full_duration
|
|
730
|
+
effective_duration = max(0.0, effective_end - effective_start)
|
|
731
|
+
|
|
732
|
+
focused = start_sec is not None or end_sec is not None
|
|
733
|
+
if focused:
|
|
734
|
+
fps, target = auto_fps_focus(effective_duration, max_frames=max_frames)
|
|
735
|
+
else:
|
|
736
|
+
fps, target = auto_fps(effective_duration, max_frames=max_frames)
|
|
737
|
+
if fps_override is not None:
|
|
738
|
+
fps = fps_override
|
|
739
|
+
target = max(1, int(round(fps * effective_duration)))
|
|
740
|
+
|
|
741
|
+
frames = extract(
|
|
742
|
+
video, out,
|
|
743
|
+
fps=fps,
|
|
744
|
+
resolution=resolution,
|
|
745
|
+
max_frames=max_frames,
|
|
746
|
+
start_seconds=start_sec,
|
|
747
|
+
end_seconds=end_sec,
|
|
748
|
+
)
|
|
749
|
+
deduped_count = 0
|
|
750
|
+
if dedup:
|
|
751
|
+
frames, deduped_count = dedupe_perceptual(frames)
|
|
752
|
+
print(json.dumps(
|
|
753
|
+
{
|
|
754
|
+
"meta": meta, "fps": fps, "target": target, "focused": focused,
|
|
755
|
+
"deduped_count": deduped_count, "frames": frames,
|
|
756
|
+
},
|
|
757
|
+
indent=2,
|
|
758
|
+
))
|