anymd 0.0.4 → 0.0.5
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/package.json +1 -1
- package/src/bootstrap.ts +2 -2
- package/src/tui-data.ts +1 -1
- package/tui.tsx +277 -377
package/package.json
CHANGED
package/src/bootstrap.ts
CHANGED
|
@@ -9,8 +9,8 @@ interface BootstrapCallbacks {
|
|
|
9
9
|
onStep: (message: string) => void
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const REQUIRED_PACKAGES = ['marker', 'markitdown', 'mlx_vlm', 'pypdfium2']
|
|
13
|
-
const PIP_PACKAGES = ['marker-pdf', 'markitdown', 'mlx-vlm', 'pypdfium2']
|
|
12
|
+
const REQUIRED_PACKAGES = ['marker', 'markitdown', 'mammoth', 'mlx_vlm', 'pypdfium2', 'torchvision']
|
|
13
|
+
const PIP_PACKAGES = ['marker-pdf', 'markitdown[docx]', 'mlx-vlm', 'pypdfium2', 'torchvision']
|
|
14
14
|
const CHANDRA_MODEL_ID = 'mlx-community/chandra-8bit'
|
|
15
15
|
|
|
16
16
|
const checkImportable = async (py: string, pkg: string): Promise<boolean> => {
|
package/src/tui-data.ts
CHANGED
|
@@ -223,7 +223,7 @@ const runClassify = async (onProgress: (p: ClassifyProgress) => void): Promise<C
|
|
|
223
223
|
total: results.length
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
mkdirSync(getPaths().
|
|
226
|
+
mkdirSync(getPaths().outputDir, { recursive: true })
|
|
227
227
|
await writeFile(getPaths().classification, `${JSON.stringify(classification, null, 2)}\n`)
|
|
228
228
|
return classification
|
|
229
229
|
}
|
package/tui.tsx
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from '~/tui-data'
|
|
27
27
|
|
|
28
28
|
const DIM = '#888888'
|
|
29
|
+
const SIDEBAR_WIDTH = 38
|
|
29
30
|
|
|
30
31
|
const setTerminalTitle = (title: string): void => {
|
|
31
32
|
process.stdout.write(`\u001B]0;${title}\u0007`)
|
|
@@ -195,10 +196,10 @@ const SpinnerDots = ({ fg }: { fg?: string }) => {
|
|
|
195
196
|
return <text fg={fg ?? 'yellow'}>{SPINNER_FRAMES[frame]} </text>
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
const PROGRESS_FULL = '
|
|
199
|
-
const PROGRESS_EMPTY = '
|
|
199
|
+
const PROGRESS_FULL = '█'
|
|
200
|
+
const PROGRESS_EMPTY = '░'
|
|
200
201
|
|
|
201
|
-
const ProgressBarSimple = ({ value, width =
|
|
202
|
+
const ProgressBarSimple = ({ value, width = 12 }: { value: number; width?: number }) => {
|
|
202
203
|
const filled = Math.round((value / 100) * width)
|
|
203
204
|
const empty = width - filled
|
|
204
205
|
return (
|
|
@@ -227,20 +228,19 @@ const ElapsedTimer = ({ startedAt }: { startedAt: number }) => {
|
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
const RecentFiles = ({ files }: { files: { duration: number; name: string; pages: number; per_page: number }[] }) => {
|
|
230
|
-
const display = files.slice(-
|
|
231
|
+
const display = files.slice(-3)
|
|
231
232
|
return (
|
|
232
233
|
<box flexDirection='column'>
|
|
233
234
|
<text fg={DIM}>
|
|
234
|
-
<b
|
|
235
|
+
<b>── recent ──</b>
|
|
235
236
|
</text>
|
|
236
237
|
{display.map((f, i) => (
|
|
237
238
|
// eslint-disable-next-line react/no-array-index-key
|
|
238
239
|
<text key={i}>
|
|
239
|
-
<span fg='green'
|
|
240
|
-
<span fg='cyan'>{f.name.slice(0,
|
|
241
|
-
<span> {f.pages.toString().padStart(
|
|
242
|
-
<span fg='yellow'>{formatDuration(f.duration)
|
|
243
|
-
<span fg={DIM}> ({formatDuration(f.per_page)}/p)</span>
|
|
240
|
+
<span fg='green'>✓</span>
|
|
241
|
+
<span fg='cyan'> {f.name.slice(0, 18)}</span>
|
|
242
|
+
<span> {f.pages.toString().padStart(2)}p</span>
|
|
243
|
+
<span fg='yellow'> {formatDuration(f.duration)}</span>
|
|
244
244
|
</text>
|
|
245
245
|
))}
|
|
246
246
|
</box>
|
|
@@ -265,41 +265,41 @@ const OcrLiveInfo = ({ progress }: { progress: OcrProgress }) => {
|
|
|
265
265
|
}, [progress.current_file, progress.current_file_started])
|
|
266
266
|
|
|
267
267
|
return (
|
|
268
|
-
<box flexDirection='column' paddingLeft={
|
|
268
|
+
<box flexDirection='column' paddingLeft={2}>
|
|
269
269
|
{progress.current_file === '-' ? null : (
|
|
270
270
|
<text>
|
|
271
|
-
<span fg={DIM}>
|
|
272
|
-
<b fg='cyan'>{progress.current_file}</b>
|
|
273
|
-
{progress.current_pages_total ? <span fg={DIM}> ({progress.current_pages_total}p)</span> : null}
|
|
274
|
-
<span fg={DIM}> page </span>
|
|
275
|
-
<span>{progress.current_page}</span>
|
|
276
|
-
<span fg={DIM}>{' '}elapsed: </span>
|
|
277
|
-
<span fg='yellow'>{formatDuration(elapsed)}</span>
|
|
271
|
+
<span fg={DIM}>Now: </span>
|
|
272
|
+
<b fg='cyan'>{progress.current_file.slice(0, 20)}</b>
|
|
278
273
|
</text>
|
|
279
274
|
)}
|
|
280
|
-
|
|
281
|
-
<text>
|
|
282
|
-
<span fg={DIM}>ETA: </span>
|
|
283
|
-
<span fg='yellow'>{progress.eta}</span>
|
|
284
|
-
</text>
|
|
285
|
-
<text>
|
|
286
|
-
<span fg={DIM}>avg: </span>
|
|
287
|
-
<span>{progress.avg_per_file}/file</span>
|
|
288
|
-
</text>
|
|
275
|
+
{progress.current_file === '-' ? null : (
|
|
289
276
|
<text>
|
|
290
|
-
<span fg={DIM}>
|
|
291
|
-
<span
|
|
277
|
+
<span fg={DIM}>p</span>
|
|
278
|
+
<span>{progress.current_page}</span>
|
|
279
|
+
{progress.current_pages_total ? <span fg={DIM}>/{progress.current_pages_total}</span> : null}
|
|
280
|
+
<span fg={DIM}> </span>
|
|
281
|
+
<span fg='yellow'>{formatDuration(elapsed)}</span>
|
|
292
282
|
</text>
|
|
293
|
-
|
|
283
|
+
)}
|
|
284
|
+
<text>
|
|
285
|
+
<span fg={DIM}>ETA </span>
|
|
286
|
+
<span fg='yellow'>{progress.eta}</span>
|
|
287
|
+
<span fg={DIM}> avg </span>
|
|
288
|
+
<span>{progress.avg_per_file}</span>
|
|
289
|
+
</text>
|
|
290
|
+
<text>
|
|
291
|
+
<span fg={DIM}>err </span>
|
|
292
|
+
<span fg={progress.errors > 0 ? 'red' : 'green'}>{progress.errors}</span>
|
|
293
|
+
</text>
|
|
294
294
|
{progress.recent_files?.length ? <RecentFiles files={progress.recent_files} /> : null}
|
|
295
295
|
</box>
|
|
296
296
|
)
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
const getStepIcon = (isDone: boolean, isFailed: boolean): string => {
|
|
300
|
-
if (isFailed) return '
|
|
301
|
-
if (isDone) return '
|
|
302
|
-
return '
|
|
300
|
+
if (isFailed) return '✗'
|
|
301
|
+
if (isDone) return '✓'
|
|
302
|
+
return '·'
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
const getStepColor = (isRunning: boolean, isDone: boolean, isFailed: boolean): string => {
|
|
@@ -328,100 +328,7 @@ const StepIcon = ({
|
|
|
328
328
|
)
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
const
|
|
332
|
-
color,
|
|
333
|
-
completedDuration,
|
|
334
|
-
done,
|
|
335
|
-
failedCount,
|
|
336
|
-
isActive,
|
|
337
|
-
isDone,
|
|
338
|
-
isRunning,
|
|
339
|
-
pct,
|
|
340
|
-
step,
|
|
341
|
-
stepStartedAt,
|
|
342
|
-
total
|
|
343
|
-
}: {
|
|
344
|
-
color: string
|
|
345
|
-
completedDuration?: number
|
|
346
|
-
done: number
|
|
347
|
-
failedCount?: number
|
|
348
|
-
isActive: boolean
|
|
349
|
-
isDone: boolean
|
|
350
|
-
isRunning: boolean
|
|
351
|
-
pct: number
|
|
352
|
-
step: StepConfig
|
|
353
|
-
stepStartedAt: number
|
|
354
|
-
total: number
|
|
355
|
-
}) => {
|
|
356
|
-
const fgColor = isActive ? (color === 'gray' ? undefined : color) : DIM
|
|
357
|
-
return (
|
|
358
|
-
<box gap={1}>
|
|
359
|
-
<box width={3}>
|
|
360
|
-
<StepIcon color={color} isDone={isDone} isFailed={false} isRunning={isRunning} />
|
|
361
|
-
</box>
|
|
362
|
-
<text fg={color}>
|
|
363
|
-
<b>[{step.stepNum}]</b>
|
|
364
|
-
</text>
|
|
365
|
-
<text fg={fgColor}>{isActive ? <b>{step.name}</b> : step.name}</text>
|
|
366
|
-
{isRunning ? <ElapsedTimer startedAt={stepStartedAt} /> : null}
|
|
367
|
-
{isDone && !isRunning && completedDuration !== undefined ? (
|
|
368
|
-
<text fg={DIM}>{formatDuration(completedDuration)}</text>
|
|
369
|
-
) : null}
|
|
370
|
-
<box width={20}>
|
|
371
|
-
<ProgressBarSimple value={pct} />
|
|
372
|
-
</box>
|
|
373
|
-
<text>
|
|
374
|
-
<span fg='green'>{done}</span>
|
|
375
|
-
<span fg={DIM}>
|
|
376
|
-
/{total > 0 ? total : '?'} {step.unit}
|
|
377
|
-
</span>
|
|
378
|
-
{failedCount && failedCount > 0 ? <span fg='red'> ({failedCount} failed)</span> : null}
|
|
379
|
-
</text>
|
|
380
|
-
</box>
|
|
381
|
-
)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const StepDetails = ({
|
|
385
|
-
isRunning,
|
|
386
|
-
ocrProgress,
|
|
387
|
-
requires,
|
|
388
|
-
runningStatus,
|
|
389
|
-
showOcr,
|
|
390
|
-
texts
|
|
391
|
-
}: {
|
|
392
|
-
isRunning: boolean
|
|
393
|
-
ocrProgress?: null | OcrProgress
|
|
394
|
-
requires?: string
|
|
395
|
-
runningStatus: string
|
|
396
|
-
showOcr: boolean
|
|
397
|
-
texts: string[]
|
|
398
|
-
}) => (
|
|
399
|
-
<>
|
|
400
|
-
{texts.length > 0 ? (
|
|
401
|
-
<box paddingLeft={6}>
|
|
402
|
-
{texts.map((d, i) => (
|
|
403
|
-
// eslint-disable-next-line react/no-array-index-key
|
|
404
|
-
<text fg={DIM} key={i}>
|
|
405
|
-
{d}
|
|
406
|
-
</text>
|
|
407
|
-
))}
|
|
408
|
-
</box>
|
|
409
|
-
) : null}
|
|
410
|
-
{isRunning && runningStatus !== '' ? (
|
|
411
|
-
<box paddingLeft={6}>
|
|
412
|
-
<text fg='cyan'>{runningStatus}</text>
|
|
413
|
-
</box>
|
|
414
|
-
) : null}
|
|
415
|
-
{requires ? (
|
|
416
|
-
<box paddingLeft={6}>
|
|
417
|
-
<text fg='yellow'>{`\u26A0 Requires: ${requires}`}</text>
|
|
418
|
-
</box>
|
|
419
|
-
) : null}
|
|
420
|
-
{showOcr && ocrProgress ? <OcrLiveInfo progress={ocrProgress} /> : null}
|
|
421
|
-
</>
|
|
422
|
-
)
|
|
423
|
-
|
|
424
|
-
const StepCard = ({
|
|
331
|
+
const SidebarStep = ({
|
|
425
332
|
completedDuration,
|
|
426
333
|
failedCount,
|
|
427
334
|
isFailed,
|
|
@@ -441,6 +348,7 @@ const StepCard = ({
|
|
|
441
348
|
step: StepConfig
|
|
442
349
|
stepData: StepData | undefined
|
|
443
350
|
stepStartedAt: number
|
|
351
|
+
// eslint-disable-next-line complexity, max-statements
|
|
444
352
|
}) => {
|
|
445
353
|
const done = stepData?.done ?? 0
|
|
446
354
|
const total = stepData?.total ?? 0
|
|
@@ -452,67 +360,52 @@ const StepCard = ({
|
|
|
452
360
|
const showOcr = isRunning && step.command === 'ocr'
|
|
453
361
|
|
|
454
362
|
return (
|
|
455
|
-
<box flexDirection='column'
|
|
456
|
-
<
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
363
|
+
<box flexDirection='column'>
|
|
364
|
+
<box>
|
|
365
|
+
<box width={2}>
|
|
366
|
+
<StepIcon color={color} isDone={isDone} isFailed={false} isRunning={isRunning} />
|
|
367
|
+
</box>
|
|
368
|
+
<text fg={isActive ? color : DIM}>{step.name}</text>
|
|
369
|
+
{isRunning ? <ElapsedTimer startedAt={stepStartedAt} /> : null}
|
|
370
|
+
{isDone && !isRunning && completedDuration !== undefined ? (
|
|
371
|
+
<text fg={DIM}> {formatDuration(completedDuration)}</text>
|
|
372
|
+
) : null}
|
|
373
|
+
</box>
|
|
374
|
+
<box paddingLeft={2}>
|
|
375
|
+
<ProgressBarSimple value={pct} />
|
|
376
|
+
<text>
|
|
377
|
+
<span fg='green'> {done}</span>
|
|
378
|
+
<span fg={DIM}>/{total > 0 ? total : '?'}</span>
|
|
379
|
+
{failedCount && failedCount > 0 ? <span fg='red'> {failedCount}✗</span> : null}
|
|
380
|
+
</text>
|
|
381
|
+
</box>
|
|
382
|
+
{stepData?.details?.length ? (
|
|
383
|
+
<box flexDirection='column' paddingLeft={2}>
|
|
384
|
+
{stepData.details.map((d, i) => (
|
|
385
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
386
|
+
<text fg={DIM} key={i}>
|
|
387
|
+
{d}
|
|
388
|
+
</text>
|
|
389
|
+
))}
|
|
390
|
+
</box>
|
|
391
|
+
) : null}
|
|
392
|
+
{isRunning && runningStatus !== '' ? (
|
|
393
|
+
<box paddingLeft={2}>
|
|
394
|
+
<text fg='cyan'>{runningStatus}</text>
|
|
395
|
+
</box>
|
|
396
|
+
) : null}
|
|
397
|
+
{showRequires ? (
|
|
398
|
+
<box paddingLeft={2}>
|
|
399
|
+
<text fg='yellow'>⚠ {stepData?.requires}</text>
|
|
400
|
+
</box>
|
|
401
|
+
) : null}
|
|
402
|
+
{showOcr && ocrProgress ? <OcrLiveInfo progress={ocrProgress} /> : null}
|
|
477
403
|
</box>
|
|
478
404
|
)
|
|
479
405
|
}
|
|
480
406
|
|
|
481
407
|
const ERROR_PATTERN = /\b(?:ERROR|Error:|Failed:|failed|FAILED|\u2716|exception|traceback)/iu
|
|
482
408
|
|
|
483
|
-
const OutputBox = ({ lines, status }: { lines: string[]; status: string }) => {
|
|
484
|
-
if (lines.length === 0 && status === '') return null
|
|
485
|
-
return (
|
|
486
|
-
<box
|
|
487
|
-
border
|
|
488
|
-
borderColor='gray'
|
|
489
|
-
borderStyle='rounded'
|
|
490
|
-
flexDirection='column'
|
|
491
|
-
marginTop={1}
|
|
492
|
-
paddingLeft={1}
|
|
493
|
-
paddingRight={1}>
|
|
494
|
-
<text fg={DIM}>
|
|
495
|
-
<b>Output</b>
|
|
496
|
-
</text>
|
|
497
|
-
<box flexDirection='column'>
|
|
498
|
-
{lines.length > 0 ? (
|
|
499
|
-
lines.map((line, i) => {
|
|
500
|
-
const isError = ERROR_PATTERN.test(line)
|
|
501
|
-
return (
|
|
502
|
-
// eslint-disable-next-line react/no-array-index-key
|
|
503
|
-
<text fg={isError ? 'red' : DIM} key={i}>
|
|
504
|
-
{line}
|
|
505
|
-
</text>
|
|
506
|
-
)
|
|
507
|
-
})
|
|
508
|
-
) : (
|
|
509
|
-
<text fg='cyan'>{status}</text>
|
|
510
|
-
)}
|
|
511
|
-
</box>
|
|
512
|
-
</box>
|
|
513
|
-
)
|
|
514
|
-
}
|
|
515
|
-
|
|
516
409
|
const PreflightBanner = ({ errors, warnings }: { errors: string[]; warnings: string[] }) => {
|
|
517
410
|
if (errors.length === 0 && warnings.length === 0) return null
|
|
518
411
|
return (
|
|
@@ -555,98 +448,20 @@ const PreflightBanner = ({ errors, warnings }: { errors: string[]; warnings: str
|
|
|
555
448
|
)
|
|
556
449
|
}
|
|
557
450
|
|
|
558
|
-
const LogOverlay = ({ lines }: { lines: string[] }) => {
|
|
559
|
-
const { height } = useTerminalDimensions()
|
|
560
|
-
const logHeight = Math.max(5, height - 18)
|
|
561
|
-
|
|
562
|
-
return (
|
|
563
|
-
<box
|
|
564
|
-
border
|
|
565
|
-
borderColor='magenta'
|
|
566
|
-
borderStyle='rounded'
|
|
567
|
-
flexDirection='column'
|
|
568
|
-
marginTop={1}
|
|
569
|
-
paddingLeft={1}
|
|
570
|
-
paddingRight={1}>
|
|
571
|
-
<box justifyContent='space-between'>
|
|
572
|
-
<text fg='magenta'>
|
|
573
|
-
<b>Log</b>
|
|
574
|
-
</text>
|
|
575
|
-
<text fg={DIM}>L/ESC close · ↑↓ scroll</text>
|
|
576
|
-
</box>
|
|
577
|
-
<scrollbox focused height={logHeight} marginTop={1} stickyScroll>
|
|
578
|
-
{lines.length > 0 ? (
|
|
579
|
-
lines.map((line, i) => (
|
|
580
|
-
// eslint-disable-next-line react/no-array-index-key
|
|
581
|
-
<text fg={DIM} key={i}>
|
|
582
|
-
{line}
|
|
583
|
-
</text>
|
|
584
|
-
))
|
|
585
|
-
) : (
|
|
586
|
-
<text fg={DIM}>No log entries yet</text>
|
|
587
|
-
)}
|
|
588
|
-
</scrollbox>
|
|
589
|
-
</box>
|
|
590
|
-
)
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const TitleBar = ({ allDone, failed }: { allDone: boolean; failed: boolean }) => (
|
|
594
|
-
<box justifyContent='space-between' paddingLeft={1} paddingRight={1}>
|
|
595
|
-
<text fg='cyan'>
|
|
596
|
-
<b>Document Pipeline</b>
|
|
597
|
-
</text>
|
|
598
|
-
<box gap={2}>
|
|
599
|
-
{failed ? (
|
|
600
|
-
<>
|
|
601
|
-
<text>
|
|
602
|
-
<b fg='yellow'>[R]</b>
|
|
603
|
-
<span fg={DIM}> Retry</span>
|
|
604
|
-
</text>
|
|
605
|
-
<text>
|
|
606
|
-
<b fg='yellow'>[S]</b>
|
|
607
|
-
<span fg={DIM}> Skip</span>
|
|
608
|
-
</text>
|
|
609
|
-
</>
|
|
610
|
-
) : null}
|
|
611
|
-
{allDone ? (
|
|
612
|
-
<text fg='green'>
|
|
613
|
-
<b>✓ Pipeline complete</b>
|
|
614
|
-
</text>
|
|
615
|
-
) : null}
|
|
616
|
-
<text>
|
|
617
|
-
<b fg='magenta'>[L]</b>
|
|
618
|
-
<span fg={DIM}> Log</span>
|
|
619
|
-
</text>
|
|
620
|
-
<text>
|
|
621
|
-
<b fg='red'>[Q]</b>
|
|
622
|
-
<span fg={DIM}> Quit</span>
|
|
623
|
-
</text>
|
|
624
|
-
</box>
|
|
625
|
-
</box>
|
|
626
|
-
)
|
|
627
|
-
|
|
628
|
-
const formatChars = (chars: number): string => {
|
|
629
|
-
if (chars >= 1_000_000) return `${(chars / 1_000_000).toFixed(1)}M`
|
|
630
|
-
if (chars >= 1000) return `${(chars / 1000).toFixed(1)}K`
|
|
631
|
-
return String(chars)
|
|
632
|
-
}
|
|
633
|
-
|
|
634
451
|
// eslint-disable-next-line max-statements
|
|
635
|
-
const
|
|
452
|
+
const TitleBarTop = ({
|
|
636
453
|
allDone,
|
|
637
454
|
backgroundOcr,
|
|
638
|
-
|
|
455
|
+
failed,
|
|
639
456
|
pipelineStartedAt,
|
|
640
457
|
runningCommand,
|
|
641
|
-
stepDurations,
|
|
642
458
|
stepsData
|
|
643
459
|
}: {
|
|
644
460
|
allDone: boolean
|
|
645
461
|
backgroundOcr: boolean
|
|
646
|
-
|
|
462
|
+
failed: boolean
|
|
647
463
|
pipelineStartedAt: number
|
|
648
464
|
runningCommand: CommandKey | null
|
|
649
|
-
stepDurations: Partial<Record<CommandKey, number>>
|
|
650
465
|
stepsData: AllStepsData | null
|
|
651
466
|
}) => {
|
|
652
467
|
const [elapsed, setElapsed] = useState(0)
|
|
@@ -661,83 +476,148 @@ const PipelineSummary = ({
|
|
|
661
476
|
return () => clearInterval(timer)
|
|
662
477
|
}, [pipelineStartedAt])
|
|
663
478
|
|
|
664
|
-
|
|
665
|
-
|
|
479
|
+
let statusText = ''
|
|
480
|
+
if (allDone) statusText = '✓ Complete'
|
|
481
|
+
else if (failed) statusText = '✗ Failed'
|
|
482
|
+
else if (runningCommand && stepsData) {
|
|
483
|
+
let currentIdx = 0
|
|
484
|
+
let currentName = ''
|
|
485
|
+
for (const s of STEPS)
|
|
486
|
+
if (s.command === runningCommand) {
|
|
487
|
+
currentIdx = s.stepNum
|
|
488
|
+
currentName = s.name
|
|
489
|
+
break
|
|
490
|
+
}
|
|
491
|
+
if (currentIdx > 0) {
|
|
492
|
+
const parallelLabel = backgroundOcr && runningCommand === 'pipeline' ? '2+3/5 Convert+OCR' : null
|
|
493
|
+
statusText = parallelLabel ?? `${currentIdx}/5 ${currentName}`
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const statusColor = allDone ? 'green' : failed ? 'red' : 'cyan'
|
|
666
498
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
<
|
|
670
|
-
<
|
|
671
|
-
|
|
672
|
-
|
|
499
|
+
return (
|
|
500
|
+
<box justifyContent='space-between' paddingLeft={1} paddingRight={1}>
|
|
501
|
+
<text fg='#e0a040'>
|
|
502
|
+
<b>anymd</b>
|
|
503
|
+
</text>
|
|
504
|
+
<box gap={2}>
|
|
505
|
+
{statusText === '' ? null : (
|
|
506
|
+
<text fg={statusColor}>
|
|
507
|
+
<b>{statusText}</b>
|
|
673
508
|
</text>
|
|
674
|
-
|
|
675
|
-
</
|
|
676
|
-
|
|
677
|
-
|
|
509
|
+
)}
|
|
510
|
+
{pipelineStartedAt > 0 ? <text fg={DIM}>{formatDuration(elapsed)}</text> : null}
|
|
511
|
+
</box>
|
|
512
|
+
</box>
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const formatChars = (chars: number): string => {
|
|
517
|
+
if (chars >= 1_000_000) return `${(chars / 1_000_000).toFixed(1)}M`
|
|
518
|
+
if (chars >= 1000) return `${(chars / 1000).toFixed(1)}K`
|
|
519
|
+
return String(chars)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const SidebarSummary = ({
|
|
523
|
+
allDone,
|
|
524
|
+
datasetResult,
|
|
525
|
+
stepDurations
|
|
526
|
+
}: {
|
|
527
|
+
allDone: boolean
|
|
528
|
+
datasetResult: DatasetResult | null
|
|
529
|
+
stepDurations: Partial<Record<CommandKey, number>>
|
|
530
|
+
}) => {
|
|
531
|
+
if (!allDone) return null
|
|
532
|
+
return (
|
|
533
|
+
<box flexDirection='column' paddingLeft={1} paddingTop={1}>
|
|
534
|
+
<text fg='green'>
|
|
535
|
+
<b>✓ All 5 steps complete</b>
|
|
536
|
+
</text>
|
|
537
|
+
{datasetResult ? (
|
|
538
|
+
<box flexDirection='column' paddingLeft={1}>
|
|
539
|
+
<text>
|
|
540
|
+
<span fg={DIM}>entries </span>
|
|
541
|
+
<b fg='green'>{datasetResult.entries.toLocaleString()}</b>
|
|
542
|
+
</text>
|
|
543
|
+
<text>
|
|
544
|
+
<span fg={DIM}>chars </span>
|
|
545
|
+
<b>{formatChars(datasetResult.totalChars)}</b>
|
|
546
|
+
</text>
|
|
547
|
+
{datasetResult.skipped > 0 ? (
|
|
678
548
|
<text>
|
|
679
|
-
<span fg={DIM}>
|
|
680
|
-
<
|
|
549
|
+
<span fg={DIM}>skipped </span>
|
|
550
|
+
<span fg='yellow'>{datasetResult.skipped}</span>
|
|
681
551
|
</text>
|
|
552
|
+
) : null}
|
|
553
|
+
{datasetResult.duplicates > 0 ? (
|
|
682
554
|
<text>
|
|
683
|
-
<span fg={DIM}>
|
|
684
|
-
<
|
|
555
|
+
<span fg={DIM}>deduped </span>
|
|
556
|
+
<span fg='yellow'>{datasetResult.duplicates}</span>
|
|
685
557
|
</text>
|
|
686
|
-
|
|
687
|
-
<text>
|
|
688
|
-
<span fg={DIM}>skipped: </span>
|
|
689
|
-
<span fg='yellow'>{datasetResult.skipped}</span>
|
|
690
|
-
</text>
|
|
691
|
-
) : null}
|
|
692
|
-
{datasetResult.duplicates > 0 ? (
|
|
693
|
-
<text>
|
|
694
|
-
<span fg={DIM}>deduped: </span>
|
|
695
|
-
<span fg='yellow'>{datasetResult.duplicates}</span>
|
|
696
|
-
</text>
|
|
697
|
-
) : null}
|
|
698
|
-
</box>
|
|
699
|
-
) : null}
|
|
700
|
-
<box gap={2} paddingLeft={2}>
|
|
701
|
-
{STEPS.map(step => {
|
|
702
|
-
const dur = stepDurations[step.command]
|
|
703
|
-
if (dur === undefined) return null
|
|
704
|
-
return (
|
|
705
|
-
<text key={step.command}>
|
|
706
|
-
<span fg={DIM}>{step.name}: </span>
|
|
707
|
-
<span fg='cyan'>{formatDuration(dur)}</span>
|
|
708
|
-
</text>
|
|
709
|
-
)
|
|
710
|
-
})}
|
|
558
|
+
) : null}
|
|
711
559
|
</box>
|
|
560
|
+
) : null}
|
|
561
|
+
<box flexDirection='column' paddingLeft={1}>
|
|
562
|
+
{STEPS.map(step => {
|
|
563
|
+
const dur = stepDurations[step.command]
|
|
564
|
+
if (dur === undefined) return null
|
|
565
|
+
return (
|
|
566
|
+
<text key={step.command}>
|
|
567
|
+
<span fg={DIM}>{step.name.slice(0, 14).padEnd(14)} </span>
|
|
568
|
+
<span fg='cyan'>{formatDuration(dur)}</span>
|
|
569
|
+
</text>
|
|
570
|
+
)
|
|
571
|
+
})}
|
|
712
572
|
</box>
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
let currentName = ''
|
|
717
|
-
for (const s of STEPS)
|
|
718
|
-
if (s.command === runningCommand) {
|
|
719
|
-
currentIdx = s.stepNum
|
|
720
|
-
currentName = s.name
|
|
721
|
-
break
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
if (currentIdx === 0) return null
|
|
573
|
+
</box>
|
|
574
|
+
)
|
|
575
|
+
}
|
|
725
576
|
|
|
726
|
-
|
|
577
|
+
const HelpDialog = ({ height, width }: { height: number; width: number }) => {
|
|
578
|
+
const boxW = 40
|
|
579
|
+
const boxH = 12
|
|
580
|
+
const left = Math.max(0, Math.floor((width - boxW) / 2))
|
|
581
|
+
const top = Math.max(0, Math.floor((height - boxH) / 2))
|
|
727
582
|
|
|
728
583
|
return (
|
|
729
|
-
<box
|
|
584
|
+
<box
|
|
585
|
+
border
|
|
586
|
+
borderColor='#e0a040'
|
|
587
|
+
borderStyle='rounded'
|
|
588
|
+
flexDirection='column'
|
|
589
|
+
height={boxH}
|
|
590
|
+
marginLeft={left}
|
|
591
|
+
marginTop={top}
|
|
592
|
+
paddingLeft={2}
|
|
593
|
+
paddingRight={2}
|
|
594
|
+
width={boxW}>
|
|
595
|
+
<text fg='#e0a040'>
|
|
596
|
+
<b>Keybinds</b>
|
|
597
|
+
</text>
|
|
598
|
+
<text> </text>
|
|
730
599
|
<text>
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
600
|
+
<b fg='cyan'>Q</b>
|
|
601
|
+
<span fg={DIM}>{' '}Quit</span>
|
|
602
|
+
</text>
|
|
603
|
+
<text>
|
|
604
|
+
<b fg='cyan'>L</b>
|
|
605
|
+
<span fg={DIM}>{' '}Toggle log / output view</span>
|
|
606
|
+
</text>
|
|
607
|
+
<text>
|
|
608
|
+
<b fg='cyan'>R</b>
|
|
609
|
+
<span fg={DIM}>{' '}Retry failed step</span>
|
|
610
|
+
</text>
|
|
611
|
+
<text>
|
|
612
|
+
<b fg='cyan'>S</b>
|
|
613
|
+
<span fg={DIM}>{' '}Skip failed step</span>
|
|
614
|
+
</text>
|
|
615
|
+
<text>
|
|
616
|
+
<b fg='cyan'>?</b>
|
|
617
|
+
<span fg={DIM}>{' '}Toggle this help</span>
|
|
739
618
|
</text>
|
|
740
|
-
<text
|
|
619
|
+
<text> </text>
|
|
620
|
+
<text fg={DIM}>Press ? or Esc to close</text>
|
|
741
621
|
</box>
|
|
742
622
|
)
|
|
743
623
|
}
|
|
@@ -757,13 +637,6 @@ const computeTerminalTitle = (s: AppState): string => {
|
|
|
757
637
|
return `Doc Pipeline \u2014 ${label}`
|
|
758
638
|
}
|
|
759
639
|
|
|
760
|
-
const RunningFooter = () => (
|
|
761
|
-
<box justifyContent='space-between' marginTop={1} paddingLeft={1} paddingRight={1}>
|
|
762
|
-
<text fg={DIM}>Ctrl+C safe — progress saved, re-run to resume</text>
|
|
763
|
-
<text fg={DIM}>↻ auto-refresh 2s</text>
|
|
764
|
-
</box>
|
|
765
|
-
)
|
|
766
|
-
|
|
767
640
|
// eslint-disable-next-line max-statements
|
|
768
641
|
const readStream = async (stream: ReadableStream<Uint8Array>, onLine: (line: string) => void): Promise<void> => {
|
|
769
642
|
const reader = stream.getReader()
|
|
@@ -792,6 +665,8 @@ const App = () => {
|
|
|
792
665
|
const ocrProcRef = useRef<null | ReturnType<typeof Bun.spawn>>(null)
|
|
793
666
|
const busyRef = useRef(false)
|
|
794
667
|
const errorLogClearedRef = useRef(false)
|
|
668
|
+
const [showHelp, setShowHelp] = useState(false)
|
|
669
|
+
const { height, width } = useTerminalDimensions()
|
|
795
670
|
|
|
796
671
|
useEffect(() => {
|
|
797
672
|
setTerminalTitle(computeTerminalTitle(state))
|
|
@@ -1093,9 +968,13 @@ const App = () => {
|
|
|
1093
968
|
|
|
1094
969
|
// eslint-disable-next-line complexity, max-statements
|
|
1095
970
|
useKeyboard(key => {
|
|
1096
|
-
if (
|
|
1097
|
-
if (key.name === '
|
|
971
|
+
if (showHelp) {
|
|
972
|
+
if (key.name === '?' || key.name === 'escape') setShowHelp(false)
|
|
973
|
+
return
|
|
974
|
+
}
|
|
1098
975
|
|
|
976
|
+
if (key.name === '?') {
|
|
977
|
+
setShowHelp(true)
|
|
1099
978
|
return
|
|
1100
979
|
}
|
|
1101
980
|
|
|
@@ -1129,55 +1008,76 @@ const App = () => {
|
|
|
1129
1008
|
}
|
|
1130
1009
|
})
|
|
1131
1010
|
|
|
1011
|
+
const logHeight = Math.max(5, height - 3)
|
|
1012
|
+
const displayLines = state.showLog ? state.logLines : state.runningLines
|
|
1013
|
+
|
|
1132
1014
|
return (
|
|
1133
|
-
<box flexDirection='column'>
|
|
1134
|
-
<
|
|
1135
|
-
<PipelineSummary
|
|
1015
|
+
<box flexDirection='column' height={height}>
|
|
1016
|
+
<TitleBarTop
|
|
1136
1017
|
allDone={state.allDone}
|
|
1137
1018
|
backgroundOcr={state.backgroundOcr}
|
|
1138
|
-
|
|
1019
|
+
failed={state.failed}
|
|
1139
1020
|
pipelineStartedAt={state.pipelineStartedAt}
|
|
1140
1021
|
runningCommand={state.runningCommand}
|
|
1141
|
-
stepDurations={state.stepDurations}
|
|
1142
1022
|
stepsData={state.stepsData}
|
|
1143
1023
|
/>
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1024
|
+
<box flexGrow={1}>
|
|
1025
|
+
<box flexDirection='column' paddingLeft={1} width={SIDEBAR_WIDTH}>
|
|
1026
|
+
{state.stepsData ? (
|
|
1027
|
+
<box flexDirection='column'>
|
|
1028
|
+
{STEPS.map(step => {
|
|
1029
|
+
const isFg = state.runningCommand === step.command
|
|
1030
|
+
const isBgOcr = step.command === 'ocr' && state.backgroundOcr
|
|
1031
|
+
const isActive = isFg || isBgOcr
|
|
1032
|
+
const sd = state.stepsData ? state.stepsData[step.command] : undefined
|
|
1033
|
+
const failures = sd?.failed ?? state.stepFailures[step.command]
|
|
1034
|
+
return (
|
|
1035
|
+
<SidebarStep
|
|
1036
|
+
completedDuration={state.stepDurations[step.command]}
|
|
1037
|
+
failedCount={failures}
|
|
1038
|
+
isFailed={state.failed ? isFg : false}
|
|
1039
|
+
isRunning={!state.failed && isActive}
|
|
1040
|
+
key={step.command}
|
|
1041
|
+
ocrProgress={step.command === 'ocr' ? state.stepsData?.ocr.progress : undefined}
|
|
1042
|
+
runningStatus={isFg ? state.runningStatus : ''}
|
|
1043
|
+
step={step}
|
|
1044
|
+
stepData={sd}
|
|
1045
|
+
stepStartedAt={isActive ? state.stepStartedAt : 0}
|
|
1046
|
+
/>
|
|
1047
|
+
)
|
|
1048
|
+
})}
|
|
1049
|
+
</box>
|
|
1050
|
+
) : (
|
|
1051
|
+
<text fg={DIM}>Loading...</text>
|
|
1052
|
+
)}
|
|
1053
|
+
<SidebarSummary
|
|
1054
|
+
allDone={state.allDone}
|
|
1055
|
+
datasetResult={state.datasetResult}
|
|
1056
|
+
stepDurations={state.stepDurations}
|
|
1057
|
+
/>
|
|
1168
1058
|
</box>
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1059
|
+
<box flexDirection='column' flexGrow={1} paddingLeft={2}>
|
|
1060
|
+
{state.preflightErrors.length > 0 || state.preflightWarnings.length > 0 ? (
|
|
1061
|
+
<PreflightBanner errors={state.preflightErrors} warnings={state.preflightWarnings} />
|
|
1062
|
+
) : null}
|
|
1063
|
+
<scrollbox focused height={logHeight} paddingLeft={1} stickyScroll>
|
|
1064
|
+
{displayLines.length > 0 ? (
|
|
1065
|
+
displayLines.map((line, i) => {
|
|
1066
|
+
const isError = ERROR_PATTERN.test(line)
|
|
1067
|
+
return (
|
|
1068
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
1069
|
+
<text fg={isError ? 'red' : DIM} key={i}>
|
|
1070
|
+
{line}
|
|
1071
|
+
</text>
|
|
1072
|
+
)
|
|
1073
|
+
})
|
|
1074
|
+
) : (
|
|
1075
|
+
<text fg={DIM}>Waiting for output...</text>
|
|
1076
|
+
)}
|
|
1077
|
+
</scrollbox>
|
|
1078
|
+
</box>
|
|
1079
|
+
</box>
|
|
1080
|
+
{showHelp ? <HelpDialog height={height} width={width} /> : null}
|
|
1181
1081
|
</box>
|
|
1182
1082
|
)
|
|
1183
1083
|
}
|