eprec 1.0.1 → 1.2.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/README.md CHANGED
@@ -13,45 +13,115 @@
13
13
  [![MIT License][license-badge]][license]
14
14
  <!-- prettier-ignore-end -->
15
15
 
16
- ## Installation
16
+ ## Overview
17
+
18
+ A Bun-based CLI that processes recorded course videos by splitting chapter
19
+ markers into separate files, trimming silence at the start/end, and optionally
20
+ removing spoken "Jarvis" command windows via transcript timestamps refined with
21
+ audio-based silence detection.
22
+
23
+ ## Requirements
17
24
 
18
- To install dependencies:
25
+ - **Bun** - runtime and package manager
26
+ - **ffmpeg + ffprobe** - must be available on PATH
27
+ - **whisper-cli** _(optional)_ - from
28
+ [whisper.cpp](https://github.com/ggerganov/whisper.cpp), required for
29
+ transcription
30
+ - Pass `--whisper-binary-path` if not on PATH
31
+ - Model file auto-downloads to `.cache/whispercpp/ggml-small.en.bin`
32
+ - **Silero VAD model** - auto-downloads to `.cache/silero-vad.onnx` on first use
33
+
34
+ ## Installation
19
35
 
20
36
  ```bash
21
37
  bun install
22
38
  ```
23
39
 
24
- To run:
40
+ ## Quick Start
25
41
 
26
42
  ```bash
27
- bun run index.ts
43
+ bun process-course-video.ts "/path/to/input.mp4" "/path/to/output" \
44
+ --enable-transcription \
45
+ --keep-intermediates \
46
+ --write-logs
28
47
  ```
29
48
 
30
- ## Whisper.cpp transcription (optional)
49
+ ## Web UI (experimental)
31
50
 
32
- Install the local whisper.cpp CLI (Homebrew):
51
+ Start the Remix-powered UI shell with the CLI:
33
52
 
34
53
  ```bash
35
- brew install whisper-cpp
54
+ bun cli.ts app start
55
+ ```
56
+
57
+ Then open `http://127.0.0.1:3000`. Use `--port` or `--host` to override the
58
+ defaults.
59
+
60
+ ## CLI Options
61
+
62
+ | Option | Alias | Description | Default |
63
+ | ------------------------ | ----- | --------------------------------------- | ----------- |
64
+ | `input` | | Input video file (mp4/mkv) | _required_ |
65
+ | `outputDir` | | Output directory | `output` |
66
+ | `--min-chapter-seconds` | `-m` | Skip chapters shorter than this | `15` |
67
+ | `--dry-run` | `-d` | Don't write files or run ffmpeg | `false` |
68
+ | `--keep-intermediates` | `-k` | Keep `.tmp` files for debugging | `false` |
69
+ | `--write-logs` | `-l` | Write log files for skips/fallbacks | `false` |
70
+ | `--enable-transcription` | | Run whisper.cpp for command detection | `false` |
71
+ | `--whisper-model-path` | | Path to whisper.cpp model file | auto-cached |
72
+ | `--whisper-language` | | Language for whisper | `en` |
73
+ | `--whisper-binary-path` | | Path to `whisper-cli` binary | system PATH |
74
+ | `--chapter` | `-c` | Filter to specific chapters (see below) | all |
75
+
76
+ ## Chapter Selection
77
+
78
+ The `--chapter` flag supports flexible selection:
79
+
80
+ - Single: `--chapter 4`
81
+ - Range: `--chapter 4-6`
82
+ - Open range: `--chapter 4-*` (chapter 4 to end)
83
+ - Multiple: `--chapter 4,6,9-12`
84
+
85
+ Chapter numbers are 1-based by default.
86
+
87
+ ## Output Structure
88
+
89
+ Final files are written to the output directory with names like:
90
+
91
+ ```
92
+ chapter-01-intro.mp4
93
+ chapter-02-getting-started.mp4
94
+ chapter-03-custom-title.mp4
36
95
  ```
37
96
 
38
- The default small English model is downloaded on first use and cached at
39
- `.cache/whispercpp/ggml-small.en.bin`. Replace that file (or pass
40
- `--whisper-model-path`) to use a different model.
97
+ When `--keep-intermediates` is enabled, intermediate files go to `output/.tmp/`:
98
+
99
+ | File Pattern | Description |
100
+ | --------------------- | ------------------------------------------------ |
101
+ | `*-raw.mp4` | Raw chapter clip with initial padding removed |
102
+ | `*-normalized.mp4` | Audio normalized (highpass + denoise + loudnorm) |
103
+ | `*-transcribe.wav` | Audio extracted for whisper |
104
+ | `*-transcribe.json` | Whisper JSON output |
105
+ | `*-transcribe.txt` | Whisper text output |
106
+ | `*-splice-*.mp4` | Segments before/after command windows |
107
+ | `*-spliced.mp4` | Concatenated output after command removal |
108
+ | `*.log` | Per-chapter skip/fallback logs |
109
+ | `process-summary.log` | Overall processing summary |
110
+
111
+ ## Voice Commands
112
+
113
+ Commands are spoken in the format: `jarvis <command> ... thanks`
41
114
 
42
- Enable transcription with `--enable-transcription` when running
43
- `process-course-video.ts` to skip chapters that include "jarvis bad take" or
44
- "bad take jarvis". If the CLI isn't on your PATH, pass `--whisper-binary-path`
45
- with the full path to `whisper-cli`.
115
+ | Command | Effect |
116
+ | --------------------------------------- | ----------------------- |
117
+ | `jarvis bad take thanks` | Skip the entire chapter |
118
+ | `jarvis filename my-custom-name thanks` | Rename output file |
46
119
 
47
- Customize skip phrases by repeating `--whisper-skip-phrase` (do not use
48
- comma-separated values because phrases may include commas).
120
+ The command window (from "jarvis" to "thanks") is removed from the final video.
49
121
 
50
- Manual test checklist:
122
+ ## More Details
51
123
 
52
- - Run with `--enable-transcription` and confirm whisper.cpp runs locally.
53
- - Verify a chapter containing the phrase is skipped and logged.
54
- - Verify a normal chapter still renders and writes output.
124
+ Implementation notes and pipeline details live in `docs/README.md`.
55
125
 
56
126
  This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com)
