pedrottic commited on
Commit
de23cee
·
1 Parent(s): 062d16c
Files changed (2) hide show
  1. app.py +247 -185
  2. requirements.txt +2 -2
app.py CHANGED
@@ -1,16 +1,30 @@
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
@@ -18,7 +32,6 @@ 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
@@ -26,213 +39,262 @@ 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 16bit 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
-
203
- # escapa crases e aspas duplas para não quebrar o JS
204
- escaped_soap = soap.replace('`', '\\`').replace('"', '\\"')
205
-
206
- copy_btn = (
207
- f'<button onclick="navigator.clipboard.writeText(`{escaped_soap}`)">Copiar SOAP</button>'
208
- )
209
-
210
- soap_html = f"<h3>Nota SOAP</h3><pre>{soap}</pre>{download_link}<br>{copy_btn}"
211
- return state.transcript_full, "\n\n".join(state.bullets), soap_html
212
-
213
- # -------------------------------------------------------
214
- # Interface Gradio
215
- # -------------------------------------------------------
216
- with gr.Blocks(title="Transcrição Inteligente – Demo") as demo:
217
- gr.Markdown("## Transcrição inteligente de consultas médicas em tempo real")
218
-
219
- with gr.Row():
220
- contexto_txt = gr.Textbox(label="Contexto da consulta (opcional)", lines=2, placeholder="Ex.: Paciente com dispneia crônica...")
221
- btn_start = gr.Button("Iniciar", variant="primary")
222
- btn_stop = gr.Button("Finalizar", variant="stop")
223
-
224
- with gr.Row():
225
- md_transcript = gr.Markdown("", label="Transcrição em tempo real")
226
- md_summary = gr.Markdown("", label="Resumo (bullet points)")
227
-
228
- md_soap = gr.HTML("", label="Nota SOAP final")
229
-
230
- mic = gr.Audio(sources=["microphone"], type="numpy", streaming=True, label="Microfone (16 kHz)")
231
-
232
- # Eventos
233
- btn_start.click(cb_start, inputs=[contexto_txt], outputs=[btn_start])
234
- mic.stream(cb_stream, inputs=[mic, md_transcript, md_summary], outputs=[md_transcript, md_summary])
235
- btn_stop.click(cb_stop, inputs=None, outputs=[md_transcript, md_summary, md_soap])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
  if __name__ == "__main__":
238
- demo.launch()
 
 
 
1
  """
2
+ MedBuddy: Transcrição Médica em Tempo Real com OpenAI e Gradio
3
+
4
+ Versão: 2.0
5
+ Data: 20/07/2025
6
+
7
+ Este aplicativo Gradio, projetado para rodar em um Hugging Face Space, demonstra
8
+ um pipeline de transcrição e sumarização de consultas médicas em tempo real.
9
+
10
+ Funcionalidades:
11
+ - Captura de áudio do microfone via streaming.
12
+ - Transcrição ao vivo usando a Realtime API da OpenAI (gpt-4o-realtime).
13
+ - Geração de resumos periódicos em "bullet points" (gpt-4o-mini).
14
+ - Elaboração de uma nota final no formato SOAP (gpt-4o).
15
+ - Interface limpa com componentes nativos para copiar texto e baixar o áudio.
16
+
17
+ Requisitos:
18
+ - gradio
19
+ - openai
20
+ - websockets
21
+ - soundfile
22
+ - numpy
23
+
24
+ ⚠️ AVISO: Este código é um protótipo de referência. Para uso em produção,
25
+ é mandatório tratar informações de saúde protegidas (PHI) com o máximo rigor,
26
+ utilizar o sistema de Secrets do Hugging Face para chaves de API e implementar
27
+ um tratamento de erros mais abrangente.
28
  """
29
 
30
  import asyncio
 
32
  import os
33
  import tempfile
34
  import time
 
35
 
36
  import gradio as gr
37
  import numpy as np
 
39
  import soundfile as sf
40
  import websockets
41
 
42
+ # -------------------------------------------------------------------
43
  # Configuração
44
+ # -------------------------------------------------------------------
45
+ # Chave de API da OpenAI (carregada a partir dos Secrets do HF Space)
46
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
47
  if not OPENAI_API_KEY:
