@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/.turbo/turbo-build.log +28 -0
- package/README.md +760 -0
- package/ng-package.json +10 -0
- package/package.json +41 -0
- package/src/assets/pcm-processor.js +72 -0
- package/src/lib/components/recorder/recorder.component.ts +743 -0
- package/src/lib/services/audio-capture.service.ts +190 -0
- package/src/lib/services/scribe-socket.service.ts +264 -0
- package/src/lib/services/vad.service.ts +65 -0
- package/src/lib/worklets/audio-worklet.d.ts +14 -0
- package/src/lib/worklets/pcm-processor.worklet.ts +81 -0
- package/src/public-api.ts +5 -0
- package/tsconfig.lib.json +24 -0
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
|