pedrottic commited on
Commit
1860fb0
·
1 Parent(s): aa7f27c

first commit

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. app.py +231 -4
  3. requirements.txt +5 -0
README.md CHANGED
@@ -8,7 +8,7 @@ sdk_version: 5.38.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
- short_description: Open-Surce Intelligent Transcription for medical encounters
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
+ short_description: Open-Source Intelligent Transcription for medical encounters
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py CHANGED
@@ -1,7 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
1
+ # app.py – Transcrição Inteligente de Consultas Médicas em Tempo Real
2
+ # Autor: OpenAI ChatGPT (o3)
3
+ """
4
+ Este aplicativo Gradio roda em um Hugging Face Space e demonstra:
5
+ • Captura de áudio do microfone em tempo real
6
+ • Envio de chunks para a Realtime API da OpenAI (modelo gpt‑4o‑transcribe)
7
+ • Exibição da transcrição ao vivo
8
+ • Resumo de ~60 s em bullet points
9
+ • Geração de nota SOAP final
10
+ • Download do áudio e botão para copiar texto
11
+
12
+ ⚠️ Este código é um protótipo de referência. Em produção, trate PHI com rigor, use
13
+ chaves de API via Secrets do Space e adicione controle de erros mais robusto.
14
+ """
15
+
16
+ import asyncio
17
+ import json
18
+ import os
19
+ import tempfile
20
+ import time
21
+ from datetime import datetime
22
+
23
  import gradio as gr
