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

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 ADDED
@@ -0,0 +1,760 @@
1
+ # ngx-jaimes-scribe
2
+
3
+ Biblioteca Angular para transcrição de áudio em tempo real com suporte a diarização de speakers e geração automática de documentos clínicos via IA.
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ npm install @jaimes/ngx-jaimes-scribe
9
+ # ou
10
+ pnpm add @jaimes/ngx-jaimes-scribe
11
+ ```
12
+
13
+ ## Uso Básico
14
+
15
+ ```typescript
16
+ import { Component } from '@angular/core';
17
+ import { RecorderComponent } from '@jaimes/ngx-jaimes-scribe';
18
+
19
+ @Component({
20
+ selector: 'app-consultation',
21
+ standalone: true,
22
+ imports: [RecorderComponent],
23
+ template: `
24
+ <ngx-jaimes-scribe-recorder
25
+ [wsUrl]="wsUrl"
26
+ [token]="token"
27
+ (sessionStarted)="onSessionStarted($event)"
28
+ (sessionFinished)="onSessionFinished($event)"
29
+ (error)="onError($event)"
30
+ />
31
+ `,
32
+ })
33
+ export class ConsultationComponent {
34
+ wsUrl = 'wss://stream.example.com/stream';
35
+ token = 'your-jwt-token';
36
+
37
+ onSessionStarted(sessionId: string): void {
38
+ console.log('Sessão iniciada:', sessionId);
39
+ }
40
+
41
+ onSessionFinished(result: { transcript: string; entries: unknown[] }): void {
42
+ console.log('Transcrição completa:', result.transcript);
43
+ }
44
+
45
+ onError(error: Error): void {
46
+ console.error('Erro:', error.message);
47
+ }
48
+ }
49
+ ```
50
+
51
+ ---
52
+
53
+ ## API Reference
54
+
55
+ ### Selector
56
+
57
+ ```html
58
+ <ngx-jaimes-scribe-recorder />
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Inputs
64
+
65
+ | Input | Tipo | Obrigatório | Default | Descrição |
66
+ |-------|------|-------------|---------|-----------|
67
+ | `wsUrl` | `string` | ✅ Sim | - | URL do WebSocket do serviço de streaming |
68
+ | `token` | `string` | Não | `''` | Token JWT para autenticação |
69
+ | `premium` | `boolean` | Não | `false` | Ativa tier premium (ElevenLabs + Sonnet) |
70
+ | `speakerLabels` | `Record<number, string>` | Não | `{}` | Labels customizados para speakers |
71
+ | `templates` | `TemplateOption[]` | Não | `[]` | Lista de templates para geração de documentos |
72
+ | `lambdaUrl` | `string` | Não | `''` | URL base do Lambda para geração de documentos |
73
+
74
+ ### Detalhes dos Inputs
75
+
76
+ #### `wsUrl` (obrigatório)
77
+
78
+ URL completa do WebSocket de streaming. O componente adiciona automaticamente os query parameters `token` e `premium`.
79
+
80
+ ```html
81
+ <ngx-jaimes-scribe-recorder
82
+ [wsUrl]="'wss://stream.jaimes.example.com/stream'"
83
+ />
84
+ ```
85
+
86
+ #### `token`
87
+
88
+ Token JWT para autenticação. Usado tanto na conexão WebSocket quanto nas chamadas ao Lambda.
89
+
90
+ ```html
91
+ <ngx-jaimes-scribe-recorder
92
+ [wsUrl]="wsUrl"
93
+ [token]="authService.getToken()"
94
+ />
95
+ ```
96
+
97
+ #### `premium`
98
+
99
+ Quando `true`, ativa o tier premium:
100
+ - **Transcrição**: ElevenLabs Scribe v2 (ou Deepgram Nova-3 via env)
101
+ - **LLM**: Claude Sonnet 4.5
102
+
103
+ Quando `false` (default):
104
+ - **Transcrição**: Deepgram Enhanced
105
+ - **LLM**: Claude Sonnet 4.5
106
+
107
+ ```html
108
+ <!-- Tier Standard -->
109
+ <ngx-jaimes-scribe-recorder [wsUrl]="wsUrl" [token]="token" />
110
+
111
+ <!-- Tier Premium -->
112
+ <ngx-jaimes-scribe-recorder [wsUrl]="wsUrl" [token]="token" [premium]="true" />
113
+ ```
114
+
115
+ #### `speakerLabels`
116
+
117
+ Mapeamento de índices de speaker para labels customizados. Útil para identificar médico e paciente.
118
+
119
+ ```typescript
120
+ speakerLabels: Record<number, string> = {
121
+ 0: 'Dr. Silva',
122
+ 1: 'Paciente',
123
+ };
124
+ ```
125
+
126
+ ```html
127
+ <ngx-jaimes-scribe-recorder
128
+ [wsUrl]="wsUrl"
129
+ [token]="token"
130
+ [speakerLabels]="speakerLabels"
131
+ />
132
+ ```
133
+
134
+ **Labels padrão** (quando não especificado):
135
+ - `0`: "Pessoa 1"
136
+ - `1`: "Pessoa 2"
137
+ - `2`: "Pessoa 3"
138
+ - `3`: "Pessoa 4"
139
+
140
+ #### `templates`
141
+
142
+ Lista de templates disponíveis para geração de documentos. O botão "Gerar Resumo" só aparece se houver templates configurados e uma sessão finalizada.
143
+
144
+ ```typescript
145
+ import { TemplateOption } from '@jaimes/ngx-jaimes-scribe';
146
+
147
+ templates: TemplateOption[] = [
148
+ {
149
+ id: 'anamnese',
150
+ name: 'Anamnese Completa',
151
+ description: 'Documento SOAP padrão',
152
+ content: {
153
+ type: 'object',
154
+ properties: {
155
+ queixa_principal: {
156
+ type: 'string',
157
+ description: 'Queixa principal do paciente'
158
+ },
159
+ historia_doenca_atual: {
160
+ type: 'string',
161
+ description: 'História da doença atual'
162
+ },
163
+ hipotese_diagnostica: {
164
+ type: 'string',
165
+ description: 'Hipótese diagnóstica'
166
+ },
167
+ conduta: {
168
+ type: 'string',
169
+ description: 'Conduta médica proposta'
170
+ },
171
+ },
172
+ required: ['queixa_principal', 'hipotese_diagnostica', 'conduta'],
173
+ },
174
+ },
175
+ {
176
+ id: 'resumo',
177
+ name: 'Resumo Rápido',
178
+ description: 'Resumo simplificado da consulta',
179
+ content: {
180
+ type: 'object',
181
+ properties: {
182
+ resumo: { type: 'string', description: 'Resumo da consulta' },
183
+ proximos_passos: { type: 'string', description: 'Próximos passos' },
184
+ },
185
+ required: ['resumo'],
186
+ },
187
+ },
188
+ ];
189
+ ```
190
+
191
+ #### `lambdaUrl`
192
+
193
+ URL base do Lambda de geração de documentos. O componente faz POST para `{lambdaUrl}/generate`.
194
+
195
+ ```html
196
+ <ngx-jaimes-scribe-recorder
197
+ [wsUrl]="wsUrl"
198
+ [token]="token"
199
+ [templates]="templates"
200
+ [lambdaUrl]="'https://api.example.com'"
201
+ />
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Outputs
207
+
208
+ | Output | Tipo | Descrição |
209
+ |--------|------|-----------|
210
+ | `sessionStarted` | `string` | Emitido quando a gravação inicia, com o sessionId |
211
+ | `sessionFinished` | `{ transcript: string; entries: TranscriptEntry[] }` | Emitido ao finalizar, com transcrição completa |
212
+ | `documentGenerated` | `GeneratedDocument` | Emitido quando um documento é gerado com sucesso |
213
+ | `generationError` | `Error` | Emitido quando há erro na geração do documento |
214
+ | `error` | `Error` | Emitido em erros de gravação, conexão ou transcrição |
215
+
216
+ ### Detalhes dos Outputs
217
+
218
+ #### `sessionStarted`
219
+
220
+ Emitido após a conexão WebSocket ser estabelecida e a captura de áudio iniciar.
221
+
222
+ ```typescript
223
+ onSessionStarted(sessionId: string): void {
224
+ this.currentSessionId = sessionId;
225
+ console.log('Gravação iniciada:', sessionId);
226
+ }
227
+ ```
228
+
229
+ #### `sessionFinished`
230
+
231
+ Emitido ao clicar em "Finalizar". Contém a transcrição completa e todas as entradas individuais.
232
+
233
+ ```typescript
234
+ interface TranscriptEntry {
235
+ id: number;
236
+ text: string;
237
+ speaker?: number;
238
+ isFinal: boolean;
239
+ startTime?: number;
240
+ endTime?: number;
241
+ }
242
+
243
+ onSessionFinished(result: { transcript: string; entries: TranscriptEntry[] }): void {
244
+ console.log('Palavras:', result.transcript.split(' ').length);
245
+ console.log('Segmentos:', result.entries.length);
246
+
247
+ // Salvar no prontuário
248
+ this.ehr.saveTranscription(result.transcript);
249
+ }
250
+ ```
251
+
252
+ #### `documentGenerated`
253
+
254
+ Emitido após a geração bem-sucedida de um documento via Lambda.
255
+
256
+ ```typescript
257
+ import { GeneratedDocument } from '@jaimes/ngx-jaimes-scribe';
258
+
259
+ onDocumentGenerated(doc: GeneratedDocument): void {
260
+ console.log('Template usado:', doc.templateId);
261
+ console.log('Conteúdo:', doc.content);
262
+
263
+ // Exemplo de conteúdo para template "anamnese"
264
+ // {
265
+ // queixa_principal: "Dor de cabeça há 3 dias",
266
+ // historia_doenca_atual: "Paciente relata...",
267
+ // hipotese_diagnostica: "Cefaleia tensional",
268
+ // conduta: "Dipirona 500mg 6/6h..."
269
+ // }
270
+ }
271
+ ```
272
+
273
+ #### `generationError`
274
+
275
+ Emitido quando há falha na geração do documento.
276
+
277
+ ```typescript
278
+ onGenerationError(error: Error): void {
279
+ console.error('Falha na geração:', error.message);
280
+ // Possíveis erros:
281
+ // - "Lambda URL not provided"
282
+ // - "No session available"
283
+ // - "HTTP 500" (erro do servidor)
284
+ // - "Transcription not found" (sessão expirou)
285
+ }
286
+ ```
287
+
288
+ #### `error`
289
+
290
+ Emitido em erros gerais de gravação ou conexão.
291
+
292
+ ```typescript
293
+ onError(error: Error): void {
294
+ console.error('Erro:', error.message);
295
+ // Possíveis erros:
296
+ // - "Permission denied" (microfone negado)
297
+ // - "WebSocket connection failed"
298
+ // - "Transcription service unavailable"
299
+ }
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Interfaces
305
+
306
+ ### TemplateOption
307
+
308
+ ```typescript
309
+ interface TemplateOption {
310
+ id: string; // Identificador único do template
311
+ name: string; // Nome exibido no menu
312
+ description?: string; // Descrição opcional
313
+ content: Record<string, unknown>; // JSON Schema para geração
314
+ }
315
+ ```
316
+
317
+ ### GeneratedDocument
318
+
319
+ ```typescript
320
+ interface GeneratedDocument {
321
+ sessionId: string; // ID da sessão de gravação
322
+ templateId: string; // ID do template usado
323
+ content: Record<string, unknown>; // Documento gerado
324
+ generatedAt: string; // ISO timestamp
325
+ }
326
+ ```
327
+
328
+ ### TranscriptionEvent
329
+
330
+ ```typescript
331
+ interface TranscriptionEvent {
332
+ type: 'connected' | 'partial' | 'final' | 'error';
333
+ sessionId: string;
334
+ transcript?: string;
335
+ timestamp?: number;
336
+ message?: string; // Mensagem de erro (quando type='error')
337
+ words?: TranscriptionWord[];
338
+ speaker?: number; // Índice do speaker (diarização)
339
+ start?: number; // Tempo inicial (segundos)
340
+ end?: number; // Tempo final (segundos)
341
+ confidence?: number; // Confiança da transcrição (0-1)
342
+ }
343
+
344
+ interface TranscriptionWord {
345
+ word: string;
346
+ start: number;
347
+ end: number;
348
+ confidence?: number;
349
+ speaker?: number;
350
+ }
351
+ ```
352
+
353
+ ---
354
+
355
+ ## Customização Visual (CSS Variables)
356
+
357
+ O componente usa CSS Custom Properties para permitir customização completa sem sobrescrever estilos.
358
+
359
+ ### Cores Principais
360
+
361
+ ```css
362
+ ngx-jaimes-scribe-recorder {
363
+ --scribe-primary: #4caf50; /* Cor principal (botões, destaques) */
364
+ --scribe-primary-dark: #388e3c; /* Hover da cor principal */
365
+ --scribe-primary-light: #81c784; /* Estado connecting */
366
+ --scribe-danger: #f44336; /* Botão gravando (vermelho) */
367
+ --scribe-danger-dark: #d32f2f; /* Hover do danger */
368
+ }
369
+ ```
370
+
371
+ ### Cores de Texto
372
+
373
+ ```css
374
+ ngx-jaimes-scribe-recorder {
375
+ --scribe-text-color: #212121; /* Texto principal */
376
+ --scribe-text-partial: #9e9e9e; /* Texto parcial/placeholder */
377
+ }
378
+ ```
379
+
380
+ ### Tipografia
381
+
382
+ ```css
383
+ ngx-jaimes-scribe-recorder {
384
+ --scribe-font-family: inherit; /* Herda da aplicação */
385
+ --scribe-font-size: 1rem; /* Tamanho base */
386
+ }
387
+ ```
388
+
389
+ ### Backgrounds e Bordas
390
+
391
+ ```css
392
+ ngx-jaimes-scribe-recorder {
393
+ --scribe-bg: #ffffff; /* Background do componente */
394
+ --scribe-bg-transcript: #f5f5f5; /* Background da área de transcrição */
395
+ --scribe-border-radius: 8px; /* Arredondamento */
396
+ --scribe-border-color: #e0e0e0; /* Cor das bordas */
397
+ }
398
+ ```
399
+
400
+ ### Indicador de Nível de Áudio
401
+
402
+ ```css
403
+ ngx-jaimes-scribe-recorder {
404
+ --scribe-level-bg: #e0e0e0; /* Background da barra */
405
+ --scribe-level-fill: #4caf50; /* Preenchimento (nível) */
406
+ }
407
+ ```
408
+
409
+ ### Cores dos Speakers (Diarização)
410
+
411
+ ```css
412
+ ngx-jaimes-scribe-recorder {
413
+ --scribe-speaker-0: #2196f3; /* Speaker 0 (azul) */
414
+ --scribe-speaker-1: #9c27b0; /* Speaker 1 (roxo) */
415
+ --scribe-speaker-2: #ff9800; /* Speaker 2 (laranja) */
416
+ --scribe-speaker-3: #009688; /* Speaker 3 (teal) */
417
+ }
418
+ ```
419
+
420
+ ### Exemplo: Tema Escuro
421
+
422
+ ```css
423
+ ngx-jaimes-scribe-recorder {
424
+ --scribe-bg: #1e1e1e;
425
+ --scribe-bg-transcript: #2d2d2d;
426
+ --scribe-text-color: #ffffff;
427
+ --scribe-text-partial: #888888;
428
+ --scribe-border-color: #444444;
429
+ --scribe-level-bg: #444444;
430
+ --scribe-primary: #66bb6a;
431
+ --scribe-primary-dark: #4caf50;
432
+ }
433
+ ```
434
+
435
+ ### Exemplo: Tema Médico (Azul)
436
+
437
+ ```css
438
+ ngx-jaimes-scribe-recorder {
439
+ --scribe-primary: #1976d2;
440
+ --scribe-primary-dark: #1565c0;
441
+ --scribe-primary-light: #64b5f6;
442
+ --scribe-level-fill: #1976d2;
443
+ --scribe-speaker-0: #1976d2;
444
+ --scribe-speaker-1: #e91e63;
445
+ }
446
+ ```
447
+
448
+ ---
449
+
450
+ ## Exemplo Completo
451
+
452
+ ```typescript
453
+ import { Component, signal } from '@angular/core';
454
+ import { CommonModule } from '@angular/common';
455
+ import {
456
+ RecorderComponent,
457
+ TemplateOption,
458
+ GeneratedDocument
459
+ } from '@jaimes/ngx-jaimes-scribe';
460
+
461
+ @Component({
462
+ selector: 'app-medical-consultation',
463
+ standalone: true,
464
+ imports: [CommonModule, RecorderComponent],
465
+ template: `
466
+ <div class="consultation-container">
467
+ <header>
468
+ <h1>Consulta - {{ patientName }}</h1>
469
+ </header>
470
+
471
+ <ngx-jaimes-scribe-recorder
472
+ [wsUrl]="wsUrl"
473
+ [token]="token"
474
+ [premium]="isPremium"
475
+ [speakerLabels]="speakerLabels"
476
+ [templates]="templates"
477
+ [lambdaUrl]="lambdaUrl"
478
+ (sessionStarted)="onSessionStarted($event)"
479
+ (sessionFinished)="onSessionFinished($event)"
480
+ (documentGenerated)="onDocumentGenerated($event)"
481
+ (generationError)="onGenerationError($event)"
482
+ (error)="onError($event)"
483
+ />
484
+
485
+ @if (generatedDoc()) {
486
+ <section class="generated-document">
487
+ <h2>Documento Gerado</h2>
488
+ @for (field of getDocFields(); track field.key) {
489
+ <div class="field">
490
+ <label>{{ field.key }}</label>
491
+ <p>{{ field.value }}</p>
492
+ </div>
493
+ }
494
+ <button (click)="saveToEHR()">Salvar no Prontuário</button>
495
+ </section>
496
+ }
497
+
498
+ @if (errorMessage()) {
499
+ <div class="error-toast">{{ errorMessage() }}</div>
500
+ }
501
+ </div>
502
+ `,
503
+ styles: [`
504
+ .consultation-container {
505
+ max-width: 800px;
506
+ margin: 0 auto;
507
+ padding: 2rem;
508
+ }
509
+
510
+ ngx-jaimes-scribe-recorder {
511
+ --scribe-primary: #1976d2;
512
+ --scribe-border-radius: 12px;
513
+ }
514
+
515
+ .generated-document {
516
+ margin-top: 2rem;
517
+ padding: 1.5rem;
518
+ background: #f5f5f5;
519
+ border-radius: 8px;
520
+ }
521
+
522
+ .field {
523
+ margin-bottom: 1rem;
524
+ }
525
+
526
+ .field label {
527
+ font-weight: 600;
528
+ color: #1976d2;
529
+ text-transform: capitalize;
530
+ }
531
+
532
+ .error-toast {
533
+ position: fixed;
534
+ bottom: 1rem;
535
+ right: 1rem;
536
+ padding: 1rem;
537
+ background: #f44336;
538
+ color: white;
539
+ border-radius: 4px;
540
+ }
541
+ `],
542
+ })
543
+ export class MedicalConsultationComponent {
544
+ patientName = 'João da Silva';
545
+
546
+ wsUrl = 'wss://stream.jaimes.example.com/stream';
547
+ token = 'eyJhbGciOiJIUzI1NiIs...';
548
+ lambdaUrl = 'https://api.jaimes.example.com';
549
+ isPremium = true;
550
+
551
+ speakerLabels: Record<number, string> = {
552
+ 0: 'Dr. Oliveira',
553
+ 1: 'Paciente',
554
+ };
555
+
556
+ templates: TemplateOption[] = [
557
+ {
558
+ id: 'soap',
559
+ name: 'SOAP',
560
+ description: 'Subjetivo, Objetivo, Avaliação, Plano',
561
+ content: {
562
+ type: 'object',
563
+ properties: {
564
+ subjetivo: { type: 'string', description: 'Queixas e história relatada' },
565
+ objetivo: { type: 'string', description: 'Achados do exame físico' },
566
+ avaliacao: { type: 'string', description: 'Diagnóstico/hipóteses' },
567
+ plano: { type: 'string', description: 'Conduta e tratamento' },
568
+ },
569
+ required: ['subjetivo', 'avaliacao', 'plano'],
570
+ },
571
+ },
572
+ {
573
+ id: 'receita',
574
+ name: 'Receituário',
575
+ description: 'Prescrição médica',
576
+ content: {
577
+ type: 'object',
578
+ properties: {
579
+ medicamentos: {
580
+ type: 'array',
581
+ items: {
582
+ type: 'object',
583
+ properties: {
584
+ nome: { type: 'string' },
585
+ dose: { type: 'string' },
586
+ posologia: { type: 'string' },
587
+ },
588
+ },
589
+ },
590
+ orientacoes: { type: 'string' },
591
+ },
592
+ },
593
+ },
594
+ ];
595
+
596
+ currentSessionId = signal<string | null>(null);
597
+ generatedDoc = signal<GeneratedDocument | null>(null);
598
+ errorMessage = signal<string | null>(null);
599
+
600
+ onSessionStarted(sessionId: string): void {
601
+ this.currentSessionId.set(sessionId);
602
+ console.log('Gravação iniciada:', sessionId);
603
+ }
604
+
605
+ onSessionFinished(result: { transcript: string; entries: unknown[] }): void {
606
+ console.log('Transcrição:', result.transcript);
607
+ console.log('Segmentos:', result.entries.length);
608
+ }
609
+
610
+ onDocumentGenerated(doc: GeneratedDocument): void {
611
+ this.generatedDoc.set(doc);
612
+ console.log('Documento gerado:', doc);
613
+ }
614
+
615
+ onGenerationError(error: Error): void {
616
+ this.showError(`Erro na geração: ${error.message}`);
617
+ }
618
+
619
+ onError(error: Error): void {
620
+ this.showError(error.message);
621
+ }
622
+
623
+ getDocFields(): { key: string; value: string }[] {
624
+ const doc = this.generatedDoc();
625
+ if (!doc) return [];
626
+ return Object.entries(doc.content).map(([key, value]) => ({
627
+ key,
628
+ value: typeof value === 'string' ? value : JSON.stringify(value, null, 2),
629
+ }));
630
+ }
631
+
632
+ saveToEHR(): void {
633
+ const doc = this.generatedDoc();
634
+ if (!doc) return;
635
+ // Integrar com seu sistema de prontuário
636
+ console.log('Salvando no prontuário...', doc);
637
+ }
638
+
639
+ private showError(message: string): void {
640
+ this.errorMessage.set(message);
641
+ setTimeout(() => this.errorMessage.set(null), 5000);
642
+ }
643
+ }
644
+ ```
645
+
646
+ ---
647
+
648
+ ## Arquitetura de Comunicação
649
+
650
+ ```
651
+ ┌─────────────────────────────────────────────────────────────────┐
652
+ │ Angular App │
653
+ │ ┌─────────────────────────────────────────────────────────┐ │
654
+ │ │ RecorderComponent │ │
655
+ │ │ ┌─────────────────┐ ┌─────────────────────────────┐ │ │
656
+ │ │ │ AudioCapture │ │ ScribeSocket │ │ │
657
+ │ │ │ Service │ │ Service │ │ │
658
+ │ │ │ │ │ │ │ │
659
+ │ │ │ - Microfone │ │ - WebSocket │ │ │
660
+ │ │ │ - VAD │──┼─► Streaming │ │ │
661
+ │ │ │ - PCM 16kHz │ │ - Eventos │ │ │
662
+ │ │ └─────────────────┘ └──────────────┬──────────────┘ │ │
663
+ │ └──────────────────────────────────────┼──────────────────┘ │
664
+ └─────────────────────────────────────────┼───────────────────────┘
665
+ │ WSS
666
+
667
+ ┌─────────────────────────────────────────────────────────────────┐
668
+ │ AWS Infrastructure │
669
+ │ │
670
+ │ ┌──────────────┐ ┌─────────────────────────────────────┐ │
671
+ │ │ ALB │───►│ ECS Fargate (Stream Service) │ │
672
+ │ └──────────────┘ │ │ │
673
+ │ │ ┌─────────────┐ ┌─────────────┐ │ │
674
+ │ │ │ Deepgram │ │ ElevenLabs │ │ │
675
+ │ │ │ (Standard) │ │ (Premium) │ │ │
676
+ │ │ └─────────────┘ └─────────────┘ │ │
677
+ │ │ │ │
678
+ │ │ ┌─────────────────────────────┐ │ │
679
+ │ │ │ SessionManager │ │ │
680
+ │ │ │ - S3 (áudio + transcrição) │ │ │
681
+ │ │ │ - DynamoDB (metadados) │ │ │
682
+ │ │ └─────────────────────────────┘ │ │
683
+ │ └─────────────────────────────────────┘ │
684
+ │ │
685
+ │ ┌──────────────┐ ┌─────────────────────────────────────┐ │
686
+ │ │ API Gateway │───►│ Lambda GenAI │ │
687
+ │ │ POST/generate│ │ │ │
688
+ │ └──────────────┘ │ ┌─────────────────────────────┐ │ │
689
+ │ │ │ AWS Bedrock │ │ │
690
+ │ │ │ Claude Sonnet 4.5 │ │ │
691
+ │ │ └─────────────────────────────┘ │ │
692
+ │ └─────────────────────────────────────┘ │
693
+ └─────────────────────────────────────────────────────────────────┘
694
+ ```
695
+
696
+ ---
697
+
698
+ ## Fluxo de Operação
699
+
700
+ ### 1. Gravação
701
+
702
+ 1. Usuário clica no botão de microfone
703
+ 2. Componente solicita permissão do microfone
704
+ 3. Conexão WebSocket é estabelecida
705
+ 4. Áudio é capturado, processado (VAD) e enviado em chunks
706
+ 5. Transcrições parciais e finais são recebidas e exibidas
707
+ 6. Usuário pode pausar/retomar a qualquer momento
708
+ 7. Ao finalizar, `sessionFinished` é emitido
709
+
710
+ ### 2. Geração de Documento
711
+
712
+ 1. Após finalizar, botão "Gerar Resumo" aparece
713
+ 2. Usuário seleciona um template
714
+ 3. Componente faz POST para Lambda com `sessionId` e `outputSchema`
715
+ 4. Lambda recupera transcrição do S3
716
+ 5. Bedrock gera documento estruturado
717
+ 6. `documentGenerated` é emitido com o resultado
718
+
719
+ ---
720
+
721
+ ## Requisitos
722
+
723
+ - Angular 19+
724
+ - Navegador com suporte a:
725
+ - Web Audio API
726
+ - MediaDevices API
727
+ - WebSocket
728
+ - Permissão de microfone
729
+
730
+ ---
731
+
732
+ ## Troubleshooting
733
+
734
+ ### Microfone não detectado
735
+
736
+ ```typescript
737
+ // Verifique permissões
738
+ const permission = await navigator.permissions.query({ name: 'microphone' as PermissionName });
739
+ if (permission.state === 'denied') {
740
+ // Instruir usuário a permitir nas configurações
741
+ }
742
+ ```
743
+
744
+ ### Conexão WebSocket falha
745
+
746
+ - Verifique se a URL usa `wss://` (não `ws://`)
747
+ - Confirme que o token JWT é válido
748
+ - Verifique conectividade de rede
749
+
750
+ ### Geração de documento retorna erro
751
+
752
+ - Confirme que `lambdaUrl` está configurado
753
+ - Verifique se a sessão ainda existe no S3 (TTL de 30 dias)
754
+ - Confira formato do `content` no template (JSON Schema válido)
755
+
756
+ ---
757
+
758
+ ## Licença
759
+
760
+ Proprietário - MEDC Sistemas de Saúde