hyperframes 0.6.97 → 0.6.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/beat-analyzer.global.js +326 -0
  2. package/dist/cli.js +12428 -4680
  3. package/dist/commands/layout-audit.browser.js +86 -0
  4. package/dist/hyperframe-runtime.js +22 -22
  5. package/dist/hyperframe.manifest.json +1 -1
  6. package/dist/hyperframe.runtime.iife.js +22 -22
  7. package/dist/skills/hyperframes-cli/SKILL.md +67 -103
  8. package/dist/skills/hyperframes-cli/references/doctor-browser.md +45 -0
  9. package/dist/skills/hyperframes-cli/references/init-and-scaffold.md +51 -0
  10. package/dist/skills/hyperframes-cli/references/lambda.md +132 -0
  11. package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +93 -0
  12. package/dist/skills/hyperframes-cli/references/preview-render.md +107 -0
  13. package/dist/skills/hyperframes-cli/references/upgrade-info-misc.md +75 -0
  14. package/dist/studio/assets/hyperframes-player-DgsMQSvV.js +418 -0
  15. package/dist/studio/assets/index-B62bDCQv.css +1 -0
  16. package/dist/studio/assets/{index-HveJ0MuV.js → index-C52IT_lp.js} +1 -1
  17. package/dist/studio/assets/index-DOh7E1uj.js +1 -0
  18. package/dist/studio/assets/index-DrwSRbsl.js +252 -0
  19. package/dist/studio/index.html +2 -2
  20. package/dist/templates/_shared/AGENTS.md +46 -21
  21. package/dist/templates/_shared/CLAUDE.md +16 -14
  22. package/package.json +3 -2
  23. package/dist/pngDecodeBlitWorker.js +0 -239
  24. package/dist/skills/gsap/SKILL.md +0 -240
  25. package/dist/skills/gsap/references/effects.md +0 -297
  26. package/dist/skills/gsap/scripts/extract-audio-data.py +0 -188
  27. package/dist/skills/hyperframes/SKILL.md +0 -491
  28. package/dist/skills/hyperframes/data-in-motion.md +0 -19
  29. package/dist/skills/hyperframes/house-style.md +0 -73
  30. package/dist/skills/hyperframes/palettes/bold-energetic.md +0 -14
  31. package/dist/skills/hyperframes/palettes/clean-corporate.md +0 -14
  32. package/dist/skills/hyperframes/palettes/dark-premium.md +0 -14
  33. package/dist/skills/hyperframes/palettes/jewel-rich.md +0 -14
  34. package/dist/skills/hyperframes/palettes/monochrome.md +0 -14
  35. package/dist/skills/hyperframes/palettes/nature-earth.md +0 -14
  36. package/dist/skills/hyperframes/palettes/neon-electric.md +0 -14
  37. package/dist/skills/hyperframes/palettes/pastel-soft.md +0 -14
  38. package/dist/skills/hyperframes/palettes/warm-editorial.md +0 -14
  39. package/dist/skills/hyperframes/patterns.md +0 -191
  40. package/dist/skills/hyperframes/references/audio-reactive.md +0 -76
  41. package/dist/skills/hyperframes/references/beat-direction.md +0 -171
  42. package/dist/skills/hyperframes/references/captions.md +0 -163
  43. package/dist/skills/hyperframes/references/css-patterns.md +0 -373
  44. package/dist/skills/hyperframes/references/design-picker.md +0 -117
  45. package/dist/skills/hyperframes/references/dynamic-techniques.md +0 -102
  46. package/dist/skills/hyperframes/references/html-in-canvas-patterns.md +0 -507
  47. package/dist/skills/hyperframes/references/motion-principles.md +0 -150
  48. package/dist/skills/hyperframes/references/narration.md +0 -92
  49. package/dist/skills/hyperframes/references/prompt-expansion.md +0 -68
  50. package/dist/skills/hyperframes/references/techniques.md +0 -525
  51. package/dist/skills/hyperframes/references/text-effects.md +0 -64
  52. package/dist/skills/hyperframes/references/transcript-guide.md +0 -107
  53. package/dist/skills/hyperframes/references/transitions/catalog.md +0 -117
  54. package/dist/skills/hyperframes/references/transitions/css-3d.md +0 -12
  55. package/dist/skills/hyperframes/references/transitions/css-blur.md +0 -51
  56. package/dist/skills/hyperframes/references/transitions/css-cover.md +0 -43
  57. package/dist/skills/hyperframes/references/transitions/css-destruction.md +0 -95
  58. package/dist/skills/hyperframes/references/transitions/css-dissolve.md +0 -66
  59. package/dist/skills/hyperframes/references/transitions/css-distortion.md +0 -45
  60. package/dist/skills/hyperframes/references/transitions/css-grid.md +0 -10
  61. package/dist/skills/hyperframes/references/transitions/css-light.md +0 -49
  62. package/dist/skills/hyperframes/references/transitions/css-mechanical.md +0 -30
  63. package/dist/skills/hyperframes/references/transitions/css-other.md +0 -25
  64. package/dist/skills/hyperframes/references/transitions/css-push.md +0 -41
  65. package/dist/skills/hyperframes/references/transitions/css-radial.md +0 -37
  66. package/dist/skills/hyperframes/references/transitions/css-scale.md +0 -24
  67. package/dist/skills/hyperframes/references/transitions.md +0 -138
  68. package/dist/skills/hyperframes/references/typography.md +0 -175
  69. package/dist/skills/hyperframes/references/video-composition.md +0 -62
  70. package/dist/skills/hyperframes/scripts/animation-map.mjs +0 -601
  71. package/dist/skills/hyperframes/scripts/contrast-report.mjs +0 -348
  72. package/dist/skills/hyperframes/scripts/package-loader.mjs +0 -269
  73. package/dist/skills/hyperframes/templates/design-picker.html +0 -1432
  74. package/dist/skills/hyperframes/visual-styles.md +0 -443
  75. package/dist/studio/assets/hyperframes-player-Daj5djxa.js +0 -418
  76. package/dist/studio/assets/index-B0twsRu0.css +0 -1
  77. package/dist/studio/assets/index-Cfye9xzo.js +0 -251