48
+ raise ValueError("A variável de ambiente OPENAI_API_KEY não foi definida.")
 
49
  openai.api_key = OPENAI_API_KEY
50
 
51
+ # Modelos da OpenAI (verificar documentação para os nomes mais recentes)
52
+ STT_MODEL = "gpt-4o-realtime-preview-2025-06-03"
53
+ SUMMARY_MODEL = "gpt-4o-mini"
54
+ SOAP_MODEL = "gpt-4o"
55
+
56
+ # Configurações de áudio
57
+ SAMPLE_RATE = 16000 # Hz
58
+ SUMMARY_EVERY_SEC = 60 # Gerar resumo a cada 60 segundos
59
+
60
+ # -------------------------------------------------------------------
61
+ # Gerenciamento de Estado da Sessão
62
+ # -------------------------------------------------------------------
63
+ # Nota: Para um ambiente multiusuário, o ideal é usar `gr.State`
64
+ # para gerenciar o estado de cada sessão individualmente.
65
+ # Para este demo de usuário único, uma classe global simplifica o código.
66
  class SessionState:
67
+ """Encapsula o estado de uma sessão de gravação ativa."""
68
  def __init__(self):
69
+ self.ws: websockets.WebSocketClientProtocol | None = None
70
+ self.running: bool = False
71
+ self.transcript_full: str = ""
72
+ self.bullets: list[str] = []
73
+ self.last_summary_ts: float = 0.0
74
+ self.audio_chunks: list[np.ndarray] = []
75
+ self.contexto: str = ""
76
 
77
  state = SessionState()
78
 
79
+ # -------------------------------------------------------------------
80
+ # Funções de Comunicação com a API OpenAI
81
+ # -------------------------------------------------------------------
82
  async def open_realtime_ws() -> websockets.WebSocketClientProtocol:
83
+ """Abre e valida a conexão WebSocket com a Realtime API."""
84
+ uri = f"wss://api.openai.com/v1/realtime?model={STT_MODEL}&sample_rate={SAMPLE_RATE}"
85
+ try:
86
+ ws = await websockets.connect(
87
+ uri,
88
+ extra_headers={"Authorization": f"Bearer {OPENAI_API_KEY}"},
89
+ subprotocols=["realtime"],
90
+ max_size=2 * 1024 * 1024, # 2 MB
91
+ )
92
+ # O primeiro evento deve ser a confirmação da criação da sessão
93
+ evt = json.loads(await asyncio.wait_for(ws.recv(), timeout=10))
94
+ if evt.get("type") != "session.created":
95
+ raise ConnectionRefusedError(f"Falha ao criar sessão: {evt}")
96
+ return ws
97
+ except Exception as e:
98
+ print(f"Erro ao conectar ao WebSocket: {e}")
99
+ raise
100
 
101
  async def pcm_from_numpy(chunk: np.ndarray) -> bytes:
102
+ """Converte um array numpy float32 para bytes no formato PCM 16-bit little-endian."""
103
+ # Garante que o array é float32 e está no range [-1, 1]
104
  if chunk.dtype != np.float32:
105
  chunk = chunk.astype(np.float32)
106
+ chunk = np.clip(chunk, -1.0, 1.0)
107
+
108
+ # Converte para int16
109
+ pcm16 = (chunk * 32767).astype(np.int16)
110
  return pcm16.tobytes()
111
 
112
+ # -------------------------------------------------------------------
113
+ # Funções de Geração de Texto (Sumário e SOAP)
114
+ # -------------------------------------------------------------------
 
 
 
115
  async def summarize_block(text: str) -> str:
116
+ """Gera um resumo conciso (bullet points) para um trecho da transcrição."""
117
+ if not text.strip():
118
+ return ""
119
+ try:
120
+ response = await openai.chat.completions.create(
121
+ model=SUMMARY_MODEL,
122
+ messages=[
123
+ {"role": "system", "content": "Você é um escriba clínico. Resuma o texto a seguir em até 5 bullet points concisos, em português do Brasil."},
124
+ {"role": "user", "content": text},
125
+ ],
126
+ temperature=0.3,
127
+ max_tokens=200,
128
+ )
129
+ return response.choices[0].message.content.strip()
130
+ except Exception as e:
131
+ print(f"Erro ao gerar sumário: {e}")
132
+ return "[Erro ao gerar sumário parcial]"
133
 
