video-sampler 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vedang Danej
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,582 @@
1
+ # video-sampler
2
+
3
+ Extract WAV audio and adaptive, non-duplicate sample frames from video files.
4
+
5
+ `video-sampler` is a Node.js package and CLI for extracting useful media outputs from video files. It can:
6
+
7
+ - extract clean WAV audio from a video
8
+ - sample representative visual frames across the full video
9
+ - avoid near-duplicate frames with perceptual image hashing
10
+ - reject mostly black, mostly white, or very low-information frames
11
+ - work with bundled FFmpeg/FFprobe binaries or custom binary paths
12
+
13
+ It is useful when you need audio, representative frames, or both from a video. Common use cases include video previews, media indexing, moderation pipelines, content analysis, scene sampling, presentation recordings, interview recordings, video resumes, async assessments, and any workflow where processing every frame would be wasteful.
14
+
15
+ ## Table of Contents
16
+
17
+ - [Installation](#installation)
18
+ - [Requirements](#requirements)
19
+ - [Quick Start](#quick-start)
20
+ - [CLI](#cli)
21
+ - [Library](#library)
22
+ - [Demo](#demo)
23
+ - [What It Does](#what-it-does)
24
+ - [Audio Extraction](#audio-extraction)
25
+ - [Frame Sampling](#frame-sampling)
26
+ - [CLI Usage](#cli-usage)
27
+ - [CLI Options](#cli-options)
28
+ - [CLI Examples](#cli-examples)
29
+ - [Library API](#library-api)
30
+ - [`extractAudio(videoPath, options)`](#extractaudiovideopath-options)
31
+ - [`sampleFrames(videoPath, options)`](#sampleframesvideopath-options)
32
+ - [Frame Result](#frame-result)
33
+ - [Common Recipes](#common-recipes)
34
+ - [Error Handling](#error-handling)
35
+ - [Notes](#notes)
36
+ - [License](#license)
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ npm install video-sampler
42
+ ```
43
+
44
+ ## Requirements
45
+
46
+ This package is ESM-only.
47
+
48
+ Use ESM imports:
49
+
50
+ ```js
51
+ import { extractAudio, sampleFrames } from 'video-sampler';
52
+ ```
53
+
54
+ If your project uses CommonJS, use dynamic `import()`:
55
+
56
+ ```js
57
+ async function main() {
58
+ const { extractAudio, sampleFrames } = await import('video-sampler');
59
+ }
60
+ ```
61
+
62
+ It includes default FFmpeg and FFprobe binaries through:
63
+
64
+ - `@ffmpeg-installer/ffmpeg`
65
+ - `@ffprobe-installer/ffprobe`
66
+
67
+ Advanced users can pass custom `ffmpegPath` and `ffprobePath` values if they want to use system-installed binaries or a specific FFmpeg build.
68
+
69
+ ## Quick Start
70
+
71
+ ### CLI
72
+
73
+ ```bash
74
+ npx video-sampler ./video.webm
75
+ ```
76
+
77
+ By default, the CLI writes:
78
+
79
+ - WAV audio to `./audio`
80
+ - accepted frame images to `./frames`
81
+
82
+ Example with custom outputs:
83
+
84
+ ```bash
85
+ npx video-sampler ./video.mp4 --audio-dir ./output/audio --frames-dir ./output/frames --frames 20 --similarity-threshold 10 --min-gap-ms 1000
86
+ ```
87
+
88
+ ### Library
89
+
90
+ ```js
91
+ import { extractAudio, sampleFrames } from 'video-sampler';
92
+
93
+ const audio = await extractAudio('./video.webm', {
94
+ outputDir: './output/audio',
95
+ });
96
+
97
+ const frames = await sampleFrames('./video.webm', {
98
+ outputDir: './output/frames',
99
+ targetFrames: 20,
100
+ similarityThreshold: 10,
101
+ minGapMs: 1000,
102
+ });
103
+
104
+ console.log(audio.path);
105
+ console.log(frames.frames.map((frame) => frame.path));
106
+ ```
107
+
108
+ Only need frames:
109
+
110
+ ```js
111
+ import { sampleFrames } from 'video-sampler';
112
+
113
+ const result = await sampleFrames('./video.mp4', {
114
+ outputDir: './frames',
115
+ targetFrames: 30,
116
+ minGapMs: 100,
117
+ similarityThreshold: 5,
118
+ });
119
+
120
+ console.log(result.frames.map((frame) => frame.path));
121
+ ```
122
+
123
+ ## Demo
124
+
125
+ This demo uses a lecture video, where most of the useful visual information is in presentation slides.
126
+
127
+ [<img src="https://raw.githubusercontent.com/vedangdanej/video-sampler/main/demo/thumbnail.jpg" alt="Watch the MIT lecture video used for the video-sampler demo" width="720">](https://www.youtube.com/watch?v=4fTOrb1yBFU)
128
+
129
+ Command:
130
+
131
+ ```bash
132
+ npx video-sampler ./video.mp4 --target-frames 15 --min-gap 500 --similarity-threshold 7
133
+ ```
134
+
135
+ Output: 10 representative frames. `similarityThreshold` is set to `7` so progressive slide reveals are not filtered too aggressively.
136
+
137
+ <img src="https://raw.githubusercontent.com/vedangdanej/video-sampler/main/demo/frame-grid.jpg" alt="10 representative demo frames extracted by video-sampler" width="760">
138
+
139
+ ## What It Does
140
+
141
+ ### Audio Extraction
142
+
143
+ `extractAudio()` converts the input video's audio track into a WAV file.
144
+
145
+ The generated WAV is:
146
+
147
+ - PCM signed 16-bit little-endian
148
+ - 16 kHz
149
+ - mono
150
+ - MIME type `audio/wav`
151
+
152
+ This is a practical format for speech-to-text, transcription, audio indexing, and other audio analysis systems.
153
+
154
+ ### Frame Sampling
155
+
156
+ `sampleFrames()` does not simply take one frame every N seconds.
157
+
158
+ Instead, it:
159
+
160
+ 1. probes video duration and metadata with FFprobe
161
+ 2. calculates a realistic frame target based on duration and options
162
+ 3. generates more candidate timestamps than the final target
163
+ 4. extracts candidate frames with FFmpeg
164
+ 5. rejects low-information frames
165
+ 6. hashes candidates with perceptual image hashing
166
+ 7. avoids near-duplicates using Hamming distance
167
+ 8. keeps accepted frames and cleans up rejected candidates by default
168
+ 9. adds fallback coverage frames when a video is visually repetitive
169
+
170
+ The product contract is:
171
+
172
+ > Give me up to N representative frames, spread across the full video, avoiding near-duplicates.
173
+
174
+ This works well for both mostly static videos and fast-moving clips. For a mostly still video, fallback coverage helps return useful frames across the timeline. For a fast-moving video, you can lower `minGapMs` and `similarityThreshold` to collect frames closer together while still avoiding exact duplicates.
175
+
176
+ ## CLI Usage
177
+
178
+ ```bash
179
+ video-sampler [videoPath] [options]
180
+ ```
181
+
182
+ You can also provide the video path with `--video`:
183
+
184
+ ```bash
185
+ video-sampler --video ./video.webm
186
+ ```
187
+
188
+ If no video path is provided, the CLI tries to use the only supported video file in the current directory.
189
+
190
+ ### CLI Options
191
+
192
+ | Option | Description | Default |
193
+ | ---------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
194
+ | `--video <path>` | Video file to process. | Uses positional path or the only supported video in the current directory |
195
+ | `--frames <n>` | Target number of frames to return. Same as `--target-frames`. | `12` |
196
+ | `--target-frames <n>` | Target number of frames to return. | `12` |
197
+ | `--max-frames <n>` | Hard upper limit for returned frames. | `24` |
198
+ | `--min-frames <n>` | Minimum frames to try to return when feasible. | `3` |
199
+ | `--min-gap <ms>` | Soft minimum gap between selected frames, in milliseconds. | `1000` |
200
+ | `--min-gap-ms <ms>` | Same as `--min-gap`. | `1000` |
201
+ | `--start-time-ms <ms>` | Start sampling at this timestamp, in milliseconds. | `0` |
202
+ | `--end-before-ms <ms>` | Stop sampling this many milliseconds before the video ends. | `0` |
203
+ | `--candidate-multiplier <n>` | Candidate count multiplier. More candidates can improve variety but increase processing time. | `4` |
204
+ | `--max-candidates <n>` | Maximum candidate frames to inspect. | `160` |
205
+ | `--similarity-threshold <n>` | Perceptual hash Hamming distance threshold for duplicate rejection. | `10` |
206
+ | `--compare-recent <n>` | Number of recent accepted frames to compare after early selection. | `5` |
207
+ | `--frames-dir <path>` | Output folder for accepted frames. | `./frames` |
208
+ | `--audio-dir <path>` | Output folder for WAV audio. | `./audio` |
209
+ | `--format <jpg\|png\|webp>` | Frame image format. | `jpg` |
210
+ | `--jpeg-quality <n>` | FFmpeg `q:v` value for JPG output. Lower is better quality. | `2` |
211
+ | `--ffmpeg-path <path>` | Custom FFmpeg binary path. | Bundled FFmpeg |
212
+ | `--ffprobe-path <path>` | Custom FFprobe binary path. | Bundled FFprobe |
213
+ | `--debug` | Print FFmpeg debug output from frame extraction. | `false` |
214
+ | `--no-fallback` | Disable fallback frame coverage. | Fallback enabled |
215
+ | `--keep-rejected` | Keep rejected candidate frame files for inspection. | Rejected candidates deleted |
216
+ | `--help`, `-h` | Print CLI help. | - |
217
+
218
+ ### CLI Examples
219
+
220
+ Extract audio and about 20 representative frames:
221
+
222
+ ```bash
223
+ npx video-sampler ./video.mp4 --frames 20
224
+ ```
225
+
226
+ Use custom output folders:
227
+
228
+ ```bash
229
+ npx video-sampler ./video.webm --audio-dir ./processed/audio --frames-dir ./processed/frames
230
+ ```
231
+
232
+ Be more strict about duplicate frames:
233
+
234
+ ```bash
235
+ npx video-sampler ./video.mp4 --frames 20 --similarity-threshold 16
236
+ ```
237
+
238
+ Skip the intro:
239
+
240
+ ```bash
241
+ npx video-sampler ./video.mp4 --frames 12 --start-time-ms 5000
242
+ ```
243
+
244
+ Skip ending fade-outs or closing screens:
245
+
246
+ ```bash
247
+ npx video-sampler ./video.mp4 --frames 12 --end-before-ms 500
248
+ ```
249
+
250
+ Keep all rejected candidate images for debugging:
251
+
252
+ ```bash
253
+ npx video-sampler ./video.mp4 --keep-rejected --debug
254
+ ```
255
+
256
+ Use custom FFmpeg and FFprobe binaries:
257
+
258
+ ```bash
259
+ npx video-sampler ./video.mp4 --ffmpeg-path /usr/local/bin/ffmpeg --ffprobe-path /usr/local/bin/ffprobe
260
+ ```
261
+
262
+ ## Library API
263
+
264
+ ```js
265
+ import { extractAudio, sampleFrames } from 'video-sampler';
266
+ ```
267
+
268
+ The package includes TypeScript declarations, so editors can show option names, defaults, and descriptions on hover.
269
+
270
+ ## `extractAudio(videoPath, options)`
271
+
272
+ Extracts WAV audio from a video.
273
+
274
+ ```js
275
+ const audio = await extractAudio('./video.mp4', {
276
+ outputDir: './audio',
277
+ });
278
+ ```
279
+
280
+ ### Audio Options
281
+
282
+ | Option | Type | Description | Default |
283
+ | ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------- |
284
+ | `outputDir` | `string` | Directory where the WAV file should be written. Created automatically if missing. Ignored when `outputPath` is provided. | `process.cwd()` |
285
+ | `outputPath` | `string` | Exact output path for the WAV file. Parent directories are created automatically. | `<outputDir>/<input-video-basename>.wav` |
286
+ | `ffmpegPath` | `string` | Custom FFmpeg binary path. | Bundled FFmpeg |
287
+
288
+ ### Audio Result
289
+
290
+ ```js
291
+ {
292
+ path: '/absolute/path/to/video.wav',
293
+ mime: 'audio/wav',
294
+ ext: 'wav'
295
+ }
296
+ ```
297
+
298
+ ### `outputDir` vs `outputPath`
299
+
300
+ Use `outputDir` when you want the package to name the WAV file for you:
301
+
302
+ ```js
303
+ await extractAudio('./videos/video.mp4', {
304
+ outputDir: './audio',
305
+ });
306
+ ```
307
+
308
+ This writes:
309
+
310
+ ```text
311
+ ./audio/video.wav
312
+ ```
313
+
314
+ Use `outputPath` when you want full control over the final file path:
315
+
316
+ ```js
317
+ await extractAudio('./videos/video.mp4', {
318
+ outputPath: './audio/session-123.wav',
319
+ });
320
+ ```
321
+
322
+ ## `sampleFrames(videoPath, options)`
323
+
324
+ Extracts representative frames from a video.
325
+
326
+ ```js
327
+ const result = await sampleFrames('./video.mp4', {
328
+ outputDir: './frames',
329
+ targetFrames: 12,
330
+ });
331
+ ```
332
+
333
+ Accepted frame files are left on disk for downstream processing. Rejected candidate files are deleted by default.
334
+ Accepted frame filenames are ordered by selection order and timestamp, for example `frame-001-t001158ms.jpg`.
335
+ When `cleanupRejected` is `false`, rejected candidate filenames include their candidate order and timestamp, for example `candidate-048-t238350ms.jpg`.
336
+
337
+ ### Frame Options
338
+
339
+ | Option | Type | Description | Default |
340
+ | ----------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------- |
341
+ | `targetFrames` | `number` | Ideal number of frames to return. This is a target, not a guarantee. The sampler may return fewer frames when the video is short, visually repetitive, or has too few usable frames. | `12` |
342
+ | `maxFrames` | `number` | Hard upper limit for returned frames. The function never returns more than this. | `24` |
343
+ | `minFrames` | `number` | Minimum frames to try to return when feasible. This may still be missed if too few usable frames can be extracted. | `3` |
344
+ | `minGapMs` | `number` | Soft minimum spacing between selected frames, in milliseconds. The sampler may relax this during fallback selection to satisfy `minFrames`. | `1000` |
345
+ | `startTimeMs` | `number` | Start sampling at this timestamp, in milliseconds. Negative values are clamped to `0` with a warning. | `0` |
346
+ | `endBeforeMs` | `number` | Stop sampling this many milliseconds before the video ends. Useful for avoiding fade-outs, closing screens, or black ending frames. | `0` |
347
+ | `candidateMultiplier` | `number` | Multiplier used to decide how many candidate frames to inspect. With a resolved target of 12 and multiplier 4, the sampler tries about 48 candidates before filtering. | `4` |
348
+ | `maxCandidates` | `number` | Maximum candidate frames to inspect. Prevents excessive FFmpeg and hashing work on long videos. | `160` |
349
+ | `similarityThreshold` | `number` | Perceptual hash Hamming distance required for a frame to count as visually distinct. Higher values reject duplicates more strictly. | `10` |
350
+ | `compareRecentCount` | `number` | Number of recent accepted frames to compare once the accepted set is large. Early selection compares against all accepted frames. | `5` |
351
+ | `outputDir` | `string` | Directory where accepted frame files should be written. Created automatically if missing. | `process.cwd()` |
352
+ | `imageFormat` | `'jpg' \| 'jpeg' \| 'png' \| 'webp'` | Image format for extracted frames. `jpeg` is normalized to `jpg`. | `'jpg'` |
353
+ | `jpegQuality` | `number` | FFmpeg `q:v` value for JPEG output. Lower values produce better quality and larger files. Only applies to JPG output. | `2` |
354
+ | `cleanupRejected` | `boolean` | Delete rejected candidate frame files after selection. Set to `false` to inspect all candidates while debugging. | `true` |
355
+ | `includeFallbackFrames` | `boolean` | Add fallback frames for coverage when duplicate filtering returns too few frames. Useful for mostly still videos. | `true` |
356
+ | `debug` | `boolean` | Print FFmpeg extraction output. | `false` |
357
+ | `ffmpegPath` | `string` | Custom FFmpeg binary path. | Bundled FFmpeg |
358
+ | `ffprobePath` | `string` | Custom FFprobe binary path. | Bundled FFprobe |
359
+
360
+ ### Understanding Key Frame Options
361
+
362
+ #### `targetFrames`
363
+
364
+ This is the number of frames you would like to get, not a strict promise.
365
+
366
+ For example, if you request 20 frames from a very short or visually repetitive video, the sampler may return fewer frames because it avoids near-duplicates and low-information images. If you want denser frame sampling from a fast-moving clip, lower `minGapMs` and use a lower `similarityThreshold`.
367
+
368
+ #### `candidateMultiplier`
369
+
370
+ The sampler extracts more candidate frames than it plans to return.
371
+
372
+ For example:
373
+
374
+ ```js
375
+ await sampleFrames('./video.mp4', {
376
+ targetFrames: 12,
377
+ candidateMultiplier: 4,
378
+ });
379
+ ```
380
+
381
+ This asks the sampler to inspect about 48 candidate timestamps before selecting the best representative frames.
382
+
383
+ Higher values can improve visual variety, but they also increase FFmpeg extraction and image hashing work.
384
+
385
+ #### `startTimeMs` and `endBeforeMs`
386
+
387
+ By default, the sampler considers the full usable video timeline. Use these options when you only want frames from part of a video:
388
+
389
+ ```js
390
+ const result = await sampleFrames('./video.mp4', {
391
+ startTimeMs: 30_000,
392
+ endBeforeMs: 500,
393
+ });
394
+ ```
395
+
396
+ `startTimeMs` skips the beginning of the video. `endBeforeMs` skips the end of the video, which is useful when recordings finish on fade-outs, app-closing screens, or black frames.
397
+
398
+ #### `similarityThreshold`
399
+
400
+ Each candidate frame is converted into a perceptual image hash. The sampler compares hashes using Hamming distance.
401
+
402
+ `similarityThreshold` is the minimum number of differing hash bits required for a candidate to count as visually distinct.
403
+
404
+ - lower values are more permissive and may allow similar frames
405
+ - higher values are stricter and may return fewer frames
406
+ - the default `10` is a practical starting point for general representative frame sampling
407
+
408
+ #### `cleanupRejected`
409
+
410
+ This is `true` by default.
411
+
412
+ When enabled, only accepted frame files remain in the output directory. Rejected candidate images are deleted.
413
+
414
+ Set it to `false` when debugging:
415
+
416
+ ```js
417
+ await sampleFrames('./video.mp4', {
418
+ outputDir: './frames-debug',
419
+ cleanupRejected: false,
420
+ });
421
+ ```
422
+
423
+ ## Frame Result
424
+
425
+ `sampleFrames()` returns metadata, resolved options, selected frames, stats, and warnings.
426
+
427
+ ```js
428
+ {
429
+ video: {
430
+ path: './video.mp4',
431
+ durationSeconds: 180.42,
432
+ width: 1920,
433
+ height: 1080,
434
+ fps: 30
435
+ },
436
+ output: {
437
+ directory: '/absolute/path/to/frames'
438
+ },
439
+ options: {
440
+ resolvedTargetFrames: 12,
441
+ maxFrames: 24,
442
+ minFrames: 3,
443
+ minGapMs: 1000,
444
+ startTimeMs: 0,
445
+ endBeforeMs: 0,
446
+ candidateCount: 48,
447
+ similarityThreshold: 10
448
+ },
449
+ frames: [
450
+ {
451
+ path: '/absolute/path/to/frames/frame-id.jpg',
452
+ timestamp: 14.29,
453
+ hash: '...',
454
+ score: {
455
+ minDistanceToRecentAccepted: null,
456
+ acceptedReason: 'first_frame'
457
+ }
458
+ }
459
+ ],
460
+ stats: {
461
+ candidatesExtracted: 48,
462
+ candidatesRejectedAsSimilar: 20,
463
+ candidatesRejectedForSpacing: 4,
464
+ candidatesRejectedAsLowInformation: 1,
465
+ fallbackFramesAdded: 0,
466
+ returnedFrames: 12
467
+ },
468
+ warnings: []
469
+ }
470
+ ```
471
+
472
+ `minDistanceToRecentAccepted` is `null` for the first accepted frame because there are no earlier accepted frames to compare
473
+ against.
474
+
475
+ ### Accepted Frame Reasons
476
+
477
+ | Reason | Meaning |
478
+ | ------------------------ | ------------------------------------------------------------------------------------------ |
479
+ | `first_frame` | The first usable candidate frame was accepted. |
480
+ | `visually_distinct` | The frame was far enough from recently accepted frames by perceptual hash distance. |
481
+ | `fallback_even_coverage` | The frame was added to preserve basic coverage when the video was too visually repetitive. |
482
+
483
+ ## Common Recipes
484
+
485
+ ### Process a video upload
486
+
487
+ ```js
488
+ import { extractAudio, sampleFrames } from 'video-sampler';
489
+
490
+ const videoPath = './uploads/video.webm';
491
+
492
+ const [audio, frames] = await Promise.all([
493
+ extractAudio(videoPath, {
494
+ outputDir: './processed/audio',
495
+ }),
496
+ sampleFrames(videoPath, {
497
+ outputDir: './processed/frames',
498
+ targetFrames: 12,
499
+ maxFrames: 24,
500
+ minGapMs: 1000,
501
+ }),
502
+ ]);
503
+
504
+ console.log({
505
+ audio: audio.path,
506
+ frames: frames.frames.map((frame) => frame.path),
507
+ });
508
+ ```
509
+
510
+ ### Prefer more visual variety
511
+
512
+ ```js
513
+ const result = await sampleFrames('./lecture.mp4', {
514
+ targetFrames: 20,
515
+ candidateMultiplier: 6,
516
+ similarityThreshold: 14,
517
+ });
518
+ ```
519
+
520
+ ### Sample a fast-moving video more densely
521
+
522
+ ```js
523
+ const result = await sampleFrames('./short-clip.mp4', {
524
+ targetFrames: 20,
525
+ minGapMs: 150,
526
+ similarityThreshold: 5,
527
+ });
528
+ ```
529
+
530
+ ### Use custom binaries
531
+
532
+ ```js
533
+ const result = await sampleFrames('./video.mp4', {
534
+ ffmpegPath: '/opt/ffmpeg/bin/ffmpeg',
535
+ ffprobePath: '/opt/ffmpeg/bin/ffprobe',
536
+ });
537
+ ```
538
+
539
+ ```js
540
+ const audio = await extractAudio('./video.mp4', {
541
+ ffmpegPath: '/opt/ffmpeg/bin/ffmpeg',
542
+ });
543
+ ```
544
+
545
+ ## Error Handling
546
+
547
+ Both functions throw errors for fatal failures.
548
+
549
+ Examples include:
550
+
551
+ - input video file does not exist
552
+ - video duration cannot be probed
553
+ - FFmpeg cannot extract usable frames
554
+ - perceptual hash generation fails completely
555
+ - audio conversion fails
556
+
557
+ `sampleFrames()` may also return non-fatal `warnings`, such as deprecated option usage or fallback extraction behavior.
558
+
559
+ ```js
560
+ try {
561
+ const result = await sampleFrames('./video.mp4');
562
+
563
+ for (const warning of result.warnings) {
564
+ console.warn(warning);
565
+ }
566
+ } catch (error) {
567
+ console.error('Video processing failed:', error.message);
568
+ }
569
+ ```
570
+
571
+ ## Notes
572
+
573
+ - Frame sampling is adaptive. Requested frame counts are targets, not guarantees.
574
+ - By default, frame candidates are spread across the full usable video duration.
575
+ - Accepted frame files stay on disk for downstream processing.
576
+ - Rejected candidate files are deleted by default.
577
+ - The package does not perform facial recognition, emotion detection, pose detection, transcription, or scoring.
578
+ - Processing happens locally with FFmpeg, FFprobe, Sharp, and perceptual hashing.
579
+
580
+ ## License
581
+
582
+ MIT (c) Vedang Danej
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "video-sampler",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "description": "Extract WAV audio and adaptive, non-duplicate sample frames from video files.",
6
+ "author": "Vedang Danej",
7
+ "license": "MIT",
8
+ "main": "./src/lib.js",
9
+ "types": "./src/lib.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./src/lib.d.ts",
13
+ "import": "./src/lib.js"
14
+ },
15
+ "./cli": "./src/index.js"
16
+ },
17
+ "bin": {
18
+ "video-sampler": "./src/index.js"
19
+ },
20
+ "files": [
21
+ "src"
22
+ ],
23
+ "keywords": [
24
+ "video",
25
+ "video-processing",
26
+ "frame-extraction",
27
+ "sample-frames",
28
+ "representative-frames",
29
+ "audio-extraction",
30
+ "wav",
31
+ "ffmpeg",
32
+ "ffprobe",
33
+ "media"
34
+ ],
35
+ "dependencies": {
36
+ "@ffmpeg-installer/ffmpeg": "^1.1.0",
37
+ "@ffprobe-installer/ffprobe": "^2.1.2",
38
+ "fluent-ffmpeg": "^2.1.3",
39
+ "sharp": "^0.34.5",
40
+ "sharp-phash": "^2.2.0",
41
+ "uuid": "^14.0.0"
42
+ }
43
+ }