@@ -1,297 +0,0 @@
1
- # GSAP Effects for HyperFrames
2
-
3
- Drop-in animation patterns for HyperFrames compositions. Each effect is self-contained with HTML, CSS, and code.
4
-
5
- All effects follow HyperFrames composition rules — deterministic, no randomness, timelines registered via `window.__timelines`.
6
-
7
- ## Table of Contents
8
-
9
- - [Typewriter](#typewriter)
10
- - [Audio Visualizer](#audio-visualizer)
11
-
12
- ---
13
-
14
- ## Typewriter
15
-
16
- Reveal text character by character using GSAP's TextPlugin.
17
-
18
- ### Required Plugin
19
-
20
- ```html
21
- <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
22
- <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/TextPlugin.min.js"></script>
23
- <script>
24
- gsap.registerPlugin(TextPlugin);
25
- </script>
26
- ```
27
-
28
- ### Basic Typewriter
29
-
30
- ```js
31
- const text = "Hello, world!";
32
- const cps = 10; // chars per second: 3-5 dramatic, 8-12 conversational, 15-20 energetic
33
- tl.to(
34
- "#typed-text",
35
- { text: { value: text }, duration: text.length / cps, ease: "none" },
36
- startTime,
37
- );
38
- ```
39
-
40
- ### With Blinking Cursor
41
-
42
- Three rules:
43
-
44
- 1. **One cursor visible at a time** — hide previous before showing next.
45
- 2. **Cursor must blink when idle** — after typing, during pauses.
46
- 3. **No gap between text and cursor** — elements must be flush in HTML.
47
-
48
- ```html
49
- <span id="typed-text"></span><span id="cursor" class="cursor-blink">|</span>
50
- ```
51
-
52
- ```css
53
- @keyframes blink {
54
- 0%,
55
- 100% {
56
- opacity: 1;
57
- }
58
- 50% {
59
- opacity: 0;
60
- }
61
- }
62
- .cursor-blink {
63
- animation: blink 0.8s step-end infinite;
64
- }
65
- .cursor-solid {
66
- animation: none;
67
- opacity: 1;
68
- }
69
- .cursor-hide {
70
- animation: none;
71
- opacity: 0;
72
- }
73
- ```
74
-
75
- Pattern: blink → solid (typing starts) → type → solid → blink (typing done).
76
-
77
- ```js
78
- tl.call(() => cursor.classList.replace("cursor-blink", "cursor-solid"), [], startTime);
79
- tl.to("#typed-text", { text: { value: text }, duration: dur, ease: "none" }, startTime);
80
- tl.call(() => cursor.classList.replace("cursor-solid", "cursor-blink"), [], startTime + dur);
81
- ```
82
-
83
- ### Backspacing
84
-
85
- TextPlugin removes from front — wrong for backspace. Use manual substring removal:
86
-
87
- ```js
88
- function backspace(tl, selector, word, startTime, cps) {
89
- const el = document.querySelector(selector);
90
- const interval = 1 / cps;
91
- for (let i = word.length - 1; i >= 0; i--) {
92
- tl.call(
93
- () => {
94
- el.textContent = word.slice(0, i);
95
- },
96
- [],
97
- startTime + (word.length - i) * interval,
98
- );
99
- }
100
- return word.length * interval;
101
- }
102
- ```
103
-
104
- ### Spacing with Static Text
105
-
106
- When a typewriter word sits next to static text, use `margin-left` on a wrapper span. Don't use flex gap (spaces cursor from text) or trailing space in static text (collapses when dynamic is empty).
107
-
108
- ```html
109
- <div style="display:flex; align-items:baseline;">
110
- <span style="font-size:40px; color:#555;">Ship something</span>
111
- <span style="margin-left:14px;"><span id="word"></span><span id="cursor">|</span></span>
112
- </div>
113
- ```
114
-
115
- ### Word Rotation
116
-
117
- Type → hold → backspace → next word. Cursor blinks during every idle moment (holds, after backspace).
118
-
119
- ```js
120
- words.forEach((word, i) => {
121
- const typeDur = word.length / 10;
122
- // Solid while typing
123
- tl.call(() => cursor.classList.replace("cursor-blink", "cursor-solid"), [], offset);
124
- tl.to("#typed-text", { text: { value: word }, duration: typeDur, ease: "none" }, offset);
125
- // Blink during hold
126
- tl.call(() => cursor.classList.replace("cursor-solid", "cursor-blink"), [], offset + typeDur);
127
- offset += typeDur + 1.5; // hold
128
-
129
- if (i < words.length - 1) {
130
- tl.call(() => cursor.classList.replace("cursor-blink", "cursor-solid"), [], offset);
131
- const clearDur = backspace(tl, el, word, offset, 20);
132
- tl.call(() => cursor.classList.replace("cursor-solid", "cursor-blink"), [], offset + clearDur);
133
- offset += clearDur + 0.3;
134
- }
135
- });
136
- ```
137
-
138
- ### Appending Words
139
-
140
- Build a sentence word-by-word into the same element:
141
-
142
- ```js
143
- let accumulated = "";
144
- words.forEach((word) => {
145
- const target = accumulated + (accumulated ? " " : "") + word;
146
- const newChars = target.length - accumulated.length;
147
- tl.to("#typed-text", { text: { value: target }, duration: newChars / 10, ease: "none" }, offset);
148
- accumulated = target;
149
- offset += newChars / 10 + 0.3;
150
- });
151
- ```
152
-
153
- ### Multi-Line Cursor Handoff
154
-
155
- When handing off between typewriter lines: hide previous → blink new → pause → solid when typing. Never go hidden→solid (skips idle state).
156
-
157
- ```js
158
- tl.call(
159
- () => {
160
- prevCursor.classList.replace("cursor-blink", "cursor-hide");
161
- nextCursor.classList.replace("cursor-hide", "cursor-blink");
162
- },
163
- [],
164
- handoffTime,
165
- );
166
-
167
- const typeStart = handoffTime + 0.5; // brief blink pause
168
- tl.call(() => nextCursor.classList.replace("cursor-blink", "cursor-solid"), [], typeStart);
169
- tl.to("#next-text", { text: { value: text }, duration: dur, ease: "none" }, typeStart);
170
- tl.call(() => nextCursor.classList.replace("cursor-solid", "cursor-blink"), [], typeStart + dur);
171
- ```
172
-
173
- ### Timing Guide
174
-
175
- | CPS | Feel | Good for |
176
- | ----- | ---------------- | -------------------------- |
177
- | 3-5 | Slow, deliberate | Dramatic reveals, suspense |
178
- | 8-12 | Natural typing | Dialogue, narration |
179
- | 15-20 | Fast, energetic | Tech demos, code |
180
- | 30+ | Near-instant | Filling long blocks |
181
-
182
- ---
183
-
184
- ## Audio Visualizer
185
-
186
- Pre-extract audio data, drive canvas/DOM rendering from GSAP timeline.
187
-
188
- ### Extract Audio Data
189
-
190
- ```bash
191
- python scripts/extract-audio-data.py audio.mp3 -o audio-data.json
192
- python scripts/extract-audio-data.py video.mp4 --fps 30 --bands 16 -o audio-data.json
193
- ```
194
-
195
- Requires ffmpeg and numpy.
196
-
197
- ### Data Format
198
-
199
- ```json
200
- {
201
- "fps": 30, "totalFrames": 5415,
202
- "frames": [{ "time": 0.0, "rms": 0.42, "bands": [0.8, 0.6, 0.3, ...] }]
203
- }
204
- ```
205
-
206
- - **rms** (0-1): overall loudness, normalized across track
207
- - **bands[]** (0-1): frequency magnitudes. Index 0 = bass, higher = treble. Each normalized independently.
208
-
209
- ### Loading the Data
210
-
211
- ```js
212
- // Option A: inline (small files, under ~500KB)
213
- var AUDIO_DATA = {
214
- /* paste audio-data.json contents */
215
- };
216
-
217
- // Option B: sync XHR (large files — must be synchronous for deterministic timeline construction)
218
- var xhr = new XMLHttpRequest();
219
- xhr.open("GET", "audio-data.json", false);
220
- xhr.send();
221
- var AUDIO_DATA = JSON.parse(xhr.responseText);
222
- ```
223
-
224
- **Do NOT use async `fetch()` to load audio data.** HyperFrames requires synchronous timeline construction — the capture engine reads `window.__timelines` synchronously after page load. Building timelines inside `.then()` callbacks means the timeline isn't ready when capture starts.
225
-
226
- ### Rendering Approaches
227
-
228
- **Canvas 2D** (most common — bars, waveforms, circles, gradients):
229
-
230
- ```js
231
- for (let f = 0; f < AUDIO_DATA.totalFrames; f++) {
232
- tl.call(
233
- () => {
234
- const frame = AUDIO_DATA.frames[f];
235
- ctx.clearRect(0, 0, canvas.width, canvas.height);
236
- // draw using frame.rms and frame.bands
237
- },
238
- [],
239
- f / AUDIO_DATA.fps,
240
- );
241
- }
242
- ```
243
-
244
- **WebGL / Three.js** — HyperFrames patches `THREE.Clock` for deterministic time. Update uniforms from audio data each frame.
245
-
246
- **DOM Elements** — fine for < 20 elements, less performant than Canvas for many.
247
-
248
- ### Spatial Mapping
249
-
250
- - **Horizontal**: bass left, treble right (iterate bands left-to-right)
251
- - **Vertical**: bass bottom, treble top
252
- - **Circular**: bass at 12 o'clock, wrap clockwise; mirror for full circle
253
-
254
- ### Smoothing
255
-
256
- ```js
257
- let prev = null;
258
- const smoothing = 0.25; // 0.1-0.2 snappy, 0.3-0.5 flowing
259
- function smooth(f) {
260
- const raw = AUDIO_DATA.frames[f];
261
- if (!prev) {
262
- prev = { rms: raw.rms, bands: [...raw.bands] };
263
- return prev;
264
- }
265
- prev = {
266
- rms: prev.rms * smoothing + raw.rms * (1 - smoothing),
267
- bands: raw.bands.map((b, i) => prev.bands[i] * smoothing + b * (1 - smoothing)),
268
- };
269
- return prev;
270
- }
271
- ```
272
-
273
- ### Motion Principles
274
-
275
- - **Bass drives big moves** — scale, glow, position shifts
276
- - **Treble drives detail** — shimmer, flicker, edge effects
277
- - **RMS drives globals** — background brightness, overall energy
278
- - Pick 2-3 properties to animate. More looks noisy.
279
- - Keep minimums above zero — quiet sections need life.
280
-
281
- ### Band Count
282
-
283
- | Bands | Detail | Good for |
284
- | ----- | --------- | -------------------------- |
285
- | 4 | Low | Background glow, pulsing |
286
- | 8 | Medium | Bar charts, basic spectrum |
287
- | 16 | High | Detailed EQ (default) |
288
- | 32 | Very high | Dense radial layouts |
289
-
290
- ### Layering
291
-
292
- Layer multiple canvases with CSS z-index for depth — a background layer driven by bass/rms and a foreground layer driven by individual bands creates depth without complexity.
293
-
294
- ```html
295
- <canvas id="bg-layer" style="position:absolute;top:0;left:0;z-index:1;"></canvas>
296
- <canvas id="main-layer" style="position:absolute;top:0;left:0;z-index:2;"></canvas>
297
- ```
@@ -1,188 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Extract per-frame audio visualization data from an audio or video file.
4
-
5
- Outputs JSON with RMS amplitude and frequency band data at the target FPS,
6
- ready to embed in a HyperFrames composition.
7
-
8
- Usage:
9
- python extract-audio-data.py input.mp3 -o audio-data.json
10
- python extract-audio-data.py input.mp4 --fps 30 --bands 16 -o audio-data.json
11
-
12
- Requirements:
13
- - Python 3.9+
14
- - ffmpeg (for decoding audio)
15
- - numpy (pip install numpy)
16
- """
17
-
18
- import argparse
19
- import json
20
- import subprocess
21
- import sys
22
-
23
- import numpy as np
24
-
25
- # ---------------------------------------------------------------------------
26
- # FFT parameters
27
- #
28
- # A 4096-sample window gives ~10.8 Hz per bin at 44100Hz — enough to resolve
29
- # low-frequency bands cleanly. The per-frame audio slice (44100/30 = 1470
30
- # samples at 30fps) is too small and causes low bands to map to the same bins.
31
- #
32
- # Frequency range 30Hz–16kHz covers the useful range for music. Below 30Hz is
33
- # sub-bass most speakers can't reproduce; above 16kHz is noise/harmonics that
34
- # don't contribute to perceived rhythm or melody.
35
- # ---------------------------------------------------------------------------
36
-
37
- SAMPLE_RATE = 44100
38
- FFT_SIZE = 4096
39
- MIN_FREQ = 30.0
40
- MAX_FREQ = 16000.0
41
-
42
-
43
- def decode_audio(path: str) -> np.ndarray:
44
- """Decode audio to mono float32 samples via ffmpeg."""
45
- cmd = [
46
- "ffmpeg", "-i", path,
47
- "-vn", "-ac", "1", "-ar", str(SAMPLE_RATE),
48
- "-f", "s16le", "-acodec", "pcm_s16le",
49
- "-loglevel", "error",
50
- "pipe:1",
51
- ]
52
- result = subprocess.run(cmd, capture_output=True)
53
- if result.returncode != 0:
54
- print(f"ffmpeg error: {result.stderr.decode()}", file=sys.stderr)
55
- sys.exit(1)
56
- return np.frombuffer(result.stdout, dtype=np.int16).astype(np.float32) / 32768.0
57
-
58
-
59
- def compute_band_edges(n_bands: int) -> np.ndarray:
60
- """Logarithmically-spaced frequency band edges from MIN_FREQ to MAX_FREQ."""
61
- return np.array([
62
- MIN_FREQ * (MAX_FREQ / MIN_FREQ) ** (i / n_bands)
63
- for i in range(n_bands + 1)
64
- ])
65
-
66
-
67
- def compute_fft_bands(
68
- windowed: np.ndarray, freq_per_bin: float, n_bins: int,
69
- band_edges: np.ndarray, n_bands: int,
70
- ) -> np.ndarray:
71
- """Compute peak magnitude in logarithmically-spaced frequency bands."""
72
- magnitudes = np.abs(np.fft.rfft(windowed))
73
-
74
- bands = np.zeros(n_bands)
75
- for b in range(n_bands):
76
- low_bin = max(0, int(band_edges[b] / freq_per_bin))
77
- high_bin = min(n_bins, int(band_edges[b + 1] / freq_per_bin))
78
- if high_bin <= low_bin:
79
- high_bin = low_bin + 1
80
- # Clamp to valid range to avoid empty slices
81
- low_bin = min(low_bin, n_bins - 1)
82
- high_bin = min(high_bin, n_bins)
83
- bands[b] = np.max(magnitudes[low_bin:high_bin])
84
-
85
- return bands
86
-
87
-
88
- def extract(path: str, fps: int, n_bands: int) -> dict:
89
- """Extract per-frame audio data."""
90
- print(f"Decoding audio from {path}...", file=sys.stderr)
91
- samples = decode_audio(path)
92
- duration = len(samples) / SAMPLE_RATE
93
- frame_step = SAMPLE_RATE // fps
94
- total_frames = int(duration * fps)
95
-
96
- print(f"Duration: {duration:.1f}s, {total_frames} frames at {fps}fps", file=sys.stderr)
97
- print(f"FFT window: {FFT_SIZE} samples ({SAMPLE_RATE / FFT_SIZE:.1f} Hz/bin)", file=sys.stderr)
98
- print(f"Frequency range: {MIN_FREQ:.0f}-{MAX_FREQ:.0f} Hz, {n_bands} bands", file=sys.stderr)
99
-
100
- # Precompute constants
101
- hann = np.hanning(FFT_SIZE)
102
- band_edges = compute_band_edges(n_bands)
103
- freq_per_bin = SAMPLE_RATE / FFT_SIZE
104
- n_bins = FFT_SIZE // 2 + 1
105
- half_fft = FFT_SIZE // 2
106
-
107
- # Pass 1: extract raw values
108
- rms_values = np.zeros(total_frames)
109
- band_values = np.zeros((total_frames, n_bands))
110
-
111
- for f in range(total_frames):
112
- # RMS from the frame's audio slice
113
- rms_start = f * frame_step
114
- rms_end = rms_start + frame_step
115
- frame_slice = samples[rms_start:min(rms_end, len(samples))]
116
- if len(frame_slice) > 0:
117
- rms_values[f] = np.sqrt(np.mean(frame_slice ** 2))
118
-
119
- # FFT from a centered 4096-sample window
120
- center = rms_start + frame_step // 2
121
- win_start = center - half_fft
122
- win_end = center + half_fft
123
-
124
- if win_start >= 0 and win_end <= len(samples):
125
- window = samples[win_start:win_end] * hann
126
- else:
127
- # Zero-pad at edges
128
- padded = np.zeros(FFT_SIZE)
129
- src_start = max(0, win_start)
130
- src_end = min(len(samples), win_end)
131
- dst_start = src_start - win_start
132
- dst_end = dst_start + (src_end - src_start)
133
- padded[dst_start:dst_end] = samples[src_start:src_end]
134
- window = padded * hann
135
-
136
- band_values[f] = compute_fft_bands(window, freq_per_bin, n_bins, band_edges, n_bands)
137
-
138
- # Pass 2: normalize
139
- peak_rms = rms_values.max() if total_frames > 0 else 1.0
140
- if peak_rms > 0:
141
- rms_values /= peak_rms
142
-
143
- # Per-band normalization so treble is visible alongside louder bass
144
- band_peaks = band_values.max(axis=0)
145
- band_peaks[band_peaks == 0] = 1.0
146
- band_values /= band_peaks
147
-
148
- # Build output
149
- frames = []
150
- for f in range(total_frames):
151
- frames.append({
152
- "time": round(f / fps, 4),
153
- "rms": round(float(rms_values[f]), 4),
154
- "bands": [round(float(b), 4) for b in band_values[f]],
155
- })
156
-
157
- return {
158
- "duration": round(duration, 4),
159
- "fps": fps,
160
- "bands": n_bands,
161
- "totalFrames": total_frames,
162
- "frames": frames,
163
- }
164
-
165
-
166
- def main():
167
- parser = argparse.ArgumentParser(description="Extract per-frame audio visualization data")
168
- parser.add_argument("input", help="Audio or video file")
169
- parser.add_argument("-o", "--output", default="audio-data.json", help="Output JSON path")
170
- parser.add_argument("--fps", type=int, default=30, help="Frames per second (default: 30)")
171
- parser.add_argument("--bands", type=int, default=16, help="Number of frequency bands (default: 16)")
172
- args = parser.parse_args()
173
-
174
- if args.fps < 1:
175
- parser.error("--fps must be at least 1")
176
- if args.bands < 1:
177
- parser.error("--bands must be at least 1")
178
-
179
- data = extract(args.input, args.fps, args.bands)
180
-
181
- with open(args.output, "w") as f:
182
- json.dump(data, f)
183
-
184
- print(f"Wrote {args.output} ({data['totalFrames']} frames, {data['bands']} bands)", file=sys.stderr)
185
-
186
-
187
- if __name__ == "__main__":
188
- main()