134
  async def generate_soap(full_txt: str, bullets: list[str], contexto: str) -> str:
135
+ """Gera a nota final no formato SOAP a partir do contexto e da transcrição."""
136
+ if not full_txt.strip():
137
+ return "Nenhuma transcrição foi gerada para criar a nota SOAP."
138
+
139
+ bullet_summary = "\n".join(bullets)
140
+ prompt_context = f"Contexto prévio da consulta: {contexto if contexto else 'Nenhum'}"
141
+
142
+ try:
143
+ response = await openai.chat.completions.create(
144
+ model=SOAP_MODEL,
145
+ messages=[
146
+ {"role": "system", "content": "Você é um assistente médico sênior especializado em documentação clínica. Sua tarefa é criar uma nota no formato SOAP (Subjetivo, Objetivo, Avaliação, Plano) baseada na transcrição da consulta."},
147
+ {"role": "user", "content": f"{prompt_context}\n\nResumo dos pontos chave (para guia):\n{bullet_summary}\n\nUse a transcrição completa a seguir para elaborar a nota SOAP final em português do Brasil, de forma estruturada e profissional:\n\n---\n{full_txt}"},
148
+ ],
149
+ temperature=0.2,
150
+ max_tokens=1500,
151
+ )
152
+ return response.choices[0].message.content.strip()
153
+ except Exception as e:
154
+ print(f"Erro ao gerar nota SOAP: {e}")
155
+ return f"[Erro ao gerar nota SOAP final]\n\nTranscrição completa:\n{full_txt}"
156
+
157
+ # -------------------------------------------------------------------
158
+ # Callbacks da Interface Gradio
159
+ # -------------------------------------------------------------------
160
  async def cb_start(contexto: str):
161
+ """Callback: Inicia a gravação."""
162
  if state.running:
163
+ return
164
+
165
+ state.__init__() # Reseta o estado da sessão
166
  state.contexto = contexto
167
  state.running = True
168
+
169
+ try:
170
+ state.ws = await open_realtime_ws()
171
+ state.last_summary_ts = time.time()
172
+ print("Sessão de gravação iniciada.")
173
+ # Limpa os campos da UI
174
+ return "", "", "", None
175
+ except Exception as e:
176
+ state.running = False
177
+ gr.Warning(f"Não foi possível iniciar a gravação: {e}")
178
+ return "", "", "", None
179
+
180
+ async def cb_stream(audio_stream, live_txt, live_sum):
181
+ """Callback: Processa o stream de áudio em tempo real."""
182
+ if not state.running or audio_stream is None or not state.ws:
183
  return live_txt, live_sum
184
 
 
 
 
 
 
 
 
 
 
185
  try:
186
+ await state.ws.send(await pcm_from_numpy(audio_stream))
187
+ state.audio_chunks.append(audio_stream)
188
+
189
+ # Processa mensagens recebidas do WebSocket de forma não-bloqueante
190
+ while True:
191
+ try:
192
+ msg = await asyncio.wait_for(state.ws.recv(), timeout=0.01)
193
+ evt = json.loads(msg)
194
+ if evt.get("type") == "transcript" and (text := evt.get("transcript", {}).get("text")):
195
+ state.transcript_full += text + " "
196
+ except asyncio.TimeoutError:
197
+ break # Não mais mensagens no buffer, sai do loop
198
+
199
+ # Gera resumo periódico
200
+ if (time.time() - state.last_summary_ts) >= SUMMARY_EVERY_SEC:
201
+ # Pega os últimos ~4000 caracteres para o resumo parcial
202
+ transcript_slice = state.transcript_full[-4000:]
203
+ bullet = await summarize_block(transcript_slice)
204
+ if bullet:
205
+ state.bullets.append(bullet)
206
+ state.last_summary_ts = time.time()
207
+
208
+ live_summary_md = "\n\n".join(state.bullets)
209
+ return state.transcript_full, live_summary_md
210
+
211
+ except (websockets.exceptions.ConnectionClosed, Exception) as e:
212
+ print(f"Erro durante o streaming: {e}")
213
+ await cb_stop() # Tenta finalizar a sessão de forma limpa
214
+ return live_txt, live_sum
215
 
