@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 +331 -280
- package/bin/cli.js +359 -293
- package/dist/decoder/ffmpeg-decoder.d.ts +15 -3
- package/dist/decoder/ffmpeg-decoder.d.ts.map +1 -1
- package/dist/decoder/ffmpeg-decoder.js +138 -15
- package/dist/decoder/ffmpeg-decoder.js.map +1 -1
- package/dist/detection/detector.d.ts.map +1 -1
- package/dist/detection/detector.js +134 -17
- package/dist/detection/detector.js.map +1 -1
- package/dist/detection/temporal-smoother.d.ts +32 -0
- package/dist/detection/temporal-smoother.d.ts.map +1 -0
- package/dist/detection/temporal-smoother.js +88 -0
- package/dist/detection/temporal-smoother.js.map +1 -0
- package/dist/detection/wasm-bridge.d.ts +26 -23
- package/dist/detection/wasm-bridge.d.ts.map +1 -1
- package/dist/detection/wasm-bridge.js +107 -62
- package/dist/detection/wasm-bridge.js.map +1 -1
- package/dist/detection.wasm.js +2 -2
- package/dist/detection.wasm.wasm +0 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -1
- package/dist/index.js.map +1 -1
- package/dist/keyframes.cjs.js +492 -95
- package/dist/keyframes.cjs.js.map +1 -1
- package/dist/keyframes.esm.js +490 -96
- package/dist/keyframes.esm.js.map +1 -1
- package/dist/types/index.d.ts +36 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +79 -77
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
|
|
8
|
-
- **Accurate**: Uses Xvid's proven motion estimation algorithm
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
"frameNumber": 143,
|
|
130
|
-
"timestamp": 5.964,
|
|
131
|
-
"timecode": "00:00:05.964"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
{
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
-
|
|
236
|
-
-
|
|
237
|
-
-
|
|
238
|
-
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
-
|
|
279
|
-
-
|
|
280
|
-
-
|
|
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)
|