@medc-com-br/ngx-jaimes-scribe 0.1.8 → 0.1.12
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 +79 -13
- package/fesm2022/medc-com-br-ngx-jaimes-scribe.mjs +569 -313
- 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,17 @@ class RecorderComponent {
|
|
|
554
879
|
return groups;
|
|
555
880
|
});
|
|
556
881
|
constructor() {
|
|
557
|
-
this.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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 }) => {
|
|
568
|
-
this.socket.sendAudioChunk(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 }) => {
|
|
884
|
+
if (this.isRecording() && !this.isPaused()) {
|
|
885
|
+
this.socket.sendAudioChunk(data, isSilence);
|
|
886
|
+
}
|
|
569
887
|
}));
|
|
570
888
|
effect(() => {
|
|
571
889
|
const sid = this.sessionId();
|
|
572
890
|
const api = this.apiUrl();
|
|
573
891
|
if (sid && api && this.mode() === 'playback') {
|
|
574
|
-
this.
|
|
892
|
+
this.loadPlaybackSession();
|
|
575
893
|
}
|
|
576
894
|
});
|
|
577
895
|
effect(() => {
|
|
@@ -581,56 +899,36 @@ class RecorderComponent {
|
|
|
581
899
|
this.resumeSession(resumeId);
|
|
582
900
|
}
|
|
583
901
|
});
|
|
902
|
+
effect(() => {
|
|
903
|
+
if (this.mode() !== 'recording')
|
|
904
|
+
return;
|
|
905
|
+
const allEntries = this.entries();
|
|
906
|
+
if (this.speakerService.shouldIdentify(allEntries)) {
|
|
907
|
+
this.speakerService.scheduleIdentification(() => this.triggerIdentification());
|
|
908
|
+
}
|
|
909
|
+
});
|
|
584
910
|
}
|
|
585
911
|
ngOnDestroy() {
|
|
586
912
|
this.subscriptions.forEach((sub) => sub.unsubscribe());
|
|
587
913
|
this.generateAbortController?.abort();
|
|
588
914
|
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
|
-
}
|
|
915
|
+
this.playback.cleanup();
|
|
916
|
+
this.speakerService.reset();
|
|
611
917
|
}
|
|
612
918
|
getSpeakerLabel(speaker) {
|
|
613
|
-
|
|
614
|
-
if (customLabels[speaker]) {
|
|
615
|
-
return customLabels[speaker];
|
|
616
|
-
}
|
|
617
|
-
return SPEAKER_LABELS[speaker] || `Pessoa ${speaker + 1}`;
|
|
919
|
+
return this.speakerService.getLabel(speaker, this.speakerLabels(), this.doctorName(), this.patientName(), this.companionName());
|
|
618
920
|
}
|
|
619
921
|
shouldShowSpeakerLabel(segmentIndex) {
|
|
620
|
-
const segs = this.segments
|
|
922
|
+
const segs = this.playback.segments;
|
|
621
923
|
if (segmentIndex === 0)
|
|
622
924
|
return true;
|
|
623
|
-
|
|
624
|
-
const previousSpeaker = segs[segmentIndex - 1]?.speaker;
|
|
625
|
-
return currentSpeaker !== previousSpeaker;
|
|
925
|
+
return segs[segmentIndex]?.speaker !== segs[segmentIndex - 1]?.speaker;
|
|
626
926
|
}
|
|
627
927
|
getMicButtonLabel() {
|
|
628
|
-
if (this.isPaused())
|
|
928
|
+
if (this.isPaused())
|
|
629
929
|
return 'Retomar gravação';
|
|
630
|
-
|
|
631
|
-
if (this.isRecording()) {
|
|
930
|
+
if (this.isRecording())
|
|
632
931
|
return 'Pausar gravação';
|
|
633
|
-
}
|
|
634
932
|
return 'Iniciar gravação';
|
|
635
933
|
}
|
|
636
934
|
async toggleRecording() {
|
|
@@ -698,10 +996,7 @@ class RecorderComponent {
|
|
|
698
996
|
method: 'POST',
|
|
699
997
|
headers,
|
|
700
998
|
signal: this.generateAbortController.signal,
|
|
701
|
-
body: JSON.stringify({
|
|
702
|
-
sessionId,
|
|
703
|
-
outputSchema: template.content,
|
|
704
|
-
}),
|
|
999
|
+
body: JSON.stringify({ sessionId, outputSchema: template.content }),
|
|
705
1000
|
});
|
|
706
1001
|
if (!response.ok) {
|
|
707
1002
|
const errorData = await response.json().catch(() => ({}));
|
|
@@ -724,32 +1019,51 @@ class RecorderComponent {
|
|
|
724
1019
|
this.isGenerating.set(false);
|
|
725
1020
|
}
|
|
726
1021
|
}
|
|
727
|
-
|
|
1022
|
+
onSeek(event) {
|
|
1023
|
+
const input = event.target;
|
|
1024
|
+
if (input) {
|
|
1025
|
+
this.playback.seekTo(parseFloat(input.value));
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
onPlaybackRateChange(event) {
|
|
1029
|
+
const select = event.target;
|
|
1030
|
+
if (select) {
|
|
1031
|
+
this.playback.setRate(parseFloat(select.value));
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
onWordClick(word, event) {
|
|
1035
|
+
event.stopPropagation();
|
|
1036
|
+
this.playback.seekToWord(word);
|
|
1037
|
+
}
|
|
1038
|
+
onSpeakerLabelClick(event, speaker) {
|
|
1039
|
+
event.stopPropagation();
|
|
1040
|
+
this.speakerService.toggleMenu(speaker);
|
|
1041
|
+
}
|
|
1042
|
+
setSpeakerRole(speaker, role) {
|
|
1043
|
+
this.speakerService.setRole(speaker, role);
|
|
1044
|
+
}
|
|
1045
|
+
getSpeakerColor(speaker) {
|
|
1046
|
+
if (speaker === undefined)
|
|
1047
|
+
return 'var(--scribe-border-color)';
|
|
1048
|
+
const colors = [
|
|
1049
|
+
'var(--scribe-speaker-0)',
|
|
1050
|
+
'var(--scribe-speaker-1)',
|
|
1051
|
+
'var(--scribe-speaker-2)',
|
|
1052
|
+
'var(--scribe-speaker-3)',
|
|
1053
|
+
];
|
|
1054
|
+
return colors[speaker % colors.length];
|
|
1055
|
+
}
|
|
1056
|
+
async loadPlaybackSession() {
|
|
728
1057
|
const sid = this.sessionId();
|
|
729
1058
|
const api = this.apiUrl();
|
|
730
1059
|
if (!sid || !api)
|
|
731
1060
|
return;
|
|
732
|
-
this.isLoadingSession.set(true);
|
|
733
1061
|
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);
|
|
1062
|
+
await this.playback.loadSession(api, sid, this.token());
|
|
746
1063
|
}
|
|
747
1064
|
catch (err) {
|
|
748
1065
|
this.error.emit(err instanceof Error ? err : new Error(String(err)));
|
|
749
1066
|
}
|
|
750
|
-
finally {
|
|
751
|
-
this.isLoadingSession.set(false);
|
|
752
|
-
}
|
|
753
1067
|
}
|
|
754
1068
|
async resumeSession(sessionId) {
|
|
755
1069
|
const api = this.apiUrl();
|
|
@@ -786,125 +1100,6 @@ class RecorderComponent {
|
|
|
786
1100
|
this.isLoadingSession.set(false);
|
|
787
1101
|
}
|
|
788
1102
|
}
|
|
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
1103
|
async startRecording() {
|
|
909
1104
|
try {
|
|
910
1105
|
await this.socket.connect(this.wsUrl(), this.token(), this.premium());
|
|
@@ -954,78 +1149,83 @@ class RecorderComponent {
|
|
|
954
1149
|
this.currentSpeaker.set(event.speaker);
|
|
955
1150
|
}
|
|
956
1151
|
}
|
|
1152
|
+
async triggerIdentification() {
|
|
1153
|
+
const api = this.apiUrl() || this.lambdaUrl();
|
|
1154
|
+
const sessionId = this.socket.getSessionId() || this.lastSessionId();
|
|
1155
|
+
await this.speakerService.identify(this.entries(), api, this.token(), sessionId, this.premium());
|
|
1156
|
+
}
|
|
957
1157
|
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: `
|
|
1158
|
+
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
1159
|
<div class="scribe-recorder">
|
|
960
1160
|
@if (mode() === 'playback') {
|
|
961
1161
|
<!-- Playback Mode -->
|
|
962
1162
|
<div class="scribe-playback">
|
|
963
|
-
@if (
|
|
1163
|
+
@if (playback.isLoading()) {
|
|
964
1164
|
<div class="scribe-loading">
|
|
965
1165
|
<svg class="scribe-icon scribe-icon--spinner" viewBox="0 0 24 24" width="24" height="24">
|
|
966
1166
|
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
|
|
967
1167
|
</svg>
|
|
968
1168
|
<span>Carregando sessão...</span>
|
|
969
1169
|
</div>
|
|
970
|
-
} @else if (sessionData()) {
|
|
1170
|
+
} @else if (playback.sessionData()) {
|
|
971
1171
|
<!-- Audio Player -->
|
|
972
1172
|
<div class="scribe-player">
|
|
973
|
-
<
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
<
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
<
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
</button>
|
|
1173
|
+
<button
|
|
1174
|
+
class="scribe-btn scribe-btn--play"
|
|
1175
|
+
(click)="playback.togglePlayback()"
|
|
1176
|
+
[attr.aria-label]="playback.isPlaying() ? 'Pausar' : 'Reproduzir'"
|
|
1177
|
+
>
|
|
1178
|
+
@if (playback.isPlaying()) {
|
|
1179
|
+
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1180
|
+
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
|
1181
|
+
</svg>
|
|
1182
|
+
} @else {
|
|
1183
|
+
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1184
|
+
<path d="M8 5v14l11-7z"/>
|
|
1185
|
+
</svg>
|
|
1186
|
+
}
|
|
1187
|
+
</button>
|
|
989
1188
|
|
|
1189
|
+
<div class="scribe-timeline">
|
|
990
1190
|
<input
|
|
991
1191
|
type="range"
|
|
992
1192
|
class="scribe-timeline-slider"
|
|
993
1193
|
[min]="0"
|
|
994
|
-
[max]="
|
|
995
|
-
[value]="
|
|
1194
|
+
[max]="playback.duration()"
|
|
1195
|
+
[value]="playback.currentTime()"
|
|
996
1196
|
step="0.1"
|
|
997
|
-
(input)="
|
|
1197
|
+
(input)="onSeek($event)"
|
|
998
1198
|
aria-label="Posição do áudio"
|
|
999
1199
|
/>
|
|
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()) }}
|
|
1200
|
+
<div class="scribe-time">
|
|
1201
|
+
{{ playback.formatTime(playback.currentTime()) }} / {{ playback.formatTime(playback.duration()) }}
|
|
1202
|
+
</div>
|
|
1017
1203
|
</div>
|
|
1204
|
+
|
|
1205
|
+
<select
|
|
1206
|
+
class="scribe-speed"
|
|
1207
|
+
[value]="playback.playbackRate()"
|
|
1208
|
+
(change)="onPlaybackRateChange($event)"
|
|
1209
|
+
aria-label="Velocidade de reprodução"
|
|
1210
|
+
>
|
|
1211
|
+
<option value="0.5">0.5x</option>
|
|
1212
|
+
<option value="0.75">0.75x</option>
|
|
1213
|
+
<option value="1">1x</option>
|
|
1214
|
+
<option value="1.25">1.25x</option>
|
|
1215
|
+
<option value="1.5">1.5x</option>
|
|
1216
|
+
<option value="2">2x</option>
|
|
1217
|
+
</select>
|
|
1018
1218
|
</div>
|
|
1019
1219
|
|
|
1020
1220
|
<!-- Transcription with Highlight -->
|
|
1021
1221
|
<div class="scribe-transcript-playback">
|
|
1022
|
-
@for (segment of segments
|
|
1222
|
+
@for (segment of playback.segments; track segment.id; let idx = $index) {
|
|
1023
1223
|
<div
|
|
1024
1224
|
class="scribe-segment"
|
|
1025
|
-
[class.scribe-segment--active]="activeSegmentId() === segment.id"
|
|
1225
|
+
[class.scribe-segment--active]="playback.activeSegmentId() === segment.id"
|
|
1026
1226
|
[style.--speaker-color]="getSpeakerColor(segment.speaker)"
|
|
1027
1227
|
[attr.data-segment-id]="segment.id"
|
|
1028
|
-
(click)="seekToSegment(segment)"
|
|
1228
|
+
(click)="playback.seekToSegment(segment)"
|
|
1029
1229
|
>
|
|
1030
1230
|
@if (segment.speaker !== undefined && shouldShowSpeakerLabel(idx)) {
|
|
1031
1231
|
<span class="scribe-speaker-label" [class]="'scribe-speaker--' + segment.speaker">
|
|
@@ -1038,8 +1238,8 @@ class RecorderComponent {
|
|
|
1038
1238
|
@for (word of segment.words; track $index; let i = $index) {
|
|
1039
1239
|
<span
|
|
1040
1240
|
class="scribe-word"
|
|
1041
|
-
[class.scribe-word--active]="activeSegmentId() === segment.id && activeWordIndex() === i"
|
|
1042
|
-
(click)="
|
|
1241
|
+
[class.scribe-word--active]="playback.activeSegmentId() === segment.id && playback.activeWordIndex() === i"
|
|
1242
|
+
(click)="onWordClick(word, $event)"
|
|
1043
1243
|
>{{ word.word }}</span>{{ ' ' }}
|
|
1044
1244
|
}
|
|
1045
1245
|
} @else {
|
|
@@ -1048,7 +1248,7 @@ class RecorderComponent {
|
|
|
1048
1248
|
</p>
|
|
1049
1249
|
</div>
|
|
1050
1250
|
}
|
|
1051
|
-
@if (segments
|
|
1251
|
+
@if (playback.segments.length === 0) {
|
|
1052
1252
|
<span class="scribe-placeholder">Nenhuma transcrição disponível.</span>
|
|
1053
1253
|
}
|
|
1054
1254
|
</div>
|
|
@@ -1084,17 +1284,14 @@ class RecorderComponent {
|
|
|
1084
1284
|
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
|
|
1085
1285
|
</svg>
|
|
1086
1286
|
} @else if (isPaused()) {
|
|
1087
|
-
<!-- Play icon when paused -->
|
|
1088
1287
|
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1089
1288
|
<path d="M8 5v14l11-7z"/>
|
|
1090
1289
|
</svg>
|
|
1091
1290
|
} @else if (isRecording()) {
|
|
1092
|
-
<!-- Pause icon when recording -->
|
|
1093
1291
|
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1094
1292
|
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
|
1095
1293
|
</svg>
|
|
1096
1294
|
} @else {
|
|
1097
|
-
<!-- Mic icon when idle -->
|
|
1098
1295
|
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1099
1296
|
<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
1297
|
<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 +1366,39 @@ class RecorderComponent {
|
|
|
1169
1366
|
@for (group of speakerGroups(); track $index) {
|
|
1170
1367
|
<div class="scribe-speaker-block" [attr.data-speaker]="group.speaker">
|
|
1171
1368
|
@if (group.speaker !== undefined && group.speaker !== null) {
|
|
1172
|
-
<span class="scribe-speaker-label
|
|
1173
|
-
|
|
1369
|
+
<span class="scribe-speaker-label-wrapper">
|
|
1370
|
+
<button
|
|
1371
|
+
type="button"
|
|
1372
|
+
class="scribe-speaker-label scribe-speaker-label--editable"
|
|
1373
|
+
[class]="'scribe-speaker--' + group.speaker"
|
|
1374
|
+
[class.scribe-speaker-label--low-confidence]="speakerService.hasLowConfidence(group.speaker)"
|
|
1375
|
+
(click)="onSpeakerLabelClick($event, group.speaker)"
|
|
1376
|
+
>
|
|
1377
|
+
{{ getSpeakerLabel(group.speaker) }}
|
|
1378
|
+
<svg class="scribe-chevron" viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
|
1379
|
+
<path d="M7 10l5 5 5-5z"/>
|
|
1380
|
+
</svg>
|
|
1381
|
+
@if (speakerService.hasLowConfidence(group.speaker)) {
|
|
1382
|
+
<span class="scribe-warning" title="Confiança baixa - verifique">⚠️</span>
|
|
1383
|
+
}
|
|
1384
|
+
</button>
|
|
1385
|
+
@if (speakerService.editingSpeaker() === group.speaker) {
|
|
1386
|
+
<div class="scribe-speaker-dropdown">
|
|
1387
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'doctor')">
|
|
1388
|
+
<span class="scribe-dropdown-icon">🩺</span> Médico
|
|
1389
|
+
</button>
|
|
1390
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'patient')">
|
|
1391
|
+
<span class="scribe-dropdown-icon">🙋</span> Paciente
|
|
1392
|
+
</button>
|
|
1393
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'companion')">
|
|
1394
|
+
<span class="scribe-dropdown-icon">👥</span> Acompanhante
|
|
1395
|
+
</button>
|
|
1396
|
+
<div class="scribe-dropdown-divider"></div>
|
|
1397
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'other')">
|
|
1398
|
+
<span class="scribe-dropdown-icon">✏️</span> Outro
|
|
1399
|
+
</button>
|
|
1400
|
+
</div>
|
|
1401
|
+
}
|
|
1174
1402
|
</span>
|
|
1175
1403
|
}
|
|
1176
1404
|
<div class="scribe-speaker-text">
|
|
@@ -1197,81 +1425,81 @@ class RecorderComponent {
|
|
|
1197
1425
|
}
|
|
1198
1426
|
}
|
|
1199
1427
|
</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;
|
|
1428
|
+
`, 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
1429
|
}
|
|
1202
1430
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: RecorderComponent, decorators: [{
|
|
1203
1431
|
type: Component,
|
|
1204
|
-
args: [{ selector: 'ngx-jaimes-scribe-recorder', standalone: true, imports: [CommonModule], template: `
|
|
1432
|
+
args: [{ selector: 'ngx-jaimes-scribe-recorder', standalone: true, imports: [CommonModule], providers: [SpeakerIdentificationService, PlaybackService], template: `
|
|
1205
1433
|
<div class="scribe-recorder">
|
|
1206
1434
|
@if (mode() === 'playback') {
|
|
1207
1435
|
<!-- Playback Mode -->
|
|
1208
1436
|
<div class="scribe-playback">
|
|
1209
|
-
@if (
|
|
1437
|
+
@if (playback.isLoading()) {
|
|
1210
1438
|
<div class="scribe-loading">
|
|
1211
1439
|
<svg class="scribe-icon scribe-icon--spinner" viewBox="0 0 24 24" width="24" height="24">
|
|
1212
1440
|
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
|
|
1213
1441
|
</svg>
|
|
1214
1442
|
<span>Carregando sessão...</span>
|
|
1215
1443
|
</div>
|
|
1216
|
-
} @else if (sessionData()) {
|
|
1444
|
+
} @else if (playback.sessionData()) {
|
|
1217
1445
|
<!-- Audio Player -->
|
|
1218
1446
|
<div class="scribe-player">
|
|
1219
|
-
<
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
<
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
<
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
</button>
|
|
1447
|
+
<button
|
|
1448
|
+
class="scribe-btn scribe-btn--play"
|
|
1449
|
+
(click)="playback.togglePlayback()"
|
|
1450
|
+
[attr.aria-label]="playback.isPlaying() ? 'Pausar' : 'Reproduzir'"
|
|
1451
|
+
>
|
|
1452
|
+
@if (playback.isPlaying()) {
|
|
1453
|
+
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1454
|
+
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
|
1455
|
+
</svg>
|
|
1456
|
+
} @else {
|
|
1457
|
+
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1458
|
+
<path d="M8 5v14l11-7z"/>
|
|
1459
|
+
</svg>
|
|
1460
|
+
}
|
|
1461
|
+
</button>
|
|
1235
1462
|
|
|
1463
|
+
<div class="scribe-timeline">
|
|
1236
1464
|
<input
|
|
1237
1465
|
type="range"
|
|
1238
1466
|
class="scribe-timeline-slider"
|
|
1239
1467
|
[min]="0"
|
|
1240
|
-
[max]="
|
|
1241
|
-
[value]="
|
|
1468
|
+
[max]="playback.duration()"
|
|
1469
|
+
[value]="playback.currentTime()"
|
|
1242
1470
|
step="0.1"
|
|
1243
|
-
(input)="
|
|
1471
|
+
(input)="onSeek($event)"
|
|
1244
1472
|
aria-label="Posição do áudio"
|
|
1245
1473
|
/>
|
|
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()) }}
|
|
1474
|
+
<div class="scribe-time">
|
|
1475
|
+
{{ playback.formatTime(playback.currentTime()) }} / {{ playback.formatTime(playback.duration()) }}
|
|
1476
|
+
</div>
|
|
1263
1477
|
</div>
|
|
1478
|
+
|
|
1479
|
+
<select
|
|
1480
|
+
class="scribe-speed"
|
|
1481
|
+
[value]="playback.playbackRate()"
|
|
1482
|
+
(change)="onPlaybackRateChange($event)"
|
|
1483
|
+
aria-label="Velocidade de reprodução"
|
|
1484
|
+
>
|
|
1485
|
+
<option value="0.5">0.5x</option>
|
|
1486
|
+
<option value="0.75">0.75x</option>
|
|
1487
|
+
<option value="1">1x</option>
|
|
1488
|
+
<option value="1.25">1.25x</option>
|
|
1489
|
+
<option value="1.5">1.5x</option>
|
|
1490
|
+
<option value="2">2x</option>
|
|
1491
|
+
</select>
|
|
1264
1492
|
</div>
|
|
1265
1493
|
|
|
1266
1494
|
<!-- Transcription with Highlight -->
|
|
1267
1495
|
<div class="scribe-transcript-playback">
|
|
1268
|
-
@for (segment of segments
|
|
1496
|
+
@for (segment of playback.segments; track segment.id; let idx = $index) {
|
|
1269
1497
|
<div
|
|
1270
1498
|
class="scribe-segment"
|
|
1271
|
-
[class.scribe-segment--active]="activeSegmentId() === segment.id"
|
|
1499
|
+
[class.scribe-segment--active]="playback.activeSegmentId() === segment.id"
|
|
1272
1500
|
[style.--speaker-color]="getSpeakerColor(segment.speaker)"
|
|
1273
1501
|
[attr.data-segment-id]="segment.id"
|
|
1274
|
-
(click)="seekToSegment(segment)"
|
|
1502
|
+
(click)="playback.seekToSegment(segment)"
|
|
1275
1503
|
>
|
|
1276
1504
|
@if (segment.speaker !== undefined && shouldShowSpeakerLabel(idx)) {
|
|
1277
1505
|
<span class="scribe-speaker-label" [class]="'scribe-speaker--' + segment.speaker">
|
|
@@ -1284,8 +1512,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
1284
1512
|
@for (word of segment.words; track $index; let i = $index) {
|
|
1285
1513
|
<span
|
|
1286
1514
|
class="scribe-word"
|
|
1287
|
-
[class.scribe-word--active]="activeSegmentId() === segment.id && activeWordIndex() === i"
|
|
1288
|
-
(click)="
|
|
1515
|
+
[class.scribe-word--active]="playback.activeSegmentId() === segment.id && playback.activeWordIndex() === i"
|
|
1516
|
+
(click)="onWordClick(word, $event)"
|
|
1289
1517
|
>{{ word.word }}</span>{{ ' ' }}
|
|
1290
1518
|
}
|
|
1291
1519
|
} @else {
|
|
@@ -1294,7 +1522,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
1294
1522
|
</p>
|
|
1295
1523
|
</div>
|
|
1296
1524
|
}
|
|
1297
|
-
@if (segments
|
|
1525
|
+
@if (playback.segments.length === 0) {
|
|
1298
1526
|
<span class="scribe-placeholder">Nenhuma transcrição disponível.</span>
|
|
1299
1527
|
}
|
|
1300
1528
|
</div>
|
|
@@ -1330,17 +1558,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
1330
1558
|
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
|
|
1331
1559
|
</svg>
|
|
1332
1560
|
} @else if (isPaused()) {
|
|
1333
|
-
<!-- Play icon when paused -->
|
|
1334
1561
|
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1335
1562
|
<path d="M8 5v14l11-7z"/>
|
|
1336
1563
|
</svg>
|
|
1337
1564
|
} @else if (isRecording()) {
|
|
1338
|
-
<!-- Pause icon when recording -->
|
|
1339
1565
|
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1340
1566
|
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
|
1341
1567
|
</svg>
|
|
1342
1568
|
} @else {
|
|
1343
|
-
<!-- Mic icon when idle -->
|
|
1344
1569
|
<svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
1345
1570
|
<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
1571
|
<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 +1640,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
1415
1640
|
@for (group of speakerGroups(); track $index) {
|
|
1416
1641
|
<div class="scribe-speaker-block" [attr.data-speaker]="group.speaker">
|
|
1417
1642
|
@if (group.speaker !== undefined && group.speaker !== null) {
|
|
1418
|
-
<span class="scribe-speaker-label
|
|
1419
|
-
|
|
1643
|
+
<span class="scribe-speaker-label-wrapper">
|
|
1644
|
+
<button
|
|
1645
|
+
type="button"
|
|
1646
|
+
class="scribe-speaker-label scribe-speaker-label--editable"
|
|
1647
|
+
[class]="'scribe-speaker--' + group.speaker"
|
|
1648
|
+
[class.scribe-speaker-label--low-confidence]="speakerService.hasLowConfidence(group.speaker)"
|
|
1649
|
+
(click)="onSpeakerLabelClick($event, group.speaker)"
|
|
1650
|
+
>
|
|
1651
|
+
{{ getSpeakerLabel(group.speaker) }}
|
|
1652
|
+
<svg class="scribe-chevron" viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
|
1653
|
+
<path d="M7 10l5 5 5-5z"/>
|
|
1654
|
+
</svg>
|
|
1655
|
+
@if (speakerService.hasLowConfidence(group.speaker)) {
|
|
1656
|
+
<span class="scribe-warning" title="Confiança baixa - verifique">⚠️</span>
|
|
1657
|
+
}
|
|
1658
|
+
</button>
|
|
1659
|
+
@if (speakerService.editingSpeaker() === group.speaker) {
|
|
1660
|
+
<div class="scribe-speaker-dropdown">
|
|
1661
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'doctor')">
|
|
1662
|
+
<span class="scribe-dropdown-icon">🩺</span> Médico
|
|
1663
|
+
</button>
|
|
1664
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'patient')">
|
|
1665
|
+
<span class="scribe-dropdown-icon">🙋</span> Paciente
|
|
1666
|
+
</button>
|
|
1667
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'companion')">
|
|
1668
|
+
<span class="scribe-dropdown-icon">👥</span> Acompanhante
|
|
1669
|
+
</button>
|
|
1670
|
+
<div class="scribe-dropdown-divider"></div>
|
|
1671
|
+
<button type="button" class="scribe-dropdown-item" (click)="setSpeakerRole(group.speaker, 'other')">
|
|
1672
|
+
<span class="scribe-dropdown-icon">✏️</span> Outro
|
|
1673
|
+
</button>
|
|
1674
|
+
</div>
|
|
1675
|
+
}
|
|
1420
1676
|
</span>
|
|
1421
1677
|
}
|
|
1422
1678
|
<div class="scribe-speaker-text">
|
|
@@ -1443,7 +1699,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
1443
1699
|
}
|
|
1444
1700
|
}
|
|
1445
1701
|
</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;
|
|
1702
|
+
`, 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
1703
|
}], ctorParameters: () => [], propDecorators: { onDocumentClick: [{
|
|
1448
1704
|
type: HostListener,
|
|
1449
1705
|
args: ['document:click', ['$event']]
|