@medc-com-br/ngx-jaimes-scribe
v0.1.23
Published
Angular library for real-time medical transcription
Maintainers
Readme
ngx-jaimes-scribe
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.
Instalação
npm install @medc-com-br/ngx-jaimes-scribe
# ou
pnpm add @medc-com-br/ngx-jaimes-scribeUso Básico
import { Component } from '@angular/core';
import { RecorderComponent } from '@medc-com-br/ngx-jaimes-scribe';
@Component({
selector: 'app-consultation',
standalone: true,
imports: [RecorderComponent],
template: `
<ngx-jaimes-scribe-recorder
[wsUrl]="wsUrl"
[token]="token"
(sessionStarted)="onSessionStarted($event)"
(sessionFinished)="onSessionFinished($event)"
(error)="onError($event)"
/>
`,
})
export class ConsultationComponent {
wsUrl = 'wss://stream.example.com/stream';
token = 'your-jwt-token';
onSessionStarted(sessionId: string): void {
console.log('Sessão iniciada:', sessionId);
}
onSessionFinished(result: { transcript: string; entries: unknown[] }): void {
console.log('Transcrição completa:', result.transcript);
}
onError(error: Error): void {
console.error('Erro:', error.message);
}
}API Reference
Selector
<ngx-jaimes-scribe-recorder />Inputs
| Input | Tipo | Obrigatório | Default | Descrição |
|-------|------|-------------|---------|-----------|
| wsUrl | string | ✅ Sim* | - | URL do WebSocket do serviço de streaming |
| token | string | Não | '' | Token JWT para autenticação |
| speakerLabels | Record<number, string> | Não | {} | Labels customizados para speakers |
| doctorName | string | Não | '' | Nome do médico (exibido quando speaker é identificado como doctor) |
| patientName | string | Não | '' | Nome do paciente (exibido quando speaker é identificado como patient) |
| companionName | string | Não | '' | Nome do acompanhante (exibido quando speaker é identificado como companion) |
| templates | TemplateOption[] | Não | [] | Lista de templates para geração de documentos |
| apiUrl | string | Não | '' | URL base da API (geração de documentos, identificação de speakers, playback) |
| sessionId | string | Não | - | ID de sessão para modo playback (player de áudio + transcrição) |
| resumeSessionId | string | Não | - | ID de sessão para restaurar estado após refresh (sem player de áudio) |
* wsUrl é obrigatório apenas no modo gravação (quando sessionId e resumeSessionId não são fornecidos)
Detalhes dos Inputs
wsUrl (obrigatório)
URL completa do WebSocket de streaming. O componente adiciona automaticamente o query parameter token.
<ngx-jaimes-scribe-recorder
[wsUrl]="'wss://stream.jaimes.example.com/stream'"
/>token
Token JWT para autenticação. Usado tanto na conexão WebSocket quanto nas chamadas ao Lambda.
<ngx-jaimes-scribe-recorder
[wsUrl]="wsUrl"
[token]="authService.getToken()"
/>speakerLabels
Mapeamento de índices de speaker para labels customizados. Útil para identificar médico e paciente.
speakerLabels: Record<number, string> = {
0: 'Dr. Silva',
1: 'Paciente',
};<ngx-jaimes-scribe-recorder
[wsUrl]="wsUrl"
[token]="token"
[speakerLabels]="speakerLabels"
/>Labels padrão (quando não especificado):
0: "Pessoa 1"1: "Pessoa 2"2: "Pessoa 3"3: "Pessoa 4"
doctorName, patientName, companionName
Nomes para exibir na UI quando os speakers são identificados automaticamente. A identificação usa Claude Sonnet para analisar o conteúdo da conversa e determinar quem é médico, paciente ou acompanhante.
<ngx-jaimes-scribe-recorder
[wsUrl]="wsUrl"
[token]="token"
[doctorName]="'Dr. João Silva'"
[patientName]="'Maria Santos'"
[companionName]="'José Santos'"
/>Na UI: Exibe "Dr. João Silva", "Maria Santos", etc. Na API/Resumo: Usa labels genéricos "Médico", "Paciente", "Acompanhante".
Identificação Automática de Speakers
O componente identifica automaticamente quem é cada speaker (Médico, Paciente, Acompanhante) usando IA.
Fluxo
- Gravação em andamento: Diarização ativa (Deepgram retorna speaker IDs 0, 1, 2...)
- Acumulação: Sistema aguarda 2+ speakers com pelo menos 100 caracteres cada
- Identificação automática: Envia amostra para
/identify-speakers→ Claude Sonnet analisa contexto - Atualização da UI: Labels mudam de "Pessoa 1" → "Dr. João Silva" (ou "Médico" se nome não fornecido)
- Re-identificação: Se novo speaker aparecer, identifica novamente incluindo o novo
Correção Manual
O médico pode clicar no label de qualquer speaker para corrigir a identificação:
[Clica no label "Médico"]
↓
[Dropdown aparece]
├── 🩺 Médico
├── 🙋 Paciente
├── 👥 Acompanhante
└── ✏️ Outro...Indicador de Baixa Confiança
Quando a IA tem confiança < 70%, aparece um indicador ⚠️ para o médico revisar.
Persistência
O mapeamento de speakers é salvo automaticamente no Jaimes (S3) e retornado via GET /session/{sessionId}. Não é necessário persistir no EHR.
Labels na Geração de Anamnese
Quando o documento é gerado, o Lambda /generate usa os roles identificados:
[Médico]: Bom dia, como posso ajudá-lo?
[Paciente]: Doutor, estou com dor nas costas há duas semanas...
[Acompanhante]: Ele também tem dormido mal por causa disso.templates
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.
import { TemplateOption } from '@medc-com-br/ngx-jaimes-scribe';
templates: TemplateOption[] = [
{
id: 'anamnese',
name: 'Anamnese Completa',
description: 'Documento SOAP padrão',
content: {
type: 'object',
properties: {
queixa_principal: {
type: 'string',
description: 'Queixa principal do paciente'
},
historia_doenca_atual: {
type: 'string',
description: 'História da doença atual'
},
hipotese_diagnostica: {
type: 'string',
description: 'Hipótese diagnóstica'
},
conduta: {
type: 'string',
description: 'Conduta médica proposta'
},
},
required: ['queixa_principal', 'hipotese_diagnostica', 'conduta'],
},
},
{
id: 'resumo',
name: 'Resumo Rápido',
description: 'Resumo simplificado da consulta',
content: {
type: 'object',
properties: {
resumo: { type: 'string', description: 'Resumo da consulta' },
proximos_passos: { type: 'string', description: 'Próximos passos' },
},
required: ['resumo'],
},
},
];apiUrl
URL base da API para todas as operações do componente:
POST {apiUrl}/generate- Geração de documentosPOST {apiUrl}/identify-speakers- Identificação de speakers (médico/paciente)GET {apiUrl}/session/{sessionId}- Carregar sessões anteriores (playback)
<ngx-jaimes-scribe-recorder
[apiUrl]="'https://api.example.com'"
[sessionId]="previousSessionId"
[token]="token"
/>sessionId (Modo Playback)
Quando fornecido, o componente entra em modo playback:
- Carrega áudio e transcrição da sessão anterior
- Exibe player de áudio com controles (play/pause, seek, velocidade)
- Sincroniza transcrição com posição do áudio (highlight de palavra atual)
- Permite clicar em segmentos/palavras para pular para aquele ponto
<!-- Modo Playback - Revisar sessão anterior com áudio -->
<ngx-jaimes-scribe-recorder
[sessionId]="'session-abc123'"
[apiUrl]="apiUrl"
[token]="token"
[speakerLabels]="{ 0: 'Médico', 1: 'Paciente' }"
/>resumeSessionId (Restaurar Estado)
Quando fornecido, o componente restaura o estado de uma sessão finalizada:
- Carrega apenas a transcrição (sem player de áudio)
- Exibe a transcrição como se a sessão tivesse acabado de ser finalizada
- Habilita o botão "Gerar Resumo" para gerar novos documentos
- Útil para refresh de página ou navegação de volta à consulta
<!-- Restaurar estado após refresh -->
<ngx-jaimes-scribe-recorder
[resumeSessionId]="savedSessionId"
[apiUrl]="apiUrl"
[token]="token"
[templates]="templates"
/>Exemplo de uso com persistência:
@Component({
template: `
<ngx-jaimes-scribe-recorder
[wsUrl]="wsUrl"
[token]="token"
[apiUrl]="apiUrl"
[resumeSessionId]="savedSessionId()"
[templates]="templates"
(sessionFinished)="onSessionFinished($event)"
/>
`
})
export class ConsultationComponent implements OnInit {
savedSessionId = signal<string | undefined>(undefined);
ngOnInit() {
// Restaurar sessionId salvo (localStorage, URL param, etc)
const saved = localStorage.getItem('currentSessionId');
if (saved) {
this.savedSessionId.set(saved);
}
}
onSessionFinished(result: { transcript: string }) {
const sessionId = this.recorderComponent.lastSessionId();
// Salvar para poder restaurar após refresh
localStorage.setItem('currentSessionId', sessionId);
}
}Diferença entre sessionId e resumeSessionId:
| Aspecto | sessionId (Playback) | resumeSessionId (Restaurar) |
|---------|------------------------|-------------------------------|
| Player de áudio | ✅ Sim | ❌ Não |
| Transcrição | ✅ Com highlight sincronizado | ✅ Estática |
| Botão "Gerar Resumo" | ❌ Não | ✅ Sim |
| Controles de gravação | ❌ Não | ❌ Não |
| Caso de uso | Revisar consulta passada | Continuar após refresh |
Outputs
| Output | Tipo | Descrição |
|--------|------|-----------|
| sessionStarted | string | Emitido quando a gravação inicia, com o sessionId |
| sessionFinished | { transcript: string; entries: TranscriptEntry[] } | Emitido ao finalizar, com transcrição completa |
| documentGenerated | GeneratedDocument | Emitido quando um documento é gerado com sucesso |
| generationError | Error | Emitido quando há erro na geração do documento |
| error | Error | Emitido em erros de gravação, conexão ou transcrição |
Detalhes dos Outputs
sessionStarted
Emitido após a conexão WebSocket ser estabelecida e a captura de áudio iniciar.
onSessionStarted(sessionId: string): void {
this.currentSessionId = sessionId;
console.log('Gravação iniciada:', sessionId);
}sessionFinished
Emitido ao clicar em "Finalizar". Contém a transcrição completa e todas as entradas individuais.
interface TranscriptEntry {
id: number;
text: string;
speaker?: number;
isFinal: boolean;
startTime?: number;
endTime?: number;
}
onSessionFinished(result: { transcript: string; entries: TranscriptEntry[] }): void {
console.log('Palavras:', result.transcript.split(' ').length);
console.log('Segmentos:', result.entries.length);
// Salvar no prontuário
this.ehr.saveTranscription(result.transcript);
}documentGenerated
Emitido após a geração bem-sucedida de um documento via Lambda.
import { GeneratedDocument } from '@medc-com-br/ngx-jaimes-scribe';
onDocumentGenerated(doc: GeneratedDocument): void {
console.log('Template usado:', doc.templateId);
console.log('Conteúdo:', doc.content);
// Exemplo de conteúdo para template "anamnese"
// {
// queixa_principal: "Dor de cabeça há 3 dias",
// historia_doenca_atual: "Paciente relata...",
// hipotese_diagnostica: "Cefaleia tensional",
// conduta: "Dipirona 500mg 6/6h..."
// }
}generationError
Emitido quando há falha na geração do documento.
onGenerationError(error: Error): void {
console.error('Falha na geração:', error.message);
// Possíveis erros:
// - "Lambda URL not provided"
// - "No session available"
// - "HTTP 500" (erro do servidor)
// - "Transcription not found" (sessão expirou)
}error
Emitido em erros gerais de gravação ou conexão.
onError(error: Error): void {
console.error('Erro:', error.message);
// Possíveis erros:
// - "Permission denied" (microfone negado)
// - "WebSocket connection failed"
// - "Transcription service unavailable"
}Interfaces
TemplateOption
interface TemplateOption {
id: string; // Identificador único do template
name: string; // Nome exibido no menu
description?: string; // Descrição opcional
content: Record<string, unknown>; // JSON Schema para geração
}GeneratedDocument
interface GeneratedDocument {
sessionId: string; // ID da sessão de gravação
templateId: string; // ID do template usado
content: Record<string, unknown>; // Documento gerado
generatedAt: string; // ISO timestamp
}TranscriptionEvent
interface TranscriptionEvent {
type: 'connected' | 'partial' | 'final' | 'error';
sessionId: string;
transcript?: string;
timestamp?: number;
message?: string; // Mensagem de erro (quando type='error')
words?: TranscriptionWord[];
speaker?: number; // Índice do speaker (diarização)
start?: number; // Tempo inicial (segundos)
end?: number; // Tempo final (segundos)
confidence?: number; // Confiança da transcrição (0-1)
}
interface TranscriptionWord {
word: string;
start: number;
end: number;
confidence?: number;
speaker?: number;
}Customização Visual (CSS Variables)
O componente usa CSS Custom Properties para permitir customização completa sem sobrescrever estilos.
Cores Principais
ngx-jaimes-scribe-recorder {
--scribe-primary: #4caf50; /* Cor principal (botões, destaques) */
--scribe-primary-dark: #388e3c; /* Hover da cor principal */
--scribe-primary-light: #81c784; /* Estado connecting */
--scribe-danger: #f44336; /* Botão gravando (vermelho) */
--scribe-danger-dark: #d32f2f; /* Hover do danger */
}Cores de Texto
ngx-jaimes-scribe-recorder {
--scribe-text-color: #212121; /* Texto principal */
--scribe-text-partial: #9e9e9e; /* Texto parcial/placeholder */
}Tipografia
ngx-jaimes-scribe-recorder {
--scribe-font-family: inherit; /* Herda da aplicação */
--scribe-font-size: 1rem; /* Tamanho base */
}Backgrounds e Bordas
ngx-jaimes-scribe-recorder {
--scribe-bg: #ffffff; /* Background do componente */
--scribe-bg-transcript: #f5f5f5; /* Background da área de transcrição */
--scribe-border-radius: 8px; /* Arredondamento */
--scribe-border-color: #e0e0e0; /* Cor das bordas */
}Indicador de Nível de Áudio
ngx-jaimes-scribe-recorder {
--scribe-level-bg: #e0e0e0; /* Background da barra */
--scribe-level-fill: #4caf50; /* Preenchimento (nível) */
}Cores dos Speakers (Diarização)
ngx-jaimes-scribe-recorder {
--scribe-speaker-0: #2196f3; /* Speaker 0 (azul) */
--scribe-speaker-1: #9c27b0; /* Speaker 1 (roxo) */
--scribe-speaker-2: #ff9800; /* Speaker 2 (laranja) */
--scribe-speaker-3: #009688; /* Speaker 3 (teal) */
}Exemplo: Tema Escuro
ngx-jaimes-scribe-recorder {
--scribe-bg: #1e1e1e;
--scribe-bg-transcript: #2d2d2d;
--scribe-text-color: #ffffff;
--scribe-text-partial: #888888;
--scribe-border-color: #444444;
--scribe-level-bg: #444444;
--scribe-primary: #66bb6a;
--scribe-primary-dark: #4caf50;
}Exemplo: Tema Médico (Azul)
ngx-jaimes-scribe-recorder {
--scribe-primary: #1976d2;
--scribe-primary-dark: #1565c0;
--scribe-primary-light: #64b5f6;
--scribe-level-fill: #1976d2;
--scribe-speaker-0: #1976d2;
--scribe-speaker-1: #e91e63;
}Exemplo Completo
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
RecorderComponent,
TemplateOption,
GeneratedDocument
} from '@medc-com-br/ngx-jaimes-scribe';
@Component({
selector: 'app-medical-consultation',
standalone: true,
imports: [CommonModule, RecorderComponent],
template: `
<div class="consultation-container">
<header>
<h1>Consulta - {{ patientName }}</h1>
</header>
<ngx-jaimes-scribe-recorder
[wsUrl]="wsUrl"
[token]="token"
[speakerLabels]="speakerLabels"
[templates]="templates"
[apiUrl]="apiUrl"
(sessionStarted)="onSessionStarted($event)"
(sessionFinished)="onSessionFinished($event)"
(documentGenerated)="onDocumentGenerated($event)"
(generationError)="onGenerationError($event)"
(error)="onError($event)"
/>
@if (generatedDoc()) {
<section class="generated-document">
<h2>Documento Gerado</h2>
@for (field of getDocFields(); track field.key) {
<div class="field">
<label>{{ field.key }}</label>
<p>{{ field.value }}</p>
</div>
}
<button (click)="saveToEHR()">Salvar no Prontuário</button>
</section>
}
@if (errorMessage()) {
<div class="error-toast">{{ errorMessage() }}</div>
}
</div>
`,
styles: [`
.consultation-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
ngx-jaimes-scribe-recorder {
--scribe-primary: #1976d2;
--scribe-border-radius: 12px;
}
.generated-document {
margin-top: 2rem;
padding: 1.5rem;
background: #f5f5f5;
border-radius: 8px;
}
.field {
margin-bottom: 1rem;
}
.field label {
font-weight: 600;
color: #1976d2;
text-transform: capitalize;
}
.error-toast {
position: fixed;
bottom: 1rem;
right: 1rem;
padding: 1rem;
background: #f44336;
color: white;
border-radius: 4px;
}
`],
})
export class MedicalConsultationComponent {
patientName = 'João da Silva';
wsUrl = 'wss://stream.jaimes.example.com/stream';
token = 'eyJhbGciOiJIUzI1NiIs...';
apiUrl = 'https://api.jaimes.example.com';
speakerLabels: Record<number, string> = {
0: 'Dr. Oliveira',
1: 'Paciente',
};
templates: TemplateOption[] = [
{
id: 'soap',
name: 'SOAP',
description: 'Subjetivo, Objetivo, Avaliação, Plano',
content: {
type: 'object',
properties: {
subjetivo: { type: 'string', description: 'Queixas e história relatada' },
objetivo: { type: 'string', description: 'Achados do exame físico' },
avaliacao: { type: 'string', description: 'Diagnóstico/hipóteses' },
plano: { type: 'string', description: 'Conduta e tratamento' },
},
required: ['subjetivo', 'avaliacao', 'plano'],
},
},
{
id: 'receita',
name: 'Receituário',
description: 'Prescrição médica',
content: {
type: 'object',
properties: {
medicamentos: {
type: 'array',
items: {
type: 'object',
properties: {
nome: { type: 'string' },
dose: { type: 'string' },
posologia: { type: 'string' },
},
},
},
orientacoes: { type: 'string' },
},
},
},
];
currentSessionId = signal<string | null>(null);
generatedDoc = signal<GeneratedDocument | null>(null);
errorMessage = signal<string | null>(null);
onSessionStarted(sessionId: string): void {
this.currentSessionId.set(sessionId);
console.log('Gravação iniciada:', sessionId);
}
onSessionFinished(result: { transcript: string; entries: unknown[] }): void {
console.log('Transcrição:', result.transcript);
console.log('Segmentos:', result.entries.length);
}
onDocumentGenerated(doc: GeneratedDocument): void {
this.generatedDoc.set(doc);
console.log('Documento gerado:', doc);
}
onGenerationError(error: Error): void {
this.showError(`Erro na geração: ${error.message}`);
}
onError(error: Error): void {
this.showError(error.message);
}
getDocFields(): { key: string; value: string }[] {
const doc = this.generatedDoc();
if (!doc) return [];
return Object.entries(doc.content).map(([key, value]) => ({
key,
value: typeof value === 'string' ? value : JSON.stringify(value, null, 2),
}));
}
saveToEHR(): void {
const doc = this.generatedDoc();
if (!doc) return;
// Integrar com seu sistema de prontuário
console.log('Salvando no prontuário...', doc);
}
private showError(message: string): void {
this.errorMessage.set(message);
setTimeout(() => this.errorMessage.set(null), 5000);
}
}Arquitetura de Comunicação
┌─────────────────────────────────────────────────────────────────┐
│ Angular App │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ RecorderComponent │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ AudioCapture │ │ ScribeSocket │ │ │
│ │ │ Service │ │ Service │ │ │
│ │ │ │ │ │ │ │
│ │ │ - Microfone │ │ - WebSocket │ │ │
│ │ │ - VAD │──┼─► Streaming │ │ │
│ │ │ - PCM 16kHz │ │ - Eventos │ │ │
│ │ └─────────────────┘ └──────────────┬──────────────┘ │ │
│ └──────────────────────────────────────┼──────────────────┘ │
└─────────────────────────────────────────┼───────────────────────┘
│ WSS
▼
┌─────────────────────────────────────────────────────────────────┐
│ AWS Infrastructure │
│ │
│ ┌──────────────┐ ┌─────────────────────────────────────┐ │
│ │ ALB │───►│ ECS Fargate (Stream Service) │ │
│ └──────────────┘ │ │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ Deepgram Nova-3 │ │ │
│ │ └───────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ SessionManager │ │ │
│ │ │ - S3 (áudio + transcrição) │ │ │
│ │ │ - DynamoDB (metadados) │ │ │
│ │ └─────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌─────────────────────────────────────┐ │
│ │ API Gateway │───►│ Lambda GenAI │ │
│ │ POST/generate│ │ │ │
│ └──────────────┘ │ ┌─────────────────────────────┐ │ │
│ │ │ AWS Bedrock │ │ │
│ │ │ Claude Sonnet 4.5 │ │ │
│ │ └─────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘Fluxo de Operação
1. Gravação
- Usuário clica no botão de microfone
- Componente solicita permissão do microfone
- Conexão WebSocket é estabelecida
- Áudio é capturado, processado (VAD) e enviado em chunks
- Transcrições parciais e finais são recebidas e exibidas
- Usuário pode pausar/retomar a qualquer momento
- Ao finalizar,
sessionFinishedé emitido
2. Geração de Documento
- Após finalizar, botão "Gerar Resumo" aparece
- Usuário seleciona um template
- Componente faz POST para Lambda com
sessionIdeoutputSchema - Lambda recupera transcrição do S3
- Bedrock gera documento estruturado
documentGeneratedé emitido com o resultado
Requisitos
- Angular 19+
- Navegador com suporte a:
- Web Audio API
- MediaDevices API
- WebSocket
- Permissão de microfone
Troubleshooting
Microfone não detectado
// Verifique permissões
const permission = await navigator.permissions.query({ name: 'microphone' as PermissionName });
if (permission.state === 'denied') {
// Instruir usuário a permitir nas configurações
}Conexão WebSocket falha
- Verifique se a URL usa
wss://(nãows://) - Confirme que o token JWT é válido
- Verifique conectividade de rede
Geração de documento retorna erro
- Confirme que
apiUrlestá configurado - Verifique se a sessão ainda existe no S3 (TTL de 30 dias)
- Confira formato do
contentno template (JSON Schema válido)
Licença
Proprietário - MEDC Sistemas de Saúde
