@medc-com-br/ngx-jaimes-scribe 0.1.8 → 0.1.11
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/README.md +72 -6
- package/fesm2022/medc-com-br-ngx-jaimes-scribe.mjs +566 -312
- package/fesm2022/medc-com-br-ngx-jaimes-scribe.mjs.map +1 -1
- package/lib/components/recorder/recorder.component.d.ts +17 -25
- package/lib/services/playback.service.d.ts +33 -0
- package/lib/services/speaker-identification.service.d.ts +32 -0
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, InjectionToken, inject, NgZone, ElementRef, input, output,
|
|
2
|
+
import { Injectable, InjectionToken, inject, NgZone, signal, ElementRef, input, output, computed, effect, HostListener, Component } from '@angular/core';
|
|
3
3
|
import { BehaviorSubject, Subject } from 'rxjs';
|
|
4
4
|
import { CommonModule } from '@angular/common';
|
|
5
|
+
import { ROLE_LABELS } from '@medc-com-br/jaimes-shared';
|
|
5
6
|
|
|
6
7
|
const DEFAULT_CONFIG$2 = {
|
|
7
8
|
speechThreshold: 8,
|
|
@@ -431,6 +432,158 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
431
432
|
args: [{ providedIn: 'root' }]
|
|
432
433
|
}], ctorParameters: () => [] });
|
|
433
434
|
|
|
435
|
+
const MIN_CHARS_FOR_IDENTIFICATION = 100;
|
|
436
|
+
const MAX_SAMPLE_SIZE = 20;
|
|
437
|
+
const LOW_CONFIDENCE_THRESHOLD = 0.7;
|
|
438
|
+
const IDENTIFICATION_DEBOUNCE_MS = 2000;
|
|
439
|
+
const DEFAULT_SPEAKER_LABELS = {
|
|
440
|
+
0: 'Pessoa 1',
|
|
441
|
+
1: 'Pessoa 2',
|
|
442
|
+
2: 'Pessoa 3',
|
|
443
|
+
3: 'Pessoa 4',
|
|
444
|
+
};
|
|
445
|
+
class SpeakerIdentificationService {
|
|
446
|
+
speakerMapping = signal({});
|
|
447
|
+
identifiedSpeakers = signal(new Set());
|
|
448
|
+
isIdentifying = signal(false);
|
|
449
|
+
editingSpeaker = signal(null);
|
|
450
|
+
debounceTimer = null;
|
|
451
|
+
hasLowConfidence(speaker) {
|
|
452
|
+
const mapping = this.speakerMapping()[speaker.toString()];
|
|
453
|
+
return (mapping?.confidence ?? 1) < LOW_CONFIDENCE_THRESHOLD;
|
|
454
|
+
}
|
|
455
|
+
getLabel(speaker, customLabels, doctorName, patientName, companionName) {
|
|
456
|
+
if (customLabels[speaker]) {
|
|
457
|
+
return customLabels[speaker];
|
|
458
|
+
}
|
|
459
|
+
const mapping = this.speakerMapping()[speaker.toString()];
|
|
460
|
+
if (mapping) {
|
|
461
|
+
switch (mapping.role) {
|
|
462
|
+
case 'doctor':
|
|
463
|
+
return doctorName || ROLE_LABELS.doctor;
|
|
464
|
+
case 'patient':
|
|
465
|
+
return patientName || ROLE_LABELS.patient;
|
|
466
|
+
case 'companion':
|
|
467
|
+
return companionName || ROLE_LABELS.companion;
|
|
468
|
+
default:
|
|
469
|
+
return ROLE_LABELS[mapping.role] || `Pessoa ${speaker + 1}`;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return DEFAULT_SPEAKER_LABELS[speaker] || `Pessoa ${speaker + 1}`;
|
|
473
|
+
}
|
|
474
|
+
getSpeakerStats(entries) {
|
|
475
|
+
const charCount = {};
|
|
476
|
+
const speakers = new Set();
|
|
477
|
+
for (const entry of entries) {
|
|
478
|
+
if (entry.speaker !== undefined && entry.isFinal) {
|
|
479
|
+
speakers.add(entry.speaker);
|
|
480
|
+
charCount[entry.speaker] = (charCount[entry.speaker] || 0) + entry.text.length;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
speakers: Array.from(speakers),
|
|
485
|
+
uniqueSpeakers: speakers.size,
|
|
486
|
+
charCount,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
shouldIdentify(entries) {
|
|
490
|
+
if (this.isIdentifying())
|
|
491
|
+
return false;
|
|
492
|
+
const stats = this.getSpeakerStats(entries);
|
|
493
|
+
const identifiedSet = this.identifiedSpeakers();
|
|
494
|
+
const hasNewSpeaker = stats.speakers.some((s) => !identifiedSet.has(s) && stats.charCount[s] >= MIN_CHARS_FOR_IDENTIFICATION);
|
|
495
|
+
const allSpeakersHaveEnoughContext = stats.speakers.every((s) => stats.charCount[s] >= MIN_CHARS_FOR_IDENTIFICATION);
|
|
496
|
+
return (stats.uniqueSpeakers >= 2 &&
|
|
497
|
+
allSpeakersHaveEnoughContext &&
|
|
498
|
+
(identifiedSet.size === 0 || hasNewSpeaker));
|
|
499
|
+
}
|
|
500
|
+
scheduleIdentification(callback) {
|
|
501
|
+
if (this.debounceTimer) {
|
|
502
|
+
clearTimeout(this.debounceTimer);
|
|
503
|
+
}
|
|
504
|
+
this.debounceTimer = setTimeout(() => {
|
|
505
|
+
callback();
|
|
506
|
+
}, IDENTIFICATION_DEBOUNCE_MS);
|
|
507
|
+
}
|
|
508
|
+
async identify(entries, apiUrl, token, sessionId, premium) {
|
|
509
|
+
if (entries.length < 3 || !apiUrl)
|
|
510
|
+
return;
|
|
511
|
+
this.isIdentifying.set(true);
|
|
512
|
+
try {
|
|
513
|
+
const sample = entries
|
|
514
|
+
.filter((e) => e.isFinal && e.speaker !== undefined)
|
|
515
|
+
.slice(0, MAX_SAMPLE_SIZE)
|
|
516
|
+
.map((e) => ({ speaker: e.speaker, text: e.text }));
|
|
517
|
+
if (sample.length < 2)
|
|
518
|
+
return;
|
|
519
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
520
|
+
if (token) {
|
|
521
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
522
|
+
}
|
|
523
|
+
const response = await fetch(`${apiUrl}/identify-speakers`, {
|
|
524
|
+
method: 'POST',
|
|
525
|
+
headers,
|
|
526
|
+
body: JSON.stringify({
|
|
527
|
+
segments: sample,
|
|
528
|
+
context: { sessionId, premium },
|
|
529
|
+
}),
|
|
530
|
+
});
|
|
531
|
+
if (!response.ok) {
|
|
532
|
+
throw new Error(`HTTP ${response.status}`);
|
|
533
|
+
}
|
|
534
|
+
const data = await response.json();
|
|
535
|
+
this.speakerMapping.set(data.speakerMapping);
|
|
536
|
+
const newIdentified = new Set(this.identifiedSpeakers());
|
|
537
|
+
for (const speakerId of Object.keys(data.speakerMapping)) {
|
|
538
|
+
newIdentified.add(parseInt(speakerId, 10));
|
|
539
|
+
}
|
|
540
|
+
this.identifiedSpeakers.set(newIdentified);
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
console.error('Failed to identify speakers:', err);
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
this.isIdentifying.set(false);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
setRole(speaker, role) {
|
|
550
|
+
const current = this.speakerMapping();
|
|
551
|
+
const updated = {
|
|
552
|
+
...current,
|
|
553
|
+
[speaker.toString()]: {
|
|
554
|
+
role,
|
|
555
|
+
confidence: 1,
|
|
556
|
+
isManualOverride: true,
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
this.speakerMapping.set(updated);
|
|
560
|
+
this.editingSpeaker.set(null);
|
|
561
|
+
return updated;
|
|
562
|
+
}
|
|
563
|
+
toggleMenu(speaker) {
|
|
564
|
+
const current = this.editingSpeaker();
|
|
565
|
+
this.editingSpeaker.set(current === speaker ? null : speaker);
|
|
566
|
+
}
|
|
567
|
+
closeMenu() {
|
|
568
|
+
this.editingSpeaker.set(null);
|
|
569
|
+
}
|
|
570
|
+
reset() {
|
|
571
|
+
this.speakerMapping.set({});
|
|
572
|
+
this.identifiedSpeakers.set(new Set());
|
|
573
|
+
this.isIdentifying.set(false);
|
|
574
|
+
this.editingSpeaker.set(null);
|
|
575
|
+
if (this.debounceTimer) {
|
|
576
|
+
clearTimeout(this.debounceTimer);
|
|
577
|
+
this.debounceTimer = null;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SpeakerIdentificationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
581
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SpeakerIdentificationService });
|
|
582
|
+
}
|
|
583
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SpeakerIdentificationService, decorators: [{
|
|
584
|
+
type: Injectable
|
|
585
|
+
}] });
|
|
586
|
+
|
|
434
587
|
const SAMPLE_RATE = 16000;
|
|
435
588
|
const NUM_CHANNELS = 1;
|
|
436
589
|
const BITS_PER_SAMPLE = 16;
|
|
@@ -478,26 +631,207 @@ function formatTime(seconds) {
|
|
|
478
631
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
479
632
|
}
|
|
480
633
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
634
|
+
class PlaybackService {
|
|
635
|
+
isPlaying = signal(false);
|
|
636
|
+
isLoading = signal(false);
|
|
637
|
+
currentTime = signal(0);
|
|
638
|
+
duration = signal(0);
|
|
639
|
+
playbackRate = signal(1);
|
|
640
|
+
sessionData = signal(null);
|
|
641
|
+
activeSegmentId = signal(null);
|
|
642
|
+
activeWordIndex = signal(null);
|
|
643
|
+
audioElement = null;
|
|
644
|
+
audioBlobUrl = null;
|
|
645
|
+
elementRef = null;
|
|
646
|
+
eventHandlers = {};
|
|
647
|
+
setElementRef(ref) {
|
|
648
|
+
this.elementRef = ref;
|
|
649
|
+
}
|
|
650
|
+
get segments() {
|
|
651
|
+
return this.sessionData()?.transcription.segments ?? [];
|
|
652
|
+
}
|
|
653
|
+
async loadSession(apiUrl, sessionId, token) {
|
|
654
|
+
this.isLoading.set(true);
|
|
655
|
+
try {
|
|
656
|
+
const headers = {};
|
|
657
|
+
if (token) {
|
|
658
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
659
|
+
}
|
|
660
|
+
const response = await fetch(`${apiUrl}/session/${sessionId}`, { headers });
|
|
661
|
+
if (!response.ok) {
|
|
662
|
+
throw new Error(`HTTP ${response.status}`);
|
|
663
|
+
}
|
|
664
|
+
const data = await response.json();
|
|
665
|
+
this.sessionData.set(data);
|
|
666
|
+
await this.loadAudio(data.audioUrl);
|
|
667
|
+
}
|
|
668
|
+
finally {
|
|
669
|
+
this.isLoading.set(false);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async loadAudio(pcmUrl) {
|
|
673
|
+
this.cleanup();
|
|
674
|
+
const response = await fetch(pcmUrl);
|
|
675
|
+
if (!response.ok) {
|
|
676
|
+
throw new Error(`Failed to load audio: HTTP ${response.status}`);
|
|
677
|
+
}
|
|
678
|
+
const pcmData = await response.arrayBuffer();
|
|
679
|
+
const wavData = pcmToWav(pcmData);
|
|
680
|
+
this.audioBlobUrl = createAudioBlobUrl(wavData);
|
|
681
|
+
this.audioElement = new Audio(this.audioBlobUrl);
|
|
682
|
+
this.eventHandlers.loadedmetadata = () => {
|
|
683
|
+
if (this.audioElement) {
|
|
684
|
+
this.duration.set(this.audioElement.duration);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
this.eventHandlers.timeupdate = () => {
|
|
688
|
+
if (this.audioElement) {
|
|
689
|
+
this.currentTime.set(this.audioElement.currentTime);
|
|
690
|
+
this.updateActiveSegment();
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
this.eventHandlers.ended = () => {
|
|
694
|
+
this.isPlaying.set(false);
|
|
695
|
+
};
|
|
696
|
+
this.audioElement.addEventListener('loadedmetadata', this.eventHandlers.loadedmetadata);
|
|
697
|
+
this.audioElement.addEventListener('timeupdate', this.eventHandlers.timeupdate);
|
|
698
|
+
this.audioElement.addEventListener('ended', this.eventHandlers.ended);
|
|
699
|
+
}
|
|
700
|
+
togglePlayback() {
|
|
701
|
+
if (!this.audioElement)
|
|
702
|
+
return;
|
|
703
|
+
if (this.isPlaying()) {
|
|
704
|
+
this.audioElement.pause();
|
|
705
|
+
this.isPlaying.set(false);
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
this.audioElement.play();
|
|
709
|
+
this.isPlaying.set(true);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
seekTo(time) {
|
|
713
|
+
if (this.audioElement && !isNaN(time)) {
|
|
714
|
+
this.audioElement.currentTime = time;
|
|
715
|
+
this.currentTime.set(time);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
seekToSegment(segment) {
|
|
719
|
+
if (this.audioElement) {
|
|
720
|
+
this.audioElement.currentTime = segment.startTime;
|
|
721
|
+
this.currentTime.set(segment.startTime);
|
|
722
|
+
if (!this.isPlaying()) {
|
|
723
|
+
this.togglePlayback();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
seekToWord(word) {
|
|
728
|
+
if (this.audioElement) {
|
|
729
|
+
this.audioElement.currentTime = word.start;
|
|
730
|
+
this.currentTime.set(word.start);
|
|
731
|
+
if (!this.isPlaying()) {
|
|
732
|
+
this.togglePlayback();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
setRate(rate) {
|
|
737
|
+
if (isNaN(rate))
|
|
738
|
+
return;
|
|
739
|
+
this.playbackRate.set(rate);
|
|
740
|
+
if (this.audioElement) {
|
|
741
|
+
this.audioElement.playbackRate = rate;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
formatTime(seconds) {
|
|
745
|
+
return formatTime(seconds);
|
|
746
|
+
}
|
|
747
|
+
updateActiveSegment() {
|
|
748
|
+
const time = this.currentTime();
|
|
749
|
+
const segs = this.segments;
|
|
750
|
+
const seg = segs.find((s) => time >= s.startTime && time <= s.endTime);
|
|
751
|
+
this.activeSegmentId.set(seg?.id ?? null);
|
|
752
|
+
if (seg?.words) {
|
|
753
|
+
const wordIdx = seg.words.findIndex((w) => time >= w.start && time <= w.end);
|
|
754
|
+
this.activeWordIndex.set(wordIdx >= 0 ? wordIdx : null);
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
this.activeWordIndex.set(null);
|
|
758
|
+
}
|
|
759
|
+
this.scrollToActiveSegment();
|
|
760
|
+
}
|
|
761
|
+
scrollToActiveSegment() {
|
|
762
|
+
const activeId = this.activeSegmentId();
|
|
763
|
+
if (!activeId || !this.elementRef)
|
|
764
|
+
return;
|
|
765
|
+
const container = this.elementRef.nativeElement.querySelector('.scribe-transcript-playback');
|
|
766
|
+
const activeEl = container?.querySelector(`[data-segment-id="${activeId}"]`);
|
|
767
|
+
if (activeEl && container) {
|
|
768
|
+
const containerRect = container.getBoundingClientRect();
|
|
769
|
+
const activeRect = activeEl.getBoundingClientRect();
|
|
770
|
+
if (activeRect.top < containerRect.top || activeRect.bottom > containerRect.bottom) {
|
|
771
|
+
activeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
cleanup() {
|
|
776
|
+
if (this.audioElement) {
|
|
777
|
+
this.audioElement.pause();
|
|
778
|
+
if (this.eventHandlers.loadedmetadata) {
|
|
779
|
+
this.audioElement.removeEventListener('loadedmetadata', this.eventHandlers.loadedmetadata);
|
|
780
|
+
}
|
|
781
|
+
if (this.eventHandlers.timeupdate) {
|
|
782
|
+
this.audioElement.removeEventListener('timeupdate', this.eventHandlers.timeupdate);
|
|
783
|
+
}
|
|
784
|
+
if (this.eventHandlers.ended) {
|
|
785
|
+
this.audioElement.removeEventListener('ended', this.eventHandlers.ended);
|
|
786
|
+
}
|
|
787
|
+
this.eventHandlers = {};
|
|
788
|
+
this.audioElement = null;
|
|
789
|
+
}
|
|
790
|
+
if (this.audioBlobUrl) {
|
|
791
|
+
URL.revokeObjectURL(this.audioBlobUrl);
|
|
792
|
+
this.audioBlobUrl = null;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
reset() {
|
|
796
|
+
this.cleanup();
|
|
797
|
+
this.isPlaying.set(false);
|
|
798
|
+
this.isLoading.set(false);
|
|
799
|
+
this.currentTime.set(0);
|
|
800
|
+
this.duration.set(0);
|
|
801
|
+
this.playbackRate.set(1);
|
|
802
|
+
this.sessionData.set(null);
|
|
803
|
+
this.activeSegmentId.set(null);
|
|
804
|
+
this.activeWordIndex.set(null);
|
|
805
|
+
}
|
|
806
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: PlaybackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
807
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: PlaybackService });
|
|
808
|
+
}
|
|
809
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: PlaybackService, decorators: [{
|
|
810
|
+
type: Injectable
|
|
811
|
+
}] });
|
|
812
|
+
|
|
487
813
|
class RecorderComponent {
|
|
488
814
|
audioCapture = inject(AudioCaptureService);
|
|
489
815
|
socket = inject(ScribeSocketService);
|
|
490
816
|
elementRef = inject(ElementRef);
|
|
817
|
+
speakerService = inject(SpeakerIdentificationService);
|
|
818
|
+
playback = inject(PlaybackService);
|
|
491
819
|
subscriptions = [];
|
|
492
820
|
entryIdCounter = 0;
|
|
493
821
|
generateAbortController = null;
|
|
494
822
|
onDocumentClick(event) {
|
|
495
|
-
if (!this.showTemplateMenu())
|
|
496
|
-
return;
|
|
497
823
|
const target = event.target;
|
|
498
|
-
|
|
824
|
+
const isInside = this.elementRef.nativeElement.contains(target);
|
|
825
|
+
if (this.showTemplateMenu() && !isInside) {
|
|
499
826
|
this.showTemplateMenu.set(false);
|
|
500
827
|
}
|
|
828
|
+
if (this.speakerService.editingSpeaker() !== null) {
|
|
829
|
+
const isDropdownClick = target.closest('.scribe-speaker-dropdown') ||
|
|
830
|
+
target.closest('.scribe-speaker-label--editable');
|
|
831
|
+
if (!isDropdownClick) {
|
|
832
|
+
this.speakerService.closeMenu();
|
|
833
|
+
}
|
|
834
|
+
}
|
|
501
835
|
}
|
|
502
836
|
wsUrl = input('');
|
|
503
837
|
token = input('');
|
|
@@ -508,6 +842,9 @@ class RecorderComponent {
|
|
|
508
842
|
sessionId = input();
|
|
509
843
|
apiUrl = input('');
|
|
510
844
|
resumeSessionId = input();
|
|
845
|
+
doctorName = input('');
|
|
846
|
+
patientName = input('');
|
|
847
|
+
companionName = input('');
|
|
511
848
|
sessionStarted = output();
|
|
512
849
|
sessionFinished = output();
|
|
513
850
|
error = output();
|
|
@@ -523,21 +860,9 @@ class RecorderComponent {
|
|
|
523
860
|
showTemplateMenu = signal(false);
|
|
524
861
|
isGenerating = signal(false);
|
|
525
862
|
lastSessionId = signal(null);
|
|
526
|
-
isPlaying = signal(false);
|
|
527
863
|
isLoadingSession = signal(false);
|
|
528
|
-
audioCurrentTime = signal(0);
|
|
529
|
-
audioDuration = signal(0);
|
|
530
|
-
playbackRate = signal(1);
|
|
531
|
-
sessionData = signal(null);
|
|
532
|
-
activeSegmentId = signal(null);
|
|
533
|
-
activeWordIndex = signal(null);
|
|
534
|
-
audioElement = null;
|
|
535
|
-
audioBlobUrl = null;
|
|
536
864
|
mode = computed(() => this.sessionId() ? 'playback' : 'recording');
|
|
537
|
-
|
|
538
|
-
hasFinishedSession = computed(() => this.lastSessionId() !== null &&
|
|
539
|
-
!this.isRecording() &&
|
|
540
|
-
!this.isPaused());
|
|
865
|
+
hasFinishedSession = computed(() => this.lastSessionId() !== null && !this.isRecording() && !this.isPaused());
|
|
541
866
|
speakerGroups = computed(() => {
|
|
542
867
|
const allEntries = this.entries();
|
|
543
868
|
if (allEntries.length === 0)
|
|
@@ -554,24 +879,15 @@ class RecorderComponent {
|
|
|
554
879
|
return groups;
|
|
555
880
|
});
|
|
556
881
|
constructor() {
|
|
557
|
-
this.
|
|
558
|
-
|
|
559
|
-
}), this.socket.connectionState$.subscribe((state) => {
|
|
560
|
-
this.connectionState.set(state);
|
|
561
|
-
}), this.socket.transcription$.subscribe((event) => {
|
|
562
|
-
this.handleTranscription(event);
|
|
563
|
-
}), this.socket.error$.subscribe((err) => {
|
|
564
|
-
this.error.emit(err);
|
|
565
|
-
}), this.audioCapture.error$.subscribe((err) => {
|
|
566
|
-
this.error.emit(err);
|
|
567
|
-
}), this.audioCapture.audioChunk$.subscribe(({ data, isSilence }) => {
|
|
882
|
+
this.playback.setElementRef(this.elementRef);
|
|
883
|
+
this.subscriptions.push(this.audioCapture.audioLevel$.subscribe((level) => this.audioLevel.set(level)), this.socket.connectionState$.subscribe((state) => this.connectionState.set(state)), this.socket.transcription$.subscribe((event) => this.handleTranscription(event)), this.socket.error$.subscribe((err) => this.error.emit(err)), this.audioCapture.error$.subscribe((err) => this.error.emit(err)), this.audioCapture.audioChunk$.subscribe(({ data, isSilence }) => {
|
|
568
884
|
this.socket.sendAudioChunk(data, isSilence);
|
|
569
885
|
}));
|
|
570
886
|
effect(() => {
|
|
571
887
|
const sid = this.sessionId();
|
|
572
888
|
const api = this.apiUrl();
|
|
573
889
|
if (sid && api && this.mode() === 'playback') {
|
|
574
|
-
this.
|
|
890
|
+
this.loadPlaybackSession();
|
|
575
891
|
}
|
|
576
892
|
});
|
|
577
893
|
effect(() => {
|
|
@@ -581,56 +897,36 @@ class RecorderComponent {
|
|
|
581
897
|
this.resumeSession(resumeId);
|
|
582
898
|
}
|
|
583
899
|
});
|
|
900
|
+
effect(() => {
|
|
901
|
+
if (this.mode() !== 'recording')
|
|
902
|
+
return;
|
|
903
|
+
const allEntries = this.entries();
|
|
904
|
+
if (this.speakerService.shouldIdentify(allEntries)) {
|
|
905
|
+
this.speakerService.scheduleIdentification(() => this.triggerIdentification());
|
|
906
|
+
}
|
|
907
|
+
});
|
|
584
908
|
}
|
|
585
909
|
ngOnDestroy() {
|
|
586
910
|
this.subscriptions.forEach((sub) => sub.unsubscribe());
|
|
587
911
|
this.generateAbortController?.abort();
|
|
588
912
|
this.cleanup();
|
|
589
|
-
this.
|
|
590
|
-
|
|
591
|
-
audioEventHandlers = {};
|
|
592
|
-
cleanupPlayback() {
|
|
593
|
-
if (this.audioElement) {
|
|
594
|
-
this.audioElement.pause();
|
|
595
|
-
if (this.audioEventHandlers.loadedmetadata) {
|
|
596
|
-
this.audioElement.removeEventListener('loadedmetadata', this.audioEventHandlers.loadedmetadata);
|
|
597
|
-
}
|
|
598
|
-
if (this.audioEventHandlers.timeupdate) {
|
|
599
|
-
this.audioElement.removeEventListener('timeupdate', this.audioEventHandlers.timeupdate);
|
|
600
|
-
}
|
|
601
|
-
if (this.audioEventHandlers.ended) {
|
|
602
|
-
this.audioElement.removeEventListener('ended', this.audioEventHandlers.ended);
|
|
603
|
-
}
|
|
604
|
-
this.audioEventHandlers = {};
|
|
605
|
-
this.audioElement = null;
|
|
606
|
-
}
|
|
607
|
-
if (this.audioBlobUrl) {
|
|
608
|
-
URL.revokeObjectURL(this.audioBlobUrl);
|
|
609
|
-
this.audioBlobUrl = null;
|
|
610
|
-
}
|
|
913
|
+
this.playback.cleanup();
|
|
914
|
+
this.speakerService.reset();
|
|
611
915
|
}
|
|
612
916
|
getSpeakerLabel(speaker) {
|
|
613
|
-
|
|
614
|
-
if (customLabels[speaker]) {
|
|
615
|
-
return customLabels[speaker];
|
|
616
|
-
}
|
|
617
|
-
return SPEAKER_LABELS[speaker] || `Pessoa ${speaker + 1}`;
|
|
917
|
+
return this.speakerService.getLabel(speaker, this.speakerLabels(), this.doctorName(), this.patientName(), this.companionName());
|
|
618
918
|
}
|
|
619
919
|
shouldShowSpeakerLabel(segmentIndex) {
|
|
620
|
-
const segs = this.segments
|
|
920
|
+
const segs = this.playback.segments;
|
|
621
921
|
if (segmentIndex === 0)
|
|
622
922
|
return true;
|
|
623
|
-
|
|
624
|
-
const previousSpeaker = segs[segmentIndex - 1]?.speaker;
|
|
625
|
-
return currentSpeaker !== previousSpeaker;
|
|
923
|
+
return segs[segmentIndex]?.speaker !== segs[segmentIndex - 1]?.speaker;
|
|
626
924
|
}
|
|
627
925
|
getMicButtonLabel() {
|
|
628
|
-
if (this.isPaused())
|
|
926
|
+
if (this.isPaused())
|
|
629
927
|
return 'Retomar gravação';
|
|
630
|
-
|
|
631
|
-
if (this.isRecording()) {
|
|
928
|
+
if (this.isRecording())
|
|
632
929
|
return 'Pausar gravação';
|
|
633
|
-
}
|
|
634
930
|
return 'Iniciar gravação';
|
|
635
931
|
}
|
|
636
932
|
async toggleRecording() {
|
|
@@ -698,10 +994,7 @@ class RecorderComponent {
|
|
|
698
994
|
method: 'POST',
|
|
699
995
|
headers,
|
|
700
996
|
signal: this.generateAbortController.signal,
|
|
701
|
-
body: JSON.stringify({
|
|
702
|
-
sessionId,
|
|
703
|
-
outputSchema: template.content,
|
|
704
|
-
}),
|
|
997
|
+
body: JSON.stringify({ sessionId, outputSchema: template.content }),
|
|
705
998
|
});
|
|
706
999
|
if (!response.ok) {
|
|
707
1000
|
const errorData = await response.json().catch(() => ({}));
|
|
@@ -724,32 +1017,51 @@ class RecorderComponent {
|
|
|
724
1017
|
this.isGenerating.set(false);
|
|
725
1018
|
}
|
|
726
1019
|
}
|
|
727
|
-
|
|
1020
|
+
onSeek(event) {
|
|
1021
|
+
const input = event.target;
|
|
1022
|
+
if (input) {
|
|
1023
|
+
this.playback.seekTo(parseFloat(input.value));
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
onPlaybackRateChange(event) {
|
|
1027
|
+
const select = event.target;
|
|
1028
|
+
if (select) {
|
|
1029
|
+
this.playback.setRate(parseFloat(select.value));
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
onWordClick(word, event) {
|
|
1033
|
+
event.stopPropagation();
|
|
1034
|
+
this.playback.seekToWord(word);
|
|
1035
|
+
}
|
|
1036
|
+
onSpeakerLabelClick(event, speaker) {
|
|
1037
|
+
event.stopPropagation();
|
|
1038
|
+
this.speakerService.toggleMenu(speaker);
|
|
1039
|
+
}
|
|
1040
|
+
setSpeakerRole(speaker, role) {
|
|
1041
|
+
this.speakerService.setRole(speaker, role);
|
|
1042
|
+
}
|
|
1043
|
+
getSpeakerColor(speaker) {
|
|
1044
|
+
if (speaker === undefined)
|
|
1045
|
+
return 'var(--scribe-border-color)';
|
|
1046
|
+
const colors = [
|
|
1047
|
+
'var(--scribe-speaker-0)',
|
|
1048
|
+
'var(--scribe-speaker-1)',
|
|
1049
|
+
'var(--scribe-speaker-2)',
|
|
1050
|
+
'var(--scribe-speaker-3)',
|
|
1051
|
+
];
|
|
1052
|
+
return colors[speaker % colors.length];
|
|
1053
|
+
}
|
|
1054
|
+
async loadPlaybackSession() {
|
|
728
1055
|
const sid = this.sessionId();
|
|
729
1056
|
const api = this.apiUrl();
|
|
730
1057
|
if (!sid || !api)
|
|
731
1058
|
return;
|
|
732
|
-
this.isLoadingSession.set(true);
|
|
733
1059
|
try {
|
|
734
|
-
|
|
735
|
-
const token = this.token();
|
|
736
|
-
if (token) {
|
|
737
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
738
|
-
}
|
|
739
|
-
const response = await fetch(`${api}/session/${sid}`, { headers });
|
|
740
|
-
if (!response.ok) {
|
|
741
|
-
throw new Error(`HTTP ${response.status}`);
|
|
742
|
-
}
|
|
743
|
-
const data = await response.json();
|
|
744
|
-
this.sessionData.set(data);
|
|
745
|
-
await this.loadAudio(data.audioUrl);
|
|
1060
|
+
await this.playback.loadSession(api, sid, this.token());
|
|
746
1061
|
}
|
|
747
1062
|
catch (err) {
|
|
748
1063
|
this.error.emit(err instanceof Error ? err : new Error(String(err)));
|
|
749
1064
|
}
|
|
750
|
-
finally {
|
|
751
|
-
this.isLoadingSession.set(false);
|
|
752
|
-
}
|
|
753
1065
|
}
|
|
754
1066
|
async resumeSession(sessionId) {
|
|
755
1067
|
const api = this.apiUrl();
|
|
@@ -786,125 +1098,6 @@ class RecorderComponent {
|
|
|
786
1098
|
this.isLoadingSession.set(false);
|
|
787
1099
|
}
|
|
788
1100
|
}
|
|
789
|
-
async loadAudio(pcmUrl) {
|
|
790
|
-
this.cleanupPlayback();
|
|
791
|
-
const response = await fetch(pcmUrl);
|
|
792
|
-
if (!response.ok) {
|
|
793
|
-
throw new Error(`Failed to load audio: HTTP ${response.status}`);
|
|
794
|
-
}
|
|
795
|
-
const pcmData = await response.arrayBuffer();
|
|
796
|
-
const wavData = pcmToWav(pcmData);
|
|
797
|
-
this.audioBlobUrl = createAudioBlobUrl(wavData);
|
|
798
|
-
this.audioElement = new Audio(this.audioBlobUrl);
|
|
799
|
-
this.audioEventHandlers.loadedmetadata = () => {
|
|
800
|
-
this.audioDuration.set(this.audioElement.duration);
|
|
801
|
-
};
|
|
802
|
-
this.audioEventHandlers.timeupdate = () => {
|
|
803
|
-
this.audioCurrentTime.set(this.audioElement.currentTime);
|
|
804
|
-
this.updateActiveSegment();
|
|
805
|
-
};
|
|
806
|
-
this.audioEventHandlers.ended = () => {
|
|
807
|
-
this.isPlaying.set(false);
|
|
808
|
-
};
|
|
809
|
-
this.audioElement.addEventListener('loadedmetadata', this.audioEventHandlers.loadedmetadata);
|
|
810
|
-
this.audioElement.addEventListener('timeupdate', this.audioEventHandlers.timeupdate);
|
|
811
|
-
this.audioElement.addEventListener('ended', this.audioEventHandlers.ended);
|
|
812
|
-
}
|
|
813
|
-
togglePlayback() {
|
|
814
|
-
if (!this.audioElement)
|
|
815
|
-
return;
|
|
816
|
-
if (this.isPlaying()) {
|
|
817
|
-
this.audioElement.pause();
|
|
818
|
-
this.isPlaying.set(false);
|
|
819
|
-
}
|
|
820
|
-
else {
|
|
821
|
-
this.audioElement.play();
|
|
822
|
-
this.isPlaying.set(true);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
seekTo(event) {
|
|
826
|
-
const input = event.target;
|
|
827
|
-
if (!input)
|
|
828
|
-
return;
|
|
829
|
-
const time = parseFloat(input.value);
|
|
830
|
-
if (this.audioElement && !isNaN(time)) {
|
|
831
|
-
this.audioElement.currentTime = time;
|
|
832
|
-
this.audioCurrentTime.set(time);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
setPlaybackRate(event) {
|
|
836
|
-
const select = event.target;
|
|
837
|
-
if (!select)
|
|
838
|
-
return;
|
|
839
|
-
const rate = parseFloat(select.value);
|
|
840
|
-
if (isNaN(rate))
|
|
841
|
-
return;
|
|
842
|
-
this.playbackRate.set(rate);
|
|
843
|
-
if (this.audioElement) {
|
|
844
|
-
this.audioElement.playbackRate = rate;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
seekToSegment(segment) {
|
|
848
|
-
if (this.audioElement) {
|
|
849
|
-
this.audioElement.currentTime = segment.startTime;
|
|
850
|
-
this.audioCurrentTime.set(segment.startTime);
|
|
851
|
-
if (!this.isPlaying()) {
|
|
852
|
-
this.togglePlayback();
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
seekToWord(word, event) {
|
|
857
|
-
event.stopPropagation();
|
|
858
|
-
if (this.audioElement) {
|
|
859
|
-
this.audioElement.currentTime = word.start;
|
|
860
|
-
this.audioCurrentTime.set(word.start);
|
|
861
|
-
if (!this.isPlaying()) {
|
|
862
|
-
this.togglePlayback();
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
formatTimeDisplay(seconds) {
|
|
867
|
-
return formatTime(seconds);
|
|
868
|
-
}
|
|
869
|
-
getSpeakerColor(speaker) {
|
|
870
|
-
if (speaker === undefined)
|
|
871
|
-
return 'var(--scribe-border-color)';
|
|
872
|
-
const colors = [
|
|
873
|
-
'var(--scribe-speaker-0)',
|
|
874
|
-
'var(--scribe-speaker-1)',
|
|
875
|
-
'var(--scribe-speaker-2)',
|
|
876
|
-
'var(--scribe-speaker-3)',
|
|
877
|
-
];
|
|
878
|
-
return colors[speaker % colors.length];
|
|
879
|
-
}
|
|
880
|
-
updateActiveSegment() {
|
|
881
|
-
const time = this.audioCurrentTime();
|
|
882
|
-
const segs = this.segments();
|
|
883
|
-
const seg = segs.find((s) => time >= s.startTime && time <= s.endTime);
|
|
884
|
-
this.activeSegmentId.set(seg?.id ?? null);
|
|
885
|
-
if (seg?.words) {
|
|
886
|
-
const wordIdx = seg.words.findIndex((w) => time >= w.start && time <= w.end);
|
|
887
|
-
this.activeWordIndex.set(wordIdx >= 0 ? wordIdx : null);
|
|
888
|
-
}
|
|
889
|
-
else {
|
|
890
|
-
this.activeWordIndex.set(null);
|
|
891
|
-
}
|
|
892
|
-
this.scrollToActiveSegment();
|
|
893
|
-
}
|
|
894
|
-
scrollToActiveSegment() {
|
|
895
|
-
const activeId = this.activeSegmentId();
|
|
896
|
-
if (!activeId)
|
|
897
|
-
return;
|
|
898
|
-
const container = this.elementRef.nativeElement.querySelector('.scribe-transcript-playback');
|
|
899
|
-
const activeEl = container?.querySelector(`[data-segment-id="${activeId}"]`);
|
|
900
|
-
if (activeEl && container) {
|
|
901
|
-
const containerRect = container.getBoundingClientRect();
|
|
902
|
-
const activeRect = activeEl.getBoundingClientRect();
|
|
903
|
-
if (activeRect.top < containerRect.top || activeRect.bottom > containerRect.bottom) {
|
|
904
|
-
activeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
1101
|
async startRecording() {
|
|
909
1102
|
try {
|
|
910
1103
|
await this.socket.connect(this.wsUrl(), this.token(), this.premium());
|
|
@@ -954,78 +1147,83 @@ class RecorderComponent {
|
|
|
954
1147
|
this.currentSpeaker.set(event.speaker);
|
|
955
1148
|
}
|
|
956
1149
|
}
|
|
1150
|
+
async triggerIdentification() {
|
|
1151
|
+
const api = this.apiUrl() || this.lambdaUrl();
|
|
1152
|
+
const sessionId = this.socket.getSessionId() || this.lastSessionId();
|
|
1153
|
+
await this.speakerService.identify(this.entries(), api, this.token(), sessionId, this.premium());
|
|
1154
|
+
}
|
|
957
1155
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: RecorderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
958
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.18", type: RecorderComponent, isStandalone: true, selector: "ngx-jaimes-scribe-recorder", inputs: { wsUrl: { classPropertyName: "wsUrl", publicName: "wsUrl", isSignal: true, isRequired: false, transformFunction: null }, token: { classPropertyName: "token", publicName: "token", isSignal: true, isRequired: false, transformFunction: null }, speakerLabels: { classPropertyName: "speakerLabels", publicName: "speakerLabels", isSignal: true, isRequired: false, transformFunction: null }, premium: { classPropertyName: "premium", publicName: "premium", isSignal: true, isRequired: false, transformFunction: null }, templates: { classPropertyName: "templates", publicName: "templates", isSignal: true, isRequired: false, transformFunction: null }, lambdaUrl: { classPropertyName: "lambdaUrl", publicName: "lambdaUrl", isSignal: true, isRequired: false, transformFunction: null }, sessionId: { classPropertyName: "sessionId", publicName: "sessionId", isSignal: true, isRequired: false, transformFunction: null }, apiUrl: { classPropertyName: "apiUrl", publicName: "apiUrl", isSignal: true, isRequired: false, transformFunction: null }, resumeSessionId: { classPropertyName: "resumeSessionId", publicName: "resumeSessionId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sessionStarted: "sessionStarted", sessionFinished: "sessionFinished", error: "error", documentGenerated: "documentGenerated", generationError: "generationError" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, ngImport: i0, template: `
|
|
1156
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.18", type: RecorderComponent, isStandalone: true, selector: "ngx-jaimes-scribe-recorder", inputs: { wsUrl: { classPropertyName: "wsUrl", publicName: "wsUrl", isSignal: true, isRequired: false, transformFunction: null }, token: { classPropertyName: "token", publicName: "token", isSignal: true, isRequired: false, transformFunction: null }, speakerLabels: { classPropertyName: "speakerLabels", publicName: "speakerLabels", isSignal: true, isRequired: false, transformFunction: null }, premium: { classPropertyName: "premium", publicName: "premium", isSignal: true, isRequired: false, transformFunction: null }, templates: { classPropertyName: "templates", publicName: "templates", isSignal: true, isRequired: false, transformFunction: null }, lambdaUrl: { classPropertyName: "lambdaUrl", publicName: "lambdaUrl", isSignal: true, isRequired: false, transformFunction: null }, sessionId: { classPropertyName: "sessionId", publicName: "sessionId", isSignal: true, isRequired: false, transformFunction: null }, apiUrl: { classPropertyName: "apiUrl", publicName: "apiUrl", isSignal: true, isRequired: false, transformFunction: null }, resumeSessionId: { classPropertyName: "resumeSessionId", publicName: "resumeSessionId", isSignal: true, isRequired: false, transformFunction: null }, doctorName: { classPropertyName: "doctorName", publicName: "doctorName", isSignal: true, isRequired: false, transformFunction: null }, patientName: { classPropertyName: "patientName", publicName: "patientName", isSignal: true, isRequired: false, transformFunction: null }, companionName: { classPropertyName: "companionName", publicName: "companionName", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sessionStarted: "sessionStarted", sessionFinished: "sessionFinished", error: "error", documentGenerated: "documentGenerated", generationError: "generationError" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, providers: [SpeakerIdentificationService, PlaybackService], ngImport: i0, template: `
|
|
959
1157
|
<div class="scribe-recorder">
|
|
960
1158
|
@if (mode() === 'playback') {
|
|
961
1159
|
<!-- Playback Mode -->
|
|
962
1160
|
<div class="scribe-playback">
|
|
963
|
-
@if (
|
|
1161
|
+
@if (playback.isLoading()) {
|
|
964
1162
|
<div class="scribe-loading">
|
|
965
1163
|
<svg class="scribe-icon scribe-icon--spinner" viewBox="0 0 24 24" width="24" height="24">
|
|
966
1164
|
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
|
|
967
1165
|
</svg>
|
|
968
1166
|
<span>Carregando sessão...</span>
|
|
969
1167
|
</div>
|
|
970
|
-
} @else if (sessionData()) {
|
|
1168
|
+
} @else if (playback.sessionData()) {
|
|
971
1169
|
<!-- Audio Player -->
|
|
972
1170
|
<div class="scribe-player">
|
|
973
|
-
<
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
<
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
<
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
</button>
|
|
1171
|
+
<button
|
|
1172
|
+
class="scribe-btn scribe-btn--play"
|
|
1173
|
+
(click)="playback.togglePlayback()"
|
|
1174
|
+
[attr.aria-label]="playback.isPlaying() ? 'Pausar' : 'Reproduzir'"
|
|
1175
|
+
>
|
|
1176
|
+
@if (playback.isPlaying()) {
|
|
1177
|
+
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1178
|
+
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
|
1179
|
+
</svg>
|
|
1180
|
+
} @else {
|
|
1181
|
+
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1182
|
+
<path d="M8 5v14l11-7z"/>
|
|
1183
|
+
</svg>
|
|
1184
|
+
}
|
|
1185
|
+
</button>
|
|
989
1186
|
|
|
1187
|
+
<div class="scribe-timeline">
|
|
990
1188
|
<input
|
|
991
1189
|
type="range"
|
|
992
1190
|
class="scribe-timeline-slider"
|
|
993
1191
|
[min]="0"
|
|
994
|
-
[max]="
|
|
995
|
-
[value]="
|
|
1192
|
+
[max]="playback.duration()"
|
|
1193
|
+
[value]="playback.currentTime()"
|
|
996
1194
|
step="0.1"
|
|
997
|
-
(input)="
|
|
1195
|
+
(input)="onSeek($event)"
|
|
998
1196
|
aria-label="Posição do áudio"
|
|
999
1197
|
/>
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
[value]="playbackRate()"
|
|
1004
|
-
(change)="setPlaybackRate($event)"
|
|
1005
|
-
aria-label="Velocidade de reprodução"
|
|
1006
|
-
>
|
|
1007
|
-
<option value="0.5">0.5x</option>
|
|
1008
|
-
<option value="0.75">0.75x</option>
|
|
1009
|
-
<option value="1">1x</option>
|
|
1010
|
-
<option value="1.25">1.25x</option>
|
|
1011
|
-
<option value="1.5">1.5x</option>
|
|
1012
|
-
<option value="2">2x</option>
|
|
1013
|
-
</select>
|
|
1014
|
-
</div>
|
|
1015
|
-
<div class="scribe-time">
|
|
1016
|
-
{{ formatTimeDisplay(audioCurrentTime()) }} / {{ formatTimeDisplay(audioDuration()) }}
|
|
1198
|
+
<div class="scribe-time">
|
|
1199
|
+
{{ playback.formatTime(playback.currentTime()) }} / {{ playback.formatTime(playback.duration()) }}
|
|
1200
|
+
</div>
|
|
1017
1201
|
</div>
|
|
1202
|
+
|
|
1203
|
+
<select
|
|
1204
|
+
class="scribe-speed"
|
|
1205
|
+
[value]="playback.playbackRate()"
|
|
1206
|
+
(change)="onPlaybackRateChange($event)"
|
|
1207
|
+
aria-label="Velocidade de reprodução"
|
|
1208
|
+
>
|
|
1209
|
+
<option value="0.5">0.5x</option>
|
|
1210
|
+
<option value="0.75">0.75x</option>
|
|
1211
|
+
<option value="1">1x</option>
|
|
1212
|
+
<option value="1.25">1.25x</option>
|
|
1213
|
+
<option value="1.5">1.5x</option>
|
|
1214
|
+
<option value="2">2x</option>
|
|
1215
|
+
</select>
|
|
1018
1216
|
</div>
|
|
1019
1217
|
|
|
1020
1218
|
<!-- Transcription with Highlight -->
|
|
1021
1219
|
<div class="scribe-transcript-playback">
|
|
1022
|
-
@for (segment of segments
|
|
1220
|
+
@for (segment of playback.segments; track segment.id; let idx = $index) {
|
|
1023
1221
|
<div
|
|
1024
1222
|
class="scribe-segment"
|
|
1025
|
-
[class.scribe-segment--active]="activeSegmentId() === segment.id"
|
|
1223
|
+
[class.scribe-segment--active]="playback.activeSegmentId() === segment.id"
|
|
1026
1224
|
[style.--speaker-color]="getSpeakerColor(segment.speaker)"
|
|
1027
1225
|
[attr.data-segment-id]="segment.id"
|
|
1028
|
-
(click)="seekToSegment(segment)"
|
|
1226
|
+
(click)="playback.seekToSegment(segment)"
|
|
1029
1227
|
>
|
|
1030
1228
|
@if (segment.speaker !== undefined && shouldShowSpeakerLabel(idx)) {
|
|
1031
1229
|
<span class="scribe-speaker-label" [class]="'scribe-speaker--' + segment.speaker">
|
|
@@ -1038,8 +1236,8 @@ class RecorderComponent {
|
|
|
1038
1236
|
@for (word of segment.words; track $index; let i = $index) {
|
|
1039
1237
|
<span
|
|
1040
1238
|
class="scribe-word"
|
|
1041
|
-
[class.scribe-word--active]="activeSegmentId() === segment.id && activeWordIndex() === i"
|
|
1042
|
-
(click)="
|
|
1239
|
+
[class.scribe-word--active]="playback.activeSegmentId() === segment.id && playback.activeWordIndex() === i"
|
|
1240
|
+
(click)="onWordClick(word, $event)"
|
|
1043
1241
|
>{{ word.word }}</span>{{ ' ' }}
|
|
1044
1242
|
}
|
|
1045
1243
|
} @else {
|
|
@@ -1048,7 +1246,7 @@ class RecorderComponent {
|
|
|
1048
1246
|
</p>
|
|
1049
1247
|
</div>
|
|
1050
1248
|
}
|
|
1051
|
-
@if (segments
|
|
1249
|
+
@if (playback.segments.length === 0) {
|
|
1052
1250
|
<span class="scribe-placeholder">Nenhuma transcrição disponível.</span>
|
|
1053
1251
|
}
|
|
1054
1252
|
</div>
|
|
@@ -1084,17 +1282,14 @@ class RecorderComponent {
|
|
|
1084
1282
|
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
|
|
1085
1283
|
</svg>
|
|
1086
1284
|
} @else if (isPaused()) {
|
|
1087
|
-
<!-- Play icon when paused -->
|
|
1088
1285
|
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1089
1286
|
<path d="M8 5v14l11-7z"/>
|
|
1090
1287
|
</svg>
|
|
1091
1288
|
} @else if (isRecording()) {
|
|
1092
|
-
<!-- Pause icon when recording -->
|
|
1093
1289
|
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1094
1290
|
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
|
1095
1291
|
</svg>
|
|
1096
1292
|
} @else {
|
|
1097
|
-
<!-- Mic icon when idle -->
|
|
1098
1293
|
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1099
1294
|
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5z"/>
|
|
1100
1295
|
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
|
|
@@ -1169,8 +1364,39 @@ class RecorderComponent {
|
|
|
1169
1364
|
@for (group of speakerGroups(); track $index) {
|
|
1170
1365
|
<div class="scribe-speaker-block" [attr.data-speaker]="group.speaker">
|
|
1171
1366
|
@if (group.speaker !== undefined && group.speaker !== null) {
|
|
1172
|
-
<span class="scribe-speaker-label
|
|
1173
|
-
|
|
1367
|
+
<span class="scribe-speaker-label-wrapper">
|
|
1368
|
+
<button
|
|
1369
|
+
type="button"
|
|
1370
|
+
class="scribe-speaker-label scribe-speaker-label--editable"
|
|
1371
|
+
[class]="'scribe-speaker--' + group.speaker"
|
|
1372
|
+
[class.scribe-speaker-label--low-confidence]="speakerService.hasLowConfidence(group.speaker)"
|
|
1373
|
+
(click)="onSpeakerLabelClick($event, group.speaker)"
|
|
1374
|
+
>
|
|
1375
|
+
{{ getSpeakerLabel(group.speaker) }}
|
|
1376
|
+
<svg class="scribe-chevron" viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
|
1377
|
+
<path d="M7 10l5 5 5-5z"/>
|
|
1378
|
+
</svg>
|
|
1379
|
+
@if (speakerService.hasLowConfidence(group.speaker)) {
|
|
1380
|
+
<span class="scribe-warning" title="Confiança baixa - verifique">⚠️</span>
|
|
1381
|
+
}
|
|
1382
|
+
</button>
|
|
1383
|
+
@if (speakerService.editingSpeaker() === group.speaker) {
|
|
1384
|
+
<div class="scribe-speaker-dropdown">
|
|
1385
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'doctor')">
|
|
1386
|
+
<span class="scribe-dropdown-icon">🩺</span> Médico
|
|
1387
|
+
</button>
|
|
1388
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'patient')">
|
|
1389
|
+
<span class="scribe-dropdown-icon">🙋</span> Paciente
|
|
1390
|
+
</button>
|
|
1391
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'companion')">
|
|
1392
|
+
<span class="scribe-dropdown-icon">👥</span> Acompanhante
|
|
1393
|
+
</button>
|
|
1394
|
+
<div class="scribe-dropdown-divider"></div>
|
|
1395
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'other')">
|
|
1396
|
+
<span class="scribe-dropdown-icon">✏️</span> Outro
|
|
1397
|
+
</button>
|
|
1398
|
+
</div>
|
|
1399
|
+
}
|
|
1174
1400
|
</span>
|
|
1175
1401
|
}
|
|
1176
1402
|
<div class="scribe-speaker-text">
|
|
@@ -1197,81 +1423,81 @@ class RecorderComponent {
|
|
|
1197
1423
|
}
|
|
1198
1424
|
}
|
|
1199
1425
|
</div>
|
|
1200
|
-
`, isInline: true, styles: [":host{--scribe-primary: #4caf50;--scribe-primary-dark: #388e3c;--scribe-primary-light: #81c784;--scribe-danger: #f44336;--scribe-danger-dark: #d32f2f;--scribe-text-color: #212121;--scribe-text-partial: #9e9e9e;--scribe-font-family: inherit;--scribe-font-size: 1rem;--scribe-bg: #ffffff;--scribe-bg-transcript: #f5f5f5;--scribe-border-radius: 8px;--scribe-border-color: #e0e0e0;--scribe-level-bg: #e0e0e0;--scribe-level-fill: #4caf50;--scribe-speaker-0: #2196f3;--scribe-speaker-1: #9c27b0;--scribe-speaker-2: #ff9800;--scribe-speaker-3: #009688;display:block;font-family:var(--scribe-font-family);font-size:var(--scribe-font-size)}.scribe-recorder{background:var(--scribe-bg);border-radius:var(--scribe-border-radius)}.scribe-controls{display:flex;align-items:center;gap:1rem;padding:1rem}.scribe-btn{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:.75rem;border:none;border-radius:var(--scribe-border-radius);font-family:var(--scribe-font-family);font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s ease}.scribe-btn:disabled{opacity:.5;cursor:not-allowed}.scribe-btn--mic{width:48px;height:48px;border-radius:50%;background:var(--scribe-bg-transcript);color:var(--scribe-text-color)}.scribe-btn--mic:hover:not(:disabled){background:var(--scribe-border-color)}.scribe-btn--mic.scribe-btn--active{background:var(--scribe-danger);color:#fff;animation:pulse-recording 1.5s ease-in-out infinite}.scribe-btn--mic.scribe-btn--paused{background:var(--scribe-primary);color:#fff}.scribe-btn--mic.scribe-btn--paused:hover:not(:disabled){background:var(--scribe-primary-dark)}.scribe-btn--mic.scribe-btn--connecting{background:var(--scribe-primary-light);color:#fff}.scribe-btn--finish{padding:.75rem 1.25rem;background:var(--scribe-primary);color:#fff}.scribe-btn--finish:hover:not(:disabled){background:var(--scribe-primary-dark)}.scribe-icon{flex-shrink:0}.scribe-icon--spinner{animation:spin 1s linear infinite}.scribe-level{flex:1;height:8px;background:var(--scribe-level-bg);border-radius:4px;overflow:hidden;opacity:.5;transition:opacity .2s ease}.scribe-level--active{opacity:1}.scribe-level__fill{height:100%;background:var(--scribe-level-fill);border-radius:4px;transition:width .05s ease-out}.scribe-transcript{min-height:120px;max-height:400px;overflow-y:auto;padding:1rem;margin:0 1rem 1rem;background:var(--scribe-bg-transcript);border-radius:var(--scribe-border-radius);line-height:1.6}.scribe-transcript--empty{display:flex;align-items:center;justify-content:center}.scribe-placeholder{color:var(--scribe-text-partial);font-style:italic}.scribe-speaker-block{margin-bottom:.75rem;padding-left:.5rem;border-left:3px solid var(--scribe-border-color)}.scribe-speaker-block--partial{opacity:.7}.scribe-speaker-block[data-speaker=\"0\"]{border-left-color:var(--scribe-speaker-0)}.scribe-speaker-block[data-speaker=\"1\"]{border-left-color:var(--scribe-speaker-1)}.scribe-speaker-block[data-speaker=\"2\"]{border-left-color:var(--scribe-speaker-2)}.scribe-speaker-block[data-speaker=\"3\"]{border-left-color:var(--scribe-speaker-3)}.scribe-speaker-label{display:inline-block;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px;padding:.125rem .5rem;border-radius:4px;margin-bottom:.25rem;color:#fff;background:var(--scribe-border-color)}.scribe-speaker--0{background:var(--scribe-speaker-0)}.scribe-speaker--1{background:var(--scribe-speaker-1)}.scribe-speaker--2{background:var(--scribe-speaker-2)}.scribe-speaker--3{background:var(--scribe-speaker-3)}.scribe-speaker-text{margin-top:.25rem}.scribe-text{word-wrap:break-word}.scribe-text--final{color:var(--scribe-text-color)}.scribe-text--partial{color:var(--scribe-text-partial);font-style:italic}.scribe-generate-container{position:relative;margin-left:.5rem}.scribe-btn--generate{display:flex;align-items:center;gap:.5rem;padding:.75rem 1.25rem;background:var(--scribe-primary);color:#fff;border:none;border-radius:var(--scribe-border-radius);font-family:var(--scribe-font-family);font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s ease}.scribe-btn--generate:hover:not(:disabled){background:var(--scribe-primary-dark)}.scribe-btn--generate:disabled{opacity:.7;cursor:not-allowed}.scribe-template-menu{position:absolute;top:100%;left:0;margin-top:.25rem;min-width:220px;background:var(--scribe-bg);border:1px solid var(--scribe-border-color);border-radius:var(--scribe-border-radius);box-shadow:0 4px 12px #00000026;z-index:100;overflow:hidden}.scribe-template-item{display:flex;flex-direction:column;align-items:flex-start;width:100%;padding:.75rem 1rem;border:none;background:transparent;cursor:pointer;text-align:left;transition:background .15s ease}.scribe-template-item:hover{background:var(--scribe-bg-transcript)}.scribe-template-item:not(:last-child){border-bottom:1px solid var(--scribe-border-color)}.scribe-template-name{font-weight:500;color:var(--scribe-text-color)}.scribe-template-desc{font-size:.75rem;color:var(--scribe-text-partial);margin-top:.25rem}@keyframes pulse-recording{0%,to{box-shadow:0 0 #f4433666}50%{box-shadow:0 0 0 8px #f4433600}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.scribe-playback{padding:1rem}.scribe-loading,.scribe-error{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:2rem;color:var(--scribe-text-partial)}.scribe-player{display:flex;
|
|
1426
|
+
`, isInline: true, styles: [":host{--scribe-primary: #4caf50;--scribe-primary-dark: #388e3c;--scribe-primary-light: #81c784;--scribe-danger: #f44336;--scribe-danger-dark: #d32f2f;--scribe-text-color: #212121;--scribe-text-partial: #9e9e9e;--scribe-font-family: inherit;--scribe-font-size: 1rem;--scribe-bg: #ffffff;--scribe-bg-transcript: #f5f5f5;--scribe-border-radius: 8px;--scribe-border-color: #e0e0e0;--scribe-level-bg: #e0e0e0;--scribe-level-fill: #4caf50;--scribe-speaker-0: #2196f3;--scribe-speaker-1: #9c27b0;--scribe-speaker-2: #ff9800;--scribe-speaker-3: #009688;display:block;font-family:var(--scribe-font-family);font-size:var(--scribe-font-size)}.scribe-recorder{background:var(--scribe-bg);border-radius:var(--scribe-border-radius)}.scribe-controls{display:flex;align-items:center;gap:1rem;padding:1rem}.scribe-btn{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:.75rem;border:none;border-radius:var(--scribe-border-radius);font-family:var(--scribe-font-family);font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s ease}.scribe-btn:disabled{opacity:.5;cursor:not-allowed}.scribe-btn--mic{width:48px;height:48px;border-radius:50%;background:var(--scribe-bg-transcript);color:var(--scribe-text-color)}.scribe-btn--mic:hover:not(:disabled){background:var(--scribe-border-color)}.scribe-btn--mic.scribe-btn--active{background:var(--scribe-danger);color:#fff;animation:pulse-recording 1.5s ease-in-out infinite}.scribe-btn--mic.scribe-btn--paused{background:var(--scribe-primary);color:#fff}.scribe-btn--mic.scribe-btn--paused:hover:not(:disabled){background:var(--scribe-primary-dark)}.scribe-btn--mic.scribe-btn--connecting{background:var(--scribe-primary-light);color:#fff}.scribe-btn--finish{padding:.75rem 1.25rem;background:var(--scribe-primary);color:#fff}.scribe-btn--finish:hover:not(:disabled){background:var(--scribe-primary-dark)}.scribe-icon{flex-shrink:0}.scribe-icon--spinner{animation:spin 1s linear infinite}.scribe-level{flex:1;height:8px;background:var(--scribe-level-bg);border-radius:4px;overflow:hidden;opacity:.5;transition:opacity .2s ease}.scribe-level--active{opacity:1}.scribe-level__fill{height:100%;background:var(--scribe-level-fill);border-radius:4px;transition:width .05s ease-out}.scribe-transcript{min-height:120px;max-height:400px;overflow-y:auto;padding:1rem;margin:0 1rem 1rem;background:var(--scribe-bg-transcript);border-radius:var(--scribe-border-radius);line-height:1.6}.scribe-transcript--empty{display:flex;align-items:center;justify-content:center}.scribe-placeholder{color:var(--scribe-text-partial);font-style:italic}.scribe-speaker-block{margin-bottom:.75rem;padding-left:.5rem;border-left:3px solid var(--scribe-border-color)}.scribe-speaker-block--partial{opacity:.7}.scribe-speaker-block[data-speaker=\"0\"]{border-left-color:var(--scribe-speaker-0)}.scribe-speaker-block[data-speaker=\"1\"]{border-left-color:var(--scribe-speaker-1)}.scribe-speaker-block[data-speaker=\"2\"]{border-left-color:var(--scribe-speaker-2)}.scribe-speaker-block[data-speaker=\"3\"]{border-left-color:var(--scribe-speaker-3)}.scribe-speaker-label{display:inline-block;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px;padding:.125rem .5rem;border-radius:4px;margin-bottom:.25rem;color:#fff;background:var(--scribe-border-color)}.scribe-speaker--0{background:var(--scribe-speaker-0)}.scribe-speaker--1{background:var(--scribe-speaker-1)}.scribe-speaker--2{background:var(--scribe-speaker-2)}.scribe-speaker--3{background:var(--scribe-speaker-3)}.scribe-speaker-text{margin-top:.25rem}.scribe-text{word-wrap:break-word}.scribe-text--final{color:var(--scribe-text-color)}.scribe-text--partial{color:var(--scribe-text-partial);font-style:italic}.scribe-generate-container{position:relative;margin-left:.5rem}.scribe-btn--generate{display:flex;align-items:center;gap:.5rem;padding:.75rem 1.25rem;background:var(--scribe-primary);color:#fff;border:none;border-radius:var(--scribe-border-radius);font-family:var(--scribe-font-family);font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s ease}.scribe-btn--generate:hover:not(:disabled){background:var(--scribe-primary-dark)}.scribe-btn--generate:disabled{opacity:.7;cursor:not-allowed}.scribe-template-menu{position:absolute;top:100%;left:0;margin-top:.25rem;min-width:220px;background:var(--scribe-bg);border:1px solid var(--scribe-border-color);border-radius:var(--scribe-border-radius);box-shadow:0 4px 12px #00000026;z-index:100;overflow:hidden}.scribe-template-item{display:flex;flex-direction:column;align-items:flex-start;width:100%;padding:.75rem 1rem;border:none;background:transparent;cursor:pointer;text-align:left;transition:background .15s ease}.scribe-template-item:hover{background:var(--scribe-bg-transcript)}.scribe-template-item:not(:last-child){border-bottom:1px solid var(--scribe-border-color)}.scribe-template-name{font-weight:500;color:var(--scribe-text-color)}.scribe-template-desc{font-size:.75rem;color:var(--scribe-text-partial);margin-top:.25rem}@keyframes pulse-recording{0%,to{box-shadow:0 0 #f4433666}50%{box-shadow:0 0 0 8px #f4433600}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.scribe-playback{padding:1rem}.scribe-loading,.scribe-error{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:2rem;color:var(--scribe-text-partial)}.scribe-player{display:flex;align-items:center;gap:1rem;padding:1rem;background:var(--scribe-bg-transcript);border-radius:var(--scribe-border-radius);margin-bottom:1rem}.scribe-btn--play{width:48px;height:48px;border-radius:50%;background:var(--scribe-primary);color:#fff;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s ease;flex-shrink:0}.scribe-btn--play:hover{background:var(--scribe-primary-dark)}.scribe-timeline{flex:1;position:relative}.scribe-timeline-slider{width:100%;cursor:pointer;-webkit-appearance:none;appearance:none;background:var(--scribe-level-bg);border-radius:3px;outline:none}.scribe-timeline-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;background:var(--scribe-primary);border-radius:50%;cursor:pointer}.scribe-timeline-slider::-moz-range-thumb{width:14px;height:14px;background:var(--scribe-primary);border-radius:50%;cursor:pointer;border:none}.scribe-time{position:absolute;top:20px;left:50%;transform:translate(-50%);font-size:.75rem;color:var(--scribe-text-partial);white-space:nowrap}.scribe-speed{padding:.375rem .5rem;border-radius:4px;border:1px solid var(--scribe-border-color);background:var(--scribe-bg);font-size:.875rem;cursor:pointer}.scribe-transcript-playback{max-height:400px;overflow-y:auto;scroll-behavior:smooth;padding:.5rem}.scribe-segment{padding:.75rem 1rem;margin-bottom:.5rem;border-left:3px solid var(--speaker-color, var(--scribe-border-color));background:var(--scribe-bg);border-radius:0 var(--scribe-border-radius) var(--scribe-border-radius) 0;cursor:pointer;transition:background .2s ease}.scribe-segment:hover{background:var(--scribe-bg-transcript)}.scribe-segment--active{background:#4caf501a;border-left-color:var(--scribe-primary)}.scribe-segment-text{margin:.25rem 0 0;line-height:1.6}.scribe-word{transition:background .15s ease,color .15s ease;padding:0 1px;border-radius:2px}.scribe-word:hover{background:#0000000d;cursor:pointer}.scribe-word--active{background:var(--scribe-primary);color:#fff}.scribe-speaker-label-wrapper{position:relative;display:inline-block}.scribe-speaker-label--editable{cursor:pointer;display:inline-flex;align-items:center;gap:4px;border:1px solid transparent;transition:all .15s ease}.scribe-speaker-label--editable:hover{border-color:#ffffff4d}.scribe-speaker-label--low-confidence{border-color:var(--scribe-warning, #f59e0b)!important}.scribe-chevron{opacity:.7}.scribe-warning{font-size:.875rem;margin-left:2px}.scribe-speaker-dropdown{position:absolute;top:100%;left:0;z-index:100;min-width:140px;margin-top:4px;background:var(--scribe-bg);border:1px solid var(--scribe-border-color);border-radius:6px;box-shadow:0 4px 12px #00000026;padding:4px}.scribe-dropdown-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 12px;border:none;background:none;cursor:pointer;font-family:var(--scribe-font-family);font-size:.875rem;text-align:left;border-radius:4px;color:var(--scribe-text-color)}.scribe-dropdown-item:hover{background:var(--scribe-bg-transcript)}.scribe-dropdown-icon{font-size:1rem}.scribe-dropdown-divider{height:1px;background:var(--scribe-border-color);margin:4px 0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
1201
1427
|
}
|
|
1202
1428
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: RecorderComponent, decorators: [{
|
|
1203
1429
|
type: Component,
|
|
1204
|
-
args: [{ selector: 'ngx-jaimes-scribe-recorder', standalone: true, imports: [CommonModule], template: `
|
|
1430
|
+
args: [{ selector: 'ngx-jaimes-scribe-recorder', standalone: true, imports: [CommonModule], providers: [SpeakerIdentificationService, PlaybackService], template: `
|
|
1205
1431
|
<div class="scribe-recorder">
|
|
1206
1432
|
@if (mode() === 'playback') {
|
|
1207
1433
|
<!-- Playback Mode -->
|
|
1208
1434
|
<div class="scribe-playback">
|
|
1209
|
-
@if (
|
|
1435
|
+
@if (playback.isLoading()) {
|
|
1210
1436
|
<div class="scribe-loading">
|
|
1211
1437
|
<svg class="scribe-icon scribe-icon--spinner" viewBox="0 0 24 24" width="24" height="24">
|
|
1212
1438
|
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
|
|
1213
1439
|
</svg>
|
|
1214
1440
|
<span>Carregando sessão...</span>
|
|
1215
1441
|
</div>
|
|
1216
|
-
} @else if (sessionData()) {
|
|
1442
|
+
} @else if (playback.sessionData()) {
|
|
1217
1443
|
<!-- Audio Player -->
|
|
1218
1444
|
<div class="scribe-player">
|
|
1219
|
-
<
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
<
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
<
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
</button>
|
|
1445
|
+
<button
|
|
1446
|
+
class="scribe-btn scribe-btn--play"
|
|
1447
|
+
(click)="playback.togglePlayback()"
|
|
1448
|
+
[attr.aria-label]="playback.isPlaying() ? 'Pausar' : 'Reproduzir'"
|
|
1449
|
+
>
|
|
1450
|
+
@if (playback.isPlaying()) {
|
|
1451
|
+
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1452
|
+
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
|
1453
|
+
</svg>
|
|
1454
|
+
} @else {
|
|
1455
|
+
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1456
|
+
<path d="M8 5v14l11-7z"/>
|
|
1457
|
+
</svg>
|
|
1458
|
+
}
|
|
1459
|
+
</button>
|
|
1235
1460
|
|
|
1461
|
+
<div class="scribe-timeline">
|
|
1236
1462
|
<input
|
|
1237
1463
|
type="range"
|
|
1238
1464
|
class="scribe-timeline-slider"
|
|
1239
1465
|
[min]="0"
|
|
1240
|
-
[max]="
|
|
1241
|
-
[value]="
|
|
1466
|
+
[max]="playback.duration()"
|
|
1467
|
+
[value]="playback.currentTime()"
|
|
1242
1468
|
step="0.1"
|
|
1243
|
-
(input)="
|
|
1469
|
+
(input)="onSeek($event)"
|
|
1244
1470
|
aria-label="Posição do áudio"
|
|
1245
1471
|
/>
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
[value]="playbackRate()"
|
|
1250
|
-
(change)="setPlaybackRate($event)"
|
|
1251
|
-
aria-label="Velocidade de reprodução"
|
|
1252
|
-
>
|
|
1253
|
-
<option value="0.5">0.5x</option>
|
|
1254
|
-
<option value="0.75">0.75x</option>
|
|
1255
|
-
<option value="1">1x</option>
|
|
1256
|
-
<option value="1.25">1.25x</option>
|
|
1257
|
-
<option value="1.5">1.5x</option>
|
|
1258
|
-
<option value="2">2x</option>
|
|
1259
|
-
</select>
|
|
1260
|
-
</div>
|
|
1261
|
-
<div class="scribe-time">
|
|
1262
|
-
{{ formatTimeDisplay(audioCurrentTime()) }} / {{ formatTimeDisplay(audioDuration()) }}
|
|
1472
|
+
<div class="scribe-time">
|
|
1473
|
+
{{ playback.formatTime(playback.currentTime()) }} / {{ playback.formatTime(playback.duration()) }}
|
|
1474
|
+
</div>
|
|
1263
1475
|
</div>
|
|
1476
|
+
|
|
1477
|
+
<select
|
|
1478
|
+
class="scribe-speed"
|
|
1479
|
+
[value]="playback.playbackRate()"
|
|
1480
|
+
(change)="onPlaybackRateChange($event)"
|
|
1481
|
+
aria-label="Velocidade de reprodução"
|
|
1482
|
+
>
|
|
1483
|
+
<option value="0.5">0.5x</option>
|
|
1484
|
+
<option value="0.75">0.75x</option>
|
|
1485
|
+
<option value="1">1x</option>
|
|
1486
|
+
<option value="1.25">1.25x</option>
|
|
1487
|
+
<option value="1.5">1.5x</option>
|
|
1488
|
+
<option value="2">2x</option>
|
|
1489
|
+
</select>
|
|
1264
1490
|
</div>
|
|
1265
1491
|
|
|
1266
1492
|
<!-- Transcription with Highlight -->
|
|
1267
1493
|
<div class="scribe-transcript-playback">
|
|
1268
|
-
@for (segment of segments
|
|
1494
|
+
@for (segment of playback.segments; track segment.id; let idx = $index) {
|
|
1269
1495
|
<div
|
|
1270
1496
|
class="scribe-segment"
|
|
1271
|
-
[class.scribe-segment--active]="activeSegmentId() === segment.id"
|
|
1497
|
+
[class.scribe-segment--active]="playback.activeSegmentId() === segment.id"
|
|
1272
1498
|
[style.--speaker-color]="getSpeakerColor(segment.speaker)"
|
|
1273
1499
|
[attr.data-segment-id]="segment.id"
|
|
1274
|
-
(click)="seekToSegment(segment)"
|
|
1500
|
+
(click)="playback.seekToSegment(segment)"
|
|
1275
1501
|
>
|
|
1276
1502
|
@if (segment.speaker !== undefined && shouldShowSpeakerLabel(idx)) {
|
|
1277
1503
|
<span class="scribe-speaker-label" [class]="'scribe-speaker--' + segment.speaker">
|
|
@@ -1284,8 +1510,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
1284
1510
|
@for (word of segment.words; track $index; let i = $index) {
|
|
1285
1511
|
<span
|
|
1286
1512
|
class="scribe-word"
|
|
1287
|
-
[class.scribe-word--active]="activeSegmentId() === segment.id && activeWordIndex() === i"
|
|
1288
|
-
(click)="
|
|
1513
|
+
[class.scribe-word--active]="playback.activeSegmentId() === segment.id && playback.activeWordIndex() === i"
|
|
1514
|
+
(click)="onWordClick(word, $event)"
|
|
1289
1515
|
>{{ word.word }}</span>{{ ' ' }}
|
|
1290
1516
|
}
|
|
1291
1517
|
} @else {
|
|
@@ -1294,7 +1520,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
1294
1520
|
</p>
|
|
1295
1521
|
</div>
|
|
1296
1522
|
}
|
|
1297
|
-
@if (segments
|
|
1523
|
+
@if (playback.segments.length === 0) {
|
|
1298
1524
|
<span class="scribe-placeholder">Nenhuma transcrição disponível.</span>
|
|
1299
1525
|
}
|
|
1300
1526
|
</div>
|
|
@@ -1330,17 +1556,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
1330
1556
|
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
|
|
1331
1557
|
</svg>
|
|
1332
1558
|
} @else if (isPaused()) {
|
|
1333
|
-
<!-- Play icon when paused -->
|
|
1334
1559
|
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1335
1560
|
<path d="M8 5v14l11-7z"/>
|
|
1336
1561
|
</svg>
|
|
1337
1562
|
} @else if (isRecording()) {
|
|
1338
|
-
<!-- Pause icon when recording -->
|
|
1339
1563
|
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1340
1564
|
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
|
1341
1565
|
</svg>
|
|
1342
1566
|
} @else {
|
|
1343
|
-
<!-- Mic icon when idle -->
|
|
1344
1567
|
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1345
1568
|
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5z"/>
|
|
1346
1569
|
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
|
|
@@ -1415,8 +1638,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
1415
1638
|
@for (group of speakerGroups(); track $index) {
|
|
1416
1639
|
<div class="scribe-speaker-block" [attr.data-speaker]="group.speaker">
|
|
1417
1640
|
@if (group.speaker !== undefined && group.speaker !== null) {
|
|
1418
|
-
<span class="scribe-speaker-label
|
|
1419
|
-
|
|
1641
|
+
<span class="scribe-speaker-label-wrapper">
|
|
1642
|
+
<button
|
|
1643
|
+
type="button"
|
|
1644
|
+
class="scribe-speaker-label scribe-speaker-label--editable"
|
|
1645
|
+
[class]="'scribe-speaker--' + group.speaker"
|
|
1646
|
+
[class.scribe-speaker-label--low-confidence]="speakerService.hasLowConfidence(group.speaker)"
|
|
1647
|
+
(click)="onSpeakerLabelClick($event, group.speaker)"
|
|
1648
|
+
>
|
|
1649
|
+
{{ getSpeakerLabel(group.speaker) }}
|
|
1650
|
+
<svg class="scribe-chevron" viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
|
1651
|
+
<path d="M7 10l5 5 5-5z"/>
|
|
1652
|
+
</svg>
|
|
1653
|
+
@if (speakerService.hasLowConfidence(group.speaker)) {
|
|
1654
|
+
<span class="scribe-warning" title="Confiança baixa - verifique">⚠️</span>
|
|
1655
|
+
}
|
|
1656
|
+
</button>
|
|
1657
|
+
@if (speakerService.editingSpeaker() === group.speaker) {
|
|
1658
|
+
<div class="scribe-speaker-dropdown">
|
|
1659
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'doctor')">
|
|
1660
|
+
<span class="scribe-dropdown-icon">🩺</span> Médico
|
|
1661
|
+
</button>
|
|
1662
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'patient')">
|
|
1663
|
+
<span class="scribe-dropdown-icon">🙋</span> Paciente
|
|
1664
|
+
</button>
|
|
1665
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'companion')">
|
|
1666
|
+
<span class="scribe-dropdown-icon">👥</span> Acompanhante
|
|
1667
|
+
</button>
|
|
1668
|
+
<div class="scribe-dropdown-divider"></div>
|
|
1669
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'other')">
|
|
1670
|
+
<span class="scribe-dropdown-icon">✏️</span> Outro
|
|
1671
|
+
</button>
|
|
1672
|
+
</div>
|
|
1673
|
+
}
|
|
1420
1674
|
</span>
|
|
1421
1675
|
}
|
|
1422
1676
|
<div class="scribe-speaker-text">
|
|
@@ -1443,7 +1697,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
1443
1697
|
}
|
|
1444
1698
|
}
|
|
1445
1699
|
</div>
|
|
1446
|
-
`, styles: [":host{--scribe-primary: #4caf50;--scribe-primary-dark: #388e3c;--scribe-primary-light: #81c784;--scribe-danger: #f44336;--scribe-danger-dark: #d32f2f;--scribe-text-color: #212121;--scribe-text-partial: #9e9e9e;--scribe-font-family: inherit;--scribe-font-size: 1rem;--scribe-bg: #ffffff;--scribe-bg-transcript: #f5f5f5;--scribe-border-radius: 8px;--scribe-border-color: #e0e0e0;--scribe-level-bg: #e0e0e0;--scribe-level-fill: #4caf50;--scribe-speaker-0: #2196f3;--scribe-speaker-1: #9c27b0;--scribe-speaker-2: #ff9800;--scribe-speaker-3: #009688;display:block;font-family:var(--scribe-font-family);font-size:var(--scribe-font-size)}.scribe-recorder{background:var(--scribe-bg);border-radius:var(--scribe-border-radius)}.scribe-controls{display:flex;align-items:center;gap:1rem;padding:1rem}.scribe-btn{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:.75rem;border:none;border-radius:var(--scribe-border-radius);font-family:var(--scribe-font-family);font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s ease}.scribe-btn:disabled{opacity:.5;cursor:not-allowed}.scribe-btn--mic{width:48px;height:48px;border-radius:50%;background:var(--scribe-bg-transcript);color:var(--scribe-text-color)}.scribe-btn--mic:hover:not(:disabled){background:var(--scribe-border-color)}.scribe-btn--mic.scribe-btn--active{background:var(--scribe-danger);color:#fff;animation:pulse-recording 1.5s ease-in-out infinite}.scribe-btn--mic.scribe-btn--paused{background:var(--scribe-primary);color:#fff}.scribe-btn--mic.scribe-btn--paused:hover:not(:disabled){background:var(--scribe-primary-dark)}.scribe-btn--mic.scribe-btn--connecting{background:var(--scribe-primary-light);color:#fff}.scribe-btn--finish{padding:.75rem 1.25rem;background:var(--scribe-primary);color:#fff}.scribe-btn--finish:hover:not(:disabled){background:var(--scribe-primary-dark)}.scribe-icon{flex-shrink:0}.scribe-icon--spinner{animation:spin 1s linear infinite}.scribe-level{flex:1;height:8px;background:var(--scribe-level-bg);border-radius:4px;overflow:hidden;opacity:.5;transition:opacity .2s ease}.scribe-level--active{opacity:1}.scribe-level__fill{height:100%;background:var(--scribe-level-fill);border-radius:4px;transition:width .05s ease-out}.scribe-transcript{min-height:120px;max-height:400px;overflow-y:auto;padding:1rem;margin:0 1rem 1rem;background:var(--scribe-bg-transcript);border-radius:var(--scribe-border-radius);line-height:1.6}.scribe-transcript--empty{display:flex;align-items:center;justify-content:center}.scribe-placeholder{color:var(--scribe-text-partial);font-style:italic}.scribe-speaker-block{margin-bottom:.75rem;padding-left:.5rem;border-left:3px solid var(--scribe-border-color)}.scribe-speaker-block--partial{opacity:.7}.scribe-speaker-block[data-speaker=\"0\"]{border-left-color:var(--scribe-speaker-0)}.scribe-speaker-block[data-speaker=\"1\"]{border-left-color:var(--scribe-speaker-1)}.scribe-speaker-block[data-speaker=\"2\"]{border-left-color:var(--scribe-speaker-2)}.scribe-speaker-block[data-speaker=\"3\"]{border-left-color:var(--scribe-speaker-3)}.scribe-speaker-label{display:inline-block;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px;padding:.125rem .5rem;border-radius:4px;margin-bottom:.25rem;color:#fff;background:var(--scribe-border-color)}.scribe-speaker--0{background:var(--scribe-speaker-0)}.scribe-speaker--1{background:var(--scribe-speaker-1)}.scribe-speaker--2{background:var(--scribe-speaker-2)}.scribe-speaker--3{background:var(--scribe-speaker-3)}.scribe-speaker-text{margin-top:.25rem}.scribe-text{word-wrap:break-word}.scribe-text--final{color:var(--scribe-text-color)}.scribe-text--partial{color:var(--scribe-text-partial);font-style:italic}.scribe-generate-container{position:relative;margin-left:.5rem}.scribe-btn--generate{display:flex;align-items:center;gap:.5rem;padding:.75rem 1.25rem;background:var(--scribe-primary);color:#fff;border:none;border-radius:var(--scribe-border-radius);font-family:var(--scribe-font-family);font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s ease}.scribe-btn--generate:hover:not(:disabled){background:var(--scribe-primary-dark)}.scribe-btn--generate:disabled{opacity:.7;cursor:not-allowed}.scribe-template-menu{position:absolute;top:100%;left:0;margin-top:.25rem;min-width:220px;background:var(--scribe-bg);border:1px solid var(--scribe-border-color);border-radius:var(--scribe-border-radius);box-shadow:0 4px 12px #00000026;z-index:100;overflow:hidden}.scribe-template-item{display:flex;flex-direction:column;align-items:flex-start;width:100%;padding:.75rem 1rem;border:none;background:transparent;cursor:pointer;text-align:left;transition:background .15s ease}.scribe-template-item:hover{background:var(--scribe-bg-transcript)}.scribe-template-item:not(:last-child){border-bottom:1px solid var(--scribe-border-color)}.scribe-template-name{font-weight:500;color:var(--scribe-text-color)}.scribe-template-desc{font-size:.75rem;color:var(--scribe-text-partial);margin-top:.25rem}@keyframes pulse-recording{0%,to{box-shadow:0 0 #f4433666}50%{box-shadow:0 0 0 8px #f4433600}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.scribe-playback{padding:1rem}.scribe-loading,.scribe-error{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:2rem;color:var(--scribe-text-partial)}.scribe-player{display:flex;
|
|
1700
|
+
`, styles: [":host{--scribe-primary: #4caf50;--scribe-primary-dark: #388e3c;--scribe-primary-light: #81c784;--scribe-danger: #f44336;--scribe-danger-dark: #d32f2f;--scribe-text-color: #212121;--scribe-text-partial: #9e9e9e;--scribe-font-family: inherit;--scribe-font-size: 1rem;--scribe-bg: #ffffff;--scribe-bg-transcript: #f5f5f5;--scribe-border-radius: 8px;--scribe-border-color: #e0e0e0;--scribe-level-bg: #e0e0e0;--scribe-level-fill: #4caf50;--scribe-speaker-0: #2196f3;--scribe-speaker-1: #9c27b0;--scribe-speaker-2: #ff9800;--scribe-speaker-3: #009688;display:block;font-family:var(--scribe-font-family);font-size:var(--scribe-font-size)}.scribe-recorder{background:var(--scribe-bg);border-radius:var(--scribe-border-radius)}.scribe-controls{display:flex;align-items:center;gap:1rem;padding:1rem}.scribe-btn{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:.75rem;border:none;border-radius:var(--scribe-border-radius);font-family:var(--scribe-font-family);font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s ease}.scribe-btn:disabled{opacity:.5;cursor:not-allowed}.scribe-btn--mic{width:48px;height:48px;border-radius:50%;background:var(--scribe-bg-transcript);color:var(--scribe-text-color)}.scribe-btn--mic:hover:not(:disabled){background:var(--scribe-border-color)}.scribe-btn--mic.scribe-btn--active{background:var(--scribe-danger);color:#fff;animation:pulse-recording 1.5s ease-in-out infinite}.scribe-btn--mic.scribe-btn--paused{background:var(--scribe-primary);color:#fff}.scribe-btn--mic.scribe-btn--paused:hover:not(:disabled){background:var(--scribe-primary-dark)}.scribe-btn--mic.scribe-btn--connecting{background:var(--scribe-primary-light);color:#fff}.scribe-btn--finish{padding:.75rem 1.25rem;background:var(--scribe-primary);color:#fff}.scribe-btn--finish:hover:not(:disabled){background:var(--scribe-primary-dark)}.scribe-icon{flex-shrink:0}.scribe-icon--spinner{animation:spin 1s linear infinite}.scribe-level{flex:1;height:8px;background:var(--scribe-level-bg);border-radius:4px;overflow:hidden;opacity:.5;transition:opacity .2s ease}.scribe-level--active{opacity:1}.scribe-level__fill{height:100%;background:var(--scribe-level-fill);border-radius:4px;transition:width .05s ease-out}.scribe-transcript{min-height:120px;max-height:400px;overflow-y:auto;padding:1rem;margin:0 1rem 1rem;background:var(--scribe-bg-transcript);border-radius:var(--scribe-border-radius);line-height:1.6}.scribe-transcript--empty{display:flex;align-items:center;justify-content:center}.scribe-placeholder{color:var(--scribe-text-partial);font-style:italic}.scribe-speaker-block{margin-bottom:.75rem;padding-left:.5rem;border-left:3px solid var(--scribe-border-color)}.scribe-speaker-block--partial{opacity:.7}.scribe-speaker-block[data-speaker=\"0\"]{border-left-color:var(--scribe-speaker-0)}.scribe-speaker-block[data-speaker=\"1\"]{border-left-color:var(--scribe-speaker-1)}.scribe-speaker-block[data-speaker=\"2\"]{border-left-color:var(--scribe-speaker-2)}.scribe-speaker-block[data-speaker=\"3\"]{border-left-color:var(--scribe-speaker-3)}.scribe-speaker-label{display:inline-block;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px;padding:.125rem .5rem;border-radius:4px;margin-bottom:.25rem;color:#fff;background:var(--scribe-border-color)}.scribe-speaker--0{background:var(--scribe-speaker-0)}.scribe-speaker--1{background:var(--scribe-speaker-1)}.scribe-speaker--2{background:var(--scribe-speaker-2)}.scribe-speaker--3{background:var(--scribe-speaker-3)}.scribe-speaker-text{margin-top:.25rem}.scribe-text{word-wrap:break-word}.scribe-text--final{color:var(--scribe-text-color)}.scribe-text--partial{color:var(--scribe-text-partial);font-style:italic}.scribe-generate-container{position:relative;margin-left:.5rem}.scribe-btn--generate{display:flex;align-items:center;gap:.5rem;padding:.75rem 1.25rem;background:var(--scribe-primary);color:#fff;border:none;border-radius:var(--scribe-border-radius);font-family:var(--scribe-font-family);font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s ease}.scribe-btn--generate:hover:not(:disabled){background:var(--scribe-primary-dark)}.scribe-btn--generate:disabled{opacity:.7;cursor:not-allowed}.scribe-template-menu{position:absolute;top:100%;left:0;margin-top:.25rem;min-width:220px;background:var(--scribe-bg);border:1px solid var(--scribe-border-color);border-radius:var(--scribe-border-radius);box-shadow:0 4px 12px #00000026;z-index:100;overflow:hidden}.scribe-template-item{display:flex;flex-direction:column;align-items:flex-start;width:100%;padding:.75rem 1rem;border:none;background:transparent;cursor:pointer;text-align:left;transition:background .15s ease}.scribe-template-item:hover{background:var(--scribe-bg-transcript)}.scribe-template-item:not(:last-child){border-bottom:1px solid var(--scribe-border-color)}.scribe-template-name{font-weight:500;color:var(--scribe-text-color)}.scribe-template-desc{font-size:.75rem;color:var(--scribe-text-partial);margin-top:.25rem}@keyframes pulse-recording{0%,to{box-shadow:0 0 #f4433666}50%{box-shadow:0 0 0 8px #f4433600}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.scribe-playback{padding:1rem}.scribe-loading,.scribe-error{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:2rem;color:var(--scribe-text-partial)}.scribe-player{display:flex;align-items:center;gap:1rem;padding:1rem;background:var(--scribe-bg-transcript);border-radius:var(--scribe-border-radius);margin-bottom:1rem}.scribe-btn--play{width:48px;height:48px;border-radius:50%;background:var(--scribe-primary);color:#fff;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s ease;flex-shrink:0}.scribe-btn--play:hover{background:var(--scribe-primary-dark)}.scribe-timeline{flex:1;position:relative}.scribe-timeline-slider{width:100%;cursor:pointer;-webkit-appearance:none;appearance:none;background:var(--scribe-level-bg);border-radius:3px;outline:none}.scribe-timeline-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;background:var(--scribe-primary);border-radius:50%;cursor:pointer}.scribe-timeline-slider::-moz-range-thumb{width:14px;height:14px;background:var(--scribe-primary);border-radius:50%;cursor:pointer;border:none}.scribe-time{position:absolute;top:20px;left:50%;transform:translate(-50%);font-size:.75rem;color:var(--scribe-text-partial);white-space:nowrap}.scribe-speed{padding:.375rem .5rem;border-radius:4px;border:1px solid var(--scribe-border-color);background:var(--scribe-bg);font-size:.875rem;cursor:pointer}.scribe-transcript-playback{max-height:400px;overflow-y:auto;scroll-behavior:smooth;padding:.5rem}.scribe-segment{padding:.75rem 1rem;margin-bottom:.5rem;border-left:3px solid var(--speaker-color, var(--scribe-border-color));background:var(--scribe-bg);border-radius:0 var(--scribe-border-radius) var(--scribe-border-radius) 0;cursor:pointer;transition:background .2s ease}.scribe-segment:hover{background:var(--scribe-bg-transcript)}.scribe-segment--active{background:#4caf501a;border-left-color:var(--scribe-primary)}.scribe-segment-text{margin:.25rem 0 0;line-height:1.6}.scribe-word{transition:background .15s ease,color .15s ease;padding:0 1px;border-radius:2px}.scribe-word:hover{background:#0000000d;cursor:pointer}.scribe-word--active{background:var(--scribe-primary);color:#fff}.scribe-speaker-label-wrapper{position:relative;display:inline-block}.scribe-speaker-label--editable{cursor:pointer;display:inline-flex;align-items:center;gap:4px;border:1px solid transparent;transition:all .15s ease}.scribe-speaker-label--editable:hover{border-color:#ffffff4d}.scribe-speaker-label--low-confidence{border-color:var(--scribe-warning, #f59e0b)!important}.scribe-chevron{opacity:.7}.scribe-warning{font-size:.875rem;margin-left:2px}.scribe-speaker-dropdown{position:absolute;top:100%;left:0;z-index:100;min-width:140px;margin-top:4px;background:var(--scribe-bg);border:1px solid var(--scribe-border-color);border-radius:6px;box-shadow:0 4px 12px #00000026;padding:4px}.scribe-dropdown-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 12px;border:none;background:none;cursor:pointer;font-family:var(--scribe-font-family);font-size:.875rem;text-align:left;border-radius:4px;color:var(--scribe-text-color)}.scribe-dropdown-item:hover{background:var(--scribe-bg-transcript)}.scribe-dropdown-icon{font-size:1rem}.scribe-dropdown-divider{height:1px;background:var(--scribe-border-color);margin:4px 0}\n"] }]
|
|
1447
1701
|
}], ctorParameters: () => [], propDecorators: { onDocumentClick: [{
|
|
1448
1702
|
type: HostListener,
|
|
1449
1703
|
args: ['document:click', ['$event']]
|