eprec 1.10.2 → 1.12.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/app/assets/styles.css +43 -0
- package/app/client/editing-workspace.tsx +260 -78
- package/package.json +1 -1
- package/process-course/edits/cli.ts +59 -22
- package/process-course/edits/combined-video-editor.ts +23 -0
- package/process-course/edits/video-editor.ts +23 -0
- package/server/processing-queue.ts +441 -0
- package/src/app-server.ts +4 -0
- package/src/cli.ts +37 -12
- package/src/speech-detection.ts +31 -0
- package/src/whispercpp-transcribe.ts +14 -2
package/app/assets/styles.css
CHANGED
|
@@ -460,6 +460,40 @@ p {
|
|
|
460
460
|
padding: 0 var(--spacing-md) var(--spacing-md);
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
.processing-progress {
|
|
464
|
+
display: flex;
|
|
465
|
+
flex-direction: column;
|
|
466
|
+
gap: var(--spacing-xs);
|
|
467
|
+
padding: 0 var(--spacing-md) var(--spacing-sm);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.processing-progress-meta {
|
|
471
|
+
display: flex;
|
|
472
|
+
align-items: center;
|
|
473
|
+
justify-content: space-between;
|
|
474
|
+
font-size: var(--font-size-xs);
|
|
475
|
+
color: var(--color-text-muted);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.processing-progress-bar {
|
|
479
|
+
height: 6px;
|
|
480
|
+
border-radius: var(--radius-pill);
|
|
481
|
+
background: var(--color-border-muted);
|
|
482
|
+
overflow: hidden;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.processing-progress-fill {
|
|
486
|
+
display: block;
|
|
487
|
+
height: 100%;
|
|
488
|
+
width: var(--progress, 0%);
|
|
489
|
+
background: var(--color-primary);
|
|
490
|
+
transition: width var(--transition-normal);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.processing-error {
|
|
494
|
+
margin: 0 var(--spacing-md) var(--spacing-sm);
|
|
495
|
+
}
|
|
496
|
+
|
|
463
497
|
.processing-row.is-running {
|
|
464
498
|
box-shadow: inset 0 0 0 1px var(--color-warning-border);
|
|
465
499
|
}
|
|
@@ -468,6 +502,15 @@ p {
|
|
|
468
502
|
background: var(--color-success-surface);
|
|
469
503
|
}
|
|
470
504
|
|
|
505
|
+
.processing-row.is-error {
|
|
506
|
+
background: var(--color-danger-surface);
|
|
507
|
+
border-color: var(--color-danger-border-strong);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.processing-row.is-error .processing-progress-fill {
|
|
511
|
+
background: var(--color-danger-border);
|
|
512
|
+
}
|
|
513
|
+
|
|
471
514
|
.timeline-card {
|
|
472
515
|
gap: var(--spacing-xl);
|
|
473
516
|
}
|
|
@@ -23,6 +23,8 @@ const MIN_CUT_LENGTH = 0.2
|
|
|
23
23
|
const DEFAULT_CUT_LENGTH = 2.4
|
|
24
24
|
const PLAYHEAD_STEP = 0.1
|
|
25
25
|
const DEFAULT_PREVIEW_URL = '/e2e-test.mp4'
|
|
26
|
+
const QUEUE_API_BASE = '/api/processing-queue'
|
|
27
|
+
const QUEUE_STREAM_URL = `${QUEUE_API_BASE}/stream`
|
|
26
28
|
|
|
27
29
|
function readInitialVideoPath() {
|
|
28
30
|
if (typeof window === 'undefined') return ''
|
|
@@ -42,14 +44,36 @@ function extractVideoName(value: string) {
|
|
|
42
44
|
return last && last.length > 0 ? last : value
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
type ProcessingStatus = 'queued' | 'running' | 'done'
|
|
47
|
+
type ProcessingStatus = 'queued' | 'running' | 'done' | 'error'
|
|
46
48
|
type ProcessingCategory = 'chapter' | 'transcript' | 'export'
|
|
49
|
+
type ProcessingAction =
|
|
50
|
+
| 'edit-chapter'
|
|
51
|
+
| 'combine-chapters'
|
|
52
|
+
| 'regenerate-transcript'
|
|
53
|
+
| 'detect-command-windows'
|
|
54
|
+
| 'render-preview'
|
|
55
|
+
| 'export-final'
|
|
56
|
+
type ProcessingProgress = {
|
|
57
|
+
step: number
|
|
58
|
+
totalSteps: number
|
|
59
|
+
label: string
|
|
60
|
+
percent: number
|
|
61
|
+
}
|
|
47
62
|
type ProcessingTask = {
|
|
48
63
|
id: string
|
|
49
64
|
title: string
|
|
50
65
|
detail: string
|
|
51
66
|
status: ProcessingStatus
|
|
52
67
|
category: ProcessingCategory
|
|
68
|
+
action: ProcessingAction
|
|
69
|
+
progress?: ProcessingProgress
|
|
70
|
+
errorMessage?: string
|
|
71
|
+
updatedAt: number
|
|
72
|
+
createdAt: number
|
|
73
|
+
}
|
|
74
|
+
type ProcessingQueueSnapshot = {
|
|
75
|
+
tasks: ProcessingTask[]
|
|
76
|
+
activeTaskId: string | null
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
export function EditingWorkspace(handle: Handle) {
|
|
@@ -79,8 +103,9 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
79
103
|
let primaryChapterId = chapters[0]?.id ?? ''
|
|
80
104
|
let secondaryChapterId = chapters[1]?.id ?? chapters[0]?.id ?? ''
|
|
81
105
|
let processingQueue: ProcessingTask[] = []
|
|
82
|
-
let
|
|
83
|
-
let
|
|
106
|
+
let queueLoading = true
|
|
107
|
+
let queueError = ''
|
|
108
|
+
let queueStreamStatus: 'connecting' | 'open' | 'error' = 'connecting'
|
|
84
109
|
let manualCutId = 1
|
|
85
110
|
let previewDuration = 0
|
|
86
111
|
let previewReady = false
|
|
@@ -173,6 +198,39 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
173
198
|
})
|
|
174
199
|
}
|
|
175
200
|
|
|
201
|
+
const loadQueueSnapshot = async () => {
|
|
202
|
+
await requestQueue('', { method: 'GET' })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const connectQueueStream = () => {
|
|
206
|
+
if (typeof window === 'undefined') return
|
|
207
|
+
const stream = new EventSource(QUEUE_STREAM_URL)
|
|
208
|
+
const updateStatus = () => {
|
|
209
|
+
queueStreamStatus =
|
|
210
|
+
stream.readyState === EventSource.OPEN
|
|
211
|
+
? 'open'
|
|
212
|
+
: stream.readyState === EventSource.CONNECTING
|
|
213
|
+
? 'connecting'
|
|
214
|
+
: 'error'
|
|
215
|
+
handle.update()
|
|
216
|
+
}
|
|
217
|
+
stream.addEventListener('open', updateStatus)
|
|
218
|
+
stream.addEventListener('error', updateStatus)
|
|
219
|
+
stream.addEventListener('snapshot', (event) => {
|
|
220
|
+
try {
|
|
221
|
+
const snapshot = JSON.parse(
|
|
222
|
+
(event as MessageEvent<string>).data,
|
|
223
|
+
) as ProcessingQueueSnapshot
|
|
224
|
+
applyQueueSnapshot(snapshot)
|
|
225
|
+
} catch (error) {
|
|
226
|
+
setQueueError('Unable to parse queue updates.')
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
handle.signal.addEventListener('abort', () => {
|
|
230
|
+
stream.close()
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
176
234
|
if (initialVideoPath) {
|
|
177
235
|
void loadVideoFromPath(initialVideoPath)
|
|
178
236
|
}
|
|
@@ -309,26 +367,68 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
309
367
|
handle.update()
|
|
310
368
|
}
|
|
311
369
|
|
|
370
|
+
const applyQueueSnapshot = (snapshot: ProcessingQueueSnapshot) => {
|
|
371
|
+
processingQueue = Array.isArray(snapshot.tasks) ? snapshot.tasks : []
|
|
372
|
+
queueLoading = false
|
|
373
|
+
queueError = ''
|
|
374
|
+
handle.update()
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const setQueueError = (message: string) => {
|
|
378
|
+
queueError = message
|
|
379
|
+
queueLoading = false
|
|
380
|
+
handle.update()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const requestQueue = async (
|
|
384
|
+
path: string,
|
|
385
|
+
options: RequestInit & { body?: string } = {},
|
|
386
|
+
) => {
|
|
387
|
+
try {
|
|
388
|
+
const response = await fetch(`${QUEUE_API_BASE}${path}`, {
|
|
389
|
+
...options,
|
|
390
|
+
headers: {
|
|
391
|
+
Accept: 'application/json',
|
|
392
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
393
|
+
},
|
|
394
|
+
signal: handle.signal,
|
|
395
|
+
})
|
|
396
|
+
if (!response.ok) {
|
|
397
|
+
throw new Error(`Queue request failed (${response.status}).`)
|
|
398
|
+
}
|
|
399
|
+
const snapshot =
|
|
400
|
+
(await response.json()) as ProcessingQueueSnapshot
|
|
401
|
+
applyQueueSnapshot(snapshot)
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (handle.signal.aborted) return
|
|
404
|
+
setQueueError(
|
|
405
|
+
error instanceof Error
|
|
406
|
+
? error.message
|
|
407
|
+
: 'Unable to reach the processing queue.',
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
void loadQueueSnapshot()
|
|
413
|
+
connectQueueStream()
|
|
414
|
+
|
|
312
415
|
const queueTask = (
|
|
416
|
+
action: ProcessingAction,
|
|
313
417
|
title: string,
|
|
314
418
|
detail: string,
|
|
315
419
|
category: ProcessingCategory,
|
|
316
420
|
) => {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
title,
|
|
320
|
-
|
|
321
|
-
status: 'queued',
|
|
322
|
-
category,
|
|
323
|
-
}
|
|
324
|
-
processingQueue = [...processingQueue, task]
|
|
325
|
-
handle.update()
|
|
421
|
+
void requestQueue('/enqueue', {
|
|
422
|
+
method: 'POST',
|
|
423
|
+
body: JSON.stringify({ action, title, detail, category }),
|
|
424
|
+
})
|
|
326
425
|
}
|
|
327
426
|
|
|
328
427
|
const queueChapterEdit = () => {
|
|
329
428
|
const chapter = findChapter(primaryChapterId)
|
|
330
429
|
if (!chapter) return
|
|
331
430
|
queueTask(
|
|
431
|
+
'edit-chapter',
|
|
332
432
|
`Edit ${chapter.title}`,
|
|
333
433
|
`Review trims for ${formatTimestamp(chapter.start)} - ${formatTimestamp(
|
|
334
434
|
chapter.end,
|
|
@@ -342,6 +442,7 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
342
442
|
const secondary = findChapter(secondaryChapterId)
|
|
343
443
|
if (!primary || !secondary || primary.id === secondary.id) return
|
|
344
444
|
queueTask(
|
|
445
|
+
'combine-chapters',
|
|
345
446
|
`Combine ${primary.title} + ${secondary.title}`,
|
|
346
447
|
'Merge both chapters into a single preview export.',
|
|
347
448
|
'chapter',
|
|
@@ -350,6 +451,7 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
350
451
|
|
|
351
452
|
const queueTranscriptRegeneration = () => {
|
|
352
453
|
queueTask(
|
|
454
|
+
'regenerate-transcript',
|
|
353
455
|
'Regenerate transcript',
|
|
354
456
|
'Run Whisper alignment and refresh search cues.',
|
|
355
457
|
'transcript',
|
|
@@ -358,6 +460,7 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
358
460
|
|
|
359
461
|
const queueCommandScan = () => {
|
|
360
462
|
queueTask(
|
|
463
|
+
'detect-command-windows',
|
|
361
464
|
'Detect command windows',
|
|
362
465
|
'Scan for Jarvis commands and update cut ranges.',
|
|
363
466
|
'transcript',
|
|
@@ -366,6 +469,7 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
366
469
|
|
|
367
470
|
const queuePreviewRender = () => {
|
|
368
471
|
queueTask(
|
|
472
|
+
'render-preview',
|
|
369
473
|
'Render preview clip',
|
|
370
474
|
'Bake a short MP4 with current edits applied.',
|
|
371
475
|
'export',
|
|
@@ -374,6 +478,7 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
374
478
|
|
|
375
479
|
const queueFinalExport = () => {
|
|
376
480
|
queueTask(
|
|
481
|
+
'export-final',
|
|
377
482
|
'Export edited chapters',
|
|
378
483
|
'Render final chapters and write the export package.',
|
|
379
484
|
'export',
|
|
@@ -381,36 +486,21 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
381
486
|
}
|
|
382
487
|
|
|
383
488
|
const startNextTask = () => {
|
|
384
|
-
|
|
385
|
-
const next = processingQueue.find((task) => task.status === 'queued')
|
|
386
|
-
if (!next) return
|
|
387
|
-
activeTaskId = next.id
|
|
388
|
-
processingQueue = processingQueue.map((task) =>
|
|
389
|
-
task.id === next.id ? { ...task, status: 'running' } : task,
|
|
390
|
-
)
|
|
391
|
-
handle.update()
|
|
489
|
+
void requestQueue('/run-next', { method: 'POST' })
|
|
392
490
|
}
|
|
393
491
|
|
|
394
492
|
const markActiveDone = () => {
|
|
395
|
-
|
|
396
|
-
processingQueue = processingQueue.map((task) =>
|
|
397
|
-
task.id === activeTaskId ? { ...task, status: 'done' } : task,
|
|
398
|
-
)
|
|
399
|
-
activeTaskId = null
|
|
400
|
-
handle.update()
|
|
493
|
+
void requestQueue('/mark-done', { method: 'POST' })
|
|
401
494
|
}
|
|
402
495
|
|
|
403
496
|
const clearCompletedTasks = () => {
|
|
404
|
-
|
|
405
|
-
handle.update()
|
|
497
|
+
void requestQueue('/clear-completed', { method: 'POST' })
|
|
406
498
|
}
|
|
407
499
|
|
|
408
500
|
const removeTask = (taskId: string) => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
handle.update()
|
|
501
|
+
void requestQueue(`/task/${encodeURIComponent(taskId)}`, {
|
|
502
|
+
method: 'DELETE',
|
|
503
|
+
})
|
|
414
504
|
}
|
|
415
505
|
|
|
416
506
|
const syncVideoToPlayhead = (value: number) => {
|
|
@@ -460,8 +550,28 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
460
550
|
const completedCount = processingQueue.filter(
|
|
461
551
|
(task) => task.status === 'done',
|
|
462
552
|
).length
|
|
553
|
+
const errorCount = processingQueue.filter(
|
|
554
|
+
(task) => task.status === 'error',
|
|
555
|
+
).length
|
|
463
556
|
const runningTask =
|
|
464
557
|
processingQueue.find((task) => task.status === 'running') ?? null
|
|
558
|
+
const queueStreamMeta =
|
|
559
|
+
queueStreamStatus === 'open'
|
|
560
|
+
? { label: 'Live', className: 'status-pill--success' }
|
|
561
|
+
: queueStreamStatus === 'connecting'
|
|
562
|
+
? { label: 'Connecting', className: 'status-pill--warning' }
|
|
563
|
+
: { label: 'Offline', className: 'status-pill--danger' }
|
|
564
|
+
const runningSummary = runningTask
|
|
565
|
+
? runningTask.progress?.label
|
|
566
|
+
? `Running: ${runningTask.title} · ${runningTask.progress.label}`
|
|
567
|
+
: `Running: ${runningTask.title}`
|
|
568
|
+
: 'Idle'
|
|
569
|
+
const queueStreamSummary =
|
|
570
|
+
queueStreamStatus === 'open'
|
|
571
|
+
? 'Queue updates are live.'
|
|
572
|
+
: queueStreamStatus === 'connecting'
|
|
573
|
+
? 'Connecting to queue updates.'
|
|
574
|
+
: 'Queue updates are offline.'
|
|
465
575
|
const canCombineChapters =
|
|
466
576
|
primaryChapterId.length > 0 &&
|
|
467
577
|
secondaryChapterId.length > 0 &&
|
|
@@ -612,16 +722,37 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
612
722
|
<div class="summary-item">
|
|
613
723
|
<span class="summary-label">Queue</span>
|
|
614
724
|
<span class="summary-value">{queuedCount} queued</span>
|
|
725
|
+
<span class="summary-subtext">{runningSummary}</span>
|
|
726
|
+
</div>
|
|
727
|
+
<div class="summary-item">
|
|
728
|
+
<span class="summary-label">Errors</span>
|
|
729
|
+
<span class="summary-value">{errorCount}</span>
|
|
615
730
|
<span class="summary-subtext">
|
|
616
|
-
{
|
|
731
|
+
{errorCount > 0
|
|
732
|
+
? 'Review failed tasks below.'
|
|
733
|
+
: 'No failures yet.'}
|
|
734
|
+
</span>
|
|
735
|
+
</div>
|
|
736
|
+
<div class="summary-item">
|
|
737
|
+
<span class="summary-label">Stream</span>
|
|
738
|
+
<span
|
|
739
|
+
class={classNames(
|
|
740
|
+
'status-pill',
|
|
741
|
+
queueStreamMeta.className,
|
|
742
|
+
)}
|
|
743
|
+
>
|
|
744
|
+
{queueStreamMeta.label}
|
|
617
745
|
</span>
|
|
746
|
+
<span class="summary-subtext">{queueStreamSummary}</span>
|
|
618
747
|
</div>
|
|
619
748
|
</div>
|
|
620
749
|
<div class="actions-buttons">
|
|
621
750
|
<button
|
|
622
751
|
class="button button--primary"
|
|
623
752
|
type="button"
|
|
624
|
-
disabled={
|
|
753
|
+
disabled={
|
|
754
|
+
queueLoading || queuedCount === 0 || Boolean(runningTask)
|
|
755
|
+
}
|
|
625
756
|
on={{ click: startNextTask }}
|
|
626
757
|
>
|
|
627
758
|
Run next
|
|
@@ -629,7 +760,7 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
629
760
|
<button
|
|
630
761
|
class="button button--ghost"
|
|
631
762
|
type="button"
|
|
632
|
-
disabled={!runningTask}
|
|
763
|
+
disabled={queueLoading || !runningTask}
|
|
633
764
|
on={{ click: markActiveDone }}
|
|
634
765
|
>
|
|
635
766
|
Mark running done
|
|
@@ -637,7 +768,7 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
637
768
|
<button
|
|
638
769
|
class="button button--ghost"
|
|
639
770
|
type="button"
|
|
640
|
-
disabled={completedCount === 0}
|
|
771
|
+
disabled={queueLoading || completedCount === 0}
|
|
641
772
|
on={{ click: clearCompletedTasks }}
|
|
642
773
|
>
|
|
643
774
|
Clear completed
|
|
@@ -765,56 +896,107 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
765
896
|
<div class="panel-header">
|
|
766
897
|
<h3>Processing queue</h3>
|
|
767
898
|
<span class="summary-subtext">
|
|
768
|
-
{
|
|
899
|
+
{queueLoading
|
|
900
|
+
? 'Loading...'
|
|
901
|
+
: `${processingQueue.length} total`}
|
|
769
902
|
</span>
|
|
770
903
|
</div>
|
|
771
|
-
{
|
|
904
|
+
{queueError ? (
|
|
905
|
+
<p class="status-note status-note--danger">{queueError}</p>
|
|
906
|
+
) : null}
|
|
907
|
+
{queueLoading ? (
|
|
908
|
+
<p class="app-muted">Loading queue updates...</p>
|
|
909
|
+
) : processingQueue.length === 0 ? (
|
|
772
910
|
<p class="app-muted">
|
|
773
911
|
No actions queued yet. Use the buttons above to stage work.
|
|
774
912
|
</p>
|
|
775
913
|
) : (
|
|
776
914
|
<ul class="stacked-list processing-list">
|
|
777
|
-
{processingQueue.map((task) =>
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
915
|
+
{processingQueue.map((task) => {
|
|
916
|
+
const showProgress =
|
|
917
|
+
task.status === 'running' || task.status === 'error'
|
|
918
|
+
const progress =
|
|
919
|
+
task.progress ??
|
|
920
|
+
(task.status === 'running'
|
|
921
|
+
? {
|
|
922
|
+
step: 0,
|
|
923
|
+
totalSteps: 0,
|
|
924
|
+
label: 'Starting',
|
|
925
|
+
percent: 0,
|
|
926
|
+
}
|
|
927
|
+
: null)
|
|
928
|
+
return (
|
|
929
|
+
<li
|
|
930
|
+
class={classNames(
|
|
931
|
+
'stacked-item',
|
|
932
|
+
'processing-row',
|
|
933
|
+
task.status === 'running' && 'is-running',
|
|
934
|
+
task.status === 'done' && 'is-complete',
|
|
935
|
+
task.status === 'error' && 'is-error',
|
|
936
|
+
)}
|
|
937
|
+
>
|
|
938
|
+
<div class="processing-row-header">
|
|
939
|
+
<div>
|
|
940
|
+
<h4>{task.title}</h4>
|
|
941
|
+
<p class="app-muted">{task.detail}</p>
|
|
942
|
+
</div>
|
|
943
|
+
<span
|
|
944
|
+
class={classNames(
|
|
945
|
+
'status-pill',
|
|
946
|
+
task.status === 'queued' &&
|
|
947
|
+
'status-pill--info',
|
|
948
|
+
task.status === 'running' &&
|
|
949
|
+
'status-pill--warning',
|
|
950
|
+
task.status === 'done' &&
|
|
951
|
+
'status-pill--success',
|
|
952
|
+
task.status === 'error' &&
|
|
953
|
+
'status-pill--danger',
|
|
954
|
+
)}
|
|
811
955
|
>
|
|
812
|
-
|
|
813
|
-
</
|
|
956
|
+
{task.status}
|
|
957
|
+
</span>
|
|
958
|
+
</div>
|
|
959
|
+
{showProgress && progress ? (
|
|
960
|
+
<div class="processing-progress">
|
|
961
|
+
<div class="processing-progress-meta">
|
|
962
|
+
<span>{progress.label}</span>
|
|
963
|
+
<span>
|
|
964
|
+
{progress.totalSteps > 0
|
|
965
|
+
? `${progress.step}/${progress.totalSteps}`
|
|
966
|
+
: '...'}
|
|
967
|
+
</span>
|
|
968
|
+
</div>
|
|
969
|
+
<div class="processing-progress-bar">
|
|
970
|
+
<span
|
|
971
|
+
class="processing-progress-fill"
|
|
972
|
+
style={`--progress:${progress.percent}%`}
|
|
973
|
+
/>
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
814
976
|
) : null}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
977
|
+
{task.status === 'error' && task.errorMessage ? (
|
|
978
|
+
<p class="status-note status-note--danger processing-error">
|
|
979
|
+
{task.errorMessage}
|
|
980
|
+
</p>
|
|
981
|
+
) : null}
|
|
982
|
+
<div class="processing-row-meta">
|
|
983
|
+
<span class="summary-subtext">
|
|
984
|
+
{formatProcessingCategory(task.category)}
|
|
985
|
+
</span>
|
|
986
|
+
{task.status === 'queued' ||
|
|
987
|
+
task.status === 'error' ? (
|
|
988
|
+
<button
|
|
989
|
+
class="button button--ghost"
|
|
990
|
+
type="button"
|
|
991
|
+
on={{ click: () => removeTask(task.id) }}
|
|
992
|
+
>
|
|
993
|
+
Remove
|
|
994
|
+
</button>
|
|
995
|
+
) : null}
|
|
996
|
+
</div>
|
|
997
|
+
</li>
|
|
998
|
+
)
|
|
999
|
+
})}
|
|
818
1000
|
</ul>
|
|
819
1001
|
)}
|
|
820
1002
|
</div>
|
package/package.json
CHANGED
|
@@ -7,14 +7,18 @@ import {
|
|
|
7
7
|
PromptCancelled,
|
|
8
8
|
createInquirerPrompter,
|
|
9
9
|
createPathPicker,
|
|
10
|
+
createStepProgressReporter,
|
|
10
11
|
isInteractive,
|
|
12
|
+
pauseActiveSpinner,
|
|
11
13
|
resolveOptionalString,
|
|
14
|
+
resumeActiveSpinner,
|
|
12
15
|
type PathPicker,
|
|
13
16
|
type Prompter,
|
|
14
17
|
withSpinner,
|
|
15
18
|
} from '../../cli-ux'
|
|
16
19
|
import { editVideo, buildEditedOutputPath } from './video-editor'
|
|
17
20
|
import { combineVideos } from './combined-video-editor'
|
|
21
|
+
import { setLogHooks } from '../logging'
|
|
18
22
|
|
|
19
23
|
export type EditVideoCommandArgs = {
|
|
20
24
|
input: string
|
|
@@ -176,21 +180,33 @@ function resolvePaddingMs(value: unknown) {
|
|
|
176
180
|
export function createEditVideoHandler(options: CliUxOptions): CommandHandler {
|
|
177
181
|
return async (argv) => {
|
|
178
182
|
const args = await resolveEditVideoArgs(argv, options)
|
|
183
|
+
const progress = options.interactive
|
|
184
|
+
? createStepProgressReporter({ action: 'Editing video' })
|
|
185
|
+
: undefined
|
|
179
186
|
await withSpinner(
|
|
180
187
|
'Editing video',
|
|
181
188
|
async () => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
editedTextPath: String(args.edited),
|
|
186
|
-
outputPath: String(args.output),
|
|
187
|
-
paddingMs: args['padding-ms'],
|
|
189
|
+
setLogHooks({
|
|
190
|
+
beforeLog: pauseActiveSpinner,
|
|
191
|
+
afterLog: resumeActiveSpinner,
|
|
188
192
|
})
|
|
189
|
-
|
|
190
|
-
|
|
193
|
+
try {
|
|
194
|
+
const result = await editVideo({
|
|
195
|
+
inputPath: String(args.input),
|
|
196
|
+
transcriptJsonPath: String(args.transcript),
|
|
197
|
+
editedTextPath: String(args.edited),
|
|
198
|
+
outputPath: String(args.output),
|
|
199
|
+
paddingMs: args['padding-ms'],
|
|
200
|
+
progress,
|
|
201
|
+
})
|
|
202
|
+
if (!result.success) {
|
|
203
|
+
throw new Error(result.error ?? 'Edit failed.')
|
|
204
|
+
}
|
|
205
|
+
} finally {
|
|
206
|
+
setLogHooks({})
|
|
191
207
|
}
|
|
192
208
|
},
|
|
193
|
-
{ successText: 'Edit complete' },
|
|
209
|
+
{ successText: 'Edit complete', enabled: options.interactive },
|
|
194
210
|
)
|
|
195
211
|
console.log(`Edited video written to ${args.output}`)
|
|
196
212
|
}
|
|
@@ -201,26 +217,47 @@ export function createCombineVideosHandler(
|
|
|
201
217
|
): CommandHandler {
|
|
202
218
|
return async (argv) => {
|
|
203
219
|
const args = await resolveCombineVideosArgs(argv, options)
|
|
220
|
+
const progress = options.interactive
|
|
221
|
+
? createStepProgressReporter({ action: 'Combining videos' })
|
|
222
|
+
: undefined
|
|
223
|
+
const editProgressFactory = options.interactive
|
|
224
|
+
? (detail: string) =>
|
|
225
|
+
createStepProgressReporter({
|
|
226
|
+
action: 'Combining videos',
|
|
227
|
+
detail,
|
|
228
|
+
maxLabelLength: 28,
|
|
229
|
+
})
|
|
230
|
+
: undefined
|
|
204
231
|
let outputPath = ''
|
|
205
232
|
await withSpinner(
|
|
206
233
|
'Combining videos',
|
|
207
234
|
async () => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
video1EditedTextPath: args.edited1,
|
|
212
|
-
video2Path: String(args.video2),
|
|
213
|
-
video2TranscriptJsonPath: args.transcript2,
|
|
214
|
-
video2EditedTextPath: args.edited2,
|
|
215
|
-
outputPath: String(args.output),
|
|
216
|
-
overlapPaddingMs: args['padding-ms'],
|
|
235
|
+
setLogHooks({
|
|
236
|
+
beforeLog: pauseActiveSpinner,
|
|
237
|
+
afterLog: resumeActiveSpinner,
|
|
217
238
|
})
|
|
218
|
-
|
|
219
|
-
|
|
239
|
+
try {
|
|
240
|
+
const result = await combineVideos({
|
|
241
|
+
video1Path: String(args.video1),
|
|
242
|
+
video1TranscriptJsonPath: args.transcript1,
|
|
243
|
+
video1EditedTextPath: args.edited1,
|
|
244
|
+
video2Path: String(args.video2),
|
|
245
|
+
video2TranscriptJsonPath: args.transcript2,
|
|
246
|
+
video2EditedTextPath: args.edited2,
|
|
247
|
+
outputPath: String(args.output),
|
|
248
|
+
overlapPaddingMs: args['padding-ms'],
|
|
249
|
+
progress,
|
|
250
|
+
editProgressFactory,
|
|
251
|
+
})
|
|
252
|
+
if (!result.success) {
|
|
253
|
+
throw new Error(result.error ?? 'Combine failed.')
|
|
254
|
+
}
|
|
255
|
+
outputPath = result.outputPath
|
|
256
|
+
} finally {
|
|
257
|
+
setLogHooks({})
|
|
220
258
|
}
|
|
221
|
-
outputPath = result.outputPath
|
|
222
259
|
},
|
|
223
|
-
{ successText: 'Combine complete' },
|
|
260
|
+
{ successText: 'Combine complete', enabled: options.interactive },
|
|
224
261
|
)
|
|
225
262
|
console.log(`Combined video written to ${outputPath}`)
|
|
226
263
|
}
|