@joewinke/jatui 0.2.20 → 0.2.22
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.
|
@@ -375,13 +375,13 @@
|
|
|
375
375
|
transition: background 0.15s, border-color 0.15s;
|
|
376
376
|
min-height: 2rem;
|
|
377
377
|
cursor: pointer;
|
|
378
|
-
background: oklch(0.16 0.01 250);
|
|
379
|
-
border: 1px solid oklch(0.
|
|
380
|
-
color: oklch(0.85 0.02 250);
|
|
378
|
+
background: var(--color-base-200, oklch(0.16 0.01 250));
|
|
379
|
+
border: 1px solid color-mix(in oklch, var(--color-base-content, oklch(0.85 0.02 250)) 30%, transparent);
|
|
380
|
+
color: var(--color-base-content, oklch(0.85 0.02 250));
|
|
381
381
|
}
|
|
382
382
|
.sd-trigger:hover:not(:disabled) {
|
|
383
|
-
background: oklch(0.
|
|
384
|
-
border-color: oklch(0.
|
|
383
|
+
background: color-mix(in oklch, var(--color-base-content, oklch(0.85 0.02 250)) 8%, var(--color-base-200, oklch(0.16 0.01 250)));
|
|
384
|
+
border-color: color-mix(in oklch, var(--color-base-content, oklch(0.85 0.02 250)) 45%, transparent);
|
|
385
385
|
}
|
|
386
386
|
.sd-trigger:disabled, .sd-disabled {
|
|
387
387
|
opacity: 0.5;
|
|
@@ -394,9 +394,6 @@
|
|
|
394
394
|
min-height: 2rem;
|
|
395
395
|
padding: 0 0.75rem;
|
|
396
396
|
border-radius: var(--rounded-btn, 0.5rem);
|
|
397
|
-
background: var(--color-base-200, oklch(0.16 0.01 250));
|
|
398
|
-
border: 1px solid color-mix(in oklch, var(--color-base-content, oklch(0.85 0.02 250)) 30%, transparent);
|
|
399
|
-
color: var(--color-base-content, oklch(0.85 0.02 250));
|
|
400
397
|
}
|
|
401
398
|
.sd-sm .sd-chevron {
|
|
402
399
|
width: 0.625rem;
|
|
@@ -440,15 +437,15 @@
|
|
|
440
437
|
text-transform: uppercase;
|
|
441
438
|
letter-spacing: 0.025em;
|
|
442
439
|
background: transparent;
|
|
443
|
-
border: 1px solid color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 35%, transparent);
|
|
444
|
-
color: color-mix(in oklch, var(--sd-color, oklch(0.7 0 0)) 85%, oklch(0.80 0 0));
|
|
440
|
+
border: 1px solid color-mix(in oklch, var(--sd-color, var(--color-base-content, oklch(0.5 0 0))) 35%, transparent);
|
|
441
|
+
color: color-mix(in oklch, var(--sd-color, var(--color-base-content, oklch(0.7 0 0))) 85%, var(--color-base-content, oklch(0.80 0 0)));
|
|
445
442
|
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s, color 0.15s;
|
|
446
443
|
}
|
|
447
444
|
.sd-chip:hover:not(:disabled) {
|
|
448
|
-
background: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 15%, transparent);
|
|
449
|
-
border-color: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 50%, transparent);
|
|
450
|
-
color: var(--sd-color, oklch(0.80 0 0));
|
|
451
|
-
box-shadow: 0 0 6px color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 15%, transparent);
|
|
445
|
+
background: color-mix(in oklch, var(--sd-color, var(--color-base-content, oklch(0.5 0 0))) 15%, transparent);
|
|
446
|
+
border-color: color-mix(in oklch, var(--sd-color, var(--color-base-content, oklch(0.5 0 0))) 50%, transparent);
|
|
447
|
+
color: var(--sd-color, var(--color-base-content, oklch(0.80 0 0)));
|
|
448
|
+
box-shadow: 0 0 6px color-mix(in oklch, var(--sd-color, var(--color-base-content, oklch(0.5 0 0))) 15%, transparent);
|
|
452
449
|
}
|
|
453
450
|
|
|
454
451
|
.sd-trigger-label {
|
|
@@ -488,12 +485,12 @@
|
|
|
488
485
|
background: none;
|
|
489
486
|
border: none;
|
|
490
487
|
cursor: pointer;
|
|
491
|
-
color: oklch(0.50 0.02 250);
|
|
488
|
+
color: color-mix(in oklch, var(--color-base-content, oklch(0.50 0.02 250)) 55%, transparent);
|
|
492
489
|
transition: color 0.1s, background 0.1s;
|
|
493
490
|
}
|
|
494
491
|
.sd-clear:hover {
|
|
495
|
-
color: oklch(0.75 0.15 30);
|
|
496
|
-
background: oklch(0.75 0.15 30
|
|
492
|
+
color: var(--color-error, oklch(0.75 0.15 30));
|
|
493
|
+
background: color-mix(in oklch, var(--color-error, oklch(0.75 0.15 30)) 12%, transparent);
|
|
497
494
|
}
|
|
498
495
|
|
|
499
496
|
.sd-chevron {
|
|
@@ -501,7 +498,7 @@
|
|
|
501
498
|
height: 0.75rem;
|
|
502
499
|
flex-shrink: 0;
|
|
503
500
|
transition: transform 0.15s;
|
|
504
|
-
color: oklch(0.50 0.02 250);
|
|
501
|
+
color: color-mix(in oklch, var(--color-base-content, oklch(0.50 0.02 250)) 55%, transparent);
|
|
505
502
|
}
|
|
506
503
|
.sd-chevron-open {
|
|
507
504
|
transform: rotate(180deg);
|
|
@@ -513,7 +510,7 @@
|
|
|
513
510
|
}
|
|
514
511
|
.sd-embedded .sd-search {
|
|
515
512
|
background: transparent;
|
|
516
|
-
border-bottom: 1px solid oklch(0.22 0.02 250);
|
|
513
|
+
border-bottom: 1px solid color-mix(in oklch, var(--color-base-content, oklch(0.22 0.02 250)) 25%, transparent);
|
|
517
514
|
}
|
|
518
515
|
.sd-embedded .sd-list {
|
|
519
516
|
max-height: none;
|
|
@@ -530,8 +527,8 @@
|
|
|
530
527
|
border-radius: 0.5rem;
|
|
531
528
|
overflow: hidden;
|
|
532
529
|
box-shadow: 0 4px 24px oklch(0 0 0 / 0.4);
|
|
533
|
-
background: oklch(0.16 0.01 250);
|
|
534
|
-
border: 1px solid oklch(0.
|
|
530
|
+
background: var(--color-base-200, oklch(0.16 0.01 250));
|
|
531
|
+
border: 1px solid color-mix(in oklch, var(--color-base-content, oklch(0.85 0.02 250)) 30%, transparent);
|
|
535
532
|
}
|
|
536
533
|
|
|
537
534
|
.sd-panel-dropup {
|
|
@@ -544,21 +541,21 @@
|
|
|
544
541
|
|
|
545
542
|
/* Header section */
|
|
546
543
|
.sd-header {
|
|
547
|
-
border-bottom: 1px solid oklch(0.25 0.02 250);
|
|
544
|
+
border-bottom: 1px solid color-mix(in oklch, var(--color-base-content, oklch(0.25 0.02 250)) 25%, transparent);
|
|
548
545
|
}
|
|
549
546
|
|
|
550
547
|
/* Search section */
|
|
551
548
|
.sd-search {
|
|
552
549
|
padding: 0.375rem 0.5rem;
|
|
553
|
-
border-bottom: 1px solid oklch(0.25 0.02 250);
|
|
554
|
-
background: oklch(0.13 0.01 250);
|
|
550
|
+
border-bottom: 1px solid color-mix(in oklch, var(--color-base-content, oklch(0.25 0.02 250)) 25%, transparent);
|
|
551
|
+
background: var(--color-base-300, oklch(0.13 0.01 250));
|
|
555
552
|
}
|
|
556
553
|
.sd-search-inner {
|
|
557
554
|
display: flex;
|
|
558
555
|
align-items: center;
|
|
559
556
|
gap: 0.375rem;
|
|
560
|
-
background: oklch(0.19 0.01 250);
|
|
561
|
-
border: 1px solid oklch(0.28 0.02 250);
|
|
557
|
+
background: color-mix(in oklch, var(--color-base-content, oklch(0.19 0.01 250)) 10%, var(--color-base-200, oklch(0.19 0.01 250)));
|
|
558
|
+
border: 1px solid color-mix(in oklch, var(--color-base-content, oklch(0.28 0.02 250)) 28%, transparent);
|
|
562
559
|
border-radius: 0.25rem;
|
|
563
560
|
padding: 0.2rem 0.4rem;
|
|
564
561
|
}
|
|
@@ -566,22 +563,22 @@
|
|
|
566
563
|
width: 0.75rem;
|
|
567
564
|
height: 0.75rem;
|
|
568
565
|
flex-shrink: 0;
|
|
569
|
-
color: oklch(0.60 0.04 250);
|
|
566
|
+
color: color-mix(in oklch, var(--color-base-content, oklch(0.60 0.04 250)) 60%, transparent);
|
|
570
567
|
}
|
|
571
568
|
.sd-search-input {
|
|
572
569
|
width: 100%;
|
|
573
570
|
background: transparent;
|
|
574
571
|
font-size: 0.6875rem;
|
|
575
572
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
576
|
-
color: oklch(0.85 0.02 250);
|
|
573
|
+
color: var(--color-base-content, oklch(0.85 0.02 250));
|
|
577
574
|
border: none;
|
|
578
575
|
outline: none;
|
|
579
576
|
}
|
|
580
577
|
.sd-search-input::placeholder {
|
|
581
|
-
color: oklch(0.52 0.03 250);
|
|
578
|
+
color: color-mix(in oklch, var(--color-base-content, oklch(0.52 0.03 250)) 40%, transparent);
|
|
582
579
|
}
|
|
583
580
|
.sd-search-clear {
|
|
584
|
-
color: oklch(0.50 0.02 250);
|
|
581
|
+
color: color-mix(in oklch, var(--color-base-content, oklch(0.50 0.02 250)) 55%, transparent);
|
|
585
582
|
background: none;
|
|
586
583
|
border: none;
|
|
587
584
|
cursor: pointer;
|
|
@@ -589,7 +586,7 @@
|
|
|
589
586
|
display: flex;
|
|
590
587
|
}
|
|
591
588
|
.sd-search-clear:hover {
|
|
592
|
-
color: oklch(0.75 0.15 30);
|
|
589
|
+
color: var(--color-error, oklch(0.75 0.15 30));
|
|
593
590
|
}
|
|
594
591
|
|
|
595
592
|
/* Options list */
|
|
@@ -610,7 +607,7 @@
|
|
|
610
607
|
font-weight: 600;
|
|
611
608
|
text-transform: uppercase;
|
|
612
609
|
letter-spacing: 0.05em;
|
|
613
|
-
color: oklch(0.50 0.10 250);
|
|
610
|
+
color: color-mix(in oklch, var(--color-base-content, oklch(0.50 0.10 250)) 55%, transparent);
|
|
614
611
|
}
|
|
615
612
|
|
|
616
613
|
.sd-option {
|
|
@@ -627,22 +624,22 @@
|
|
|
627
624
|
background: transparent;
|
|
628
625
|
border: none;
|
|
629
626
|
border-left: 2px solid transparent;
|
|
630
|
-
color: oklch(0.80 0.02 250);
|
|
627
|
+
color: color-mix(in oklch, var(--color-base-content, oklch(0.80 0.02 250)) 90%, transparent);
|
|
631
628
|
}
|
|
632
629
|
.sd-option:hover {
|
|
633
|
-
background: oklch(0.19 0.01 250);
|
|
630
|
+
background: color-mix(in oklch, var(--color-base-content, oklch(0.19 0.01 250)) 8%, var(--color-base-200, oklch(0.19 0.01 250)));
|
|
634
631
|
}
|
|
635
632
|
.sd-option-selected {
|
|
636
|
-
background: oklch(0.20 0.02 250);
|
|
637
|
-
border-left-color: oklch(0.65 0.15 250);
|
|
633
|
+
background: color-mix(in oklch, var(--color-base-content, oklch(0.20 0.02 250)) 12%, var(--color-base-200, oklch(0.20 0.02 250)));
|
|
634
|
+
border-left-color: var(--color-primary, oklch(0.65 0.15 250));
|
|
638
635
|
}
|
|
639
636
|
.sd-option-highlighted {
|
|
640
|
-
background: oklch(0.22 0.02 250);
|
|
641
|
-
outline: 1px solid oklch(0.45 0.10 250
|
|
637
|
+
background: color-mix(in oklch, var(--color-base-content, oklch(0.22 0.02 250)) 16%, var(--color-base-200, oklch(0.22 0.02 250)));
|
|
638
|
+
outline: 1px solid color-mix(in oklch, var(--color-primary, oklch(0.45 0.10 250)) 50%, transparent);
|
|
642
639
|
outline-offset: -1px;
|
|
643
640
|
}
|
|
644
641
|
.sd-option-highlighted.sd-option-selected {
|
|
645
|
-
background: oklch(0.24 0.03 250);
|
|
642
|
+
background: color-mix(in oklch, var(--color-base-content, oklch(0.24 0.03 250)) 20%, var(--color-base-200, oklch(0.24 0.03 250)));
|
|
646
643
|
}
|
|
647
644
|
|
|
648
645
|
/* Chip variant — options render as inline chip pills */
|
|
@@ -651,7 +648,7 @@
|
|
|
651
648
|
padding: 0.25rem 0.625rem;
|
|
652
649
|
}
|
|
653
650
|
.sd-chip-wrap .sd-option-selected {
|
|
654
|
-
background: oklch(0.19 0.01 250);
|
|
651
|
+
background: color-mix(in oklch, var(--color-base-content, oklch(0.19 0.01 250)) 8%, var(--color-base-200, oklch(0.19 0.01 250)));
|
|
655
652
|
border-left: none;
|
|
656
653
|
}
|
|
657
654
|
.sd-option-chip {
|
|
@@ -663,20 +660,20 @@
|
|
|
663
660
|
font-weight: 600;
|
|
664
661
|
text-transform: uppercase;
|
|
665
662
|
letter-spacing: 0.025em;
|
|
666
|
-
border: 1px solid color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 35%, transparent);
|
|
667
|
-
color: color-mix(in oklch, var(--sd-color, oklch(0.7 0 0)) 85%, oklch(0.80 0 0));
|
|
663
|
+
border: 1px solid color-mix(in oklch, var(--sd-color, var(--color-base-content, oklch(0.5 0 0))) 35%, transparent);
|
|
664
|
+
color: color-mix(in oklch, var(--sd-color, var(--color-base-content, oklch(0.7 0 0))) 85%, var(--color-base-content, oklch(0.80 0 0)));
|
|
668
665
|
background: transparent;
|
|
669
666
|
transition: background 0.1s, border-color 0.1s, color 0.1s;
|
|
670
667
|
}
|
|
671
668
|
.sd-chip-wrap .sd-option:hover .sd-option-chip {
|
|
672
|
-
background: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 12%, transparent);
|
|
673
|
-
border-color: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 50%, transparent);
|
|
674
|
-
color: var(--sd-color, oklch(0.80 0 0));
|
|
669
|
+
background: color-mix(in oklch, var(--sd-color, var(--color-base-content, oklch(0.5 0 0))) 12%, transparent);
|
|
670
|
+
border-color: color-mix(in oklch, var(--sd-color, var(--color-base-content, oklch(0.5 0 0))) 50%, transparent);
|
|
671
|
+
color: var(--sd-color, var(--color-base-content, oklch(0.80 0 0)));
|
|
675
672
|
}
|
|
676
673
|
.sd-chip-wrap .sd-option-selected .sd-option-chip {
|
|
677
|
-
background: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 15%, transparent);
|
|
678
|
-
border-color: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 50%, transparent);
|
|
679
|
-
color: var(--sd-color, oklch(0.80 0 0));
|
|
674
|
+
background: color-mix(in oklch, var(--sd-color, var(--color-base-content, oklch(0.5 0 0))) 15%, transparent);
|
|
675
|
+
border-color: color-mix(in oklch, var(--sd-color, var(--color-base-content, oklch(0.5 0 0))) 50%, transparent);
|
|
676
|
+
color: var(--sd-color, var(--color-base-content, oklch(0.80 0 0)));
|
|
680
677
|
}
|
|
681
678
|
|
|
682
679
|
.sd-option-icon {
|
|
@@ -691,11 +688,11 @@
|
|
|
691
688
|
height: 0.75rem;
|
|
692
689
|
flex-shrink: 0;
|
|
693
690
|
margin-left: auto;
|
|
694
|
-
color: oklch(0.70 0.15 145);
|
|
691
|
+
color: var(--color-success, oklch(0.70 0.15 145));
|
|
695
692
|
}
|
|
696
693
|
|
|
697
694
|
.sd-footer {
|
|
698
|
-
border-top: 1px solid oklch(0.22 0.02 250);
|
|
695
|
+
border-top: 1px solid color-mix(in oklch, var(--color-base-content, oklch(0.22 0.02 250)) 25%, transparent);
|
|
699
696
|
}
|
|
700
697
|
|
|
701
698
|
.sd-empty {
|
|
@@ -703,6 +700,6 @@
|
|
|
703
700
|
text-align: center;
|
|
704
701
|
font-size: 0.625rem;
|
|
705
702
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
706
|
-
color: oklch(0.45 0.02 250);
|
|
703
|
+
color: color-mix(in oklch, var(--color-base-content, oklch(0.45 0.02 250)) 45%, transparent);
|
|
707
704
|
}
|
|
708
705
|
</style>
|
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* TranscriptPlayer — synced video + transcript viewer for post-meeting
|
|
4
|
+
* recordings (LiveKit meeting recordings with diarized/speaker-aligned
|
|
5
|
+
* transcripts).
|
|
6
|
+
*
|
|
7
|
+
* Video playback drives a scrollable transcript panel (click a line to
|
|
8
|
+
* seek, active line auto-highlights + auto-scrolls), a chapter/bookmark
|
|
9
|
+
* marker strip over the scrub range, an optional solo-speaker playback
|
|
10
|
+
* mode, and a per-speaker talk-time bar.
|
|
11
|
+
*
|
|
12
|
+
* Built for the mardukos meeting-recordings admin viewer (mos-omq6g.23).
|
|
13
|
+
* Presentational only — no fetching, no LiveKit dependency. The consuming
|
|
14
|
+
* app supplies a (possibly presigned/expiring) video URL plus pre-computed
|
|
15
|
+
* transcript/chapter/bookmark/talk-time data.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface TranscriptSegment {
|
|
19
|
+
id: string;
|
|
20
|
+
startSec: number;
|
|
21
|
+
endSec: number;
|
|
22
|
+
speakerIdentity: string | null; // null = couldn't be resolved to a real participant
|
|
23
|
+
speakerName: string | null; // display name, null if unresolved
|
|
24
|
+
text: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TranscriptBookmark {
|
|
28
|
+
playbackSec: number; // seconds into the recording
|
|
29
|
+
participantName: string | null;
|
|
30
|
+
label: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TranscriptChapter {
|
|
34
|
+
startSec: number;
|
|
35
|
+
title: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TranscriptSpeakerTalkTime {
|
|
39
|
+
speakerIdentity: string | null;
|
|
40
|
+
speakerName: string | null;
|
|
41
|
+
seconds: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Small distinct oklch palette, cycled per speaker (mirrors ChapterTimeline's SIGNAL_COLORS approach). */
|
|
45
|
+
const TALK_TIME_PALETTE = [
|
|
46
|
+
'oklch(0.70 0.16 250)', // blue
|
|
47
|
+
'oklch(0.68 0.18 145)', // green
|
|
48
|
+
'oklch(0.78 0.16 85)', // amber
|
|
49
|
+
'oklch(0.68 0.19 25)', // red
|
|
50
|
+
'oklch(0.68 0.16 300)', // violet
|
|
51
|
+
'oklch(0.72 0.14 200)', // cyan
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const ALL_KEY = '__all__';
|
|
55
|
+
const UNKNOWN_KEY = '__unknown__';
|
|
56
|
+
|
|
57
|
+
let {
|
|
58
|
+
src,
|
|
59
|
+
segments,
|
|
60
|
+
chapters = [],
|
|
61
|
+
bookmarks = [],
|
|
62
|
+
talkTime = [],
|
|
63
|
+
onAddBookmark = (_playbackSec: number, _label: string) => {},
|
|
64
|
+
}: {
|
|
65
|
+
src: string;
|
|
66
|
+
segments: TranscriptSegment[];
|
|
67
|
+
chapters?: TranscriptChapter[];
|
|
68
|
+
bookmarks?: TranscriptBookmark[];
|
|
69
|
+
talkTime?: TranscriptSpeakerTalkTime[];
|
|
70
|
+
onAddBookmark?: (playbackSec: number, label: string) => void;
|
|
71
|
+
} = $props();
|
|
72
|
+
|
|
73
|
+
let videoEl: HTMLVideoElement | undefined = $state();
|
|
74
|
+
let currentTime = $state(0);
|
|
75
|
+
let duration = $state(0);
|
|
76
|
+
let paused = $state(true);
|
|
77
|
+
|
|
78
|
+
let soloSpeakerKey = $state(ALL_KEY);
|
|
79
|
+
let bookmarkLabel = $state('');
|
|
80
|
+
|
|
81
|
+
// Row refs for auto-scroll-into-view. Plain (non-reactive) map — refs are
|
|
82
|
+
// populated on mount, read from inside the $effect below on active-row change.
|
|
83
|
+
let rowRefs: Record<string, HTMLButtonElement> = {};
|
|
84
|
+
|
|
85
|
+
function speakerKey(seg: TranscriptSegment): string {
|
|
86
|
+
return seg.speakerIdentity ?? UNKNOWN_KEY;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const effectiveDuration = $derived(Number.isFinite(duration) && duration > 0 ? duration : 0);
|
|
90
|
+
const useHours = $derived(effectiveDuration >= 3600 || currentTime >= 3600);
|
|
91
|
+
const playedPct = $derived(Math.min(100, Math.max(0, (currentTime / (effectiveDuration || 1)) * 100)));
|
|
92
|
+
|
|
93
|
+
const activeSegment = $derived(
|
|
94
|
+
segments.find((s) => currentTime >= s.startSec && currentTime < s.endSec) ?? null
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Chapters are assumed sorted ascending by startSec (same convention as segments).
|
|
98
|
+
const activeChapterIndex = $derived.by(() => {
|
|
99
|
+
let idx = -1;
|
|
100
|
+
for (let i = 0; i < chapters.length; i++) {
|
|
101
|
+
if (chapters[i].startSec <= currentTime) idx = i;
|
|
102
|
+
else break;
|
|
103
|
+
}
|
|
104
|
+
return idx;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const speakerOptions = $derived.by(() => {
|
|
108
|
+
const seen = new Map<string, string>();
|
|
109
|
+
for (const seg of segments) {
|
|
110
|
+
const key = speakerKey(seg);
|
|
111
|
+
if (!seen.has(key)) seen.set(key, seg.speakerName ?? 'Unknown speaker');
|
|
112
|
+
}
|
|
113
|
+
return Array.from(seen.entries()).map(([key, label]) => ({ key, label }));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const effectiveTalkTime = $derived.by(() => {
|
|
117
|
+
if (talkTime.length > 0) return talkTime;
|
|
118
|
+
const totals = new Map<string, TranscriptSpeakerTalkTime>();
|
|
119
|
+
for (const seg of segments) {
|
|
120
|
+
const key = speakerKey(seg);
|
|
121
|
+
const dur = Math.max(0, seg.endSec - seg.startSec);
|
|
122
|
+
const existing = totals.get(key);
|
|
123
|
+
if (existing) existing.seconds += dur;
|
|
124
|
+
else totals.set(key, { speakerIdentity: seg.speakerIdentity, speakerName: seg.speakerName, seconds: dur });
|
|
125
|
+
}
|
|
126
|
+
return Array.from(totals.values()).sort((a, b) => b.seconds - a.seconds);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const talkTimeTotal = $derived(effectiveTalkTime.reduce((s, t) => s + t.seconds, 0) || 1);
|
|
130
|
+
|
|
131
|
+
function paletteColor(i: number): string {
|
|
132
|
+
return TALK_TIME_PALETTE[i % TALK_TIME_PALETTE.length];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatTime(totalSeconds: number, withHours: boolean): string {
|
|
136
|
+
const t = Number.isFinite(totalSeconds) && totalSeconds > 0 ? totalSeconds : 0;
|
|
137
|
+
const s = Math.floor(t % 60);
|
|
138
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
139
|
+
if (withHours) {
|
|
140
|
+
const h = Math.floor(t / 3600);
|
|
141
|
+
const m = Math.floor((t % 3600) / 60);
|
|
142
|
+
return `${h}:${pad(m)}:${pad(s)}`;
|
|
143
|
+
}
|
|
144
|
+
const m = Math.floor(t / 60);
|
|
145
|
+
return `${m}:${pad(s)}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function formatDuration(totalSeconds: number): string {
|
|
149
|
+
const s = Math.round(Math.max(0, totalSeconds));
|
|
150
|
+
const h = Math.floor(s / 3600);
|
|
151
|
+
const m = Math.floor((s % 3600) / 60);
|
|
152
|
+
const sec = s % 60;
|
|
153
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
154
|
+
if (m > 0) return `${m}m ${sec}s`;
|
|
155
|
+
return `${sec}s`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function seekTo(sec: number) {
|
|
159
|
+
currentTime = sec; // two-way bind:currentTime pushes this to the <video> element
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Solo-speaker playback: on every timeupdate, if we're not inside a segment
|
|
164
|
+
* belonging to the selected speaker, jump forward to that speaker's next
|
|
165
|
+
* segment (segments are sorted ascending by startSec, so this is a simple
|
|
166
|
+
* forward scan). If there's no later segment for them, we pause playback
|
|
167
|
+
* rather than let it run on into someone else's talk time unchecked.
|
|
168
|
+
*/
|
|
169
|
+
function handleSoloTimeUpdate() {
|
|
170
|
+
if (soloSpeakerKey === ALL_KEY || !videoEl) return;
|
|
171
|
+
const t = videoEl.currentTime;
|
|
172
|
+
const inSelected = segments.some((s) => speakerKey(s) === soloSpeakerKey && t >= s.startSec && t < s.endSec);
|
|
173
|
+
if (inSelected) return;
|
|
174
|
+
const next = segments.find((s) => speakerKey(s) === soloSpeakerKey && s.startSec > t);
|
|
175
|
+
if (next) {
|
|
176
|
+
videoEl.currentTime = next.startSec;
|
|
177
|
+
} else {
|
|
178
|
+
videoEl.pause();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Re-run the solo scan immediately when the selection changes (don't wait
|
|
183
|
+
// for the next native timeupdate tick to react to the new speaker choice).
|
|
184
|
+
$effect(() => {
|
|
185
|
+
soloSpeakerKey;
|
|
186
|
+
if (videoEl) handleSoloTimeUpdate();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Auto-scroll the active transcript row into view as playback advances.
|
|
190
|
+
$effect(() => {
|
|
191
|
+
const seg = activeSegment;
|
|
192
|
+
if (!seg) return;
|
|
193
|
+
rowRefs[seg.id]?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
function submitBookmark() {
|
|
197
|
+
onAddBookmark(currentTime, bookmarkLabel.trim());
|
|
198
|
+
bookmarkLabel = '';
|
|
199
|
+
}
|
|
200
|
+
</script>
|
|
201
|
+
|
|
202
|
+
<div class="tp-root">
|
|
203
|
+
<video
|
|
204
|
+
bind:this={videoEl}
|
|
205
|
+
bind:currentTime
|
|
206
|
+
bind:duration
|
|
207
|
+
bind:paused
|
|
208
|
+
ontimeupdate={handleSoloTimeUpdate}
|
|
209
|
+
{src}
|
|
210
|
+
controls
|
|
211
|
+
class="tp-video"
|
|
212
|
+
>
|
|
213
|
+
<!-- Captions track present for a11y_media_has_caption compliance; consumer
|
|
214
|
+
apps that have caption files can pass a src via a wrapping component. -->
|
|
215
|
+
<track kind="captions" />
|
|
216
|
+
</video>
|
|
217
|
+
|
|
218
|
+
<div class="tp-timeline">
|
|
219
|
+
<div class="tp-rail">
|
|
220
|
+
<div class="tp-rail-fill" style="width: {playedPct}%"></div>
|
|
221
|
+
</div>
|
|
222
|
+
{#each chapters as chapter, i (chapter.startSec + ':' + i)}
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
class="tp-marker tp-chapter-marker"
|
|
226
|
+
class:active={i === activeChapterIndex}
|
|
227
|
+
style="left: {(chapter.startSec / (effectiveDuration || 1)) * 100}%"
|
|
228
|
+
title={chapter.title}
|
|
229
|
+
onclick={() => seekTo(chapter.startSec)}
|
|
230
|
+
>
|
|
231
|
+
<span class="tp-chapter-flag"></span>
|
|
232
|
+
</button>
|
|
233
|
+
{/each}
|
|
234
|
+
{#each bookmarks as bookmark, i (bookmark.playbackSec + ':' + i)}
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
class="tp-marker tp-bookmark-marker"
|
|
238
|
+
style="left: {(bookmark.playbackSec / (effectiveDuration || 1)) * 100}%"
|
|
239
|
+
title={bookmark.label || (bookmark.participantName ? `${bookmark.participantName}'s bookmark` : 'Bookmark')}
|
|
240
|
+
onclick={() => seekTo(bookmark.playbackSec)}
|
|
241
|
+
>
|
|
242
|
+
<span class="tp-bookmark-dot"></span>
|
|
243
|
+
</button>
|
|
244
|
+
{/each}
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div class="tp-toolbar">
|
|
248
|
+
<div class="tp-solo">
|
|
249
|
+
<label for="tp-solo-select">Solo speaker</label>
|
|
250
|
+
<select id="tp-solo-select" bind:value={soloSpeakerKey}>
|
|
251
|
+
<option value={ALL_KEY}>All speakers</option>
|
|
252
|
+
{#each speakerOptions as opt (opt.key)}
|
|
253
|
+
<option value={opt.key}>{opt.label}</option>
|
|
254
|
+
{/each}
|
|
255
|
+
</select>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div class="tp-bookmark-add">
|
|
259
|
+
<label for="tp-bookmark-label-input">Bookmark label</label>
|
|
260
|
+
<input
|
|
261
|
+
id="tp-bookmark-label-input"
|
|
262
|
+
type="text"
|
|
263
|
+
placeholder="Bookmark label (optional)"
|
|
264
|
+
bind:value={bookmarkLabel}
|
|
265
|
+
/>
|
|
266
|
+
<button type="button" class="tp-add-bookmark-btn" onclick={submitBookmark}>
|
|
267
|
+
+ Bookmark at {formatTime(currentTime, useHours)}
|
|
268
|
+
</button>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{#if effectiveTalkTime.length > 0}
|
|
273
|
+
<div class="tp-talktime">
|
|
274
|
+
<div class="tp-talktime-bar">
|
|
275
|
+
{#each effectiveTalkTime as tt, i (tt.speakerIdentity ?? 'unknown-' + i)}
|
|
276
|
+
<div
|
|
277
|
+
class="tp-talktime-seg"
|
|
278
|
+
style="width: {(tt.seconds / talkTimeTotal) * 100}%; background: {paletteColor(i)}"
|
|
279
|
+
title="{tt.speakerName ?? 'Unknown speaker'} — {formatDuration(tt.seconds)}"
|
|
280
|
+
></div>
|
|
281
|
+
{/each}
|
|
282
|
+
</div>
|
|
283
|
+
<div class="tp-talktime-legend">
|
|
284
|
+
{#each effectiveTalkTime as tt, i (tt.speakerIdentity ?? 'unknown-' + i)}
|
|
285
|
+
<span class="tp-legend-item">
|
|
286
|
+
<span class="tp-legend-dot" style="background: {paletteColor(i)}"></span>
|
|
287
|
+
{tt.speakerName ?? 'Unknown speaker'} — {formatDuration(tt.seconds)}
|
|
288
|
+
</span>
|
|
289
|
+
{/each}
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
{/if}
|
|
293
|
+
|
|
294
|
+
<div class="tp-transcript">
|
|
295
|
+
{#if segments.length === 0}
|
|
296
|
+
<div class="tp-empty">No transcript available for this recording.</div>
|
|
297
|
+
{:else}
|
|
298
|
+
{#each segments as seg (seg.id)}
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
class="tp-row"
|
|
302
|
+
class:active={activeSegment?.id === seg.id}
|
|
303
|
+
bind:this={rowRefs[seg.id]}
|
|
304
|
+
onclick={() => seekTo(seg.startSec)}
|
|
305
|
+
>
|
|
306
|
+
<span class="tp-row-time">[{formatTime(seg.startSec, useHours)}]</span>
|
|
307
|
+
<span class="tp-row-speaker">{seg.speakerName ?? 'Unknown speaker'}:</span>
|
|
308
|
+
<span class="tp-row-text">{seg.text}</span>
|
|
309
|
+
</button>
|
|
310
|
+
{/each}
|
|
311
|
+
{/if}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<style>
|
|
316
|
+
.tp-root {
|
|
317
|
+
display: flex;
|
|
318
|
+
flex-direction: column;
|
|
319
|
+
gap: 0.6rem;
|
|
320
|
+
background: oklch(0.14 0.01 250);
|
|
321
|
+
border: 1px solid oklch(0.22 0.02 250);
|
|
322
|
+
border-radius: 0.6rem;
|
|
323
|
+
padding: 0.75rem;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.tp-video {
|
|
327
|
+
width: 100%;
|
|
328
|
+
border-radius: 0.4rem;
|
|
329
|
+
background: oklch(0.08 0 0);
|
|
330
|
+
display: block;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* Timeline — chapter + bookmark marker strip */
|
|
334
|
+
.tp-timeline {
|
|
335
|
+
position: relative;
|
|
336
|
+
height: 1.75rem;
|
|
337
|
+
margin: 0 0.25rem;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.tp-rail {
|
|
341
|
+
position: absolute;
|
|
342
|
+
top: 50%;
|
|
343
|
+
left: 0;
|
|
344
|
+
right: 0;
|
|
345
|
+
height: 0.35rem;
|
|
346
|
+
transform: translateY(-50%);
|
|
347
|
+
background: oklch(0.20 0.02 250);
|
|
348
|
+
border: 1px solid oklch(0.26 0.02 250);
|
|
349
|
+
border-radius: 999px;
|
|
350
|
+
overflow: hidden;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.tp-rail-fill {
|
|
354
|
+
height: 100%;
|
|
355
|
+
background: oklch(0.60 0.16 250);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.tp-marker {
|
|
359
|
+
position: absolute;
|
|
360
|
+
top: 0;
|
|
361
|
+
transform: translateX(-50%);
|
|
362
|
+
background: none;
|
|
363
|
+
border: none;
|
|
364
|
+
padding: 0.1rem;
|
|
365
|
+
cursor: pointer;
|
|
366
|
+
display: flex;
|
|
367
|
+
align-items: center;
|
|
368
|
+
justify-content: center;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.tp-chapter-flag {
|
|
372
|
+
display: block;
|
|
373
|
+
width: 0.55rem;
|
|
374
|
+
height: 0.9rem;
|
|
375
|
+
background: oklch(0.72 0.16 200);
|
|
376
|
+
border-radius: 0.15rem 0.15rem 0.15rem 0;
|
|
377
|
+
box-shadow: 0 0 0 1px oklch(0.20 0.02 250);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.tp-chapter-marker:hover .tp-chapter-flag {
|
|
381
|
+
background: oklch(0.80 0.16 200);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.tp-chapter-marker.active .tp-chapter-flag {
|
|
385
|
+
background: oklch(0.85 0.18 200);
|
|
386
|
+
box-shadow: 0 0 8px oklch(0.72 0.16 200 / 0.6);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.tp-bookmark-dot {
|
|
390
|
+
display: block;
|
|
391
|
+
width: 0.6rem;
|
|
392
|
+
height: 0.6rem;
|
|
393
|
+
margin-top: 0.55rem;
|
|
394
|
+
background: oklch(0.78 0.16 85);
|
|
395
|
+
transform: rotate(45deg);
|
|
396
|
+
box-shadow: 0 0 0 1px oklch(0.20 0.02 250);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.tp-bookmark-marker:hover .tp-bookmark-dot {
|
|
400
|
+
background: oklch(0.85 0.18 85);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* Toolbar — solo-speaker select + add-bookmark control */
|
|
404
|
+
.tp-toolbar {
|
|
405
|
+
display: flex;
|
|
406
|
+
flex-wrap: wrap;
|
|
407
|
+
align-items: center;
|
|
408
|
+
gap: 0.75rem;
|
|
409
|
+
font-size: 0.75rem;
|
|
410
|
+
color: oklch(0.75 0.04 250);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.tp-solo,
|
|
414
|
+
.tp-bookmark-add {
|
|
415
|
+
display: flex;
|
|
416
|
+
align-items: center;
|
|
417
|
+
gap: 0.4rem;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.tp-solo label {
|
|
421
|
+
color: oklch(0.60 0.03 250);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.tp-bookmark-add label {
|
|
425
|
+
/* visually hidden, still associated for a11y */
|
|
426
|
+
position: absolute;
|
|
427
|
+
width: 1px;
|
|
428
|
+
height: 1px;
|
|
429
|
+
padding: 0;
|
|
430
|
+
margin: -1px;
|
|
431
|
+
overflow: hidden;
|
|
432
|
+
clip: rect(0, 0, 0, 0);
|
|
433
|
+
white-space: nowrap;
|
|
434
|
+
border: 0;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.tp-solo select,
|
|
438
|
+
.tp-bookmark-add input {
|
|
439
|
+
background: oklch(0.18 0.01 250);
|
|
440
|
+
border: 1px solid oklch(0.26 0.02 250);
|
|
441
|
+
border-radius: 0.35rem;
|
|
442
|
+
color: oklch(0.85 0.02 250);
|
|
443
|
+
padding: 0.3rem 0.5rem;
|
|
444
|
+
font-size: 0.75rem;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.tp-bookmark-add input {
|
|
448
|
+
min-width: 12rem;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.tp-add-bookmark-btn {
|
|
452
|
+
background: oklch(0.22 0.03 250);
|
|
453
|
+
border: 1px solid oklch(0.30 0.03 250);
|
|
454
|
+
border-radius: 0.35rem;
|
|
455
|
+
color: oklch(0.85 0.04 250);
|
|
456
|
+
padding: 0.3rem 0.6rem;
|
|
457
|
+
font-size: 0.75rem;
|
|
458
|
+
cursor: pointer;
|
|
459
|
+
white-space: nowrap;
|
|
460
|
+
transition: background 0.12s ease, border-color 0.12s ease;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.tp-add-bookmark-btn:hover {
|
|
464
|
+
background: color-mix(in oklch, oklch(0.78 0.16 85) 18%, oklch(0.22 0.03 250));
|
|
465
|
+
border-color: oklch(0.78 0.16 85 / 0.5);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* Talk-time bar */
|
|
469
|
+
.tp-talktime {
|
|
470
|
+
display: flex;
|
|
471
|
+
flex-direction: column;
|
|
472
|
+
gap: 0.35rem;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.tp-talktime-bar {
|
|
476
|
+
display: flex;
|
|
477
|
+
height: 0.6rem;
|
|
478
|
+
border-radius: 999px;
|
|
479
|
+
overflow: hidden;
|
|
480
|
+
background: oklch(0.20 0.02 250);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.tp-talktime-seg {
|
|
484
|
+
height: 100%;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.tp-talktime-legend {
|
|
488
|
+
display: flex;
|
|
489
|
+
flex-wrap: wrap;
|
|
490
|
+
gap: 0.6rem;
|
|
491
|
+
font-size: 0.68rem;
|
|
492
|
+
color: oklch(0.68 0.03 250);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.tp-legend-item {
|
|
496
|
+
display: inline-flex;
|
|
497
|
+
align-items: center;
|
|
498
|
+
gap: 0.3rem;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.tp-legend-dot {
|
|
502
|
+
width: 0.5rem;
|
|
503
|
+
height: 0.5rem;
|
|
504
|
+
border-radius: 999px;
|
|
505
|
+
flex-shrink: 0;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/* Transcript panel */
|
|
509
|
+
.tp-transcript {
|
|
510
|
+
display: flex;
|
|
511
|
+
flex-direction: column;
|
|
512
|
+
gap: 0.15rem;
|
|
513
|
+
max-height: 18rem;
|
|
514
|
+
overflow-y: auto;
|
|
515
|
+
background: oklch(0.12 0.01 250);
|
|
516
|
+
border: 1px solid oklch(0.22 0.02 250);
|
|
517
|
+
border-radius: 0.4rem;
|
|
518
|
+
padding: 0.35rem;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.tp-empty {
|
|
522
|
+
font-size: 0.75rem;
|
|
523
|
+
color: oklch(0.55 0.02 250);
|
|
524
|
+
padding: 0.75rem;
|
|
525
|
+
text-align: center;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.tp-row {
|
|
529
|
+
display: flex;
|
|
530
|
+
align-items: baseline;
|
|
531
|
+
flex-wrap: wrap;
|
|
532
|
+
gap: 0.35rem;
|
|
533
|
+
background: none;
|
|
534
|
+
border: none;
|
|
535
|
+
border-radius: 0.3rem;
|
|
536
|
+
padding: 0.3rem 0.45rem;
|
|
537
|
+
text-align: left;
|
|
538
|
+
cursor: pointer;
|
|
539
|
+
transition: background 0.12s ease;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.tp-row:hover {
|
|
543
|
+
background: oklch(0.18 0.02 250);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.tp-row.active {
|
|
547
|
+
background: color-mix(in oklch, oklch(0.70 0.16 250) 16%, oklch(0.16 0.01 250));
|
|
548
|
+
box-shadow: inset 2px 0 0 oklch(0.70 0.16 250);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.tp-row-time {
|
|
552
|
+
font-family: monospace;
|
|
553
|
+
font-size: 0.68rem;
|
|
554
|
+
color: oklch(0.50 0.02 250);
|
|
555
|
+
flex-shrink: 0;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.tp-row-speaker {
|
|
559
|
+
font-size: 0.75rem;
|
|
560
|
+
font-weight: 600;
|
|
561
|
+
color: oklch(0.78 0.06 250);
|
|
562
|
+
flex-shrink: 0;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.tp-row.active .tp-row-speaker {
|
|
566
|
+
color: oklch(0.88 0.08 250);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.tp-row-text {
|
|
570
|
+
font-size: 0.78rem;
|
|
571
|
+
color: oklch(0.80 0.02 250);
|
|
572
|
+
flex: 1;
|
|
573
|
+
min-width: 10rem;
|
|
574
|
+
}
|
|
575
|
+
</style>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TranscriptPlayer — synced video + transcript viewer for post-meeting
|
|
3
|
+
* recordings (LiveKit meeting recordings with diarized/speaker-aligned
|
|
4
|
+
* transcripts).
|
|
5
|
+
*
|
|
6
|
+
* Video playback drives a scrollable transcript panel (click a line to
|
|
7
|
+
* seek, active line auto-highlights + auto-scrolls), a chapter/bookmark
|
|
8
|
+
* marker strip over the scrub range, an optional solo-speaker playback
|
|
9
|
+
* mode, and a per-speaker talk-time bar.
|
|
10
|
+
*
|
|
11
|
+
* Built for the mardukos meeting-recordings admin viewer (mos-omq6g.23).
|
|
12
|
+
* Presentational only — no fetching, no LiveKit dependency. The consuming
|
|
13
|
+
* app supplies a (possibly presigned/expiring) video URL plus pre-computed
|
|
14
|
+
* transcript/chapter/bookmark/talk-time data.
|
|
15
|
+
*/
|
|
16
|
+
export interface TranscriptSegment {
|
|
17
|
+
id: string;
|
|
18
|
+
startSec: number;
|
|
19
|
+
endSec: number;
|
|
20
|
+
speakerIdentity: string | null;
|
|
21
|
+
speakerName: string | null;
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
export interface TranscriptBookmark {
|
|
25
|
+
playbackSec: number;
|
|
26
|
+
participantName: string | null;
|
|
27
|
+
label: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface TranscriptChapter {
|
|
30
|
+
startSec: number;
|
|
31
|
+
title: string;
|
|
32
|
+
}
|
|
33
|
+
export interface TranscriptSpeakerTalkTime {
|
|
34
|
+
speakerIdentity: string | null;
|
|
35
|
+
speakerName: string | null;
|
|
36
|
+
seconds: number;
|
|
37
|
+
}
|
|
38
|
+
type $$ComponentProps = {
|
|
39
|
+
src: string;
|
|
40
|
+
segments: TranscriptSegment[];
|
|
41
|
+
chapters?: TranscriptChapter[];
|
|
42
|
+
bookmarks?: TranscriptBookmark[];
|
|
43
|
+
talkTime?: TranscriptSpeakerTalkTime[];
|
|
44
|
+
onAddBookmark?: (playbackSec: number, label: string) => void;
|
|
45
|
+
};
|
|
46
|
+
declare const TranscriptPlayer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
47
|
+
type TranscriptPlayer = ReturnType<typeof TranscriptPlayer>;
|
|
48
|
+
export default TranscriptPlayer;
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export { default as LinkedColumns } from './components/linked-columns/LinkedColu
|
|
|
9
9
|
export type { ColumnLink } from './components/linked-columns/LinkedColumns.svelte';
|
|
10
10
|
export { default as ChapterTimeline } from './components/replay/ChapterTimeline.svelte';
|
|
11
11
|
export type { ChapterBlock } from './components/replay/ChapterTimeline.svelte';
|
|
12
|
+
export { default as TranscriptPlayer } from './components/TranscriptPlayer.svelte';
|
|
13
|
+
export type { TranscriptSegment, TranscriptBookmark, TranscriptChapter, TranscriptSpeakerTalkTime, } from './components/TranscriptPlayer.svelte';
|
|
12
14
|
export { buildLinkedSession, buildLinkedSessionFromWork, rawTypeToState, sampleLinkedSession, SAMPLE_TRANSCRIPT_LINES, } from './components/session-nav/transcriptModel';
|
|
13
15
|
export type { ItemKind, TranscriptItem, FileTouchedEntry, SignalNode, LinkedSession, WorkBlockClient, WorkTranscriptItemClient, } from './components/session-nav/transcriptModel';
|
|
14
16
|
export { default as UserAvatar } from './components/UserAvatar.svelte';
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { default as Collapse } from './components/Collapse.svelte';
|
|
|
6
6
|
// Components — Session primitives (from JAT IDE, jat-jj79f.16 G1)
|
|
7
7
|
export { default as LinkedColumns } from './components/linked-columns/LinkedColumns.svelte';
|
|
8
8
|
export { default as ChapterTimeline } from './components/replay/ChapterTimeline.svelte';
|
|
9
|
+
export { default as TranscriptPlayer } from './components/TranscriptPlayer.svelte';
|
|
9
10
|
// Data models — Session transcript (from JAT IDE, jat-jj79f.16 G1)
|
|
10
11
|
export { buildLinkedSession, buildLinkedSessionFromWork, rawTypeToState, sampleLinkedSession, SAMPLE_TRANSCRIPT_LINES, } from './components/session-nav/transcriptModel';
|
|
11
12
|
// Components — Universal
|