eprec 1.11.0 → 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/server/processing-queue.ts +441 -0
- package/src/app-server.ts +4 -0
- package/src/cli.ts +8 -11
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
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
type ProcessingCategory = 'chapter' | 'transcript' | 'export'
|
|
2
|
+
type ProcessingStatus = 'queued' | 'running' | 'done' | 'error'
|
|
3
|
+
type ProcessingAction =
|
|
4
|
+
| 'edit-chapter'
|
|
5
|
+
| 'combine-chapters'
|
|
6
|
+
| 'regenerate-transcript'
|
|
7
|
+
| 'detect-command-windows'
|
|
8
|
+
| 'render-preview'
|
|
9
|
+
| 'export-final'
|
|
10
|
+
|
|
11
|
+
type ProcessingProgress = {
|
|
12
|
+
step: number
|
|
13
|
+
totalSteps: number
|
|
14
|
+
label: string
|
|
15
|
+
percent: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ProcessingTask = {
|
|
19
|
+
id: string
|
|
20
|
+
title: string
|
|
21
|
+
detail: string
|
|
22
|
+
status: ProcessingStatus
|
|
23
|
+
category: ProcessingCategory
|
|
24
|
+
action: ProcessingAction
|
|
25
|
+
progress?: ProcessingProgress
|
|
26
|
+
errorMessage?: string
|
|
27
|
+
updatedAt: number
|
|
28
|
+
createdAt: number
|
|
29
|
+
simulateError?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type ProcessingQueueSnapshot = {
|
|
33
|
+
tasks: ProcessingTask[]
|
|
34
|
+
activeTaskId: string | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type QueueListener = (snapshot: ProcessingQueueSnapshot) => void
|
|
38
|
+
|
|
39
|
+
const QUEUE_HEADERS = {
|
|
40
|
+
'Access-Control-Allow-Origin': '*',
|
|
41
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
42
|
+
'Access-Control-Allow-Headers': 'Accept, Content-Type',
|
|
43
|
+
} as const
|
|
44
|
+
|
|
45
|
+
const TASK_STEPS: Record<ProcessingAction, string[]> = {
|
|
46
|
+
'edit-chapter': [
|
|
47
|
+
'Collecting edit ranges',
|
|
48
|
+
'Updating cut list',
|
|
49
|
+
'Preparing edit workspace',
|
|
50
|
+
],
|
|
51
|
+
'combine-chapters': [
|
|
52
|
+
'Loading chapter outputs',
|
|
53
|
+
'Aligning audio padding',
|
|
54
|
+
'Rendering combined preview',
|
|
55
|
+
],
|
|
56
|
+
'regenerate-transcript': [
|
|
57
|
+
'Extracting audio',
|
|
58
|
+
'Running Whisper alignment',
|
|
59
|
+
'Refreshing transcript cues',
|
|
60
|
+
],
|
|
61
|
+
'detect-command-windows': [
|
|
62
|
+
'Scanning transcript markers',
|
|
63
|
+
'Refining command windows',
|
|
64
|
+
'Updating cut ranges',
|
|
65
|
+
],
|
|
66
|
+
'render-preview': ['Rendering preview clip', 'Optimizing output', 'Verifying'],
|
|
67
|
+
'export-final': [
|
|
68
|
+
'Rendering chapters',
|
|
69
|
+
'Packaging exports',
|
|
70
|
+
'Verifying outputs',
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const STEP_DELAY_MS = 850
|
|
75
|
+
const STEP_JITTER_MS = 350
|
|
76
|
+
|
|
77
|
+
let tasks: ProcessingTask[] = []
|
|
78
|
+
let activeTaskId: string | null = null
|
|
79
|
+
let nextTaskId = 1
|
|
80
|
+
let runController: AbortController | null = null
|
|
81
|
+
const listeners = new Set<QueueListener>()
|
|
82
|
+
|
|
83
|
+
function buildSnapshot(): ProcessingQueueSnapshot {
|
|
84
|
+
return {
|
|
85
|
+
tasks,
|
|
86
|
+
activeTaskId,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function emitSnapshot() {
|
|
91
|
+
const snapshot = buildSnapshot()
|
|
92
|
+
for (const listener of listeners) {
|
|
93
|
+
listener(snapshot)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function updateQueueState(mutate: () => void) {
|
|
98
|
+
mutate()
|
|
99
|
+
emitSnapshot()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function updateTask(taskId: string, patch: Partial<ProcessingTask>) {
|
|
103
|
+
updateQueueState(() => {
|
|
104
|
+
tasks = tasks.map((task) =>
|
|
105
|
+
task.id === taskId
|
|
106
|
+
? { ...task, ...patch, updatedAt: Date.now() }
|
|
107
|
+
: task,
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function enqueueTask(options: {
|
|
113
|
+
title: string
|
|
114
|
+
detail: string
|
|
115
|
+
category: ProcessingCategory
|
|
116
|
+
action: ProcessingAction
|
|
117
|
+
simulateError?: boolean
|
|
118
|
+
}) {
|
|
119
|
+
const task: ProcessingTask = {
|
|
120
|
+
id: `task-${nextTaskId++}`,
|
|
121
|
+
title: options.title,
|
|
122
|
+
detail: options.detail,
|
|
123
|
+
status: 'queued',
|
|
124
|
+
category: options.category,
|
|
125
|
+
action: options.action,
|
|
126
|
+
createdAt: Date.now(),
|
|
127
|
+
updatedAt: Date.now(),
|
|
128
|
+
simulateError: options.simulateError,
|
|
129
|
+
}
|
|
130
|
+
updateQueueState(() => {
|
|
131
|
+
tasks = [...tasks, task]
|
|
132
|
+
})
|
|
133
|
+
return task
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function removeTask(taskId: string) {
|
|
137
|
+
updateQueueState(() => {
|
|
138
|
+
tasks = tasks.filter((task) => task.id !== taskId)
|
|
139
|
+
if (activeTaskId === taskId) {
|
|
140
|
+
activeTaskId = null
|
|
141
|
+
runController?.abort()
|
|
142
|
+
runController = null
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function clearCompleted() {
|
|
148
|
+
updateQueueState(() => {
|
|
149
|
+
tasks = tasks.filter((task) => task.status !== 'done')
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function markActiveDone() {
|
|
154
|
+
if (!activeTaskId) return
|
|
155
|
+
updateQueueState(() => {
|
|
156
|
+
tasks = tasks.map((task) =>
|
|
157
|
+
task.id === activeTaskId
|
|
158
|
+
? {
|
|
159
|
+
...task,
|
|
160
|
+
status: 'done',
|
|
161
|
+
progress: task.progress
|
|
162
|
+
? { ...task.progress, percent: 100, label: 'Complete' }
|
|
163
|
+
: undefined,
|
|
164
|
+
updatedAt: Date.now(),
|
|
165
|
+
}
|
|
166
|
+
: task,
|
|
167
|
+
)
|
|
168
|
+
activeTaskId = null
|
|
169
|
+
runController?.abort()
|
|
170
|
+
runController = null
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildProgress(step: number, totalSteps: number, label: string) {
|
|
175
|
+
const percent =
|
|
176
|
+
totalSteps > 0 ? Math.round((step / totalSteps) * 100) : 0
|
|
177
|
+
return { step, totalSteps, label, percent }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sleep(duration: number, signal?: AbortSignal) {
|
|
181
|
+
return new Promise<void>((resolve, reject) => {
|
|
182
|
+
const timeout = setTimeout(resolve, duration)
|
|
183
|
+
const onAbort = () => {
|
|
184
|
+
clearTimeout(timeout)
|
|
185
|
+
reject(new Error('aborted'))
|
|
186
|
+
}
|
|
187
|
+
if (signal) {
|
|
188
|
+
if (signal.aborted) {
|
|
189
|
+
onAbort()
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function runTask(task: ProcessingTask) {
|
|
198
|
+
const steps = TASK_STEPS[task.action] ?? ['Starting', 'Working', 'Complete']
|
|
199
|
+
const controller = new AbortController()
|
|
200
|
+
runController = controller
|
|
201
|
+
updateQueueState(() => {
|
|
202
|
+
activeTaskId = task.id
|
|
203
|
+
tasks = tasks.map((entry) =>
|
|
204
|
+
entry.id === task.id
|
|
205
|
+
? {
|
|
206
|
+
...entry,
|
|
207
|
+
status: 'running',
|
|
208
|
+
progress: buildProgress(0, steps.length, 'Starting'),
|
|
209
|
+
errorMessage: undefined,
|
|
210
|
+
updatedAt: Date.now(),
|
|
211
|
+
}
|
|
212
|
+
: entry,
|
|
213
|
+
)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const failAtStep = task.simulateError
|
|
217
|
+
? Math.max(1, Math.ceil(steps.length * 0.6))
|
|
218
|
+
: null
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
for (let index = 0; index < steps.length; index++) {
|
|
222
|
+
if (controller.signal.aborted) return
|
|
223
|
+
const label = steps[index]
|
|
224
|
+
updateTask(task.id, {
|
|
225
|
+
progress: buildProgress(index + 1, steps.length, label),
|
|
226
|
+
})
|
|
227
|
+
if (failAtStep && index + 1 === failAtStep) {
|
|
228
|
+
throw new Error('Processing failed during render.')
|
|
229
|
+
}
|
|
230
|
+
const delay =
|
|
231
|
+
STEP_DELAY_MS + Math.round(Math.random() * STEP_JITTER_MS)
|
|
232
|
+
await sleep(delay, controller.signal)
|
|
233
|
+
}
|
|
234
|
+
updateTask(task.id, {
|
|
235
|
+
status: 'done',
|
|
236
|
+
progress: buildProgress(steps.length, steps.length, 'Complete'),
|
|
237
|
+
})
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (controller.signal.aborted) return
|
|
240
|
+
updateTask(task.id, {
|
|
241
|
+
status: 'error',
|
|
242
|
+
errorMessage:
|
|
243
|
+
error instanceof Error ? error.message : 'Processing failed.',
|
|
244
|
+
})
|
|
245
|
+
} finally {
|
|
246
|
+
runController = null
|
|
247
|
+
updateQueueState(() => {
|
|
248
|
+
if (activeTaskId === task.id) {
|
|
249
|
+
activeTaskId = null
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function runNextTask() {
|
|
256
|
+
if (activeTaskId) return
|
|
257
|
+
const nextTask = tasks.find((task) => task.status === 'queued')
|
|
258
|
+
if (!nextTask) return
|
|
259
|
+
void runTask(nextTask)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function jsonResponse(payload: unknown, status = 200) {
|
|
263
|
+
return new Response(JSON.stringify(payload), {
|
|
264
|
+
status,
|
|
265
|
+
headers: {
|
|
266
|
+
'Content-Type': 'application/json',
|
|
267
|
+
...QUEUE_HEADERS,
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function createEventStream(request: Request) {
|
|
273
|
+
const encoder = new TextEncoder()
|
|
274
|
+
return new Response(
|
|
275
|
+
new ReadableStream({
|
|
276
|
+
start(controller) {
|
|
277
|
+
let isClosed = false
|
|
278
|
+
const send = (event: string, data: unknown) => {
|
|
279
|
+
if (isClosed) return
|
|
280
|
+
controller.enqueue(
|
|
281
|
+
encoder.encode(
|
|
282
|
+
`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`,
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
const listener = (snapshot: ProcessingQueueSnapshot) => {
|
|
287
|
+
send('snapshot', snapshot)
|
|
288
|
+
}
|
|
289
|
+
listeners.add(listener)
|
|
290
|
+
send('snapshot', buildSnapshot())
|
|
291
|
+
const ping = setInterval(() => {
|
|
292
|
+
send('ping', { time: Date.now() })
|
|
293
|
+
}, 15000)
|
|
294
|
+
const close = () => {
|
|
295
|
+
if (isClosed) return
|
|
296
|
+
isClosed = true
|
|
297
|
+
clearInterval(ping)
|
|
298
|
+
listeners.delete(listener)
|
|
299
|
+
controller.close()
|
|
300
|
+
}
|
|
301
|
+
request.signal.addEventListener('abort', close)
|
|
302
|
+
},
|
|
303
|
+
cancel() {
|
|
304
|
+
// handled via abort
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
{
|
|
308
|
+
headers: {
|
|
309
|
+
'Content-Type': 'text/event-stream',
|
|
310
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
311
|
+
Connection: 'keep-alive',
|
|
312
|
+
...QUEUE_HEADERS,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isProcessingCategory(value: unknown): value is ProcessingCategory {
|
|
319
|
+
return value === 'chapter' || value === 'transcript' || value === 'export'
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function isProcessingAction(value: unknown): value is ProcessingAction {
|
|
323
|
+
return (
|
|
324
|
+
value === 'edit-chapter' ||
|
|
325
|
+
value === 'combine-chapters' ||
|
|
326
|
+
value === 'regenerate-transcript' ||
|
|
327
|
+
value === 'detect-command-windows' ||
|
|
328
|
+
value === 'render-preview' ||
|
|
329
|
+
value === 'export-final'
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function handleProcessingQueueRequest(request: Request) {
|
|
334
|
+
if (request.method === 'OPTIONS') {
|
|
335
|
+
return new Response(null, { status: 204, headers: QUEUE_HEADERS })
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const url = new URL(request.url)
|
|
339
|
+
const pathname = url.pathname
|
|
340
|
+
|
|
341
|
+
if (pathname === '/api/processing-queue/stream') {
|
|
342
|
+
if (request.method !== 'GET') {
|
|
343
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
344
|
+
}
|
|
345
|
+
return createEventStream(request)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (pathname === '/api/processing-queue') {
|
|
349
|
+
if (request.method !== 'GET') {
|
|
350
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
351
|
+
}
|
|
352
|
+
return jsonResponse(buildSnapshot())
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (pathname === '/api/processing-queue/enqueue') {
|
|
356
|
+
if (request.method !== 'POST') {
|
|
357
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
358
|
+
}
|
|
359
|
+
let payload: unknown = null
|
|
360
|
+
try {
|
|
361
|
+
payload = await request.json()
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return jsonResponse({ error: 'Invalid JSON payload.' }, 400)
|
|
364
|
+
}
|
|
365
|
+
if (
|
|
366
|
+
!payload ||
|
|
367
|
+
typeof payload !== 'object' ||
|
|
368
|
+
!('title' in payload) ||
|
|
369
|
+
!('detail' in payload) ||
|
|
370
|
+
!('category' in payload) ||
|
|
371
|
+
!('action' in payload)
|
|
372
|
+
) {
|
|
373
|
+
return jsonResponse({ error: 'Missing task fields.' }, 400)
|
|
374
|
+
}
|
|
375
|
+
const data = payload as {
|
|
376
|
+
title?: unknown
|
|
377
|
+
detail?: unknown
|
|
378
|
+
category?: unknown
|
|
379
|
+
action?: unknown
|
|
380
|
+
simulateError?: unknown
|
|
381
|
+
}
|
|
382
|
+
if (typeof data.title !== 'string' || data.title.trim().length === 0) {
|
|
383
|
+
return jsonResponse({ error: 'Task title is required.' }, 400)
|
|
384
|
+
}
|
|
385
|
+
if (typeof data.detail !== 'string') {
|
|
386
|
+
return jsonResponse({ error: 'Task detail is required.' }, 400)
|
|
387
|
+
}
|
|
388
|
+
if (!isProcessingCategory(data.category)) {
|
|
389
|
+
return jsonResponse({ error: 'Invalid task category.' }, 400)
|
|
390
|
+
}
|
|
391
|
+
if (!isProcessingAction(data.action)) {
|
|
392
|
+
return jsonResponse({ error: 'Invalid task action.' }, 400)
|
|
393
|
+
}
|
|
394
|
+
enqueueTask({
|
|
395
|
+
title: data.title,
|
|
396
|
+
detail: data.detail,
|
|
397
|
+
category: data.category,
|
|
398
|
+
action: data.action,
|
|
399
|
+
simulateError: data.simulateError === true,
|
|
400
|
+
})
|
|
401
|
+
return jsonResponse(buildSnapshot())
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (pathname === '/api/processing-queue/run-next') {
|
|
405
|
+
if (request.method !== 'POST') {
|
|
406
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
407
|
+
}
|
|
408
|
+
runNextTask()
|
|
409
|
+
return jsonResponse(buildSnapshot())
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (pathname === '/api/processing-queue/mark-done') {
|
|
413
|
+
if (request.method !== 'POST') {
|
|
414
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
415
|
+
}
|
|
416
|
+
markActiveDone()
|
|
417
|
+
return jsonResponse(buildSnapshot())
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (pathname === '/api/processing-queue/clear-completed') {
|
|
421
|
+
if (request.method !== 'POST') {
|
|
422
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
423
|
+
}
|
|
424
|
+
clearCompleted()
|
|
425
|
+
return jsonResponse(buildSnapshot())
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (pathname.startsWith('/api/processing-queue/task/')) {
|
|
429
|
+
if (request.method !== 'DELETE') {
|
|
430
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
431
|
+
}
|
|
432
|
+
const taskId = pathname.replace('/api/processing-queue/task/', '')
|
|
433
|
+
if (!taskId) {
|
|
434
|
+
return jsonResponse({ error: 'Task id is required.' }, 400)
|
|
435
|
+
}
|
|
436
|
+
removeTask(taskId)
|
|
437
|
+
return jsonResponse(buildSnapshot())
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return jsonResponse({ error: 'Not found' }, 404)
|
|
441
|
+
}
|
package/src/app-server.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { getEnv } from '../app/config/env.ts'
|
|
|
6
6
|
import { createAppRouter } from '../app/router.tsx'
|
|
7
7
|
import { handleVideoRequest } from '../app/video-api.ts'
|
|
8
8
|
import { createBundlingRoutes } from '../server/bundling.ts'
|
|
9
|
+
import { handleProcessingQueueRequest } from '../server/processing-queue.ts'
|
|
9
10
|
|
|
10
11
|
type AppServerOptions = {
|
|
11
12
|
host?: string
|
|
@@ -196,6 +197,9 @@ function startServer(port: number, hostname: string) {
|
|
|
196
197
|
if (url.pathname === '/api/video') {
|
|
197
198
|
return await handleVideoRequest(request)
|
|
198
199
|
}
|
|
200
|
+
if (url.pathname.startsWith('/api/processing-queue')) {
|
|
201
|
+
return await handleProcessingQueueRequest(request)
|
|
202
|
+
}
|
|
199
203
|
return await router.fetch(request)
|
|
200
204
|
} catch (error) {
|
|
201
205
|
console.error(error)
|
package/src/cli.ts
CHANGED
|
@@ -169,17 +169,14 @@ async function main(rawArgs = hideBin(process.argv)) {
|
|
|
169
169
|
afterLog: resumeActiveSpinner,
|
|
170
170
|
})
|
|
171
171
|
try {
|
|
172
|
-
const result = await transcribeAudio(
|
|
173
|
-
transcribeArgs.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
progress,
|
|
181
|
-
},
|
|
182
|
-
)
|
|
172
|
+
const result = await transcribeAudio(transcribeArgs.inputPath, {
|
|
173
|
+
modelPath: transcribeArgs.modelPath,
|
|
174
|
+
language: transcribeArgs.language,
|
|
175
|
+
threads: transcribeArgs.threads,
|
|
176
|
+
binaryPath: transcribeArgs.binaryPath,
|
|
177
|
+
outputBasePath: transcribeArgs.outputBasePath,
|
|
178
|
+
progress,
|
|
179
|
+
})
|
|
183
180
|
resultText = result.text
|
|
184
181
|
} finally {
|
|
185
182
|
setLogHooks({})
|