eprec 1.7.0 → 1.8.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 +135 -10
- package/app/client/editing-workspace.tsx +398 -14
- package/app/config/import-map.ts +1 -0
- package/app/routes/index.tsx +12 -0
- package/package.json +1 -1
package/app/assets/styles.css
CHANGED
|
@@ -199,6 +199,34 @@ p {
|
|
|
199
199
|
gap: var(--spacing-md);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
.stacked-list {
|
|
203
|
+
display: flex;
|
|
204
|
+
flex-direction: column;
|
|
205
|
+
gap: 0;
|
|
206
|
+
margin: 0;
|
|
207
|
+
padding: 0;
|
|
208
|
+
list-style: none;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.stacked-item {
|
|
212
|
+
border-radius: 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.stacked-item:first-child {
|
|
216
|
+
border-top-left-radius: var(--radius-lg);
|
|
217
|
+
border-top-right-radius: var(--radius-lg);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.stacked-item:last-child {
|
|
221
|
+
border-bottom-left-radius: var(--radius-lg);
|
|
222
|
+
border-bottom-right-radius: var(--radius-lg);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.stacked-item.is-selected {
|
|
226
|
+
background: var(--color-info-surface);
|
|
227
|
+
box-shadow: inset 0 0 0 1px var(--color-border-accent);
|
|
228
|
+
}
|
|
229
|
+
|
|
202
230
|
.app-list {
|
|
203
231
|
margin: 0;
|
|
204
232
|
padding-left: var(--spacing-lg);
|
|
@@ -296,6 +324,93 @@ p {
|
|
|
296
324
|
line-height: 1.4;
|
|
297
325
|
}
|
|
298
326
|
|
|
327
|
+
.actions-card {
|
|
328
|
+
gap: var(--spacing-xl);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.actions-header {
|
|
332
|
+
display: flex;
|
|
333
|
+
flex-wrap: wrap;
|
|
334
|
+
align-items: flex-start;
|
|
335
|
+
justify-content: space-between;
|
|
336
|
+
gap: var(--spacing-lg);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.actions-meta {
|
|
340
|
+
min-width: 180px;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.actions-buttons {
|
|
344
|
+
display: flex;
|
|
345
|
+
flex-wrap: wrap;
|
|
346
|
+
gap: var(--spacing-sm);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.actions-grid {
|
|
350
|
+
display: grid;
|
|
351
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
352
|
+
gap: var(--spacing-lg);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.actions-panel {
|
|
356
|
+
border: 1px solid var(--color-border);
|
|
357
|
+
border-radius: var(--radius-lg);
|
|
358
|
+
padding: var(--spacing-lg);
|
|
359
|
+
background: var(--color-surface-muted);
|
|
360
|
+
display: flex;
|
|
361
|
+
flex-direction: column;
|
|
362
|
+
gap: var(--spacing-md);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.actions-button-row {
|
|
366
|
+
display: flex;
|
|
367
|
+
flex-wrap: wrap;
|
|
368
|
+
gap: var(--spacing-sm);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.actions-queue {
|
|
372
|
+
display: flex;
|
|
373
|
+
flex-direction: column;
|
|
374
|
+
gap: var(--spacing-md);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.processing-list .stacked-item {
|
|
378
|
+
background: var(--color-surface);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.processing-row {
|
|
382
|
+
border: 1px solid var(--color-border);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.processing-row-header {
|
|
386
|
+
display: flex;
|
|
387
|
+
align-items: flex-start;
|
|
388
|
+
justify-content: space-between;
|
|
389
|
+
gap: var(--spacing-md);
|
|
390
|
+
padding: var(--spacing-md) var(--spacing-md) var(--spacing-sm);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.processing-row-header h4 {
|
|
394
|
+
margin: 0 0 var(--spacing-xs);
|
|
395
|
+
font-size: var(--font-size-base);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.processing-row-meta {
|
|
399
|
+
display: flex;
|
|
400
|
+
align-items: center;
|
|
401
|
+
justify-content: space-between;
|
|
402
|
+
gap: var(--spacing-md);
|
|
403
|
+
padding: 0 var(--spacing-md) var(--spacing-md);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.processing-row.is-running {
|
|
407
|
+
box-shadow: inset 0 0 0 1px var(--color-warning-border);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.processing-row.is-complete {
|
|
411
|
+
background: var(--color-success-surface);
|
|
412
|
+
}
|
|
413
|
+
|
|
299
414
|
.timeline-card {
|
|
300
415
|
gap: var(--spacing-xl);
|
|
301
416
|
}
|
|
@@ -540,7 +655,7 @@ p {
|
|
|
540
655
|
margin: 0;
|
|
541
656
|
display: flex;
|
|
542
657
|
flex-direction: column;
|
|
543
|
-
gap:
|
|
658
|
+
gap: 0;
|
|
544
659
|
}
|
|
545
660
|
|
|
546
661
|
.cut-row {
|
|
@@ -549,14 +664,14 @@ p {
|
|
|
549
664
|
gap: var(--spacing-md);
|
|
550
665
|
justify-content: space-between;
|
|
551
666
|
border: 1px solid var(--color-border);
|
|
552
|
-
border-radius:
|
|
667
|
+
border-radius: 0;
|
|
553
668
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
554
669
|
background: var(--color-surface-muted);
|
|
555
670
|
}
|
|
556
671
|
|
|
557
672
|
.cut-row.is-selected {
|
|
558
|
-
border-color: var(--color-border-accent);
|
|
559
673
|
background: var(--color-info-surface);
|
|
674
|
+
box-shadow: inset 0 0 0 1px var(--color-border-accent);
|
|
560
675
|
}
|
|
561
676
|
|
|
562
677
|
.cut-select {
|
|
@@ -591,13 +706,13 @@ p {
|
|
|
591
706
|
.command-list {
|
|
592
707
|
display: flex;
|
|
593
708
|
flex-direction: column;
|
|
594
|
-
gap:
|
|
709
|
+
gap: 0;
|
|
595
710
|
}
|
|
596
711
|
|
|
597
712
|
.chapter-row,
|
|
598
713
|
.command-row {
|
|
599
714
|
border: 1px solid var(--color-border);
|
|
600
|
-
border-radius:
|
|
715
|
+
border-radius: 0;
|
|
601
716
|
padding: var(--spacing-md);
|
|
602
717
|
background: var(--color-surface-muted);
|
|
603
718
|
display: flex;
|
|
@@ -673,16 +788,22 @@ p {
|
|
|
673
788
|
list-style: none;
|
|
674
789
|
margin: 0;
|
|
675
790
|
padding: 0;
|
|
676
|
-
display:
|
|
677
|
-
|
|
791
|
+
display: flex;
|
|
792
|
+
flex-direction: column;
|
|
793
|
+
gap: 0;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.transcript-results .stacked-item {
|
|
797
|
+
border: 1px solid var(--color-border);
|
|
798
|
+
background: var(--color-surface);
|
|
678
799
|
}
|
|
679
800
|
|
|
680
801
|
.transcript-result {
|
|
681
802
|
width: 100%;
|
|
682
|
-
border: 1px solid var(--color-border);
|
|
683
|
-
border-radius: var(--radius-md);
|
|
684
803
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
685
|
-
|
|
804
|
+
border: none;
|
|
805
|
+
border-radius: 0;
|
|
806
|
+
background: transparent;
|
|
686
807
|
display: grid;
|
|
687
808
|
grid-template-columns: auto 1fr;
|
|
688
809
|
gap: var(--spacing-md);
|
|
@@ -713,6 +834,10 @@ p {
|
|
|
713
834
|
overflow-x: auto;
|
|
714
835
|
}
|
|
715
836
|
|
|
837
|
+
.stacked-list .stacked-item + .stacked-item {
|
|
838
|
+
border-top: none;
|
|
839
|
+
}
|
|
840
|
+
|
|
716
841
|
@keyframes shimmer {
|
|
717
842
|
0% {
|
|
718
843
|
background-position: 0% 50%;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { matchSorter } from 'match-sorter'
|
|
1
2
|
import type { Handle } from 'remix/component'
|
|
2
3
|
import {
|
|
3
4
|
sampleEditSession,
|
|
@@ -12,15 +13,34 @@ const MIN_CUT_LENGTH = 0.2
|
|
|
12
13
|
const DEFAULT_CUT_LENGTH = 2.4
|
|
13
14
|
const PLAYHEAD_STEP = 0.1
|
|
14
15
|
|
|
16
|
+
type ProcessingStatus = 'queued' | 'running' | 'done'
|
|
17
|
+
type ProcessingCategory = 'chapter' | 'transcript' | 'export'
|
|
18
|
+
type ProcessingTask = {
|
|
19
|
+
id: string
|
|
20
|
+
title: string
|
|
21
|
+
detail: string
|
|
22
|
+
status: ProcessingStatus
|
|
23
|
+
category: ProcessingCategory
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
export function EditingWorkspace(handle: Handle) {
|
|
16
27
|
const duration = sampleEditSession.duration
|
|
17
28
|
const transcript = sampleEditSession.transcript
|
|
18
29
|
const commands = sampleEditSession.commands
|
|
30
|
+
const transcriptIndex = transcript.map((word) => ({
|
|
31
|
+
...word,
|
|
32
|
+
context: buildContext(transcript, word.index, 3),
|
|
33
|
+
}))
|
|
19
34
|
let cutRanges = sampleEditSession.cuts.map((range) => ({ ...range }))
|
|
20
35
|
let chapters = sampleEditSession.chapters.map((chapter) => ({ ...chapter }))
|
|
21
36
|
let playhead = 18.2
|
|
22
37
|
let selectedRangeId = cutRanges[0]?.id ?? null
|
|
23
38
|
let searchQuery = ''
|
|
39
|
+
let primaryChapterId = chapters[0]?.id ?? ''
|
|
40
|
+
let secondaryChapterId = chapters[1]?.id ?? chapters[0]?.id ?? ''
|
|
41
|
+
let processingQueue: ProcessingTask[] = []
|
|
42
|
+
let activeTaskId: string | null = null
|
|
43
|
+
let processingCount = 1
|
|
24
44
|
let manualCutId = 1
|
|
25
45
|
let previewDuration = 0
|
|
26
46
|
let previewReady = false
|
|
@@ -144,6 +164,128 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
144
164
|
handle.update()
|
|
145
165
|
}
|
|
146
166
|
|
|
167
|
+
const findChapter = (chapterId: string) =>
|
|
168
|
+
chapters.find((chapter) => chapter.id === chapterId) ?? null
|
|
169
|
+
|
|
170
|
+
const updatePrimaryChapter = (chapterId: string) => {
|
|
171
|
+
primaryChapterId = chapterId
|
|
172
|
+
if (secondaryChapterId === chapterId) {
|
|
173
|
+
secondaryChapterId =
|
|
174
|
+
chapters.find((chapter) => chapter.id !== chapterId)?.id ??
|
|
175
|
+
chapterId
|
|
176
|
+
}
|
|
177
|
+
handle.update()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const updateSecondaryChapter = (chapterId: string) => {
|
|
181
|
+
secondaryChapterId = chapterId
|
|
182
|
+
handle.update()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const queueTask = (
|
|
186
|
+
title: string,
|
|
187
|
+
detail: string,
|
|
188
|
+
category: ProcessingCategory,
|
|
189
|
+
) => {
|
|
190
|
+
const task: ProcessingTask = {
|
|
191
|
+
id: `task-${processingCount++}`,
|
|
192
|
+
title,
|
|
193
|
+
detail,
|
|
194
|
+
status: 'queued',
|
|
195
|
+
category,
|
|
196
|
+
}
|
|
197
|
+
processingQueue = [...processingQueue, task]
|
|
198
|
+
handle.update()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const queueChapterEdit = () => {
|
|
202
|
+
const chapter = findChapter(primaryChapterId)
|
|
203
|
+
if (!chapter) return
|
|
204
|
+
queueTask(
|
|
205
|
+
`Edit ${chapter.title}`,
|
|
206
|
+
`Review trims for ${formatTimestamp(chapter.start)} - ${formatTimestamp(
|
|
207
|
+
chapter.end,
|
|
208
|
+
)}.`,
|
|
209
|
+
'chapter',
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const queueCombineChapters = () => {
|
|
214
|
+
const primary = findChapter(primaryChapterId)
|
|
215
|
+
const secondary = findChapter(secondaryChapterId)
|
|
216
|
+
if (!primary || !secondary || primary.id === secondary.id) return
|
|
217
|
+
queueTask(
|
|
218
|
+
`Combine ${primary.title} + ${secondary.title}`,
|
|
219
|
+
'Merge both chapters into a single preview export.',
|
|
220
|
+
'chapter',
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const queueTranscriptRegeneration = () => {
|
|
225
|
+
queueTask(
|
|
226
|
+
'Regenerate transcript',
|
|
227
|
+
'Run Whisper alignment and refresh search cues.',
|
|
228
|
+
'transcript',
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const queueCommandScan = () => {
|
|
233
|
+
queueTask(
|
|
234
|
+
'Detect command windows',
|
|
235
|
+
'Scan for Jarvis commands and update cut ranges.',
|
|
236
|
+
'transcript',
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const queuePreviewRender = () => {
|
|
241
|
+
queueTask(
|
|
242
|
+
'Render preview clip',
|
|
243
|
+
'Bake a short MP4 with current edits applied.',
|
|
244
|
+
'export',
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const queueFinalExport = () => {
|
|
249
|
+
queueTask(
|
|
250
|
+
'Export edited chapters',
|
|
251
|
+
'Render final chapters and write the export package.',
|
|
252
|
+
'export',
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const startNextTask = () => {
|
|
257
|
+
if (activeTaskId) return
|
|
258
|
+
const next = processingQueue.find((task) => task.status === 'queued')
|
|
259
|
+
if (!next) return
|
|
260
|
+
activeTaskId = next.id
|
|
261
|
+
processingQueue = processingQueue.map((task) =>
|
|
262
|
+
task.id === next.id ? { ...task, status: 'running' } : task,
|
|
263
|
+
)
|
|
264
|
+
handle.update()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const markActiveDone = () => {
|
|
268
|
+
if (!activeTaskId) return
|
|
269
|
+
processingQueue = processingQueue.map((task) =>
|
|
270
|
+
task.id === activeTaskId ? { ...task, status: 'done' } : task,
|
|
271
|
+
)
|
|
272
|
+
activeTaskId = null
|
|
273
|
+
handle.update()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const clearCompletedTasks = () => {
|
|
277
|
+
processingQueue = processingQueue.filter((task) => task.status !== 'done')
|
|
278
|
+
handle.update()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const removeTask = (taskId: string) => {
|
|
282
|
+
processingQueue = processingQueue.filter((task) => task.id !== taskId)
|
|
283
|
+
if (activeTaskId === taskId) {
|
|
284
|
+
activeTaskId = null
|
|
285
|
+
}
|
|
286
|
+
handle.update()
|
|
287
|
+
}
|
|
288
|
+
|
|
147
289
|
const syncVideoToPlayhead = (value: number) => {
|
|
148
290
|
if (
|
|
149
291
|
!previewNode ||
|
|
@@ -179,12 +321,24 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
179
321
|
const currentContext = currentWord
|
|
180
322
|
? buildContext(transcript, currentWord.index, 4)
|
|
181
323
|
: 'No transcript cues found for the playhead.'
|
|
182
|
-
const query = searchQuery.trim()
|
|
324
|
+
const query = searchQuery.trim()
|
|
183
325
|
const searchResults = query
|
|
184
|
-
?
|
|
185
|
-
|
|
186
|
-
|
|
326
|
+
? matchSorter(transcriptIndex, query, {
|
|
327
|
+
keys: ['word'],
|
|
328
|
+
}).slice(0, 12)
|
|
187
329
|
: []
|
|
330
|
+
const queuedCount = processingQueue.filter(
|
|
331
|
+
(task) => task.status === 'queued',
|
|
332
|
+
).length
|
|
333
|
+
const completedCount = processingQueue.filter(
|
|
334
|
+
(task) => task.status === 'done',
|
|
335
|
+
).length
|
|
336
|
+
const runningTask =
|
|
337
|
+
processingQueue.find((task) => task.status === 'running') ?? null
|
|
338
|
+
const canCombineChapters =
|
|
339
|
+
primaryChapterId.length > 0 &&
|
|
340
|
+
secondaryChapterId.length > 0 &&
|
|
341
|
+
primaryChapterId !== secondaryChapterId
|
|
188
342
|
const commandPreview = buildCommandPreview(
|
|
189
343
|
sampleEditSession.sourceName,
|
|
190
344
|
chapters,
|
|
@@ -246,6 +400,227 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
246
400
|
</div>
|
|
247
401
|
</section>
|
|
248
402
|
|
|
403
|
+
<section class="app-card app-card--full actions-card">
|
|
404
|
+
<div class="actions-header">
|
|
405
|
+
<div>
|
|
406
|
+
<h2>Processing actions</h2>
|
|
407
|
+
<p class="app-muted">
|
|
408
|
+
Queue chapter edits, transcript cleanup, and export jobs directly
|
|
409
|
+
from the workspace.
|
|
410
|
+
</p>
|
|
411
|
+
</div>
|
|
412
|
+
<div class="actions-meta">
|
|
413
|
+
<div class="summary-item">
|
|
414
|
+
<span class="summary-label">Queue</span>
|
|
415
|
+
<span class="summary-value">{queuedCount} queued</span>
|
|
416
|
+
<span class="summary-subtext">
|
|
417
|
+
{runningTask ? `Running: ${runningTask.title}` : 'Idle'}
|
|
418
|
+
</span>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="actions-buttons">
|
|
422
|
+
<button
|
|
423
|
+
class="button button--primary"
|
|
424
|
+
type="button"
|
|
425
|
+
disabled={queuedCount === 0 || Boolean(runningTask)}
|
|
426
|
+
on={{ click: startNextTask }}
|
|
427
|
+
>
|
|
428
|
+
Run next
|
|
429
|
+
</button>
|
|
430
|
+
<button
|
|
431
|
+
class="button button--ghost"
|
|
432
|
+
type="button"
|
|
433
|
+
disabled={!runningTask}
|
|
434
|
+
on={{ click: markActiveDone }}
|
|
435
|
+
>
|
|
436
|
+
Mark running done
|
|
437
|
+
</button>
|
|
438
|
+
<button
|
|
439
|
+
class="button button--ghost"
|
|
440
|
+
type="button"
|
|
441
|
+
disabled={completedCount === 0}
|
|
442
|
+
on={{ click: clearCompletedTasks }}
|
|
443
|
+
>
|
|
444
|
+
Clear completed
|
|
445
|
+
</button>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<div class="actions-grid">
|
|
450
|
+
<article class="actions-panel">
|
|
451
|
+
<div class="panel-header">
|
|
452
|
+
<h3>Chapter processing</h3>
|
|
453
|
+
<span class="status-pill status-pill--info">Chapter</span>
|
|
454
|
+
</div>
|
|
455
|
+
<label class="input-label">
|
|
456
|
+
Primary chapter
|
|
457
|
+
<select
|
|
458
|
+
class="text-input"
|
|
459
|
+
value={primaryChapterId}
|
|
460
|
+
on={{
|
|
461
|
+
change: (event) => {
|
|
462
|
+
const target = event.currentTarget as HTMLSelectElement
|
|
463
|
+
updatePrimaryChapter(target.value)
|
|
464
|
+
},
|
|
465
|
+
}}
|
|
466
|
+
>
|
|
467
|
+
{chapters.map((chapter) => (
|
|
468
|
+
<option value={chapter.id}>{chapter.title}</option>
|
|
469
|
+
))}
|
|
470
|
+
</select>
|
|
471
|
+
</label>
|
|
472
|
+
<label class="input-label">
|
|
473
|
+
Secondary chapter
|
|
474
|
+
<select
|
|
475
|
+
class="text-input"
|
|
476
|
+
value={secondaryChapterId}
|
|
477
|
+
on={{
|
|
478
|
+
change: (event) => {
|
|
479
|
+
const target = event.currentTarget as HTMLSelectElement
|
|
480
|
+
updateSecondaryChapter(target.value)
|
|
481
|
+
},
|
|
482
|
+
}}
|
|
483
|
+
>
|
|
484
|
+
{chapters.map((chapter) => (
|
|
485
|
+
<option value={chapter.id}>{chapter.title}</option>
|
|
486
|
+
))}
|
|
487
|
+
</select>
|
|
488
|
+
</label>
|
|
489
|
+
<div class="actions-button-row">
|
|
490
|
+
<button
|
|
491
|
+
class="button button--primary"
|
|
492
|
+
type="button"
|
|
493
|
+
disabled={!primaryChapterId}
|
|
494
|
+
on={{ click: queueChapterEdit }}
|
|
495
|
+
>
|
|
496
|
+
Edit chapter
|
|
497
|
+
</button>
|
|
498
|
+
<button
|
|
499
|
+
class="button button--ghost"
|
|
500
|
+
type="button"
|
|
501
|
+
disabled={!canCombineChapters}
|
|
502
|
+
on={{ click: queueCombineChapters }}
|
|
503
|
+
>
|
|
504
|
+
Combine chapters
|
|
505
|
+
</button>
|
|
506
|
+
</div>
|
|
507
|
+
<p class="app-muted">
|
|
508
|
+
Stage edits or merge two chapters without leaving this view.
|
|
509
|
+
</p>
|
|
510
|
+
</article>
|
|
511
|
+
|
|
512
|
+
<article class="actions-panel">
|
|
513
|
+
<div class="panel-header">
|
|
514
|
+
<h3>Transcript utilities</h3>
|
|
515
|
+
<span class="status-pill status-pill--warning">Transcript</span>
|
|
516
|
+
</div>
|
|
517
|
+
<div class="actions-button-row">
|
|
518
|
+
<button
|
|
519
|
+
class="button button--ghost"
|
|
520
|
+
type="button"
|
|
521
|
+
on={{ click: queueTranscriptRegeneration }}
|
|
522
|
+
>
|
|
523
|
+
Regenerate transcript
|
|
524
|
+
</button>
|
|
525
|
+
<button
|
|
526
|
+
class="button button--ghost"
|
|
527
|
+
type="button"
|
|
528
|
+
on={{ click: queueCommandScan }}
|
|
529
|
+
>
|
|
530
|
+
Detect command windows
|
|
531
|
+
</button>
|
|
532
|
+
</div>
|
|
533
|
+
<p class="app-muted">
|
|
534
|
+
Refresh the transcript or scan for command markers on demand.
|
|
535
|
+
</p>
|
|
536
|
+
</article>
|
|
537
|
+
|
|
538
|
+
<article class="actions-panel">
|
|
539
|
+
<div class="panel-header">
|
|
540
|
+
<h3>Exports</h3>
|
|
541
|
+
<span class="status-pill status-pill--success">Export</span>
|
|
542
|
+
</div>
|
|
543
|
+
<div class="actions-button-row">
|
|
544
|
+
<button
|
|
545
|
+
class="button button--ghost"
|
|
546
|
+
type="button"
|
|
547
|
+
on={{ click: queuePreviewRender }}
|
|
548
|
+
>
|
|
549
|
+
Render preview clip
|
|
550
|
+
</button>
|
|
551
|
+
<button
|
|
552
|
+
class="button button--ghost"
|
|
553
|
+
type="button"
|
|
554
|
+
on={{ click: queueFinalExport }}
|
|
555
|
+
>
|
|
556
|
+
Export edited chapters
|
|
557
|
+
</button>
|
|
558
|
+
</div>
|
|
559
|
+
<p class="app-muted">
|
|
560
|
+
Trigger preview renders or finalize exports for the pipeline.
|
|
561
|
+
</p>
|
|
562
|
+
</article>
|
|
563
|
+
</div>
|
|
564
|
+
|
|
565
|
+
<div class="actions-queue">
|
|
566
|
+
<div class="panel-header">
|
|
567
|
+
<h3>Processing queue</h3>
|
|
568
|
+
<span class="summary-subtext">
|
|
569
|
+
{processingQueue.length} total
|
|
570
|
+
</span>
|
|
571
|
+
</div>
|
|
572
|
+
{processingQueue.length === 0 ? (
|
|
573
|
+
<p class="app-muted">
|
|
574
|
+
No actions queued yet. Use the buttons above to stage work.
|
|
575
|
+
</p>
|
|
576
|
+
) : (
|
|
577
|
+
<ul class="stacked-list processing-list">
|
|
578
|
+
{processingQueue.map((task) => (
|
|
579
|
+
<li
|
|
580
|
+
class={classNames(
|
|
581
|
+
'stacked-item',
|
|
582
|
+
'processing-row',
|
|
583
|
+
task.status === 'running' && 'is-running',
|
|
584
|
+
task.status === 'done' && 'is-complete',
|
|
585
|
+
)}
|
|
586
|
+
>
|
|
587
|
+
<div class="processing-row-header">
|
|
588
|
+
<div>
|
|
589
|
+
<h4>{task.title}</h4>
|
|
590
|
+
<p class="app-muted">{task.detail}</p>
|
|
591
|
+
</div>
|
|
592
|
+
<span
|
|
593
|
+
class={classNames(
|
|
594
|
+
'status-pill',
|
|
595
|
+
task.status === 'queued' && 'status-pill--info',
|
|
596
|
+
task.status === 'running' && 'status-pill--warning',
|
|
597
|
+
task.status === 'done' && 'status-pill--success',
|
|
598
|
+
)}
|
|
599
|
+
>
|
|
600
|
+
{task.status}
|
|
601
|
+
</span>
|
|
602
|
+
</div>
|
|
603
|
+
<div class="processing-row-meta">
|
|
604
|
+
<span class="summary-subtext">
|
|
605
|
+
{formatProcessingCategory(task.category)}
|
|
606
|
+
</span>
|
|
607
|
+
{task.status === 'queued' ? (
|
|
608
|
+
<button
|
|
609
|
+
class="button button--ghost"
|
|
610
|
+
type="button"
|
|
611
|
+
on={{ click: () => removeTask(task.id) }}
|
|
612
|
+
>
|
|
613
|
+
Remove
|
|
614
|
+
</button>
|
|
615
|
+
) : null}
|
|
616
|
+
</div>
|
|
617
|
+
</li>
|
|
618
|
+
))}
|
|
619
|
+
</ul>
|
|
620
|
+
)}
|
|
621
|
+
</div>
|
|
622
|
+
</section>
|
|
623
|
+
|
|
249
624
|
<section class="app-card app-card--full timeline-card">
|
|
250
625
|
<div class="timeline-header">
|
|
251
626
|
<div>
|
|
@@ -523,10 +898,11 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
523
898
|
)}
|
|
524
899
|
|
|
525
900
|
<h3>Cut list</h3>
|
|
526
|
-
<ul class="cut-list">
|
|
901
|
+
<ul class="cut-list stacked-list">
|
|
527
902
|
{sortedCuts.map((range) => (
|
|
528
903
|
<li
|
|
529
904
|
class={classNames(
|
|
905
|
+
'stacked-item',
|
|
530
906
|
'cut-row',
|
|
531
907
|
range.id === selectedRangeId && 'is-selected',
|
|
532
908
|
)}
|
|
@@ -562,9 +938,9 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
562
938
|
<p class="app-muted">
|
|
563
939
|
Update output names and mark chapters to skip before export.
|
|
564
940
|
</p>
|
|
565
|
-
<div class="chapter-list">
|
|
941
|
+
<div class="chapter-list stacked-list">
|
|
566
942
|
{chapters.map((chapter) => (
|
|
567
|
-
<article class="chapter-row">
|
|
943
|
+
<article class="chapter-row stacked-item">
|
|
568
944
|
<div class="chapter-header">
|
|
569
945
|
<div>
|
|
570
946
|
<h3>{chapter.title}</h3>
|
|
@@ -630,11 +1006,11 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
630
1006
|
<p class="app-muted">
|
|
631
1007
|
Apply Jarvis commands to your cut list or chapter metadata.
|
|
632
1008
|
</p>
|
|
633
|
-
<div class="command-list">
|
|
1009
|
+
<div class="command-list stacked-list">
|
|
634
1010
|
{commands.map((command) => {
|
|
635
1011
|
const applied = isCommandApplied(command, sortedCuts, chapters)
|
|
636
1012
|
return (
|
|
637
|
-
<article class="command-row">
|
|
1013
|
+
<article class="command-row stacked-item">
|
|
638
1014
|
<div class="command-header">
|
|
639
1015
|
<h3>{command.label}</h3>
|
|
640
1016
|
<span
|
|
@@ -705,10 +1081,14 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
705
1081
|
<p class="app-muted transcript-empty">
|
|
706
1082
|
Type to search the transcript words. Click a result to jump to it.
|
|
707
1083
|
</p>
|
|
1084
|
+
) : searchResults.length === 0 ? (
|
|
1085
|
+
<p class="app-muted transcript-empty">
|
|
1086
|
+
No results found for "{query}".
|
|
1087
|
+
</p>
|
|
708
1088
|
) : (
|
|
709
|
-
<ul class="transcript-results">
|
|
1089
|
+
<ul class="transcript-results stacked-list">
|
|
710
1090
|
{searchResults.map((word) => (
|
|
711
|
-
<li>
|
|
1091
|
+
<li class="stacked-item">
|
|
712
1092
|
<button
|
|
713
1093
|
class="transcript-result"
|
|
714
1094
|
type="button"
|
|
@@ -717,9 +1097,7 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
717
1097
|
<span class="transcript-time">
|
|
718
1098
|
{formatTimestamp(word.start)}
|
|
719
1099
|
</span>
|
|
720
|
-
<span class="transcript-snippet">
|
|
721
|
-
{buildContext(transcript, word.index, 3)}
|
|
722
|
-
</span>
|
|
1100
|
+
<span class="transcript-snippet">{word.context}</span>
|
|
723
1101
|
</button>
|
|
724
1102
|
</li>
|
|
725
1103
|
))}
|
|
@@ -786,6 +1164,12 @@ function formatSeconds(value: number) {
|
|
|
786
1164
|
return `${value.toFixed(1)}s`
|
|
787
1165
|
}
|
|
788
1166
|
|
|
1167
|
+
function formatProcessingCategory(category: ProcessingCategory) {
|
|
1168
|
+
if (category === 'chapter') return 'Chapter task'
|
|
1169
|
+
if (category === 'transcript') return 'Transcript task'
|
|
1170
|
+
return 'Export task'
|
|
1171
|
+
}
|
|
1172
|
+
|
|
789
1173
|
function classNames(...values: Array<string | false | null | undefined>) {
|
|
790
1174
|
return values.filter(Boolean).join(' ')
|
|
791
1175
|
}
|
package/app/config/import-map.ts
CHANGED
package/app/routes/index.tsx
CHANGED
|
@@ -17,6 +17,18 @@ const indexHandler = {
|
|
|
17
17
|
exports.
|
|
18
18
|
</p>
|
|
19
19
|
</header>
|
|
20
|
+
<section class="app-card app-card--full">
|
|
21
|
+
<h2>Processing actions</h2>
|
|
22
|
+
<p class="app-muted">
|
|
23
|
+
Queue chapter edits, transcript cleanup, and export jobs once the
|
|
24
|
+
client loads.
|
|
25
|
+
</p>
|
|
26
|
+
<ul class="app-list">
|
|
27
|
+
<li>Edit a chapter with the latest cut list.</li>
|
|
28
|
+
<li>Combine two chapters into a merged preview.</li>
|
|
29
|
+
<li>Regenerate the transcript or detect command windows.</li>
|
|
30
|
+
</ul>
|
|
31
|
+
</section>
|
|
20
32
|
<section class="app-card app-card--full">
|
|
21
33
|
<h2>Timeline editor</h2>
|
|
22
34
|
<p class="app-muted">
|