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

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.
@@ -1,743 +0,0 @@
1
- import { Component, inject, signal, input, output, OnDestroy, computed, HostListener, ElementRef } from '@angular/core';
2
- import { CommonModule } from '@angular/common';
3
- import { Subscription } from 'rxjs';
4
- import { AudioCaptureService } from '../../services/audio-capture.service';
5
- import { ScribeSocketService, ConnectionState } from '../../services/scribe-socket.service';
6
- import type { TranscriptionEvent, TemplateOption, GeneratedDocument } from '@medc-com-br/jaimes-shared';
7
-
8
- interface TranscriptEntry {
9
- id: number;
10
- text: string;
11
- speaker?: number;
12
- isFinal: boolean;
13
- startTime?: number;
14
- endTime?: number;
15
- }
16
-
17
- interface SpeakerGroup {
18
- speaker?: number;
19
- entries: TranscriptEntry[];
20
- }
21
-
22
- const SPEAKER_LABELS: Record<number, string> = {
23
- 0: 'Pessoa 1',
24
- 1: 'Pessoa 2',
25
- 2: 'Pessoa 3',
26
- 3: 'Pessoa 4',
27
- };
28
-
29
- @Component({
30
- selector: 'ngx-jaimes-scribe-recorder',
31
- standalone: true,
32
- imports: [CommonModule],
33
- template: `
34
- <div class="scribe-recorder">
35
- <div class="scribe-controls">
36
- <button
37
- class="scribe-btn scribe-btn--mic"
38
- [class.scribe-btn--active]="isRecording() && !isPaused()"
39
- [class.scribe-btn--paused]="isPaused()"
40
- [class.scribe-btn--connecting]="connectionState() === 'connecting'"
41
- [disabled]="connectionState() === 'connecting'"
42
- (click)="toggleRecording()"
43
- [attr.aria-label]="getMicButtonLabel()"
44
- >
45
- @if (connectionState() === 'connecting') {
46
- <svg class="scribe-icon scribe-icon--spinner" viewBox="0 0 24 24" width="24" height="24">
47
- <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
48
- </svg>
49
- } @else if (isPaused()) {
50
- <!-- Play icon when paused -->
51
- <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
52
- <path d="M8 5v14l11-7z"/>
53
- </svg>
54
- } @else if (isRecording()) {
55
- <!-- Pause icon when recording -->
56
- <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
57
- <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
58
- </svg>
59
- } @else {
60
- <!-- Mic icon when idle -->
61
- <svg class="scribe-icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
62
- <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"/>
63
- <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"/>
64
- </svg>
65
- }
66
- </button>
67
-
68
- <div class="scribe-level" [class.scribe-level--active]="isRecording() && !isPaused()">
69
- <div class="scribe-level__fill" [style.width.%]="isPaused() ? 0 : audioLevel()"></div>
70
- </div>
71
-
72
- <button
73
- class="scribe-btn scribe-btn--finish"
74
- [disabled]="!isRecording() && !isPaused()"
75
- (click)="finishSession()"
76
- aria-label="Finalizar gravação"
77
- >
78
- <svg class="scribe-icon" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
79
- <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
80
- </svg>
81
- <span>Finalizar</span>
82
- </button>
83
-
84
- @if (hasFinishedSession() && templates().length > 0) {
85
- <div class="scribe-generate-container">
86
- <button
87
- class="scribe-btn scribe-btn--generate"
88
- [disabled]="isGenerating()"
89
- [attr.aria-expanded]="showTemplateMenu()"
90
- aria-haspopup="menu"
91
- (click)="toggleTemplateMenu()"
92
- aria-label="Gerar resumo"
93
- >
94
- @if (isGenerating()) {
95
- <svg class="scribe-icon scribe-icon--spinner" viewBox="0 0 24 24" width="20" height="20">
96
- <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="32" stroke-linecap="round"/>
97
- </svg>
98
- <span>Gerando...</span>
99
- } @else {
100
- <svg class="scribe-icon" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
101
- <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"/>
102
- </svg>
103
- <span>Gerar Resumo</span>
104
- }
105
- </button>
106
-
107
- @if (showTemplateMenu()) {
108
- <div class="scribe-template-menu" role="menu">
109
- @for (template of templates(); track template.id) {
110
- <button
111
- class="scribe-template-item"
112
- role="menuitem"
113
- (click)="selectTemplate(template)"
114
- >
115
- <span class="scribe-template-name">{{ template.name }}</span>
116
- @if (template.description) {
117
- <span class="scribe-template-desc">{{ template.description }}</span>
118
- }
119
- </button>
120
- }
121
- </div>
122
- }
123
- </div>
124
- }
125
- </div>
126
-
127
- <div class="scribe-transcript" [class.scribe-transcript--empty]="speakerGroups().length === 0 && !partialText()">
128
- @if (speakerGroups().length === 0 && !partialText()) {
129
- <span class="scribe-placeholder">A transcrição aparecerá aqui...</span>
130
- } @else {
131
- @for (group of speakerGroups(); track $index) {
132
- <div class="scribe-speaker-block" [attr.data-speaker]="group.speaker">
133
- @if (group.speaker !== undefined && group.speaker !== null) {
134
- <span class="scribe-speaker-label" [class]="'scribe-speaker--' + group.speaker">
135
- {{ getSpeakerLabel(group.speaker) }}
136
- </span>
137
- }
138
- <div class="scribe-speaker-text">
139
- @for (entry of group.entries; track entry.id) {
140
- <span class="scribe-text scribe-text--final">{{ entry.text }} </span>
141
- }
142
- </div>
143
- </div>
144
- }
145
- @if (partialText()) {
146
- <div class="scribe-speaker-block scribe-speaker-block--partial">
147
- @if (currentSpeaker() !== undefined && currentSpeaker() !== null) {
148
- <span class="scribe-speaker-label" [class]="'scribe-speaker--' + currentSpeaker()">
149
- {{ getSpeakerLabel(currentSpeaker()!) }}
150
- </span>
151
- }
152
- <div class="scribe-speaker-text">
153
- <span class="scribe-text scribe-text--partial">{{ partialText() }}</span>
154
- </div>
155
- </div>
156
- }
157
- }
158
- </div>
159
- </div>
160
- `,
161
- styles: [`
162
- :host {
163
- --scribe-primary: #4caf50;
164
- --scribe-primary-dark: #388e3c;
165
- --scribe-primary-light: #81c784;
166
- --scribe-danger: #f44336;
167
- --scribe-danger-dark: #d32f2f;
168
- --scribe-text-color: #212121;
169
- --scribe-text-partial: #9e9e9e;
170
- --scribe-font-family: inherit;
171
- --scribe-font-size: 1rem;
172
- --scribe-bg: #ffffff;
173
- --scribe-bg-transcript: #f5f5f5;
174
- --scribe-border-radius: 8px;
175
- --scribe-border-color: #e0e0e0;
176
- --scribe-level-bg: #e0e0e0;
177
- --scribe-level-fill: #4caf50;
178
-
179
- --scribe-speaker-0: #2196f3;
180
- --scribe-speaker-1: #9c27b0;
181
- --scribe-speaker-2: #ff9800;
182
- --scribe-speaker-3: #009688;
183
-
184
- display: block;
185
- font-family: var(--scribe-font-family);
186
- font-size: var(--scribe-font-size);
187
- }
188
-
189
- .scribe-recorder {
190
- background: var(--scribe-bg);
191
- border-radius: var(--scribe-border-radius);
192
- }
193
-
194
- .scribe-controls {
195
- display: flex;
196
- align-items: center;
197
- gap: 1rem;
198
- padding: 1rem;
199
- }
200
-
201
- .scribe-btn {
202
- display: flex;
203
- align-items: center;
204
- justify-content: center;
205
- gap: 0.5rem;
206
- padding: 0.75rem;
207
- border: none;
208
- border-radius: var(--scribe-border-radius);
209
- font-family: var(--scribe-font-family);
210
- font-size: 0.875rem;
211
- font-weight: 500;
212
- cursor: pointer;
213
- transition: all 0.2s ease;
214
- }
215
-
216
- .scribe-btn:disabled {
217
- opacity: 0.5;
218
- cursor: not-allowed;
219
- }
220
-
221
- .scribe-btn--mic {
222
- width: 48px;
223
- height: 48px;
224
- border-radius: 50%;
225
- background: var(--scribe-bg-transcript);
226
- color: var(--scribe-text-color);
227
- }
228
-
229
- .scribe-btn--mic:hover:not(:disabled) {
230
- background: var(--scribe-border-color);
231
- }
232
-
233
- .scribe-btn--mic.scribe-btn--active {
234
- background: var(--scribe-danger);
235
- color: white;
236
- animation: pulse-recording 1.5s ease-in-out infinite;
237
- }
238
-
239
- .scribe-btn--mic.scribe-btn--paused {
240
- background: var(--scribe-primary);
241
- color: white;
242
- }
243
-
244
- .scribe-btn--mic.scribe-btn--paused:hover:not(:disabled) {
245
- background: var(--scribe-primary-dark);
246
- }
247
-
248
- .scribe-btn--mic.scribe-btn--connecting {
249
- background: var(--scribe-primary-light);
250
- color: white;
251
- }
252
-
253
- .scribe-btn--finish {
254
- padding: 0.75rem 1.25rem;
255
- background: var(--scribe-primary);
256
- color: white;
257
- }
258
-
259
- .scribe-btn--finish:hover:not(:disabled) {
260
- background: var(--scribe-primary-dark);
261
- }
262
-
263
- .scribe-icon {
264
- flex-shrink: 0;
265
- }
266
-
267
- .scribe-icon--spinner {
268
- animation: spin 1s linear infinite;
269
- }
270
-
271
- .scribe-level {
272
- flex: 1;
273
- height: 8px;
274
- background: var(--scribe-level-bg);
275
- border-radius: 4px;
276
- overflow: hidden;
277
- opacity: 0.5;
278
- transition: opacity 0.2s ease;
279
- }
280
-
281
- .scribe-level--active {
282
- opacity: 1;
283
- }
284
-
285
- .scribe-level__fill {
286
- height: 100%;
287
- background: var(--scribe-level-fill);
288
- border-radius: 4px;
289
- transition: width 0.05s ease-out;
290
- }
291
-
292
- .scribe-transcript {
293
- min-height: 120px;
294
- max-height: 400px;
295
- overflow-y: auto;
296
- padding: 1rem;
297
- margin: 0 1rem 1rem;
298
- background: var(--scribe-bg-transcript);
299
- border-radius: var(--scribe-border-radius);
300
- line-height: 1.6;
301
- }
302
-
303
- .scribe-transcript--empty {
304
- display: flex;
305
- align-items: center;
306
- justify-content: center;
307
- }
308
-
309
- .scribe-placeholder {
310
- color: var(--scribe-text-partial);
311
- font-style: italic;
312
- }
313
-
314
- .scribe-speaker-block {
315
- margin-bottom: 0.75rem;
316
- padding-left: 0.5rem;
317
- border-left: 3px solid var(--scribe-border-color);
318
- }
319
-
320
- .scribe-speaker-block--partial {
321
- opacity: 0.7;
322
- }
323
-
324
- .scribe-speaker-block[data-speaker="0"] {
325
- border-left-color: var(--scribe-speaker-0);
326
- }
327
-
328
- .scribe-speaker-block[data-speaker="1"] {
329
- border-left-color: var(--scribe-speaker-1);
330
- }
331
-
332
- .scribe-speaker-block[data-speaker="2"] {
333
- border-left-color: var(--scribe-speaker-2);
334
- }
335
-
336
- .scribe-speaker-block[data-speaker="3"] {
337
- border-left-color: var(--scribe-speaker-3);
338
- }
339
-
340
- .scribe-speaker-label {
341
- display: inline-block;
342
- font-size: 0.75rem;
343
- font-weight: 600;
344
- text-transform: uppercase;
345
- letter-spacing: 0.5px;
346
- padding: 0.125rem 0.5rem;
347
- border-radius: 4px;
348
- margin-bottom: 0.25rem;
349
- color: white;
350
- background: var(--scribe-border-color);
351
- }
352
-
353
- .scribe-speaker--0 {
354
- background: var(--scribe-speaker-0);
355
- }
356
-
357
- .scribe-speaker--1 {
358
- background: var(--scribe-speaker-1);
359
- }
360
-
361
- .scribe-speaker--2 {
362
- background: var(--scribe-speaker-2);
363
- }
364
-
365
- .scribe-speaker--3 {
366
- background: var(--scribe-speaker-3);
367
- }
368
-
369
- .scribe-speaker-text {
370
- margin-top: 0.25rem;
371
- }
372
-
373
- .scribe-text {
374
- word-wrap: break-word;
375
- }
376
-
377
- .scribe-text--final {
378
- color: var(--scribe-text-color);
379
- }
380
-
381
- .scribe-text--partial {
382
- color: var(--scribe-text-partial);
383
- font-style: italic;
384
- }
385
-
386
- .scribe-generate-container {
387
- position: relative;
388
- margin-left: 0.5rem;
389
- }
390
-
391
- .scribe-btn--generate {
392
- display: flex;
393
- align-items: center;
394
- gap: 0.5rem;
395
- padding: 0.75rem 1.25rem;
396
- background: var(--scribe-primary);
397
- color: white;
398
- border: none;
399
- border-radius: var(--scribe-border-radius);
400
- font-family: var(--scribe-font-family);
401
- font-size: 0.875rem;
402
- font-weight: 500;
403
- cursor: pointer;
404
- transition: all 0.2s ease;
405
- }
406
-
407
- .scribe-btn--generate:hover:not(:disabled) {
408
- background: var(--scribe-primary-dark);
409
- }
410
-
411
- .scribe-btn--generate:disabled {
412
- opacity: 0.7;
413
- cursor: not-allowed;
414
- }
415
-
416
- .scribe-template-menu {
417
- position: absolute;
418
- top: 100%;
419
- left: 0;
420
- margin-top: 0.25rem;
421
- min-width: 220px;
422
- background: var(--scribe-bg);
423
- border: 1px solid var(--scribe-border-color);
424
- border-radius: var(--scribe-border-radius);
425
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
426
- z-index: 100;
427
- overflow: hidden;
428
- }
429
-
430
- .scribe-template-item {
431
- display: flex;
432
- flex-direction: column;
433
- align-items: flex-start;
434
- width: 100%;
435
- padding: 0.75rem 1rem;
436
- border: none;
437
- background: transparent;
438
- cursor: pointer;
439
- text-align: left;
440
- transition: background 0.15s ease;
441
- }
442
-
443
- .scribe-template-item:hover {
444
- background: var(--scribe-bg-transcript);
445
- }
446
-
447
- .scribe-template-item:not(:last-child) {
448
- border-bottom: 1px solid var(--scribe-border-color);
449
- }
450
-
451
- .scribe-template-name {
452
- font-weight: 500;
453
- color: var(--scribe-text-color);
454
- }
455
-
456
- .scribe-template-desc {
457
- font-size: 0.75rem;
458
- color: var(--scribe-text-partial);
459
- margin-top: 0.25rem;
460
- }
461
-
462
- @keyframes pulse-recording {
463
- 0%, 100% { box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.4); }
464
- 50% { box-shadow: 0 0 0 8px rgba(244, 67, 54, 0); }
465
- }
466
-
467
- @keyframes spin {
468
- from { transform: rotate(0deg); }
469
- to { transform: rotate(360deg); }
470
- }
471
- `],
472
- })
473
- export class RecorderComponent implements OnDestroy {
474
- private audioCapture = inject(AudioCaptureService);
475
- private socket = inject(ScribeSocketService);
476
- private elementRef = inject(ElementRef);
477
- private subscriptions: Subscription[] = [];
478
- private entryIdCounter = 0;
479
- private generateAbortController: AbortController | null = null;
480
-
481
- @HostListener('document:click', ['$event'])
482
- onDocumentClick(event: MouseEvent): void {
483
- if (!this.showTemplateMenu()) return;
484
- const target = event.target as HTMLElement;
485
- if (!this.elementRef.nativeElement.contains(target)) {
486
- this.showTemplateMenu.set(false);
487
- }
488
- }
489
-
490
- wsUrl = input.required<string>();
491
- token = input<string>('');
492
- speakerLabels = input<Record<number, string>>({});
493
- premium = input<boolean>(false);
494
- templates = input<TemplateOption[]>([]);
495
- lambdaUrl = input<string>('');
496
-
497
- sessionStarted = output<string>();
498
- sessionFinished = output<{ transcript: string; entries: TranscriptEntry[] }>();
499
- error = output<Error>();
500
- documentGenerated = output<GeneratedDocument>();
501
- generationError = output<Error>();
502
-
503
- isRecording = signal(false);
504
- isPaused = signal(false);
505
- audioLevel = signal(0);
506
- connectionState = signal<ConnectionState>('disconnected');
507
- entries = signal<TranscriptEntry[]>([]);
508
- partialText = signal('');
509
- currentSpeaker = signal<number | undefined>(undefined);
510
- showTemplateMenu = signal(false);
511
- isGenerating = signal(false);
512
- lastSessionId = signal<string | null>(null);
513
-
514
- hasFinishedSession = computed(() =>
515
- this.lastSessionId() !== null &&
516
- !this.isRecording() &&
517
- !this.isPaused()
518
- );
519
-
520
- speakerGroups = computed<SpeakerGroup[]>(() => {
521
- const allEntries = this.entries();
522
- if (allEntries.length === 0) return [];
523
-
524
- const groups: SpeakerGroup[] = [];
525
- let currentGroup: SpeakerGroup | null = null;
526
-
527
- for (const entry of allEntries) {
528
- if (!currentGroup || currentGroup.speaker !== entry.speaker) {
529
- currentGroup = { speaker: entry.speaker, entries: [] };
530
- groups.push(currentGroup);
531
- }
532
- currentGroup.entries.push(entry);
533
- }
534
-
535
- return groups;
536
- });
537
-
538
- constructor() {
539
- this.subscriptions.push(
540
- this.audioCapture.audioLevel$.subscribe((level) => {
541
- this.audioLevel.set(level);
542
- }),
543
- this.socket.connectionState$.subscribe((state) => {
544
- this.connectionState.set(state);
545
- }),
546
- this.socket.transcription$.subscribe((event) => {
547
- this.handleTranscription(event);
548
- }),
549
- this.socket.error$.subscribe((err) => {
550
- this.error.emit(err);
551
- }),
552
- this.audioCapture.error$.subscribe((err) => {
553
- this.error.emit(err);
554
- }),
555
- this.audioCapture.audioChunk$.subscribe(({ data, isSilence }) => {
556
- this.socket.sendAudioChunk(data, isSilence);
557
- })
558
- );
559
- }
560
-
561
- ngOnDestroy(): void {
562
- this.subscriptions.forEach((sub) => sub.unsubscribe());
563
- this.generateAbortController?.abort();
564
- this.cleanup();
565
- }
566
-
567
- getSpeakerLabel(speaker: number): string {
568
- const customLabels = this.speakerLabels();
569
- if (customLabels[speaker]) {
570
- return customLabels[speaker];
571
- }
572
- return SPEAKER_LABELS[speaker] || `Pessoa ${speaker + 1}`;
573
- }
574
-
575
- getMicButtonLabel(): string {
576
- if (this.isPaused()) {
577
- return 'Retomar gravação';
578
- }
579
- if (this.isRecording()) {
580
- return 'Pausar gravação';
581
- }
582
- return 'Iniciar gravação';
583
- }
584
-
585
- async toggleRecording(): Promise<void> {
586
- if (this.isPaused()) {
587
- this.resumeRecording();
588
- } else if (this.isRecording()) {
589
- this.pauseRecording();
590
- } else {
591
- await this.startRecording();
592
- }
593
- }
594
-
595
- pauseRecording(): void {
596
- if (!this.isRecording() || this.isPaused()) return;
597
- this.audioCapture.pauseCapture();
598
- this.isPaused.set(true);
599
- }
600
-
601
- resumeRecording(): void {
602
- if (!this.isPaused()) return;
603
- this.audioCapture.resumeCapture();
604
- this.isPaused.set(false);
605
- }
606
-
607
- async finishSession(): Promise<void> {
608
- if (!this.isRecording() && !this.isPaused()) return;
609
-
610
- const sessionId = this.socket.getSessionId();
611
-
612
- await this.stopRecording();
613
-
614
- const allEntries = this.entries();
615
- const fullTranscript = allEntries.map((e) => e.text).join(' ');
616
-
617
- this.sessionFinished.emit({ transcript: fullTranscript, entries: allEntries });
618
-
619
- this.entries.set([]);
620
- this.partialText.set('');
621
- this.currentSpeaker.set(undefined);
622
- this.entryIdCounter = 0;
623
-
624
- this.lastSessionId.set(sessionId);
625
- }
626
-
627
- toggleTemplateMenu(): void {
628
- this.showTemplateMenu.update((v) => !v);
629
- }
630
-
631
- async selectTemplate(template: TemplateOption): Promise<void> {
632
- if (this.isGenerating()) return;
633
-
634
- this.showTemplateMenu.set(false);
635
-
636
- const lambdaUrl = this.lambdaUrl();
637
- if (!lambdaUrl) {
638
- this.generationError.emit(new Error('Lambda URL not provided'));
639
- return;
640
- }
641
-
642
- const sessionId = this.lastSessionId();
643
- if (!sessionId) {
644
- this.generationError.emit(new Error('No session available'));
645
- return;
646
- }
647
-
648
- this.generateAbortController?.abort();
649
- this.generateAbortController = new AbortController();
650
-
651
- this.isGenerating.set(true);
652
-
653
- try {
654
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
655
- const token = this.token();
656
- if (token) {
657
- headers['Authorization'] = `Bearer ${token}`;
658
- }
659
-
660
- const response = await fetch(`${lambdaUrl}/generate`, {
661
- method: 'POST',
662
- headers,
663
- signal: this.generateAbortController.signal,
664
- body: JSON.stringify({
665
- sessionId,
666
- outputSchema: template.content,
667
- }),
668
- });
669
-
670
- if (!response.ok) {
671
- const errorData = await response.json().catch(() => ({}));
672
- throw new Error(errorData.error || `HTTP ${response.status}`);
673
- }
674
-
675
- const result = await response.json();
676
-
677
- this.documentGenerated.emit({
678
- sessionId,
679
- templateId: template.id,
680
- content: result.content,
681
- generatedAt: result.generatedAt,
682
- });
683
- } catch (err) {
684
- if (err instanceof Error && err.name === 'AbortError') return;
685
- this.generationError.emit(err instanceof Error ? err : new Error(String(err)));
686
- } finally {
687
- this.isGenerating.set(false);
688
- }
689
- }
690
-
691
- private async startRecording(): Promise<void> {
692
- try {
693
- await this.socket.connect(this.wsUrl(), this.token(), this.premium());
694
- await this.audioCapture.startCapture();
695
- this.isRecording.set(true);
696
- this.isPaused.set(false);
697
-
698
- const sessionId = this.socket.getSessionId();
699
- if (sessionId) {
700
- this.sessionStarted.emit(sessionId);
701
- }
702
- } catch (err) {
703
- this.error.emit(err instanceof Error ? err : new Error('Failed to start recording'));
704
- await this.cleanup();
705
- }
706
- }
707
-
708
- private async stopRecording(): Promise<void> {
709
- await this.audioCapture.stopCapture();
710
- this.socket.disconnect();
711
- this.isRecording.set(false);
712
- this.isPaused.set(false);
713
- }
714
-
715
- private async cleanup(): Promise<void> {
716
- await this.audioCapture.stopCapture();
717
- this.socket.disconnect();
718
- this.isRecording.set(false);
719
- this.isPaused.set(false);
720
- }
721
-
722
- private handleTranscription(event: TranscriptionEvent): void {
723
- if (!event.transcript) return;
724
-
725
- if (event.type === 'final') {
726
- const entry: TranscriptEntry = {
727
- id: ++this.entryIdCounter,
728
- text: event.transcript,
729
- speaker: event.speaker,
730
- isFinal: true,
731
- startTime: event.start,
732
- endTime: event.end,
733
- };
734
-
735
- this.entries.update((entries) => [...entries, entry]);
736
- this.partialText.set('');
737
- this.currentSpeaker.set(undefined);
738
- } else if (event.type === 'partial') {
739
- this.partialText.set(event.transcript);
740
- this.currentSpeaker.set(event.speaker);
741
- }
742
- }
743
- }