eprec 1.11.0 → 1.13.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.
@@ -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 activeTaskId: string | null = null
83
- let processingCount = 1
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,67 @@ 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 = (await response.json()) as ProcessingQueueSnapshot
400
+ applyQueueSnapshot(snapshot)
401
+ } catch (error) {
402
+ if (handle.signal.aborted) return
403
+ setQueueError(
404
+ error instanceof Error
405
+ ? error.message
406
+ : 'Unable to reach the processing queue.',
407
+ )
408
+ }
409
+ }
410
+
411
+ void loadQueueSnapshot()
412
+ connectQueueStream()
413
+
312
414
  const queueTask = (
415
+ action: ProcessingAction,
313
416
  title: string,
314
417
  detail: string,
315
418
  category: ProcessingCategory,
316
419
  ) => {
317
- const task: ProcessingTask = {
318
- id: `task-${processingCount++}`,
319
- title,
320
- detail,
321
- status: 'queued',
322
- category,
323
- }
324
- processingQueue = [...processingQueue, task]
325
- handle.update()
420
+ void requestQueue('/enqueue', {
421
+ method: 'POST',
422
+ body: JSON.stringify({ action, title, detail, category }),
423
+ })
326
424
  }
327
425
 
328
426
  const queueChapterEdit = () => {
329
427
  const chapter = findChapter(primaryChapterId)
330
428
  if (!chapter) return
331
429
  queueTask(
430
+ 'edit-chapter',
332
431
  `Edit ${chapter.title}`,
333
432
  `Review trims for ${formatTimestamp(chapter.start)} - ${formatTimestamp(
334
433
  chapter.end,
@@ -342,6 +441,7 @@ export function EditingWorkspace(handle: Handle) {
342
441
  const secondary = findChapter(secondaryChapterId)
343
442
  if (!primary || !secondary || primary.id === secondary.id) return
344
443
  queueTask(
444
+ 'combine-chapters',
345
445
  `Combine ${primary.title} + ${secondary.title}`,
346
446
  'Merge both chapters into a single preview export.',
347
447
  'chapter',
@@ -350,6 +450,7 @@ export function EditingWorkspace(handle: Handle) {
350
450
 
351
451
  const queueTranscriptRegeneration = () => {
352
452
  queueTask(
453
+ 'regenerate-transcript',
353
454
  'Regenerate transcript',
354
455
  'Run Whisper alignment and refresh search cues.',
355
456
  'transcript',
@@ -358,6 +459,7 @@ export function EditingWorkspace(handle: Handle) {
358
459
 
359
460
  const queueCommandScan = () => {
360
461
  queueTask(
462
+ 'detect-command-windows',
361
463
  'Detect command windows',
362
464
  'Scan for Jarvis commands and update cut ranges.',
363
465
  'transcript',
@@ -366,6 +468,7 @@ export function EditingWorkspace(handle: Handle) {
366
468
 
367
469
  const queuePreviewRender = () => {
368
470
  queueTask(
471
+ 'render-preview',
369
472
  'Render preview clip',
370
473
  'Bake a short MP4 with current edits applied.',
371
474
  'export',
@@ -374,6 +477,7 @@ export function EditingWorkspace(handle: Handle) {
374
477
 
375
478
  const queueFinalExport = () => {
376
479
  queueTask(
480
+ 'export-final',
377
481
  'Export edited chapters',
378
482
  'Render final chapters and write the export package.',
379
483
  'export',
@@ -381,36 +485,25 @@ export function EditingWorkspace(handle: Handle) {
381
485
  }
382
486
 
383
487
  const startNextTask = () => {
384
- if (activeTaskId) return
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()
488
+ void requestQueue('/run-next', { method: 'POST' })
392
489
  }
393
490
 
394
491
  const markActiveDone = () => {
395
- if (!activeTaskId) return
396
- processingQueue = processingQueue.map((task) =>
397
- task.id === activeTaskId ? { ...task, status: 'done' } : task,
398
- )
399
- activeTaskId = null
400
- handle.update()
492
+ void requestQueue('/mark-done', { method: 'POST' })
493
+ }
494
+
495
+ const cancelActiveTask = (taskId: string) => {
496
+ void requestQueue(`/task/${encodeURIComponent(taskId)}`, {
497
+ method: 'DELETE',
498
+ })
401
499
  }
402
500
 
403
501
  const clearCompletedTasks = () => {
404
- processingQueue = processingQueue.filter((task) => task.status !== 'done')
405
- handle.update()
502
+ void requestQueue('/clear-completed', { method: 'POST' })
406
503
  }
407
504
 
408
505
  const removeTask = (taskId: string) => {
409
- processingQueue = processingQueue.filter((task) => task.id !== taskId)
410
- if (activeTaskId === taskId) {
411
- activeTaskId = null
412
- }
413
- handle.update()
506
+ cancelActiveTask(taskId)
414
507
  }
415
508
 
416
509
  const syncVideoToPlayhead = (value: number) => {
@@ -460,8 +553,28 @@ export function EditingWorkspace(handle: Handle) {
460
553
  const completedCount = processingQueue.filter(
461
554
  (task) => task.status === 'done',
462
555
  ).length
556
+ const errorCount = processingQueue.filter(
557
+ (task) => task.status === 'error',
558
+ ).length
463
559
  const runningTask =
464
560
  processingQueue.find((task) => task.status === 'running') ?? null
561
+ const queueStreamMeta =
562
+ queueStreamStatus === 'open'
563
+ ? { label: 'Live', className: 'status-pill--success' }
564
+ : queueStreamStatus === 'connecting'
565
+ ? { label: 'Connecting', className: 'status-pill--warning' }
566
+ : { label: 'Offline', className: 'status-pill--danger' }
567
+ const runningSummary = runningTask
568
+ ? runningTask.progress?.label
569
+ ? `Running: ${runningTask.title} · ${runningTask.progress.label}`
570
+ : `Running: ${runningTask.title}`
571
+ : 'Idle'
572
+ const queueStreamSummary =
573
+ queueStreamStatus === 'open'
574
+ ? 'Queue updates are live.'
575
+ : queueStreamStatus === 'connecting'
576
+ ? 'Connecting to queue updates.'
577
+ : 'Queue updates are offline.'
465
578
  const canCombineChapters =
466
579
  primaryChapterId.length > 0 &&
467
580
  secondaryChapterId.length > 0 &&
@@ -492,6 +605,11 @@ export function EditingWorkspace(handle: Handle) {
492
605
  Review transcript-based edits, refine command windows, and prepare
493
606
  the final CLI export in one place.
494
607
  </p>
608
+ <nav class="app-nav">
609
+ <a class="app-link" href="/trim-points">
610
+ Trim points
611
+ </a>
612
+ </nav>
495
613
  </header>
496
614
 
497
615
  <section class="app-card app-card--full source-card">
@@ -612,16 +730,34 @@ export function EditingWorkspace(handle: Handle) {
612
730
  <div class="summary-item">
613
731
  <span class="summary-label">Queue</span>
614
732
  <span class="summary-value">{queuedCount} queued</span>
733
+ <span class="summary-subtext">{runningSummary}</span>
734
+ </div>
735
+ <div class="summary-item">
736
+ <span class="summary-label">Errors</span>
737
+ <span class="summary-value">{errorCount}</span>
615
738
  <span class="summary-subtext">
616
- {runningTask ? `Running: ${runningTask.title}` : 'Idle'}
739
+ {errorCount > 0
740
+ ? 'Review failed tasks below.'
741
+ : 'No failures yet.'}
742
+ </span>
743
+ </div>
744
+ <div class="summary-item">
745
+ <span class="summary-label">Stream</span>
746
+ <span
747
+ class={classNames('status-pill', queueStreamMeta.className)}
748
+ >
749
+ {queueStreamMeta.label}
617
750
  </span>
751
+ <span class="summary-subtext">{queueStreamSummary}</span>
618
752
  </div>
619
753
  </div>
620
754
  <div class="actions-buttons">
621
755
  <button
622
756
  class="button button--primary"
623
757
  type="button"
624
- disabled={queuedCount === 0 || Boolean(runningTask)}
758
+ disabled={
759
+ queueLoading || queuedCount === 0 || Boolean(runningTask)
760
+ }
625
761
  on={{ click: startNextTask }}
626
762
  >
627
763
  Run next
@@ -629,15 +765,29 @@ export function EditingWorkspace(handle: Handle) {
629
765
  <button
630
766
  class="button button--ghost"
631
767
  type="button"
632
- disabled={!runningTask}
768
+ disabled={queueLoading || !runningTask}
633
769
  on={{ click: markActiveDone }}
634
770
  >
635
771
  Mark running done
636
772
  </button>
773
+ <button
774
+ class="button button--danger"
775
+ type="button"
776
+ disabled={queueLoading || !runningTask}
777
+ on={{
778
+ click: () => {
779
+ if (runningTask) {
780
+ cancelActiveTask(runningTask.id)
781
+ }
782
+ },
783
+ }}
784
+ >
785
+ Cancel running
786
+ </button>
637
787
  <button
638
788
  class="button button--ghost"
639
789
  type="button"
640
- disabled={completedCount === 0}
790
+ disabled={queueLoading || completedCount === 0}
641
791
  on={{ click: clearCompletedTasks }}
642
792
  >
643
793
  Clear completed
@@ -765,56 +915,111 @@ export function EditingWorkspace(handle: Handle) {
765
915
  <div class="panel-header">
766
916
  <h3>Processing queue</h3>
767
917
  <span class="summary-subtext">
768
- {processingQueue.length} total
918
+ {queueLoading
919
+ ? 'Loading...'
920
+ : `${processingQueue.length} total`}
769
921
  </span>
770
922
  </div>
771
- {processingQueue.length === 0 ? (
923
+ {queueError ? (
924
+ <p class="status-note status-note--danger">{queueError}</p>
925
+ ) : null}
926
+ {queueLoading ? (
927
+ <p class="app-muted">Loading queue updates...</p>
928
+ ) : processingQueue.length === 0 ? (
772
929
  <p class="app-muted">
773
930
  No actions queued yet. Use the buttons above to stage work.
774
931
  </p>
775
932
  ) : (
776
933
  <ul class="stacked-list processing-list">
777
- {processingQueue.map((task) => (
778
- <li
779
- class={classNames(
780
- 'stacked-item',
781
- 'processing-row',
782
- task.status === 'running' && 'is-running',
783
- task.status === 'done' && 'is-complete',
784
- )}
785
- >
786
- <div class="processing-row-header">
787
- <div>
788
- <h4>{task.title}</h4>
789
- <p class="app-muted">{task.detail}</p>
790
- </div>
791
- <span
792
- class={classNames(
793
- 'status-pill',
794
- task.status === 'queued' && 'status-pill--info',
795
- task.status === 'running' && 'status-pill--warning',
796
- task.status === 'done' && 'status-pill--success',
797
- )}
798
- >
799
- {task.status}
800
- </span>
801
- </div>
802
- <div class="processing-row-meta">
803
- <span class="summary-subtext">
804
- {formatProcessingCategory(task.category)}
805
- </span>
806
- {task.status === 'queued' ? (
807
- <button
808
- class="button button--ghost"
809
- type="button"
810
- on={{ click: () => removeTask(task.id) }}
934
+ {processingQueue.map((task) => {
935
+ const showProgress =
936
+ task.status === 'running' || task.status === 'error'
937
+ const progress =
938
+ task.progress ??
939
+ (task.status === 'running'
940
+ ? {
941
+ step: 0,
942
+ totalSteps: 0,
943
+ label: 'Starting',
944
+ percent: 0,
945
+ }
946
+ : null)
947
+ return (
948
+ <li
949
+ class={classNames(
950
+ 'stacked-item',
951
+ 'processing-row',
952
+ task.status === 'running' && 'is-running',
953
+ task.status === 'done' && 'is-complete',
954
+ task.status === 'error' && 'is-error',
955
+ )}
956
+ >
957
+ <div class="processing-row-header">
958
+ <div>
959
+ <h4>{task.title}</h4>
960
+ <p class="app-muted">{task.detail}</p>
961
+ </div>
962
+ <span
963
+ class={classNames(
964
+ 'status-pill',
965
+ task.status === 'queued' && 'status-pill--info',
966
+ task.status === 'running' && 'status-pill--warning',
967
+ task.status === 'done' && 'status-pill--success',
968
+ task.status === 'error' && 'status-pill--danger',
969
+ )}
811
970
  >
812
- Remove
813
- </button>
971
+ {task.status}
972
+ </span>
973
+ </div>
974
+ {showProgress && progress ? (
975
+ <div class="processing-progress">
976
+ <div class="processing-progress-meta">
977
+ <span>{progress.label}</span>
978
+ <span>
979
+ {progress.totalSteps > 0
980
+ ? `${progress.step}/${progress.totalSteps}`
981
+ : '...'}
982
+ </span>
983
+ </div>
984
+ <div class="processing-progress-bar">
985
+ <span
986
+ class="processing-progress-fill"
987
+ style={`--progress:${progress.percent}%`}
988
+ />
989
+ </div>
990
+ </div>
814
991
  ) : null}
815
- </div>
816
- </li>
817
- ))}
992
+ {task.status === 'error' && task.errorMessage ? (
993
+ <p class="status-note status-note--danger processing-error">
994
+ {task.errorMessage}
995
+ </p>
996
+ ) : null}
997
+ <div class="processing-row-meta">
998
+ <span class="summary-subtext">
999
+ {formatProcessingCategory(task.category)}
1000
+ </span>
1001
+ {task.status === 'running' ? (
1002
+ <button
1003
+ class="button button--danger"
1004
+ type="button"
1005
+ on={{ click: () => cancelActiveTask(task.id) }}
1006
+ >
1007
+ Cancel
1008
+ </button>
1009
+ ) : null}
1010
+ {task.status === 'queued' || task.status === 'error' ? (
1011
+ <button
1012
+ class="button button--ghost"
1013
+ type="button"
1014
+ on={{ click: () => removeTask(task.id) }}
1015
+ >
1016
+ Remove
1017
+ </button>
1018
+ ) : null}
1019
+ </div>
1020
+ </li>
1021
+ )
1022
+ })}
818
1023
  </ul>
819
1024
  )}
820
1025
  </div>