216
+ async def cb_stop(audio_filepath):
217
+ """Callback: Finaliza a gravação, gera a nota SOAP e prepara o download."""
218
  if not state.running:
219
+ return "", None # Retorna valores para sumário final e botão de download
220
 
221
+ print("Finalizando a gravação...")
222
  state.running = False
223
  if state.ws:
224
  await state.ws.close()
225
 
226
+ # Gera um último resumo se houver transcrição nova
227
  if state.transcript_full and (not state.bullets or (time.time() - state.last_summary_ts) > 15):
228
  bullet = await summarize_block(state.transcript_full[-4000:])
229
+ if bullet:
230
+ state.bullets.append(bullet)
231
+
232
+ soap_note = await generate_soap(state.transcript_full, state.bullets, state.contexto)
233
+
234
+ # O `audio_filepath` já é fornecido pelo Gradio quando `type="filepath"`
235
+ # e `stop_recording` é acionado. Não precisamos mais montar o áudio manualmente.
236
+ print(f"Áudio final salvo em: {audio_filepath}")
237
+
238
+ return soap_note, audio_filepath
239
+
240
+ # -------------------------------------------------------------------
241
+ # Definição da Interface Gráfica (Gradio)
242
+ # -------------------------------------------------------------------
243
+ with gr.Blocks(theme=gr.themes.Soft(), title="MedBuddy – Transcrição Médica") as demo:
244
+ gr.Markdown("# MedBuddy")
245
+ gr.Markdown("### Um Modelo Open-Source de Transcrição Inteligente de Consultas Médicas")
246
+
247
+ with gr.Tabs():
248
+ with gr.TabItem("Gravação e Transcrição"):
249
+ contexto_txt = gr.Textbox(
250
+ label="Contexto da Consulta (opcional)",
251
+ lines=3,
252
+ placeholder="Ex.: Paciente com histórico de dispneia crônica, fumante há 20 anos, apresentando tosse persistente."
253
+ )
254
+
255
+ mic_audio = gr.Audio(
256
+ sources=["microphone"],
257
+ type="filepath", # 'filepath' é ideal para o botão de download
258
+ label="Microfone (16kHz)",
259
+ streaming=True,
260
+ )
261
+
262
+ with gr.Row():
263
+ transcricao_txt = gr.Textbox(label="Transcrição em Tempo Real", lines=15, interactive=False)
264
+ sumario_basico_txt = gr.Textbox(label="Resumo (Bullet Points)", lines=15, interactive=False)
265
+
266
+ with gr.Accordion("Resultados Finais", open=False):
267
+ sumario_final_txt = gr.Textbox(
268
+ label="Nota SOAP Final",
269
+ lines=15,
270
+ interactive=False,
271
+ show_copy_button=True
272
+ )
273
+ baixar_btn = gr.DownloadButton(
274
+ "Baixar Áudio (.wav)",
275
+ interactive=True
276
+ )
277
+
278
+ # ---------------------------------------------------------------
279
+ # Lógica de Eventos da Interface
280
+ # ---------------------------------------------------------------
281
+ mic_audio.start_recording(
282
+ fn=cb_start,
283
+ inputs=[contexto_txt],
284
+ outputs=[transcricao_txt, sumario_basico_txt, sumario_final_txt, baixar_btn]
285
+ )
286
+
287
+ mic_audio.stream(
288
+ fn=cb_stream,
289
+ inputs=[mic_audio, transcricao_txt, sumario_basico_txt],
290
+ outputs=[transcricao_txt, sumario_basico_txt]
291
+ )
292
+
293
+ mic_audio.stop_recording(
294
+ fn=cb_stop,
295
+ inputs=[mic_audio],
296
+ outputs=[sumario_final_txt, baixar_btn]
297
+ )
298
 
299
  if __name__ == "__main__":
300
+ demo.launch(debug=True)
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
- openai
2
  gradio
3
- websockets
 
4
  soundfile
5
  numpy
 
 
1
  gradio
2
+ openai>=1.0.0
3
+ websockets>=10.0
4
  soundfile
5
  numpy