57
127
  is a fast all-in-one JavaScript runtime.
@@ -0,0 +1,592 @@
1
+ :root {
2
+ color-scheme: light;
3
+ }
4
+
5
+ * {
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ body {
10
+ margin: 0;
11
+ font-family:
12
+ 'Inter',
13
+ 'Segoe UI',
14
+ system-ui,
15
+ -apple-system,
16
+ sans-serif;
17
+ background: #f8fafc;
18
+ color: #0f172a;
19
+ }
20
+
21
+ h1,
22
+ h2,
23
+ h3,
24
+ p {
25
+ margin: 0;
26
+ }
27
+
28
+ .app-shell {
29
+ max-width: 960px;
30
+ margin: 0 auto;
31
+ padding: 48px 24px 72px;
32
+ }
33
+
34
+ .app-header {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 12px;
38
+ margin-bottom: 32px;
39
+ }
40
+
41
+ .app-kicker {
42
+ font-size: 12px;
43
+ letter-spacing: 0.16em;
44
+ text-transform: uppercase;
45
+ color: #64748b;
46
+ font-weight: 600;
47
+ }
48
+
49
+ .app-title {
50
+ font-size: 32px;
51
+ font-weight: 700;
52
+ }
53
+
54
+ .app-subtitle {
55
+ font-size: 16px;
56
+ color: #475569;
57
+ line-height: 1.6;
58
+ max-width: 640px;
59
+ }
60
+
61
+ .app-grid {
62
+ display: grid;
63
+ gap: 16px;
64
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
65
+ }
66
+
67
+ .app-card {
68
+ background: #ffffff;
69
+ border-radius: 16px;
70
+ border: 1px solid #e2e8f0;
71
+ padding: 20px;
72
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 12px;
76
+ }
77
+
78
+ .app-list {
79
+ margin: 0;
80
+ padding-left: 18px;
81
+ color: #334155;
82
+ line-height: 1.6;
83
+ }
84
+
85
+ .app-muted {
86
+ color: #64748b;
87
+ line-height: 1.5;
88
+ }
89
+
90
+ .status-pill {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ gap: 6px;
94
+ padding: 4px 12px;
95
+ border-radius: 999px;
96
+ background: #e0f2fe;
97
+ color: #0369a1;
98
+ font-size: 12px;
99
+ font-weight: 600;
100
+ width: fit-content;
101
+ }
102
+
103
+ .counter-button {
104
+ display: inline-flex;
105
+ align-items: center;
106
+ justify-content: space-between;
107
+ gap: 12px;
108
+ padding: 10px 14px;
109
+ border: none;
110
+ border-radius: 10px;
111
+ background: #0ea5e9;
112
+ color: #ffffff;
113
+ font-size: 14px;
114
+ font-weight: 600;
115
+ cursor: pointer;
116
+ transition:
117
+ transform 0.15s ease,
118
+ background 0.15s ease;
119
+ }
120
+
121
+ .counter-button:hover {
122
+ background: #0284c7;
123
+ }
124
+
125
+ .counter-button:disabled {
126
+ cursor: not-allowed;
127
+ opacity: 0.6;
128
+ }
129
+
130
+ .counter-button:active {
131
+ transform: translateY(1px);
132
+ }
133
+
134
+ .counter-value {
135
+ font-weight: 700;
136
+ }
137
+
138
+ .app-card--full {
139
+ width: 100%;
140
+ }
141
+
142
+ .summary-grid {
143
+ display: grid;
144
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
145
+ gap: 16px;
146
+ margin-top: 8px;
147
+ }
148
+
149
+ .summary-item {
150
+ display: flex;
151
+ flex-direction: column;
152
+ gap: 4px;
153
+ }
154
+
155
+ .summary-label {
156
+ font-size: 12px;
157
+ font-weight: 600;
158
+ text-transform: uppercase;
159
+ letter-spacing: 0.08em;
160
+ color: #94a3b8;
161
+ }
162
+
163
+ .summary-value {
164
+ font-size: 16px;
165
+ font-weight: 600;
166
+ color: #0f172a;
167
+ }
168
+
169
+ .summary-subtext {
170
+ font-size: 13px;
171
+ color: #64748b;
172
+ line-height: 1.4;
173
+ }
174
+
175
+ .timeline-card {
176
+ gap: 20px;
177
+ }
178
+
179
+ .timeline-header {
180
+ display: flex;
181
+ align-items: flex-start;
182
+ justify-content: space-between;
183
+ gap: 16px;
184
+ flex-wrap: wrap;
185
+ }
186
+
187
+ .timeline-layout {
188
+ display: grid;
189
+ grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
190
+ gap: 20px;
191
+ }
192
+
193
+ .timeline-preview {
194
+ display: flex;
195
+ flex-direction: column;
196
+ gap: 12px;
197
+ }
198
+
199
+ .timeline-video {
200
+ display: flex;
201
+ flex-direction: column;
202
+ gap: 10px;
203
+ padding: 12px;
204
+ border-radius: 16px;
205
+ border: 1px solid #e2e8f0;
206
+ background: #ffffff;
207
+ }
208
+
209
+ .timeline-video-header {
210
+ display: flex;
211
+ align-items: flex-start;
212
+ justify-content: space-between;
213
+ gap: 12px;
214
+ }
215
+
216
+ .timeline-video-player {
217
+ width: 100%;
218
+ border-radius: 12px;
219
+ background: #0f172a;
220
+ aspect-ratio: 16 / 9;
221
+ }
222
+
223
+ .timeline-video-meta {
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: space-between;
227
+ font-size: 12px;
228
+ color: #64748b;
229
+ }
230
+
231
+ .timeline-track {
232
+ position: relative;
233
+ height: 72px;
234
+ border-radius: 16px;
235
+ border: 1px solid #e2e8f0;
236
+ background: linear-gradient(
237
+ 90deg,
238
+ rgba(226, 232, 240, 0.7) 0%,
239
+ rgba(226, 232, 240, 0.7) 2%,
240
+ transparent 2%,
241
+ transparent 20%
242
+ );
243
+ background-size: 20% 100%;
244
+ overflow: hidden;
245
+ }
246
+
247
+ .timeline-track--skeleton {
248
+ min-height: 72px;
249
+ background: linear-gradient(90deg, #f8fafc 0%, #e2e8f0 45%, #f8fafc 90%);
250
+ background-size: 200% 100%;
251
+ animation: shimmer 1.8s infinite;
252
+ }
253
+
254
+ .timeline-range {
255
+ position: absolute;
256
+ top: 14px;
257
+ height: 44px;
258
+ border-radius: 10px;
259
+ border: 1px solid #fca5a5;
260
+ background: #fee2e2;
261
+ cursor: pointer;
262
+ left: var(--range-left);
263
+ width: var(--range-width);
264
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
265
+ }
266
+
267
+ .timeline-range--manual {
268
+ background: #fef3c7;
269
+ border-color: #fbbf24;
270
+ }
271
+
272
+ .timeline-range--command {
273
+ background: #fee2e2;
274
+ border-color: #fca5a5;
275
+ }
276
+
277
+ .timeline-range.is-selected {
278
+ box-shadow: 0 0 0 2px rgba(14, 165, 233, 0.6);
279
+ transform: translateY(-1px);
280
+ }
281
+
282
+ .timeline-playhead {
283
+ position: absolute;
284
+ top: 0;
285
+ bottom: 0;
286
+ width: 2px;
287
+ left: var(--playhead);
288
+ background: #0ea5e9;
289
+ box-shadow: 0 0 0 1px rgba(14, 165, 233, 0.4);
290
+ }
291
+
292
+ .timeline-scale {
293
+ display: flex;
294
+ justify-content: space-between;
295
+ font-size: 12px;
296
+ color: #64748b;
297
+ }
298
+
299
+ .timeline-controls {
300
+ display: grid;
301
+ grid-template-columns: auto 1fr auto auto;
302
+ gap: 12px;
303
+ align-items: center;
304
+ }
305
+
306
+ .timeline-slider {
307
+ width: 100%;
308
+ }
309
+
310
+ .control-label {
311
+ display: flex;
312
+ flex-direction: column;
313
+ font-size: 12px;
314
+ gap: 4px;
315
+ color: #64748b;
316
+ font-weight: 600;
317
+ }
318
+
319
+ .control-value {
320
+ font-size: 14px;
321
+ color: #0f172a;
322
+ }
323
+
324
+ .panel-header {
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: space-between;
328
+ gap: 12px;
329
+ margin-bottom: 8px;
330
+ }
331
+
332
+ .panel-grid {
333
+ display: grid;
334
+ grid-template-columns: repeat(2, minmax(0, 1fr));
335
+ gap: 12px;
336
+ margin-bottom: 12px;
337
+ }
338
+
339
+ .input-label {
340
+ display: flex;
341
+ flex-direction: column;
342
+ gap: 6px;
343
+ font-size: 12px;
344
+ font-weight: 600;
345
+ color: #475569;
346
+ }
347
+
348
+ .input-label--full {
349
+ grid-column: 1 / -1;
350
+ }
351
+
352
+ .text-input {
353
+ border: 1px solid #cbd5f5;
354
+ border-radius: 10px;
355
+ padding: 8px 10px;
356
+ font-size: 14px;
357
+ font-family: inherit;
358
+ }
359
+
360
+ .text-input--compact {
361
+ padding: 6px 8px;
362
+ }
363
+
364
+ .button {
365
+ border: 1px solid #cbd5f5;
366
+ background: #ffffff;
367
+ color: #0f172a;
368
+ border-radius: 10px;
369
+ padding: 8px 12px;
370
+ font-size: 13px;
371
+ font-weight: 600;
372
+ cursor: pointer;
373
+ transition: background 0.15s ease, color 0.15s ease;
374
+ }
375
+
376
+ .button--primary {
377
+ background: #0ea5e9;
378
+ color: #ffffff;
379
+ border-color: #0284c7;
380
+ }
381
+
382
+ .button--danger {
383
+ background: #fef2f2;
384
+ border-color: #fecaca;
385
+ color: #b91c1c;
386
+ }
387
+
388
+ .button--ghost {
389
+ background: #f8fafc;
390
+ border-color: #e2e8f0;
391
+ color: #475569;
392
+ }
393
+
394
+ .button:disabled {
395
+ cursor: not-allowed;
396
+ opacity: 0.6;
397
+ }
398
+
399
+ .cut-list {
400
+ list-style: none;
401
+ padding: 0;
402
+ margin: 0;
403
+ display: flex;
404
+ flex-direction: column;
405
+ gap: 10px;
406
+ }
407
+
408
+ .cut-row {
409
+ display: flex;
410
+ align-items: center;
411
+ gap: 12px;
412
+ justify-content: space-between;
413
+ border: 1px solid #e2e8f0;
414
+ border-radius: 12px;
415
+ padding: 8px 12px;
416
+ background: #f8fafc;
417
+ }
418
+
419
+ .cut-row.is-selected {
420
+ border-color: #38bdf8;
421
+ background: #e0f2fe;
422
+ }
423
+
424
+ .cut-select {
425
+ background: transparent;
426
+ border: none;
427
+ text-align: left;
428
+ cursor: pointer;
429
+ display: flex;
430
+ flex-direction: column;
431
+ gap: 4px;
432
+ font-size: 13px;
433
+ color: #0f172a;
434
+ flex: 1;
435
+ }
436
+
437
+ .cut-time {
438
+ font-weight: 600;
439
+ }
440
+
441
+ .cut-reason {
442
+ color: #64748b;
443
+ }
444
+
445
+ .app-grid--two {
446
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
447
+ }
448
+
449
+ .chapter-list,
450
+ .command-list {
451
+ display: flex;
452
+ flex-direction: column;
453
+ gap: 12px;
454
+ }
455
+
456
+ .chapter-row,
457
+ .command-row {
458
+ border: 1px solid #e2e8f0;
459
+ border-radius: 14px;
460
+ padding: 12px;
461
+ background: #f8fafc;
462
+ display: flex;
463
+ flex-direction: column;
464
+ gap: 10px;
465
+ }
466
+
467
+ .chapter-header,
468
+ .command-header {
469
+ display: flex;
470
+ align-items: center;
471
+ justify-content: space-between;
472
+ gap: 12px;
473
+ }
474
+
475
+ .chapter-time {
476
+ font-size: 12px;
477
+ color: #64748b;
478
+ }
479
+
480
+ .command-meta {
481
+ display: flex;
482
+ align-items: center;
483
+ justify-content: space-between;
484
+ gap: 12px;
485
+ }
486
+
487
+ .command-time {
488
+ font-size: 12px;
489
+ color: #64748b;
490
+ }
491
+
492
+ .status-pill--success {
493
+ background: #dcfce7;
494
+ color: #166534;
495
+ }
496
+
497
+ .status-pill--warning {
498
+ background: #fef3c7;
499
+ color: #92400e;
500
+ }
501
+
502
+ .status-pill--danger {
503
+ background: #fee2e2;
504
+ color: #b91c1c;
505
+ }
506
+
507
+ .status-pill--info {
508
+ background: #e0f2fe;
509
+ color: #0369a1;
510
+ }
511
+
512
+ .transcript-card {
513
+ gap: 16px;
514
+ }
515
+
516
+ .transcript-header {
517
+ display: flex;
518
+ align-items: flex-start;
519
+ justify-content: space-between;
520
+ gap: 16px;
521
+ flex-wrap: wrap;
522
+ }
523
+
524
+ .transcript-preview {
525
+ display: flex;
526
+ flex-direction: column;
527
+ gap: 4px;
528
+ max-width: 320px;
529
+ }
530
+
531
+ .transcript-results {
532
+ list-style: none;
533
+ margin: 0;
534
+ padding: 0;
535
+ display: grid;
536
+ gap: 8px;
537
+ }
538
+
539
+ .transcript-result {
540
+ width: 100%;
541
+ border: 1px solid #e2e8f0;
542
+ border-radius: 10px;
543
+ padding: 8px 10px;
544
+ background: #ffffff;
545
+ display: grid;
546
+ grid-template-columns: auto 1fr;
547
+ gap: 10px;
548
+ text-align: left;
549
+ cursor: pointer;
550
+ }
551
+
552
+ .transcript-time {
553
+ font-weight: 600;
554
+ color: #0f172a;
555
+ }
556
+
557
+ .transcript-snippet {
558
+ color: #64748b;
559
+ }
560
+
561
+ .transcript-empty {
562
+ margin-top: 4px;
563
+ }
564
+
565
+ .command-preview {
566
+ background: #0f172a;
567
+ color: #e2e8f0;
568
+ border-radius: 12px;
569
+ padding: 16px;
570
+ font-size: 13px;
571
+ line-height: 1.5;
572
+ overflow-x: auto;
573
+ }
574
+
575
+ @keyframes shimmer {
576
+ 0% {
577
+ background-position: 0% 50%;
578
+ }
579
+ 100% {
580
+ background-position: 200% 50%;
581
+ }
582
+ }
583
+
584
+ @media (max-width: 900px) {
585
+ .timeline-layout {
586
+ grid-template-columns: 1fr;
587
+ }
588
+
589
+ .timeline-controls {
590
+ grid-template-columns: 1fr;
591
+ }
592
+ }
@@ -0,0 +1,7 @@
1
+ import { EditingWorkspace } from './editing-workspace.tsx'
2
+
3
+ export function App() {
4
+ return () => (
5
+ <EditingWorkspace />
6
+ )
7
+ }