@doedja/scenecut 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -1,280 +1,331 @@
1
- # scenecut
2
-
3
- Fast, accurate scene change detection for Node.js using Xvid's motion estimation algorithm compiled to WebAssembly.
4
-
5
- ## Features
6
-
7
- - **Fast**: WebAssembly-accelerated motion estimation (35-45 fps on typical hardware)
8
- - **Accurate**: Uses Xvid's proven motion estimation algorithm from vapoursynth-wwxd
9
- - **Multiple output formats**: Aegisub keyframes, timecodes, CSV, JSON
10
- - **Easy to use**: Simple CLI and programmatic API
11
- - **Cross-platform**: Works on Windows, Linux, and macOS
12
-
13
- ## Installation
14
-
15
- ### Global Installation (CLI)
16
-
17
- ```bash
18
- npm install -g @doedja/scenecut
19
- ```
20
-
21
- ### Local Installation (API)
22
-
23
- ```bash
24
- npm install @doedja/scenecut
25
- ```
26
-
27
- ## CLI Usage
28
-
29
- ### Basic Usage
30
-
31
- ```bash
32
- # Simple - detects scenes and saves to Aegisub format (default)
33
- # Creates input_keyframes.txt
34
- scenecut "input.mkv"
35
-
36
- # Specify custom output filename
37
- scenecut "video.mkv" -o keyframes.txt
38
-
39
- # For timecode output
40
- scenecut "video.mp4" --format timecode -o timecodes.txt
41
-
42
- # JSON format with full metadata
43
- scenecut "movie.avi" --format json
44
-
45
- # CSV format for spreadsheet analysis
46
- scenecut "movie.avi" --format csv -o scenes.csv
47
- ```
48
-
49
- ### CLI Options
50
-
51
- | Option | Alias | Description | Default |
52
- |--------|-------|-------------|---------|
53
- | `--format` | `-f` | Output format: `aegisub`, `json`, `csv`, `timecode` | `aegisub` |
54
- | `--output` | `-o` | Output file path | `{filename}_keyframes.txt` |
55
- | `--sensitivity` | `-s` | Detection sensitivity: `low`, `medium`, `high` | `medium` |
56
- | `--quiet` | `-q` | Suppress progress output | `false` |
57
- | `--verbose` | `-v` | Show detailed output including each scene | `false` |
58
- | `--help` | `-h` | Show help message | - |
59
-
60
- ### Examples
61
-
62
- ```bash
63
- # Default - creates anime_keyframes.txt in Aegisub format
64
- scenecut "anime.mkv"
65
-
66
- # High sensitivity for subtle scene changes
67
- scenecut "anime.mkv" --sensitivity high
68
-
69
- # JSON format with full metadata
70
- scenecut "video.mp4" --format json -o results.json
71
-
72
- # Verbose mode with detailed scene information
73
- scenecut "movie.mkv" --verbose
74
- ```
75
-
76
- ## Output Formats
77
-
78
- ### Aegisub Format (`.txt`)
79
-
80
- Aegisub keyframes format for subtitle timing:
81
-
82
- ```
83
- # keyframe format v1
84
- fps 23.976
85
- 0
86
- 143
87
- 287
88
- ```
89
-
90
- **Aegisub Workflow:**
91
- 1. Generate keyframes: `scenecut "video.mkv" -f aegisub -o keyframes.txt`
92
- 2. In Aegisub: **Video** → **Open Keyframes** → Select `keyframes.txt`
93
- 3. Keyframes appear as visual markers on the timeline for precise subtitle timing
94
-
95
- ### Timecode Format (`.txt`)
96
-
97
- Simple timecode list (HH:MM:SS.mmm):
98
-
99
- ```
100
- 00:00:00.000
101
- 00:00:05.964
102
- 00:00:11.970
103
- ```
104
-
105
- ### CSV Format (`.csv`)
106
-
107
- Spreadsheet-compatible format:
108
-
109
- ```csv
110
- frame,timestamp,timecode
111
- 0,0.0,00:00:00.000
112
- 143,5.964,00:00:05.964
113
- 287,11.970,00:00:11.970
114
- ```
115
-
116
- ### JSON Format (`.json`)
117
-
118
- Complete metadata and scene information:
119
-
120
- ```json
121
- {
122
- "scenes": [
123
- {
124
- "frameNumber": 0,
125
- "timestamp": 0.0,
126
- "timecode": "00:00:00.000"
127
- },
128
- {
129
- "frameNumber": 143,
130
- "timestamp": 5.964,
131
- "timecode": "00:00:05.964"
132
- }
133
- ],
134
- "metadata": {
135
- "totalFrames": 3000,
136
- "duration": 125.08,
137
- "fps": 23.976,
138
- "resolution": {
139
- "width": 1920,
140
- "height": 1080
141
- }
142
- }
143
- }
144
- ```
145
-
146
- ## Programmatic API
147
-
148
- ### Basic Usage
149
-
150
- ```javascript
151
- const { detectSceneChanges } = require('@doedja/scenecut');
152
-
153
- (async () => {
154
- const results = await detectSceneChanges('input.mp4');
155
-
156
- console.log(`Found ${results.scenes.length} scenes`);
157
- results.scenes.forEach(scene => {
158
- console.log(`Scene at frame ${scene.frameNumber} (${scene.timecode})`);
159
- });
160
- })();
161
- ```
162
-
163
- ### Advanced Usage with Options
164
-
165
- ```javascript
166
- const { detectSceneChanges } = require('@doedja/scenecut');
167
-
168
- const results = await detectSceneChanges('input.mp4', {
169
- sensitivity: 'high', // 'low' | 'medium' | 'high'
170
- searchRange: 'medium', // Motion search range
171
-
172
- // Progress callback
173
- onProgress: (progress) => {
174
- console.log(`Progress: ${progress.percent}%`);
175
- console.log(`Frame: ${progress.currentFrame}/${progress.totalFrames}`);
176
- console.log(`FPS: ${progress.fps}, ETA: ${progress.eta}s`);
177
- },
178
-
179
- // Scene detection callback
180
- onScene: (scene) => {
181
- console.log(`Scene detected at frame ${scene.frameNumber}`);
182
- console.log(`Timecode: ${scene.timecode}`);
183
- }
184
- });
185
-
186
- console.log('Detection complete!');
187
- console.log(`Total scenes: ${results.scenes.length}`);
188
- console.log(`Video duration: ${results.metadata.duration}s`);
189
- console.log(`Resolution: ${results.metadata.resolution.width}x${results.metadata.resolution.height}`);
190
- ```
191
-
192
- ### API Reference
193
-
194
- #### `detectSceneChanges(videoPath, options?)`
195
-
196
- Detects scene changes in a video file.
197
-
198
- **Parameters:**
199
- - `videoPath` (string): Path to input video file
200
- - `options` (object, optional):
201
- - `sensitivity` ('low' | 'medium' | 'high'): Detection sensitivity (default: 'medium')
202
- - `searchRange` ('auto' | 'small' | 'medium' | 'large'): Motion search range (default: 'medium')
203
- - `onProgress` (function): Callback for progress updates
204
- - `onScene` (function): Callback for each detected scene
205
-
206
- **Returns:** Promise<DetectionResult>
207
-
208
- **DetectionResult:**
209
- ```typescript
210
- {
211
- scenes: Array<{
212
- frameNumber: number;
213
- timestamp: number; // Seconds
214
- timecode: string; // HH:MM:SS.mmm
215
- }>;
216
- metadata: {
217
- totalFrames: number;
218
- duration: number; // Seconds
219
- fps: number;
220
- resolution: {
221
- width: number;
222
- height: number;
223
- };
224
- };
225
- }
226
- ```
227
-
228
- ## Supported Video Formats
229
-
230
- Keyframes supports any video format that FFmpeg can decode, including:
231
-
232
- - MP4 (`.mp4`, `.m4v`)
233
- - Matroska (`.mkv`)
234
- - AVI (`.avi`)
235
- - WebM (`.webm`)
236
- - MOV (`.mov`)
237
- - FLV (`.flv`)
238
- - And many more...
239
-
240
- ## How It Works
241
-
242
- Keyframes uses Xvid's motion estimation algorithm to detect scene changes:
243
-
244
- 1. **Frame Extraction**: FFmpeg extracts grayscale frames from the video
245
- 2. **Motion Analysis**: WebAssembly-compiled C code analyzes motion vectors between consecutive frames
246
- 3. **Scene Detection**: Frames with high motion complexity are identified as scene changes
247
- 4. **Output Formatting**: Results are formatted according to the requested output format
248
-
249
- The algorithm is based on [vapoursynth-wwxd](https://github.com/dubhater/vapoursynth-wwxd) by dubhater, which itself uses Xvid's motion estimation code.
250
-
251
- ## Performance
252
-
253
- Optimized for speed and accuracy:
254
- - **Processing speed**: 35-45 fps on 1080p video (modern hardware)
255
- - **Memory usage**: ~200-300 MB with efficient buffer management
256
- - **Accuracy**: Matches vapoursynth-wwxd output (100% accurate)
257
- - **Optimizations**: WASM SIMD, pre-allocated buffers, ring buffer streaming
258
-
259
- ## Requirements
260
-
261
- - **Node.js**: 18.0.0 or higher
262
- - **FFmpeg**: Automatically installed via `@ffmpeg-installer/ffmpeg`
263
-
264
- ## License
265
-
266
- GPL-2.0
267
-
268
- This project is based on:
269
- - [vapoursynth-wwxd](https://github.com/dubhater/vapoursynth-wwxd) by dubhater (GPL-2.0)
270
- - Xvid's motion estimation algorithm (GPL-2.0)
271
-
272
- ## Contributing
273
-
274
- Contributions are welcome! Please feel free to submit issues or pull requests.
275
-
276
- ## Credits
277
-
278
- - Original vapoursynth-wwxd plugin: [dubhater](https://github.com/dubhater)
279
- - Xvid motion estimation algorithm: [Xvid Team](https://www.xvid.com)
280
- - JavaScript/WASM port: This project
1
+ # scenecut
2
+
3
+ Fast, accurate scene change detection for Node.js using Xvid's motion estimation algorithm compiled to WebAssembly.
4
+
5
+ ## Features
6
+
7
+ - **Fast**: WebAssembly-accelerated motion estimation with SIMD, double-buffered frame pipeline, and quick-reject filtering
8
+ - **Accurate**: Uses Xvid's proven motion estimation algorithm with configurable thresholds that actually work
9
+ - **Confidence scoring**: Each scene change includes a 0-1 confidence score from the motion estimator
10
+ - **Fade/dissolve detection**: Catches gradual transitions that per-frame analysis misses
11
+ - **Temporal smoothing**: Optional sliding window filter to suppress false positives and flash frames
12
+ - **Multiple output formats**: Aegisub keyframes, timecodes, CSV (with confidence + duration), JSON
13
+ - **Scene duration**: Each scene includes duration and frame count
14
+ - **Cancellable**: AbortController support with `--timeout` for long videos
15
+ - **Batch thumbnails**: Extract scene images in a single FFmpeg pass
16
+ - **Cross-platform**: Works on Windows, Linux, and macOS
17
+
18
+ ## Installation
19
+
20
+ ### Global Installation (CLI)
21
+
22
+ ```bash
23
+ npm install -g @doedja/scenecut
24
+ ```
25
+
26
+ ### Local Installation (API)
27
+
28
+ ```bash
29
+ npm install @doedja/scenecut
30
+ ```
31
+
32
+ ## CLI Usage
33
+
34
+ ### Basic Usage
35
+
36
+ ```bash
37
+ # Simple - detects scenes and saves to Aegisub format (default)
38
+ scenecut "input.mkv"
39
+
40
+ # Specify custom output filename
41
+ scenecut "video.mkv" -o keyframes.txt
42
+
43
+ # For timecode output
44
+ scenecut "video.mp4" --format timecode -o timecodes.txt
45
+
46
+ # JSON format with full metadata, confidence, and duration
47
+ scenecut "movie.avi" --format json
48
+
49
+ # CSV format for spreadsheet analysis
50
+ scenecut "movie.avi" --format csv -o scenes.csv
51
+
52
+ # High sensitivity for subtle scene changes
53
+ scenecut "anime.mkv" --sensitivity high --verbose
54
+
55
+ # Abort after 2 minutes
56
+ scenecut "long-movie.mkv" --timeout 120
57
+
58
+ # Extract scene thumbnails
59
+ scenecut "video.mp4" --thumbnails ./thumbs
60
+ ```
61
+
62
+ ### CLI Options
63
+
64
+ | Option | Alias | Description | Default |
65
+ |--------|-------|-------------|---------|
66
+ | `--format` | `-f` | Output format: `aegisub`, `json`, `csv`, `timecode` | `aegisub` |
67
+ | `--output` | `-o` | Output file path | `{filename}_keyframes.txt` |
68
+ | `--sensitivity` | `-s` | Detection sensitivity: `low`, `medium`, `high` | `low` |
69
+ | `--timeout` | `-t` | Abort after N seconds | no timeout |
70
+ | `--thumbnails` | | Extract scene thumbnails to directory | - |
71
+ | `--quiet` | `-q` | Suppress progress output | `false` |
72
+ | `--verbose` | `-v` | Show detailed output including confidence | `false` |
73
+ | `--help` | `-h` | Show help message | - |
74
+
75
+ ### Sensitivity Levels
76
+
77
+ | Level | IntraThresh | IntraThresh2 | Use case |
78
+ |-------|------------|-------------|----------|
79
+ | `low` | 3000 | 150 | Fewer detections, only hard cuts (default) |
80
+ | `medium` | 2000 | 90 | Balanced |
81
+ | `high` | 1000 | 50 | More detections, catches subtle transitions |
82
+
83
+ ## Output Formats
84
+
85
+ ### Aegisub Format (`.txt`)
86
+
87
+ Aegisub keyframes format for subtitle timing:
88
+
89
+ ```
90
+ # keyframe format v1
91
+ fps 23.976
92
+ 0
93
+ 143
94
+ 287
95
+ ```
96
+
97
+ **Aegisub Workflow:**
98
+ 1. Generate keyframes: `scenecut "video.mkv" -f aegisub -o keyframes.txt`
99
+ 2. In Aegisub: **Video** > **Open Keyframes** > Select `keyframes.txt`
100
+ 3. Keyframes appear as visual markers on the timeline for precise subtitle timing
101
+
102
+ ### CSV Format (`.csv`)
103
+
104
+ Includes confidence scores and scene durations:
105
+
106
+ ```csv
107
+ frame,timestamp,timecode,confidence,duration,frameCount
108
+ 0,0.0,00:00:00.000,1.0000,5.964,143
109
+ 143,5.964,00:00:05.964,0.7234,6.006,144
110
+ 287,11.970,00:00:11.970,0.8912,4.171,100
111
+ ```
112
+
113
+ ### JSON Format (`.json`)
114
+
115
+ Complete metadata with confidence, duration, and codec info:
116
+
117
+ ```json
118
+ {
119
+ "scenes": [
120
+ {
121
+ "frameNumber": 0,
122
+ "timestamp": 0.0,
123
+ "timecode": "00:00:00.000",
124
+ "confidence": 1.0,
125
+ "duration": 5.964,
126
+ "frameCount": 143
127
+ },
128
+ {
129
+ "frameNumber": 143,
130
+ "timestamp": 5.964,
131
+ "timecode": "00:00:05.964",
132
+ "confidence": 0.7234,
133
+ "duration": 6.006,
134
+ "frameCount": 144
135
+ }
136
+ ],
137
+ "metadata": {
138
+ "totalFrames": 3000,
139
+ "duration": 125.08,
140
+ "fps": 23.976,
141
+ "resolution": { "width": 1920, "height": 1080 },
142
+ "codec": "h264",
143
+ "pixelFormat": "yuv420p",
144
+ "bitrate": 5000000
145
+ },
146
+ "stats": {
147
+ "processingTime": 28.5,
148
+ "framesPerSecond": 105.2
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### Timecode Format (`.txt`)
154
+
155
+ Simple timecode list (HH:MM:SS.mmm):
156
+
157
+ ```
158
+ 00:00:00.000
159
+ 00:00:05.964
160
+ 00:00:11.970
161
+ ```
162
+
163
+ ## Programmatic API
164
+
165
+ ### Basic Usage
166
+
167
+ ```javascript
168
+ const { detectSceneChanges } = require('@doedja/scenecut');
169
+
170
+ const results = await detectSceneChanges('input.mp4');
171
+
172
+ console.log(`Found ${results.scenes.length} scenes`);
173
+ results.scenes.forEach(scene => {
174
+ console.log(`Scene at ${scene.timecode} (confidence: ${(scene.confidence * 100).toFixed(0)}%, duration: ${scene.duration?.toFixed(1)}s)`);
175
+ });
176
+ ```
177
+
178
+ ### Advanced Usage
179
+
180
+ ```javascript
181
+ const { detectSceneChanges } = require('@doedja/scenecut');
182
+
183
+ const controller = new AbortController();
184
+
185
+ // Auto-cancel after 60 seconds
186
+ setTimeout(() => controller.abort(), 60000);
187
+
188
+ const results = await detectSceneChanges('input.mp4', {
189
+ sensitivity: 'high',
190
+ searchRange: 'medium',
191
+ signal: controller.signal,
192
+
193
+ // Temporal smoothing to reduce false positives
194
+ temporalSmoothing: {
195
+ enabled: true,
196
+ windowSize: 5,
197
+ minConsecutive: 2
198
+ },
199
+
200
+ onProgress: (progress) => {
201
+ console.log(`${progress.percent}% | ${progress.fps?.toFixed(1)} fps | ETA: ${progress.eta?.toFixed(0)}s | ${progress.scenesDetected} scenes`);
202
+ },
203
+
204
+ onScene: (scene) => {
205
+ console.log(`Scene at frame ${scene.frameNumber} (${scene.timecode}) confidence: ${scene.confidence?.toFixed(2)}`);
206
+ }
207
+ });
208
+
209
+ console.log(`Total scenes: ${results.scenes.length}`);
210
+ console.log(`Video: ${results.metadata.codec} ${results.metadata.resolution.width}x${results.metadata.resolution.height}`);
211
+ ```
212
+
213
+ ### Extract Scene Thumbnails
214
+
215
+ ```javascript
216
+ const { extractSceneImages } = require('@doedja/scenecut');
217
+
218
+ const results = await extractSceneImages('input.mp4', {
219
+ sensitivity: 'low'
220
+ }, {
221
+ outputDir: './thumbnails',
222
+ format: 'jpg',
223
+ quality: 85,
224
+ filenameTemplate: 'scene_{frame}'
225
+ });
226
+ ```
227
+
228
+ ### API Reference
229
+
230
+ #### `detectSceneChanges(videoPath, options?)`
231
+
232
+ Detects scene changes in a video file.
233
+
234
+ **Parameters:**
235
+ - `videoPath` (string): Path to input video file
236
+ - `options` (DetectionOptions, optional):
237
+ - `sensitivity` ('low' | 'medium' | 'high' | 'custom'): Detection sensitivity
238
+ - `customThresholds` ({ intraThresh, intraThresh2 }): Custom threshold values (when sensitivity='custom')
239
+ - `searchRange` ('auto' | 'small' | 'medium' | 'large'): Motion search range
240
+ - `signal` (AbortSignal): For cancellation support
241
+ - `temporalSmoothing` ({ enabled, windowSize, minConsecutive }): Temporal smoothing config
242
+ - `onProgress` (function): Progress callback with fps, eta, scenesDetected
243
+ - `onScene` (function): Callback for each detected scene
244
+
245
+ **Returns:** `Promise<DetectionResult>`
246
+
247
+ ```typescript
248
+ interface DetectionResult {
249
+ scenes: Array<{
250
+ frameNumber: number;
251
+ timestamp: number; // Seconds
252
+ timecode: string; // HH:MM:SS.mmm
253
+ confidence: number; // 0-1
254
+ duration: number; // Seconds until next scene
255
+ frameCount: number; // Frames until next scene
256
+ }>;
257
+ metadata: {
258
+ totalFrames: number;
259
+ duration: number;
260
+ fps: number;
261
+ resolution: { width: number; height: number };
262
+ codec?: string;
263
+ pixelFormat?: string;
264
+ bitrate?: number;
265
+ };
266
+ stats: {
267
+ processingTime: number;
268
+ framesPerSecond: number;
269
+ };
270
+ }
271
+ ```
272
+
273
+ #### `extractSceneImages(videoPath, options?, imageOptions?)`
274
+
275
+ Detects scenes and extracts thumbnail images in a single FFmpeg pass.
276
+
277
+ **Additional parameter:**
278
+ - `imageOptions` (FrameImageOptions):
279
+ - `outputDir` (string): Output directory
280
+ - `format` ('jpg' | 'png' | 'bmp'): Image format
281
+ - `quality` (number): JPEG quality 1-100
282
+ - `width` (number): Output width (maintains aspect ratio)
283
+ - `filenameTemplate` (string): Use `{frame}` and `{timestamp}` placeholders
284
+
285
+ ## How It Works
286
+
287
+ 1. **Frame Extraction**: FFmpeg extracts grayscale frames via streaming ring buffer with zero-copy alternating buffers
288
+ 2. **Quick Reject**: Sampled pixel comparison (every 64th pixel) skips nearly-identical frames without touching WASM
289
+ 3. **Motion Analysis**: WebAssembly-compiled Xvid motion estimation with configurable thresholds and SIMD acceleration
290
+ 4. **Confidence Scoring**: Raw sSAD scores are normalized to 0-1 confidence values
291
+ 5. **Fade Detection**: Drift comparison against last keyframe catches gradual dissolves lasting 30+ frames
292
+ 6. **Temporal Smoothing**: Optional sliding window filter suppresses flashes and merges detection clusters
293
+ 7. **Double Buffering**: Previous frame stays in WASM memory between calls, halving memory copies
294
+
295
+ ## Performance
296
+
297
+ - **Processing speed**: 80-150+ fps on 1080p video (with quick-reject, most frames skip WASM entirely)
298
+ - **Memory usage**: ~200-300 MB with pre-allocated WASM buffers and buffer pooling
299
+ - **4K support**: Auto-sized ring buffer scales to any resolution
300
+ - **Optimizations**: WASM SIMD, double-buffered frames, pre-allocated macroblock array, zero-alloc frame extraction, quick-reject filtering
301
+
302
+ ## Requirements
303
+
304
+ - **Node.js**: 18.0.0 or higher
305
+ - **FFmpeg & FFprobe**: Automatically installed via `@ffmpeg-installer/ffmpeg` and `@ffprobe-installer/ffprobe`
306
+
307
+ ## Building from Source
308
+
309
+ ```bash
310
+ # Install dependencies
311
+ npm install
312
+
313
+ # Build WASM (requires Emscripten SDK)
314
+ npm run build:wasm
315
+
316
+ # Build TypeScript + bundle
317
+ npm run build
318
+ ```
319
+
320
+ ## License
321
+
322
+ GPL-2.0
323
+
324
+ This project is based on:
325
+ - [vapoursynth-wwxd](https://github.com/dubhater/vapoursynth-wwxd) by dubhater (GPL-2.0)
326
+ - Xvid's motion estimation algorithm (GPL-2.0)
327
+
328
+ ## Credits
329
+
330
+ - Original vapoursynth-wwxd plugin: [dubhater](https://github.com/dubhater)
331
+ - Xvid motion estimation algorithm: [Xvid Team](https://www.xvid.com)