@medc-com-br/ngx-jaimes-scribe 0.1.2 → 0.1.4

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.
@@ -0,0 +1,1397 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, InjectionToken, inject, NgZone, ElementRef, input, output, signal, computed, effect, HostListener, Component } from '@angular/core';
3
+ import { BehaviorSubject, Subject } from 'rxjs';
4
+ import { CommonModule } from '@angular/common';
5
+
6
+ const DEFAULT_CONFIG$2 = {
7
+ speechThreshold: 8,
8
+ silenceThreshold: 3,
9
+ silenceDebounceMs: 1500,
10
+ };
11
+ class VadService {
12
+ state = 'idle';
13
+ silenceStartTime = null;
14
+ config = { ...DEFAULT_CONFIG$2 };
15
+ state$ = new BehaviorSubject('idle');
16
+ analyzeLevel(level) {
17
+ const now = Date.now();
18
+ if (level >= this.config.speechThreshold) {
19
+ this.state = 'speech';
20
+ this.silenceStartTime = null;
21
+ this.state$.next('speech');
22
+ return { isSilence: false };
23
+ }
24
+ if (level < this.config.silenceThreshold) {
25
+ if (this.state === 'speech') {
26
+ this.silenceStartTime = now;
27
+ this.state = 'silence_pending';
28
+ }
29
+ else if (this.state === 'silence_pending' && this.silenceStartTime) {
30
+ if (now - this.silenceStartTime >= this.config.silenceDebounceMs) {
31
+ this.state = 'silence';
32
+ this.state$.next('silence');
33
+ }
34
+ }
35
+ }
36
+ else {
37
+ if (this.state === 'silence_pending') {
38
+ this.state = 'speech';
39
+ this.silenceStartTime = null;
40
+ }
41
+ }
42
+ return { isSilence: this.state === 'silence' };
43
+ }
44
+ reset() {
45
+ this.state = 'idle';
46
+ this.silenceStartTime = null;
47
+ this.state$.next('idle');
48
+ }
49
+ getState() {
50
+ return this.state;
51
+ }
52
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: VadService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
53
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: VadService, providedIn: 'root' });
54
+ }
55
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: VadService, decorators: [{
56
+ type: Injectable,
57
+ args: [{ providedIn: 'root' }]
58
+ }] });
59
+
60
+ const AUDIO_CAPTURE_CONFIG = new InjectionToken('AUDIO_CAPTURE_CONFIG');
61
+ const DEFAULT_CONFIG$1 = {
62
+ workletUrl: '/assets/pcm-processor.js',
63
+ levelUpdateInterval: 50,
64
+ };
65
+ class AudioCaptureService {
66
+ config;
67
+ ngZone;
68
+ vadService;
69
+ audioContext = null;
70
+ workletNode = null;
71
+ analyserNode = null;
72
+ source = null;
73
+ stream = null;
74
+ workletRegistered = false;
75
+ levelInterval = null;
76
+ paused = false;
77
+ audioChunkSubject = new Subject();
78
+ audioLevelSubject = new BehaviorSubject(0);
79
+ errorSubject = new Subject();
80
+ audioChunk$ = this.audioChunkSubject.asObservable();
81
+ audioLevel$ = this.audioLevelSubject.asObservable();
82
+ error$ = this.errorSubject.asObservable();
83
+ constructor() {
84
+ const injectedConfig = inject(AUDIO_CAPTURE_CONFIG, { optional: true });
85
+ this.config = injectedConfig ?? DEFAULT_CONFIG$1;
86
+ this.ngZone = inject(NgZone);
87
+ this.vadService = inject(VadService);
88
+ }
89
+ async startCapture() {
90
+ try {
91
+ this.stream = await navigator.mediaDevices.getUserMedia({
92
+ audio: {
93
+ channelCount: 1,
94
+ echoCancellation: true,
95
+ noiseSuppression: true,
96
+ },
97
+ });
98
+ this.audioContext = new AudioContext();
99
+ if (!this.workletRegistered) {
100
+ await this.audioContext.audioWorklet.addModule(this.config.workletUrl);
101
+ this.workletRegistered = true;
102
+ }
103
+ this.workletNode = new AudioWorkletNode(this.audioContext, 'pcm-processor', {
104
+ processorOptions: {
105
+ inputSampleRate: this.audioContext.sampleRate,
106
+ },
107
+ });
108
+ this.workletNode.port.onmessage = (event) => {
109
+ if (!this.paused) {
110
+ const level = this.getAudioLevel();
111
+ const { isSilence } = this.vadService.analyzeLevel(level);
112
+ this.audioChunkSubject.next({ data: event.data, isSilence });
113
+ }
114
+ };
115
+ this.analyserNode = this.audioContext.createAnalyser();
116
+ this.analyserNode.fftSize = 256;
117
+ this.analyserNode.smoothingTimeConstant = 0.8;
118
+ this.source = this.audioContext.createMediaStreamSource(this.stream);
119
+ this.source.connect(this.analyserNode);
120
+ this.source.connect(this.workletNode);
121
+ this.startLevelMonitoring();
122
+ }
123
+ catch (error) {
124
+ const err = error instanceof Error ? error : new Error('Failed to start audio capture');
125
+ this.errorSubject.next(err);
126
+ throw err;
127
+ }
128
+ }
129
+ async stopCapture() {
130
+ this.stopLevelMonitoring();
131
+ this.paused = false;
132
+ this.vadService.reset();
133
+ if (this.workletNode) {
134
+ this.workletNode.port.close();
135
+ this.workletNode.disconnect();
136
+ this.workletNode = null;
137
+ }
138
+ if (this.analyserNode) {
139
+ this.analyserNode.disconnect();
140
+ this.analyserNode = null;
141
+ }
142
+ if (this.source) {
143
+ this.source.disconnect();
144
+ this.source = null;
145
+ }
146
+ if (this.audioContext) {
147
+ await this.audioContext.close();
148
+ this.audioContext = null;
149
+ this.workletRegistered = false;
150
+ }
151
+ if (this.stream) {
152
+ this.stream.getTracks().forEach((track) => track.stop());
153
+ this.stream = null;
154
+ }
155
+ this.audioLevelSubject.next(0);
156
+ }
157
+ pauseCapture() {
158
+ if (this.isCapturing() && !this.paused) {
159
+ this.paused = true;
160
+ }
161
+ }
162
+ resumeCapture() {
163
+ if (this.isCapturing() && this.paused) {
164
+ this.paused = false;
165
+ }
166
+ }
167
+ isCapturing() {
168
+ return this.workletNode !== null && this.stream !== null;
169
+ }
170
+ isPaused() {
171
+ return this.paused;
172
+ }
173
+ getSampleRate() {
174
+ return this.audioContext?.sampleRate ?? null;
175
+ }
176
+ startLevelMonitoring() {
177
+ this.stopLevelMonitoring();
178
+ this.ngZone.runOutsideAngular(() => {
179
+ this.levelInterval = setInterval(() => {
180
+ const level = this.getAudioLevel();
181
+ this.ngZone.run(() => {
182
+ this.audioLevelSubject.next(level);
183
+ });
184
+ }, this.config.levelUpdateInterval);
185
+ });
186
+ }
187
+ stopLevelMonitoring() {
188
+ if (this.levelInterval) {
189
+ clearInterval(this.levelInterval);
190
+ this.levelInterval = null;
191
+ }
192
+ }
193
+ getAudioLevel() {
194
+ if (!this.analyserNode) {
195
+ return 0;
196
+ }
197
+ const dataArray = new Uint8Array(this.analyserNode.frequencyBinCount);
198
+ this.analyserNode.getByteFrequencyData(dataArray);
199
+ const sum = dataArray.reduce((acc, val) => acc + val, 0);
200
+ const average = sum / dataArray.length;
201
+ return Math.round((average / 255) * 100);
202
+ }
203
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: AudioCaptureService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
204
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: AudioCaptureService, providedIn: 'root' });
205
+ }
206
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: AudioCaptureService, decorators: [{
207
+ type: Injectable,
208
+ args: [{ providedIn: 'root' }]
209
+ }], ctorParameters: () => [] });
210
+
211
+ const SCRIBE_SOCKET_CONFIG = new InjectionToken('SCRIBE_SOCKET_CONFIG');
212
+ const DEFAULT_CONFIG = {
213
+ reconnectAttempts: 5,
214
+ reconnectBaseDelay: 1000,
215
+ chunkBufferInterval: 250,
216
+ };
217
+ class ScribeSocketService {
218
+ config;
219
+ ngZone;
220
+ ws = null;
221
+ wsUrl = null;
222
+ token = null;
223
+ premium = false;
224
+ reconnectAttempt = 0;
225
+ reconnectTimeout = null;
226
+ intentionalClose = false;
227
+ chunkBuffer = [];
228
+ sendInterval = null;
229
+ keepaliveInterval = null;
230
+ currentlyInSilence = false;
231
+ connectionStateSubject = new BehaviorSubject('disconnected');
232
+ transcriptionSubject = new Subject();
233
+ errorSubject = new Subject();
234
+ sessionIdSubject = new BehaviorSubject(null);
235
+ connectionState$ = this.connectionStateSubject.asObservable();
236
+ transcription$ = this.transcriptionSubject.asObservable();
237
+ error$ = this.errorSubject.asObservable();
238
+ sessionId$ = this.sessionIdSubject.asObservable();
239
+ constructor() {
240
+ const injectedConfig = inject(SCRIBE_SOCKET_CONFIG, { optional: true });
241
+ this.config = injectedConfig ?? DEFAULT_CONFIG;
242
+ this.ngZone = inject(NgZone);
243
+ }
244
+ async connect(url, token, premium = false) {
245
+ if (this.connectionStateSubject.getValue() === 'connecting') {
246
+ throw new Error('Connection already in progress');
247
+ }
248
+ if (this.isConnected()) {
249
+ this.disconnect();
250
+ }
251
+ this.clearReconnectTimeout();
252
+ this.wsUrl = url;
253
+ this.token = token;
254
+ this.premium = premium;
255
+ this.intentionalClose = false;
256
+ this.reconnectAttempt = 0;
257
+ return this.createConnection();
258
+ }
259
+ disconnect() {
260
+ this.intentionalClose = true;
261
+ this.flushAndStopBuffering();
262
+ this.clearReconnectTimeout();
263
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
264
+ this.ws.close(1000, 'Client disconnect');
265
+ }
266
+ this.ws = null;
267
+ this.connectionStateSubject.next('disconnected');
268
+ this.sessionIdSubject.next(null);
269
+ }
270
+ sendAudioChunk(chunk, isSilence) {
271
+ const message = JSON.stringify({
272
+ type: 'audio',
273
+ isSilence,
274
+ data: this.arrayBufferToBase64(chunk),
275
+ });
276
+ this.chunkBuffer.push(message);
277
+ if (isSilence && !this.currentlyInSilence) {
278
+ this.currentlyInSilence = true;
279
+ this.startKeepaliveInterval();
280
+ }
281
+ else if (!isSilence && this.currentlyInSilence) {
282
+ this.currentlyInSilence = false;
283
+ this.stopKeepaliveInterval();
284
+ }
285
+ }
286
+ startKeepaliveInterval() {
287
+ this.stopKeepaliveInterval();
288
+ this.keepaliveInterval = setInterval(() => {
289
+ if (this.isConnected()) {
290
+ this.ws.send(JSON.stringify({ type: 'keepalive' }));
291
+ }
292
+ }, 3000);
293
+ }
294
+ stopKeepaliveInterval() {
295
+ if (this.keepaliveInterval) {
296
+ clearInterval(this.keepaliveInterval);
297
+ this.keepaliveInterval = null;
298
+ }
299
+ }
300
+ arrayBufferToBase64(buffer) {
301
+ const bytes = new Uint8Array(buffer);
302
+ let binary = '';
303
+ for (let i = 0; i < bytes.byteLength; i++) {
304
+ binary += String.fromCharCode(bytes[i]);
305
+ }
306
+ return btoa(binary);
307
+ }
308
+ isConnected() {
309
+ return this.ws?.readyState === WebSocket.OPEN;
310
+ }
311
+ getSessionId() {
312
+ return this.sessionIdSubject.getValue();
313
+ }
314
+ createConnection() {
315
+ return new Promise((resolve, reject) => {
316
+ if (!this.wsUrl) {
317
+ reject(new Error('WebSocket URL is required'));
318
+ return;
319
+ }
320
+ this.connectionStateSubject.next('connecting');
321
+ const params = new URLSearchParams({ premium: String(this.premium) });
322
+ if (this.token) {
323
+ params.set('token', this.token);
324
+ }
325
+ const url = `${this.wsUrl}?${params.toString()}`;
326
+ this.ws = new WebSocket(url);
327
+ this.ws.binaryType = 'arraybuffer';
328
+ this.ws.onopen = () => {
329
+ this.ngZone.run(() => {
330
+ this.reconnectAttempt = 0;
331
+ this.connectionStateSubject.next('connected');
332
+ this.startBuffering();
333
+ resolve();
334
+ });
335
+ };
336
+ this.ws.onerror = () => {
337
+ this.ngZone.run(() => {
338
+ const error = new Error('WebSocket connection error');
339
+ this.errorSubject.next(error);
340
+ if (this.connectionStateSubject.getValue() === 'connecting') {
341
+ reject(error);
342
+ }
343
+ });
344
+ };
345
+ this.ws.onmessage = (event) => {
346
+ this.ngZone.run(() => {
347
+ try {
348
+ const data = JSON.parse(event.data);
349
+ if (data.type === 'connected' && data.sessionId) {
350
+ this.sessionIdSubject.next(data.sessionId);
351
+ }
352
+ this.transcriptionSubject.next(data);
353
+ }
354
+ catch (e) {
355
+ const error = new Error(`Failed to parse WebSocket message: ${e instanceof Error ? e.message : String(e)}`);
356
+ console.error(error.message);
357
+ this.errorSubject.next(error);
358
+ }
359
+ });
360
+ };
361
+ this.ws.onclose = (event) => {
362
+ this.ngZone.run(() => {
363
+ this.stopBuffering();
364
+ if (!this.intentionalClose && event.code !== 1000) {
365
+ this.attemptReconnect();
366
+ }
367
+ else {
368
+ this.connectionStateSubject.next('disconnected');
369
+ this.sessionIdSubject.next(null);
370
+ }
371
+ });
372
+ };
373
+ });
374
+ }
375
+ attemptReconnect() {
376
+ if (this.reconnectAttempt >= this.config.reconnectAttempts) {
377
+ this.connectionStateSubject.next('disconnected');
378
+ this.errorSubject.next(new Error('Max reconnection attempts reached'));
379
+ return;
380
+ }
381
+ this.connectionStateSubject.next('reconnecting');
382
+ const delay = this.config.reconnectBaseDelay * Math.pow(2, this.reconnectAttempt);
383
+ this.reconnectAttempt++;
384
+ this.reconnectTimeout = setTimeout(() => {
385
+ this.createConnection().catch(() => { });
386
+ }, delay);
387
+ }
388
+ clearReconnectTimeout() {
389
+ if (this.reconnectTimeout) {
390
+ clearTimeout(this.reconnectTimeout);
391
+ this.reconnectTimeout = null;
392
+ }
393
+ }
394
+ startBuffering() {
395
+ this.stopBuffering();
396
+ this.sendInterval = setInterval(() => {
397
+ if (this.chunkBuffer.length > 0 && this.isConnected()) {
398
+ for (const msg of this.chunkBuffer) {
399
+ this.ws.send(msg);
400
+ }
401
+ this.chunkBuffer = [];
402
+ }
403
+ }, this.config.chunkBufferInterval);
404
+ }
405
+ flushAndStopBuffering() {
406
+ if (this.sendInterval) {
407
+ clearInterval(this.sendInterval);
408
+ this.sendInterval = null;
409
+ }
410
+ if (this.chunkBuffer.length > 0 && this.isConnected()) {
411
+ for (const msg of this.chunkBuffer) {
412
+ this.ws.send(msg);
413
+ }
414
+ }
415
+ this.chunkBuffer = [];
416
+ this.stopKeepaliveInterval();
417
+ this.currentlyInSilence = false;
418
+ }
419
+ stopBuffering() {
420
+ if (this.sendInterval) {
421
+ clearInterval(this.sendInterval);
422
+ this.sendInterval = null;
423
+ }
424
+ this.chunkBuffer = [];
425
+ }
426
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: ScribeSocketService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
427
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: ScribeSocketService, providedIn: 'root' });
428
+ }
429
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: ScribeSocketService, decorators: [{
430
+ type: Injectable,
431
+ args: [{ providedIn: 'root' }]
432
+ }], ctorParameters: () => [] });
433
+
434
+ const SAMPLE_RATE = 16000;
435
+ const NUM_CHANNELS = 1;
436
+ const BITS_PER_SAMPLE = 16;
437
+ function writeString(view, offset, str) {
438
+ for (let i = 0; i < str.length; i++) {
439
+ view.setUint8(offset + i, str.charCodeAt(i));
440
+ }
441
+ }
442
+ function concatBuffers(buffer1, buffer2) {
443
+ const combined = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
444
+ combined.set(new Uint8Array(buffer1), 0);
445
+ combined.set(new Uint8Array(buffer2), buffer1.byteLength);
446
+ return combined.buffer;
447
+ }
448
+ function pcmToWav(pcmData, sampleRate = SAMPLE_RATE) {
449
+ const header = new ArrayBuffer(44);
450
+ const view = new DataView(header);
451
+ const byteRate = sampleRate * NUM_CHANNELS * (BITS_PER_SAMPLE / 8);
452
+ const blockAlign = NUM_CHANNELS * (BITS_PER_SAMPLE / 8);
453
+ // RIFF chunk descriptor
454
+ writeString(view, 0, 'RIFF');
455
+ view.setUint32(4, 36 + pcmData.byteLength, true);
456
+ writeString(view, 8, 'WAVE');
457
+ // fmt sub-chunk
458
+ writeString(view, 12, 'fmt ');
459
+ view.setUint32(16, 16, true); // SubChunk1Size (16 for PCM)
460
+ view.setUint16(20, 1, true); // AudioFormat (1 = PCM)
461
+ view.setUint16(22, NUM_CHANNELS, true); // NumChannels
462
+ view.setUint32(24, sampleRate, true); // SampleRate
463
+ view.setUint32(28, byteRate, true); // ByteRate
464
+ view.setUint16(32, blockAlign, true); // BlockAlign
465
+ view.setUint16(34, BITS_PER_SAMPLE, true); // BitsPerSample
466
+ // data sub-chunk
467
+ writeString(view, 36, 'data');
468
+ view.setUint32(40, pcmData.byteLength, true);
469
+ return concatBuffers(header, pcmData);
470
+ }
471
+ function createAudioBlobUrl(wavData) {
472
+ const blob = new Blob([wavData], { type: 'audio/wav' });
473
+ return URL.createObjectURL(blob);
474
+ }
475
+ function formatTime(seconds) {
476
+ const mins = Math.floor(seconds / 60);
477
+ const secs = Math.floor(seconds % 60);
478
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
479
+ }
480
+
481
+ const SPEAKER_LABELS = {
482
+ 0: 'Pessoa 1',
483
+ 1: 'Pessoa 2',
484
+ 2: 'Pessoa 3',
485
+ 3: 'Pessoa 4',
486
+ };
487
+ class RecorderComponent {
488
+ audioCapture = inject(AudioCaptureService);
489
+ socket = inject(ScribeSocketService);
490
+ elementRef = inject(ElementRef);
491
+ subscriptions = [];
492
+ entryIdCounter = 0;
493
+ generateAbortController = null;
494
+ onDocumentClick(event) {
495
+ if (!this.showTemplateMenu())
496
+ return;
497
+ const target = event.target;
498
+ if (!this.elementRef.nativeElement.contains(target)) {
499
+ this.showTemplateMenu.set(false);
500
+ }
501
+ }
502
+ wsUrl = input('');
503
+ token = input('');
504
+ speakerLabels = input({});
505
+ premium = input(false);
506
+ templates = input([]);
507
+ lambdaUrl = input('');
508
+ sessionId = input();
509
+ apiUrl = input('');
510
+ sessionStarted = output();
511
+ sessionFinished = output();
512
+ error = output();
513
+ documentGenerated = output();
514
+ generationError = output();
515
+ isRecording = signal(false);
516
+ isPaused = signal(false);
517
+ audioLevel = signal(0);
518
+ connectionState = signal('disconnected');
519
+ entries = signal([]);
520
+ partialText = signal('');
521
+ currentSpeaker = signal(undefined);
522
+ showTemplateMenu = signal(false);
523
+ isGenerating = signal(false);
524
+ lastSessionId = signal(null);
525
+ isPlaying = signal(false);
526
+ isLoadingSession = signal(false);
527
+ audioCurrentTime = signal(0);
528
+ audioDuration = signal(0);
529
+ playbackRate = signal(1);
530
+ sessionData = signal(null);
531
+ activeSegmentId = signal(null);
532
+ activeWordIndex = signal(null);
533
+ audioElement = null;
534
+ audioBlobUrl = null;
535
+ mode = computed(() => this.sessionId() ? 'playback' : 'recording');
536
+ segments = computed(() => this.sessionData()?.transcription.segments ?? []);
537
+ hasFinishedSession = computed(() => this.lastSessionId() !== null &&
538
+ !this.isRecording() &&
539
+ !this.isPaused());
540
+ speakerGroups = computed(() => {
541
+ const allEntries = this.entries();
542
+ if (allEntries.length === 0)
543
+ return [];
544
+ const groups = [];
545
+ let currentGroup = null;
546
+ for (const entry of allEntries) {
547
+ if (!currentGroup || currentGroup.speaker !== entry.speaker) {
548
+ currentGroup = { speaker: entry.speaker, entries: [] };
549
+ groups.push(currentGroup);
550
+ }
551
+ currentGroup.entries.push(entry);
552
+ }
553
+ return groups;
554
+ });
555
+ constructor() {
556
+ this.subscriptions.push(this.audioCapture.audioLevel$.subscribe((level) => {
557
+ this.audioLevel.set(level);
558
+ }), this.socket.connectionState$.subscribe((state) => {
559
+ this.connectionState.set(state);
560
+ }), this.socket.transcription$.subscribe((event) => {
561
+ this.handleTranscription(event);
562
+ }), this.socket.error$.subscribe((err) => {
563
+ this.error.emit(err);
564
+ }), this.audioCapture.error$.subscribe((err) => {
565
+ this.error.emit(err);
566
+ }), this.audioCapture.audioChunk$.subscribe(({ data, isSilence }) => {
567
+ this.socket.sendAudioChunk(data, isSilence);
568
+ }));
569
+ effect(() => {
570
+ const sid = this.sessionId();
571
+ const api = this.apiUrl();
572
+ const tok = this.token();
573
+ if (sid && api && this.mode() === 'playback') {
574
+ this.loadSession();
575
+ }
576
+ });
577
+ }
578
+ ngOnDestroy() {
579
+ this.subscriptions.forEach((sub) => sub.unsubscribe());
580
+ this.generateAbortController?.abort();
581
+ this.cleanup();
582
+ this.cleanupPlayback();
583
+ }
584
+ audioEventHandlers = {};
585
+ cleanupPlayback() {
586
+ if (this.audioElement) {
587
+ this.audioElement.pause();
588
+ if (this.audioEventHandlers.loadedmetadata) {
589
+ this.audioElement.removeEventListener('loadedmetadata', this.audioEventHandlers.loadedmetadata);
590
+ }
591
+ if (this.audioEventHandlers.timeupdate) {
592
+ this.audioElement.removeEventListener('timeupdate', this.audioEventHandlers.timeupdate);
593
+ }
594
+ if (this.audioEventHandlers.ended) {
595
+ this.audioElement.removeEventListener('ended', this.audioEventHandlers.ended);
596
+ }
597
+ this.audioEventHandlers = {};
598
+ this.audioElement = null;
599
+ }
600
+ if (this.audioBlobUrl) {
601
+ URL.revokeObjectURL(this.audioBlobUrl);
602
+ this.audioBlobUrl = null;
603
+ }
604
+ }
605
+ getSpeakerLabel(speaker) {
606
+ const customLabels = this.speakerLabels();
607
+ if (customLabels[speaker]) {
608
+ return customLabels[speaker];
609
+ }
610
+ return SPEAKER_LABELS[speaker] || `Pessoa ${speaker + 1}`;
611
+ }
612
+ shouldShowSpeakerLabel(segmentIndex) {
613
+ const segs = this.segments();
614
+ if (segmentIndex === 0)
615
+ return true;
616
+ const currentSpeaker = segs[segmentIndex]?.speaker;
617
+ const previousSpeaker = segs[segmentIndex - 1]?.speaker;
618
+ return currentSpeaker !== previousSpeaker;
619
+ }
620
+ getMicButtonLabel() {
621
+ if (this.isPaused()) {
622
+ return 'Retomar gravação';
623
+ }
624
+ if (this.isRecording()) {
625
+ return 'Pausar gravação';
626
+ }
627
+ return 'Iniciar gravação';
628
+ }
629
+ async toggleRecording() {
630
+ if (this.isPaused()) {
631
+ this.resumeRecording();
632
+ }
633
+ else if (this.isRecording()) {
634
+ this.pauseRecording();
635
+ }
636
+ else {
637
+ await this.startRecording();
638
+ }
639
+ }
640
+ pauseRecording() {
641
+ if (!this.isRecording() || this.isPaused())
642
+ return;
643
+ this.audioCapture.pauseCapture();
644
+ this.isPaused.set(true);
645
+ }
646
+ resumeRecording() {
647
+ if (!this.isPaused())
648
+ return;
649
+ this.audioCapture.resumeCapture();
650
+ this.isPaused.set(false);
651
+ }
652
+ async finishSession() {
653
+ if (!this.isRecording() && !this.isPaused())
654
+ return;
655
+ const sessionId = this.socket.getSessionId();
656
+ await this.stopRecording();
657
+ const allEntries = this.entries();
658
+ const fullTranscript = allEntries.map((e) => e.text).join(' ');
659
+ this.sessionFinished.emit({ transcript: fullTranscript, entries: allEntries });
660
+ this.partialText.set('');
661
+ this.currentSpeaker.set(undefined);
662
+ this.lastSessionId.set(sessionId);
663
+ }
664
+ toggleTemplateMenu() {
665
+ this.showTemplateMenu.update((v) => !v);
666
+ }
667
+ async selectTemplate(template) {
668
+ if (this.isGenerating())
669
+ return;
670
+ this.showTemplateMenu.set(false);
671
+ const lambdaUrl = this.lambdaUrl();
672
+ if (!lambdaUrl) {
673
+ this.generationError.emit(new Error('Lambda URL not provided'));
674
+ return;
675
+ }
676
+ const sessionId = this.lastSessionId();
677
+ if (!sessionId) {
678
+ this.generationError.emit(new Error('No session available'));
679
+ return;
680
+ }
681
+ this.generateAbortController?.abort();
682
+ this.generateAbortController = new AbortController();
683
+ this.isGenerating.set(true);
684
+ try {
685
+ const headers = { 'Content-Type': 'application/json' };
686
+ const token = this.token();
687
+ if (token) {
688
+ headers['Authorization'] = `Bearer ${token}`;
689
+ }
690
+ const response = await fetch(`${lambdaUrl}/generate`, {
691
+ method: 'POST',
692
+ headers,
693
+ signal: this.generateAbortController.signal,
694
+ body: JSON.stringify({
695
+ sessionId,
696
+ outputSchema: template.content,
697
+ }),
698
+ });
699
+ if (!response.ok) {
700
+ const errorData = await response.json().catch(() => ({}));
701
+ throw new Error(errorData.error || `HTTP ${response.status}`);
702
+ }
703
+ const result = await response.json();
704
+ this.documentGenerated.emit({
705
+ sessionId,
706
+ templateId: template.id,
707
+ content: result.content,
708
+ generatedAt: result.generatedAt,
709
+ });
710
+ }
711
+ catch (err) {
712
+ if (err instanceof Error && err.name === 'AbortError')
713
+ return;
714
+ this.generationError.emit(err instanceof Error ? err : new Error(String(err)));
715
+ }
716
+ finally {
717
+ this.isGenerating.set(false);
718
+ }
719
+ }
720
+ async loadSession() {
721
+ const sid = this.sessionId();
722
+ const api = this.apiUrl();
723
+ if (!sid || !api)
724
+ return;
725
+ this.isLoadingSession.set(true);
726
+ try {
727
+ const headers = {};
728
+ const token = this.token();
729
+ if (token) {
730
+ headers['Authorization'] = `Bearer ${token}`;
731
+ }
732
+ const response = await fetch(`${api}/session/${sid}`, { headers });
733
+ if (!response.ok) {
734
+ throw new Error(`HTTP ${response.status}`);
735
+ }
736
+ const data = await response.json();
737
+ this.sessionData.set(data);
738
+ await this.loadAudio(data.audioUrl);
739
+ }
740
+ catch (err) {
741
+ this.error.emit(err instanceof Error ? err : new Error(String(err)));
742
+ }
743
+ finally {
744
+ this.isLoadingSession.set(false);
745
+ }
746
+ }
747
+ async loadAudio(pcmUrl) {
748
+ this.cleanupPlayback();
749
+ const response = await fetch(pcmUrl);
750
+ if (!response.ok) {
751
+ throw new Error(`Failed to load audio: HTTP ${response.status}`);
752
+ }
753
+ const pcmData = await response.arrayBuffer();
754
+ const wavData = pcmToWav(pcmData);
755
+ this.audioBlobUrl = createAudioBlobUrl(wavData);
756
+ this.audioElement = new Audio(this.audioBlobUrl);
757
+ this.audioEventHandlers.loadedmetadata = () => {
758
+ this.audioDuration.set(this.audioElement.duration);
759
+ };
760
+ this.audioEventHandlers.timeupdate = () => {
761
+ this.audioCurrentTime.set(this.audioElement.currentTime);
762
+ this.updateActiveSegment();
763
+ };
764
+ this.audioEventHandlers.ended = () => {
765
+ this.isPlaying.set(false);
766
+ };
767
+ this.audioElement.addEventListener('loadedmetadata', this.audioEventHandlers.loadedmetadata);
768
+ this.audioElement.addEventListener('timeupdate', this.audioEventHandlers.timeupdate);
769
+ this.audioElement.addEventListener('ended', this.audioEventHandlers.ended);
770
+ }
771
+ togglePlayback() {
772
+ if (!this.audioElement)
773
+ return;
774
+ if (this.isPlaying()) {
775
+ this.audioElement.pause();
776
+ this.isPlaying.set(false);
777
+ }
778
+ else {
779
+ this.audioElement.play();
780
+ this.isPlaying.set(true);
781
+ }
782
+ }
783
+ seekTo(event) {
784
+ const input = event.target;
785
+ if (!input)
786
+ return;
787
+ const time = parseFloat(input.value);
788
+ if (this.audioElement && !isNaN(time)) {
789
+ this.audioElement.currentTime = time;
790
+ this.audioCurrentTime.set(time);
791
+ }
792
+ }
793
+ setPlaybackRate(event) {
794
+ const select = event.target;
795
+ if (!select)
796
+ return;
797
+ const rate = parseFloat(select.value);
798
+ if (isNaN(rate))
799
+ return;
800
+ this.playbackRate.set(rate);
801
+ if (this.audioElement) {
802
+ this.audioElement.playbackRate = rate;
803
+ }
804
+ }
805
+ seekToSegment(segment) {
806
+ if (this.audioElement) {
807
+ this.audioElement.currentTime = segment.startTime;
808
+ this.audioCurrentTime.set(segment.startTime);
809
+ if (!this.isPlaying()) {
810
+ this.togglePlayback();
811
+ }
812
+ }
813
+ }
814
+ seekToWord(word, event) {
815
+ event.stopPropagation();
816
+ if (this.audioElement) {
817
+ this.audioElement.currentTime = word.start;
818
+ this.audioCurrentTime.set(word.start);
819
+ if (!this.isPlaying()) {
820
+ this.togglePlayback();
821
+ }
822
+ }
823
+ }
824
+ formatTimeDisplay(seconds) {
825
+ return formatTime(seconds);
826
+ }
827
+ getSpeakerColor(speaker) {
828
+ if (speaker === undefined)
829
+ return 'var(--scribe-border-color)';
830
+ const colors = [
831
+ 'var(--scribe-speaker-0)',
832
+ 'var(--scribe-speaker-1)',
833
+ 'var(--scribe-speaker-2)',
834
+ 'var(--scribe-speaker-3)',
835
+ ];
836
+ return colors[speaker % colors.length];
837
+ }
838
+ updateActiveSegment() {
839
+ const time = this.audioCurrentTime();
840
+ const segs = this.segments();
841
+ const seg = segs.find((s) => time >= s.startTime && time <= s.endTime);
842
+ this.activeSegmentId.set(seg?.id ?? null);
843
+ if (seg?.words) {
844
+ const wordIdx = seg.words.findIndex((w) => time >= w.start && time <= w.end);
845
+ this.activeWordIndex.set(wordIdx >= 0 ? wordIdx : null);
846
+ }
847
+ else {
848
+ this.activeWordIndex.set(null);
849
+ }
850
+ this.scrollToActiveSegment();
851
+ }
852
+ scrollToActiveSegment() {
853
+ const activeId = this.activeSegmentId();
854
+ if (!activeId)
855
+ return;
856
+ const container = this.elementRef.nativeElement.querySelector('.scribe-transcript-playback');
857
+ const activeEl = container?.querySelector(`[data-segment-id="${activeId}"]`);
858
+ if (activeEl && container) {
859
+ const containerRect = container.getBoundingClientRect();
860
+ const activeRect = activeEl.getBoundingClientRect();
861
+ if (activeRect.top < containerRect.top || activeRect.bottom > containerRect.bottom) {
862
+ activeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
863
+ }
864
+ }
865
+ }
866
+ async startRecording() {
867
+ try {
868
+ await this.socket.connect(this.wsUrl(), this.token(), this.premium());
869
+ await this.audioCapture.startCapture();
870
+ this.isRecording.set(true);
871
+ this.isPaused.set(false);
872
+ const sessionId = this.socket.getSessionId();
873
+ if (sessionId) {
874
+ this.sessionStarted.emit(sessionId);
875
+ }
876
+ }
877
+ catch (err) {
878
+ this.error.emit(err instanceof Error ? err : new Error('Failed to start recording'));
879
+ await this.cleanup();
880
+ }
881
+ }
882
+ async stopRecording() {
883
+ await this.audioCapture.stopCapture();
884
+ this.socket.disconnect();
885
+ this.isRecording.set(false);
886
+ this.isPaused.set(false);
887
+ }
888
+ async cleanup() {
889
+ await this.audioCapture.stopCapture();
890
+ this.socket.disconnect();
891
+ this.isRecording.set(false);
892
+ this.isPaused.set(false);
893
+ }
894
+ handleTranscription(event) {
895
+ if (!event.transcript)
896
+ return;
897
+ if (event.type === 'final') {
898
+ const entry = {
899
+ id: ++this.entryIdCounter,
900
+ text: event.transcript,
901
+ speaker: event.speaker,
902
+ isFinal: true,
903
+ startTime: event.start,
904
+ endTime: event.end,
905
+ };
906
+ this.entries.update((entries) => [...entries, entry]);
907
+ this.partialText.set('');
908
+ this.currentSpeaker.set(undefined);
909
+ }
910
+ else if (event.type === 'partial') {
911
+ this.partialText.set(event.transcript);
912
+ this.currentSpeaker.set(event.speaker);
913
+ }
914
+ }
915
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: RecorderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
916
+ 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 } }, outputs: { sessionStarted: "sessionStarted", sessionFinished: "sessionFinished", error: "error", documentGenerated: "documentGenerated", generationError: "generationError" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, ngImport: i0, template: `
917
+ <div class="scribe-recorder">
918
+ @if (mode() === 'playback') {
919
+ <!-- Playback Mode -->
920
+ <div class="scribe-playback">
921
+ @if (isLoadingSession()) {
922
+ <div class="scribe-loading">
923
+ <svg class="scribe-icon scribe-icon--spinner" viewBox="0 0 24 24" width="24" height="24">
924
+ <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
925
+ </svg>
926
+ <span>Carregando sessão...</span>
927
+ </div>
928
+ } @else if (sessionData()) {
929
+ <!-- Audio Player -->
930
+ <div class="scribe-player">
931
+ <button
932
+ class="scribe-btn scribe-btn--play"
933
+ (click)="togglePlayback()"
934
+ [attr.aria-label]="isPlaying() ? 'Pausar' : 'Reproduzir'"
935
+ >
936
+ @if (isPlaying()) {
937
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
938
+ <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
939
+ </svg>
940
+ } @else {
941
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
942
+ <path d="M8 5v14l11-7z"/>
943
+ </svg>
944
+ }
945
+ </button>
946
+
947
+ <div class="scribe-timeline">
948
+ <input
949
+ type="range"
950
+ class="scribe-timeline-slider"
951
+ [min]="0"
952
+ [max]="audioDuration()"
953
+ [value]="audioCurrentTime()"
954
+ step="0.1"
955
+ (input)="seekTo($event)"
956
+ aria-label="Posição do áudio"
957
+ />
958
+ <div class="scribe-time">
959
+ {{ formatTimeDisplay(audioCurrentTime()) }} / {{ formatTimeDisplay(audioDuration()) }}
960
+ </div>
961
+ </div>
962
+
963
+ <select
964
+ class="scribe-speed"
965
+ [value]="playbackRate()"
966
+ (change)="setPlaybackRate($event)"
967
+ aria-label="Velocidade de reprodução"
968
+ >
969
+ <option value="0.5">0.5x</option>
970
+ <option value="0.75">0.75x</option>
971
+ <option value="1">1x</option>
972
+ <option value="1.25">1.25x</option>
973
+ <option value="1.5">1.5x</option>
974
+ <option value="2">2x</option>
975
+ </select>
976
+ </div>
977
+
978
+ <!-- Transcription with Highlight -->
979
+ <div class="scribe-transcript-playback">
980
+ @for (segment of segments(); track segment.id; let idx = $index) {
981
+ <div
982
+ class="scribe-segment"
983
+ [class.scribe-segment--active]="activeSegmentId() === segment.id"
984
+ [style.--speaker-color]="getSpeakerColor(segment.speaker)"
985
+ [attr.data-segment-id]="segment.id"
986
+ (click)="seekToSegment(segment)"
987
+ >
988
+ @if (segment.speaker !== undefined && shouldShowSpeakerLabel(idx)) {
989
+ <span class="scribe-speaker-label" [class]="'scribe-speaker--' + segment.speaker">
990
+ {{ getSpeakerLabel(segment.speaker) }}
991
+ </span>
992
+ }
993
+
994
+ <p class="scribe-segment-text">
995
+ @if (segment.words?.length) {
996
+ @for (word of segment.words; track $index; let i = $index) {
997
+ <span
998
+ class="scribe-word"
999
+ [class.scribe-word--active]="activeSegmentId() === segment.id && activeWordIndex() === i"
1000
+ (click)="seekToWord(word, $event)"
1001
+ >{{ word.word }}</span>{{ ' ' }}
1002
+ }
1003
+ } @else {
1004
+ {{ segment.text }}
1005
+ }
1006
+ </p>
1007
+ </div>
1008
+ }
1009
+ @if (segments().length === 0) {
1010
+ <span class="scribe-placeholder">Nenhuma transcrição disponível.</span>
1011
+ }
1012
+ </div>
1013
+ } @else {
1014
+ <div class="scribe-error">
1015
+ <span>Não foi possível carregar a sessão.</span>
1016
+ </div>
1017
+ }
1018
+ </div>
1019
+ } @else {
1020
+ <!-- Recording Mode -->
1021
+ <div class="scribe-controls">
1022
+ @if (!hasFinishedSession()) {
1023
+ <button
1024
+ class="scribe-btn scribe-btn--mic"
1025
+ [class.scribe-btn--active]="isRecording() && !isPaused()"
1026
+ [class.scribe-btn--paused]="isPaused()"
1027
+ [class.scribe-btn--connecting]="connectionState() === 'connecting'"
1028
+ [disabled]="connectionState() === 'connecting'"
1029
+ (click)="toggleRecording()"
1030
+ [attr.aria-label]="getMicButtonLabel()"
1031
+ >
1032
+ @if (connectionState() === 'connecting') {
1033
+ <svg class="scribe-icon scribe-icon--spinner" viewBox="0 0 24 24" width="24" height="24">
1034
+ <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
1035
+ </svg>
1036
+ } @else if (isPaused()) {
1037
+ <!-- Play icon when paused -->
1038
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
1039
+ <path d="M8 5v14l11-7z"/>
1040
+ </svg>
1041
+ } @else if (isRecording()) {
1042
+ <!-- Pause icon when recording -->
1043
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
1044
+ <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
1045
+ </svg>
1046
+ } @else {
1047
+ <!-- Mic icon when idle -->
1048
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
1049
+ <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"/>
1050
+ <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"/>
1051
+ </svg>
1052
+ }
1053
+ </button>
1054
+
1055
+ <div class="scribe-level" [class.scribe-level--active]="isRecording() && !isPaused()">
1056
+ <div class="scribe-level__fill" [style.width.%]="isPaused() ? 0 : audioLevel()"></div>
1057
+ </div>
1058
+
1059
+ <button
1060
+ class="scribe-btn scribe-btn--finish"
1061
+ [disabled]="!isRecording() && !isPaused()"
1062
+ (click)="finishSession()"
1063
+ aria-label="Finalizar gravação"
1064
+ >
1065
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
1066
+ <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
1067
+ </svg>
1068
+ <span>Finalizar</span>
1069
+ </button>
1070
+ }
1071
+
1072
+ @if (hasFinishedSession() && templates().length > 0) {
1073
+ <div class="scribe-generate-container">
1074
+ <button
1075
+ class="scribe-btn scribe-btn--generate"
1076
+ [disabled]="isGenerating()"
1077
+ [attr.aria-expanded]="showTemplateMenu()"
1078
+ aria-haspopup="menu"
1079
+ (click)="toggleTemplateMenu()"
1080
+ aria-label="Gerar resumo"
1081
+ >
1082
+ @if (isGenerating()) {
1083
+ <svg class="scribe-icon scribe-icon--spinner" viewBox="0 0 24 24" width="20" height="20">
1084
+ <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
1085
+ </svg>
1086
+ <span>Gerando...</span>
1087
+ } @else {
1088
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
1089
+ <path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11zm-3-7h-2v2h-2v-2H9v-2h2V9h2v2h2v2z"/>
1090
+ </svg>
1091
+ <span>Gerar Resumo</span>
1092
+ }
1093
+ </button>
1094
+
1095
+ @if (showTemplateMenu()) {
1096
+ <div class="scribe-template-menu" role="menu">
1097
+ @for (template of templates(); track template.id) {
1098
+ <button
1099
+ class="scribe-template-item"
1100
+ role="menuitem"
1101
+ (click)="selectTemplate(template)"
1102
+ >
1103
+ <span class="scribe-template-name">{{ template.name }}</span>
1104
+ @if (template.description) {
1105
+ <span class="scribe-template-desc">{{ template.description }}</span>
1106
+ }
1107
+ </button>
1108
+ }
1109
+ </div>
1110
+ }
1111
+ </div>
1112
+ }
1113
+ </div>
1114
+
1115
+ <div class="scribe-transcript" [class.scribe-transcript--empty]="speakerGroups().length === 0 && !partialText()">
1116
+ @if (speakerGroups().length === 0 && !partialText()) {
1117
+ <span class="scribe-placeholder">A transcrição aparecerá aqui...</span>
1118
+ } @else {
1119
+ @for (group of speakerGroups(); track $index) {
1120
+ <div class="scribe-speaker-block" [attr.data-speaker]="group.speaker">
1121
+ @if (group.speaker !== undefined && group.speaker !== null) {
1122
+ <span class="scribe-speaker-label" [class]="'scribe-speaker--' + group.speaker">
1123
+ {{ getSpeakerLabel(group.speaker) }}
1124
+ </span>
1125
+ }
1126
+ <div class="scribe-speaker-text">
1127
+ @for (entry of group.entries; track entry.id) {
1128
+ <span class="scribe-text scribe-text--final">{{ entry.text }} </span>
1129
+ }
1130
+ </div>
1131
+ </div>
1132
+ }
1133
+ @if (partialText()) {
1134
+ <div class="scribe-speaker-block scribe-speaker-block--partial">
1135
+ @if (currentSpeaker() !== undefined && currentSpeaker() !== null) {
1136
+ <span class="scribe-speaker-label" [class]="'scribe-speaker--' + currentSpeaker()">
1137
+ {{ getSpeakerLabel(currentSpeaker()!) }}
1138
+ </span>
1139
+ }
1140
+ <div class="scribe-speaker-text">
1141
+ <span class="scribe-text scribe-text--partial">{{ partialText() }}</span>
1142
+ </div>
1143
+ </div>
1144
+ }
1145
+ }
1146
+ </div>
1147
+ }
1148
+ </div>
1149
+ `, 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;display:flex;flex-direction:column;gap:.25rem}.scribe-timeline-slider{width:100%;cursor:pointer;height:6px;-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{font-size:.75rem;color:var(--scribe-text-partial)}.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}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
1150
+ }
1151
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: RecorderComponent, decorators: [{
1152
+ type: Component,
1153
+ args: [{ selector: 'ngx-jaimes-scribe-recorder', standalone: true, imports: [CommonModule], template: `
1154
+ <div class="scribe-recorder">
1155
+ @if (mode() === 'playback') {
1156
+ <!-- Playback Mode -->
1157
+ <div class="scribe-playback">
1158
+ @if (isLoadingSession()) {
1159
+ <div class="scribe-loading">
1160
+ <svg class="scribe-icon scribe-icon--spinner" viewBox="0 0 24 24" width="24" height="24">
1161
+ <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
1162
+ </svg>
1163
+ <span>Carregando sessão...</span>
1164
+ </div>
1165
+ } @else if (sessionData()) {
1166
+ <!-- Audio Player -->
1167
+ <div class="scribe-player">
1168
+ <button
1169
+ class="scribe-btn scribe-btn--play"
1170
+ (click)="togglePlayback()"
1171
+ [attr.aria-label]="isPlaying() ? 'Pausar' : 'Reproduzir'"
1172
+ >
1173
+ @if (isPlaying()) {
1174
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
1175
+ <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
1176
+ </svg>
1177
+ } @else {
1178
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
1179
+ <path d="M8 5v14l11-7z"/>
1180
+ </svg>
1181
+ }
1182
+ </button>
1183
+
1184
+ <div class="scribe-timeline">
1185
+ <input
1186
+ type="range"
1187
+ class="scribe-timeline-slider"
1188
+ [min]="0"
1189
+ [max]="audioDuration()"
1190
+ [value]="audioCurrentTime()"
1191
+ step="0.1"
1192
+ (input)="seekTo($event)"
1193
+ aria-label="Posição do áudio"
1194
+ />
1195
+ <div class="scribe-time">
1196
+ {{ formatTimeDisplay(audioCurrentTime()) }} / {{ formatTimeDisplay(audioDuration()) }}
1197
+ </div>
1198
+ </div>
1199
+
1200
+ <select
1201
+ class="scribe-speed"
1202
+ [value]="playbackRate()"
1203
+ (change)="setPlaybackRate($event)"
1204
+ aria-label="Velocidade de reprodução"
1205
+ >
1206
+ <option value="0.5">0.5x</option>
1207
+ <option value="0.75">0.75x</option>
1208
+ <option value="1">1x</option>
1209
+ <option value="1.25">1.25x</option>
1210
+ <option value="1.5">1.5x</option>
1211
+ <option value="2">2x</option>
1212
+ </select>
1213
+ </div>
1214
+
1215
+ <!-- Transcription with Highlight -->
1216
+ <div class="scribe-transcript-playback">
1217
+ @for (segment of segments(); track segment.id; let idx = $index) {
1218
+ <div
1219
+ class="scribe-segment"
1220
+ [class.scribe-segment--active]="activeSegmentId() === segment.id"
1221
+ [style.--speaker-color]="getSpeakerColor(segment.speaker)"
1222
+ [attr.data-segment-id]="segment.id"
1223
+ (click)="seekToSegment(segment)"
1224
+ >
1225
+ @if (segment.speaker !== undefined && shouldShowSpeakerLabel(idx)) {
1226
+ <span class="scribe-speaker-label" [class]="'scribe-speaker--' + segment.speaker">
1227
+ {{ getSpeakerLabel(segment.speaker) }}
1228
+ </span>
1229
+ }
1230
+
1231
+ <p class="scribe-segment-text">
1232
+ @if (segment.words?.length) {
1233
+ @for (word of segment.words; track $index; let i = $index) {
1234
+ <span
1235
+ class="scribe-word"
1236
+ [class.scribe-word--active]="activeSegmentId() === segment.id && activeWordIndex() === i"
1237
+ (click)="seekToWord(word, $event)"
1238
+ >{{ word.word }}</span>{{ ' ' }}
1239
+ }
1240
+ } @else {
1241
+ {{ segment.text }}
1242
+ }
1243
+ </p>
1244
+ </div>
1245
+ }
1246
+ @if (segments().length === 0) {
1247
+ <span class="scribe-placeholder">Nenhuma transcrição disponível.</span>
1248
+ }
1249
+ </div>
1250
+ } @else {
1251
+ <div class="scribe-error">
1252
+ <span>Não foi possível carregar a sessão.</span>
1253
+ </div>
1254
+ }
1255
+ </div>
1256
+ } @else {
1257
+ <!-- Recording Mode -->
1258
+ <div class="scribe-controls">
1259
+ @if (!hasFinishedSession()) {
1260
+ <button
1261
+ class="scribe-btn scribe-btn--mic"
1262
+ [class.scribe-btn--active]="isRecording() && !isPaused()"
1263
+ [class.scribe-btn--paused]="isPaused()"
1264
+ [class.scribe-btn--connecting]="connectionState() === 'connecting'"
1265
+ [disabled]="connectionState() === 'connecting'"
1266
+ (click)="toggleRecording()"
1267
+ [attr.aria-label]="getMicButtonLabel()"
1268
+ >
1269
+ @if (connectionState() === 'connecting') {
1270
+ <svg class="scribe-icon scribe-icon--spinner" viewBox="0 0 24 24" width="24" height="24">
1271
+ <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
1272
+ </svg>
1273
+ } @else if (isPaused()) {
1274
+ <!-- Play icon when paused -->
1275
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
1276
+ <path d="M8 5v14l11-7z"/>
1277
+ </svg>
1278
+ } @else if (isRecording()) {
1279
+ <!-- Pause icon when recording -->
1280
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
1281
+ <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
1282
+ </svg>
1283
+ } @else {
1284
+ <!-- Mic icon when idle -->
1285
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
1286
+ <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"/>
1287
+ <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"/>
1288
+ </svg>
1289
+ }
1290
+ </button>
1291
+
1292
+ <div class="scribe-level" [class.scribe-level--active]="isRecording() && !isPaused()">
1293
+ <div class="scribe-level__fill" [style.width.%]="isPaused() ? 0 : audioLevel()"></div>
1294
+ </div>
1295
+
1296
+ <button
1297
+ class="scribe-btn scribe-btn--finish"
1298
+ [disabled]="!isRecording() && !isPaused()"
1299
+ (click)="finishSession()"
1300
+ aria-label="Finalizar gravação"
1301
+ >
1302
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
1303
+ <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
1304
+ </svg>
1305
+ <span>Finalizar</span>
1306
+ </button>
1307
+ }
1308
+
1309
+ @if (hasFinishedSession() && templates().length > 0) {
1310
+ <div class="scribe-generate-container">
1311
+ <button
1312
+ class="scribe-btn scribe-btn--generate"
1313
+ [disabled]="isGenerating()"
1314
+ [attr.aria-expanded]="showTemplateMenu()"
1315
+ aria-haspopup="menu"
1316
+ (click)="toggleTemplateMenu()"
1317
+ aria-label="Gerar resumo"
1318
+ >
1319
+ @if (isGenerating()) {
1320
+ <svg class="scribe-icon scribe-icon--spinner" viewBox="0 0 24 24" width="20" height="20">
1321
+ <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
1322
+ </svg>
1323
+ <span>Gerando...</span>
1324
+ } @else {
1325
+ <svg class="scribe-icon" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
1326
+ <path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11zm-3-7h-2v2h-2v-2H9v-2h2V9h2v2h2v2z"/>
1327
+ </svg>
1328
+ <span>Gerar Resumo</span>
1329
+ }
1330
+ </button>
1331
+
1332
+ @if (showTemplateMenu()) {
1333
+ <div class="scribe-template-menu" role="menu">
1334
+ @for (template of templates(); track template.id) {
1335
+ <button
1336
+ class="scribe-template-item"
1337
+ role="menuitem"
1338
+ (click)="selectTemplate(template)"
1339
+ >
1340
+ <span class="scribe-template-name">{{ template.name }}</span>
1341
+ @if (template.description) {
1342
+ <span class="scribe-template-desc">{{ template.description }}</span>
1343
+ }
1344
+ </button>
1345
+ }
1346
+ </div>
1347
+ }
1348
+ </div>
1349
+ }
1350
+ </div>
1351
+
1352
+ <div class="scribe-transcript" [class.scribe-transcript--empty]="speakerGroups().length === 0 && !partialText()">
1353
+ @if (speakerGroups().length === 0 && !partialText()) {
1354
+ <span class="scribe-placeholder">A transcrição aparecerá aqui...</span>
1355
+ } @else {
1356
+ @for (group of speakerGroups(); track $index) {
1357
+ <div class="scribe-speaker-block" [attr.data-speaker]="group.speaker">
1358
+ @if (group.speaker !== undefined && group.speaker !== null) {
1359
+ <span class="scribe-speaker-label" [class]="'scribe-speaker--' + group.speaker">
1360
+ {{ getSpeakerLabel(group.speaker) }}
1361
+ </span>
1362
+ }
1363
+ <div class="scribe-speaker-text">
1364
+ @for (entry of group.entries; track entry.id) {
1365
+ <span class="scribe-text scribe-text--final">{{ entry.text }} </span>
1366
+ }
1367
+ </div>
1368
+ </div>
1369
+ }
1370
+ @if (partialText()) {
1371
+ <div class="scribe-speaker-block scribe-speaker-block--partial">
1372
+ @if (currentSpeaker() !== undefined && currentSpeaker() !== null) {
1373
+ <span class="scribe-speaker-label" [class]="'scribe-speaker--' + currentSpeaker()">
1374
+ {{ getSpeakerLabel(currentSpeaker()!) }}
1375
+ </span>
1376
+ }
1377
+ <div class="scribe-speaker-text">
1378
+ <span class="scribe-text scribe-text--partial">{{ partialText() }}</span>
1379
+ </div>
1380
+ </div>
1381
+ }
1382
+ }
1383
+ </div>
1384
+ }
1385
+ </div>
1386
+ `, 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;display:flex;flex-direction:column;gap:.25rem}.scribe-timeline-slider{width:100%;cursor:pointer;height:6px;-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{font-size:.75rem;color:var(--scribe-text-partial)}.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}\n"] }]
1387
+ }], ctorParameters: () => [], propDecorators: { onDocumentClick: [{
1388
+ type: HostListener,
1389
+ args: ['document:click', ['$event']]
1390
+ }] } });
1391
+
1392
+ /**
1393
+ * Generated bundle index. Do not edit.
1394
+ */
1395
+
1396
+ export { AUDIO_CAPTURE_CONFIG, AudioCaptureService, RecorderComponent, SCRIBE_SOCKET_CONFIG, ScribeSocketService, VadService };
1397
+ //# sourceMappingURL=medc-com-br-ngx-jaimes-scribe.mjs.map