24
+ import numpy as np
25
+ import openai
26
+ import soundfile as sf
27
+ import websockets
28
+
29
+ # -------------------------------------------------------
30
+ # Configuração
31
+ # -------------------------------------------------------
32
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
33
+ if not OPENAI_API_KEY:
34
+ raise RuntimeError("Defina OPENAI_API_KEY nas variáveis de ambiente ou Secrets do HF Space!")
35
+
36
+ openai.api_key = OPENAI_API_KEY
37
+
38
+ STT_MODEL = "gpt-4o-realtime-preview-2025-06-03" # modelo de transcrição
39
+ SUMMARY_MODEL = "gpt-4o-mini" # bullet points minuto a minuto
40
+ SOAP_MODEL = "gpt-4o" # sumário final SOAP
41
+ SAMPLE_RATE = 16000 # Hz, mono
42
+ SUMMARY_EVERY_SEC = 60 # janela de resumo
43
+
44
+ # -------------------------------------------------------
45
+ # Estado de sessão (single‑user Space simplificado)
46
+ # -------------------------------------------------------
47
+ class SessionState:
48
+ def __init__(self):
49
+ self.ws = None # conexão WebSocket com API Realtime
50
+ self.running = False # flag de captura
51
+ self.transcript_full = "" # transcrição acumulada
52
+ self.bullets = [] # lista de bullets
53
+ self.last_summary_ts = time.time()
54
+ self.audio_chunks = [] # lista de arrays numpy
55
+ self.contexto = "" # texto inserido pelo usuário
56
+
57
+ state = SessionState()
58
+
59
+ # -------------------------------------------------------
60
+ # Funções auxiliares – OpenAI Realtime API
61
+ # -------------------------------------------------------
62
+ async def open_realtime_ws() -> websockets.WebSocketClientProtocol:
63
+ """Abre e retorna a conexão WebSocket com a Realtime API."""
64
+ uri = f"wss://api.openai.com/v1/realtime?model={STT_MODEL}"
65
+ ws = await websockets.connect(
66
+ uri,
67
+ extra_headers={
68
+ "Authorization": f"Bearer {OPENAI_API_KEY}",
69
+ "OpenAI-Beta": "realtime=v1",
70
+ },
71
+ subprotocols=["realtime"],
72
+ max_size=1 * 1024 * 1024, # 1 MB
73
+ )
74
+
75
+ # Primeiro evento deve ser session.created
76
+ evt = json.loads(await ws.recv())
77
+ if evt.get("type") != "session.created":
78
+ raise RuntimeError(f"Evento inicial inesperado: {evt}")
79
+ return ws
80
+
81
+ async def pcm_from_numpy(chunk: np.ndarray) -> bytes:
82
+ """Converte numpy float32 (-1..1) → bytes PCM 16‑bit LE."""
83
+ if chunk.dtype != np.float32:
84
+ chunk = chunk.astype(np.float32)
85
+ pcm16 = (np.clip(chunk, -1, 1) * 32767).astype(np.int16)
86
+ return pcm16.tobytes()
87
+
88
+ async def send_audio_chunk(chunk: np.ndarray, ws: websockets.WebSocketClientProtocol):
89
+ await ws.send(await pcm_from_numpy(chunk))
90
+
91
+ # -------------------------------------------------------
92
+ # Funções de resumo / SOAP (Chat Completions)
93
+ # -------------------------------------------------------
94
+ async def summarize_block(text: str) -> str:
95
+ """Gera até 5 bullet points em PT‑BR para o bloco de texto fornecido."""
96
+ rsp = openai.chat.completions.create(
97
+ model=SUMMARY_MODEL,
98
+ messages=[
99
+ {
100
+ "role": "system",
101
+ "content": "Você é escriba clínico. Resuma o texto a seguir em até 5 bullet points concisos, em português.",
102
+ },
103
+ {"role": "user", "content": text},
104
+ ],
105
+ temperature=0.3,
106
+ )
107
+ return rsp.choices[0].message.content.strip()
108
+
109
+ async def generate_soap(full_txt: str, bullets: list[str], contexto: str) -> str:
110
+ """Combina transcript + bullets + contexto para gerar nota SOAP final."""
111
+ rsp = openai.chat.completions.create(
112
+ model=SOAP_MODEL,
113
+ messages=[
114
+ {"role": "system", "content": "Você é um escriba médico sênior."},
115
+ {"role": "user", "content": f"Contexto: {contexto}"},
116
+ {"role": "assistant", "content": "\n".join(bullets)},
117
+ {
118
+ "role": "user",
119
+ "content": (
120
+ "Transcrição completa a seguir. Elabore a nota final no formato SOAP, em português, "
121
+ "utilizando os bullet points como guia.\n\n" + full_txt
122
+ ),
123
+ },
124
+ ],
125
+ temperature=0.2,
126
+ )
127
+ return rsp.choices[0].message.content.strip()
128
+
129
+ # -------------------------------------------------------
130
+ # Callbacks Gradio
131
+ # -------------------------------------------------------
132
+ async def cb_start(contexto: str):
133
+ """Inicia gravação: abre WS, reseta estados."""
134
+ if state.running:
135
+ return gr.update(value="Já gravando...")
136
+
137
+ state.__init__() # reset
138
+ state.contexto = contexto
139
+ state.running = True
140
+ state.ws = await open_realtime_ws()
141
+ state.last_summary_ts = time.time()
142
+ return gr.update(value="Gravando… (clique em Finalizar para encerrar)")
143
+
144
+ async def cb_stream(audio_chunk, live_txt, live_sum):
145
+ """Callback contínuo do componente de microfone (streaming=True)."""
146
+ if not state.running or audio_chunk is None:
147
+ return live_txt, live_sum
148
+
149
+ # Garantir mono
150
+ if audio_chunk.ndim == 2:
151
+ audio_chunk = audio_chunk.mean(axis=1)
152
+
153
+ # Envia chunk para a API e guarda localmente
154
+ await send_audio_chunk(audio_chunk, state.ws)
155
+ state.audio_chunks.append(audio_chunk)
156
+
157
+ # Tenta ler rapidamente novos transcripts (non‑blocking)
158
+ try:
159
+ for _ in range(5):
160
+ msg = await asyncio.wait_for(state.ws.recv(), timeout=0.01)
161
+ evt = json.loads(msg)
162
+ if evt.get("type") == "transcript":
163
+ txt = evt["transcript"]["text"]
164
+ state.transcript_full += txt + " "
165
+ except (asyncio.TimeoutError, websockets.exceptions.ConnectionClosedOK):
166
+ pass
167
+
168
+ # Resumo a cada SUMMARY_EVERY_SEC
169
+ now = time.time()
170
+ if now - state.last_summary_ts >= SUMMARY_EVERY_SEC:
171
+ bullet = await summarize_block(state.transcript_full[-4000:])
172
+ state.bullets.append(bullet)
173
+ state.last_summary_ts = now
174
+
175
+ live_summary_md = "\n\n".join(state.bullets)
176
+ return state.transcript_full, live_summary_md
177
+
178
+ async def cb_stop():
179
+ """Finaliza gravação, gera SOAP e disponibiliza download do áudio."""
180
+ if not state.running:
181
+ return "", "", ""
182
+
183
+ state.running = False
184
+ if state.ws:
185
+ await state.ws.close()
186
+
187
+ # Garantir último resumo se necessário
188
+ if state.transcript_full and (not state.bullets or (time.time() - state.last_summary_ts) > 15):
189
+ bullet = await summarize_block(state.transcript_full[-4000:])
190
+ state.bullets.append(bullet)
191
+
192
+ soap = await generate_soap(state.transcript_full, state.bullets, state.contexto)
193
+
194
+ # Salvar áudio
195
+ wav_path = tempfile.mktemp(suffix=".wav", prefix="consulta_")
196
+ if state.audio_chunks:
197
+ audio_np = np.concatenate(state.audio_chunks)
198
+ sf.write(wav_path, audio_np, SAMPLE_RATE)
199
+
200
+ # Botões de download/cópia
201
+ download_link = f"<a href='file={wav_path}' download>Baixar áudio (.wav)</a>"
202
+ copy_btn = (
203
+ "<button onclick=\"navigator.clipboard.writeText("\" + "`" + soap.replace("`", "\\`") + "`" + ")\">Copiar SOAP</button>"
204
+ )
205
+
206
+ soap_html = f"<h3>Nota SOAP</h3><pre>{soap}</pre>{download_link}<br>{copy_btn}"
207
+ return state.transcript_full, "\n\n".join(state.bullets), soap_html
208
+
209
+ # -------------------------------------------------------
210
+ # Interface Gradio
211
+ # -------------------------------------------------------
212
+ with gr.Blocks(title="Transcrição Inteligente – Demo") as demo:
213
+ gr.Markdown("## Transcrição inteligente de consultas médicas em tempo real")
214
+
215
+ with gr.Row():
216
+ contexto_txt = gr.Textbox(label="Contexto da consulta (opcional)", lines=2, placeholder="Ex.: Paciente com dispneia crônica...")
217
+ btn_start = gr.Button("Iniciar", variant="primary")
218
+ btn_stop = gr.Button("Finalizar", variant="stop")
219
+
220
+ with gr.Row():
221
+ md_transcript = gr.Markdown("", label="Transcrição em tempo real")
222
+ md_summary = gr.Markdown("", label="Resumo (bullet points)")
223
+
224
+ md_soap = gr.HTML("", label="Nota SOAP final")
225
+
226
+ mic = gr.Audio(source="microphone", type="numpy", streaming=True, label="Microfone (16 kHz)")
227
 
228
+ # Eventos
229
+ btn_start.click(cb_start, inputs=[contexto_txt], outputs=[btn_start])
230
+ mic.stream(cb_stream, inputs=[mic, md_transcript, md_summary], outputs=[md_transcript, md_summary])
231
+ btn_stop.click(cb_stop, inputs=None, outputs=[md_transcript, md_summary, md_soap])
232
 
233
+ if __name__ == "__main__":
234
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ openai>=1.24.0
2
+ gradio>=4.27.0
3
+ websockets>=12.0
4
+ soundfile>=0.12.1
5
+ numpy>=1.26.0