sbenfenatti commited on
Commit
41bbaee
·
verified ·
1 Parent(s): 587bd75

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -143
app.py CHANGED
@@ -1,179 +1,131 @@
1
- import os, base64, tempfile, logging, asyncio
 
 
 
2
  from faster_whisper import WhisperModel
3
- from transformers import AutoTokenizer # ← NOVO
4
  import google.generativeai as genai
5
- from flask import Flask, request, jsonify, send_from_directory
6
- from flask_cors import CORS
7
- from dotenv import load_dotenv
8
  import edge_tts
9
- from asgiref.wsgi import WsgiToAsgi # Flask → ASGI
10
 
11
- # ---------- Configuração ----------
12
  load_dotenv()
 
13
  CACHE_DIR = os.getenv("HF_HUB_CACHE", "./models_cache")
14
  os.environ["MPLCONFIGDIR"] = os.path.join(CACHE_DIR, "matplotlib")
15
 
16
  LOGIN_PASSWORDS = os.getenv("LOGIN_PASSWORDS")
17
- GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
18
-
19
- # ---------- Vocabulário ----------
20
- # Termos odontológicos mais frequentes
21
- TERMS_DENTAL = [
22
- "odontologia", "dentista", "dente", "gengiva", "periodontite",
23
- "gengivite", "cárie", "profilaxia", "halitose", "bruxismo",
24
- "restauração", "implante", "tratamento de canal", "pulpite",
25
- "estomatite", "afta", "fluorose", "radiografia", "aparelho ortodôntico",
26
- "prótese", "clareamento", "raspagem", "tártaro"
27
- ]
28
-
29
- # Medicamentos e substâncias de uso rotineiro
30
- TERMS_MEDS = [
31
- "paracetamol", "dipirona", "ibuprofeno", "diclofenaco", "nimesulida",
32
- "amoxicilina", "azitromicina", "clindamicina", "clorexidina",
33
- "lidocaína", "articaína", "bupivacaína", "cetoprofeno",
34
- "cetirizina", "benzocaína", "metronidazol", "prednisona",
35
- "dexametasona", "ketorolaco", "omeprazol"
36
- ]
37
-
38
- # Junta listas em string único
39
- _INITIAL_PROMPT_RAW = ", ".join(TERMS_DENTAL + TERMS_MEDS)
40
-
41
- # ---------- Tokenizer para garantir ≤224 tokens ----------
42
- TOKENIZER = AutoTokenizer.from_pretrained("openai/whisper-small")
43
- MAX_PROMPT_TOKENS = 224
44
-
45
- def safe_prompt(text: str) -> str:
46
- """Garante que o prompt não exceda 224 tokens do Whisper."""
47
- ids = TOKENIZER.encode(text, add_special_tokens=False)
48
- return TOKENIZER.decode(ids[-MAX_PROMPT_TOKENS:], skip_special_tokens=True)
49
-
50
- INITIAL_PROMPT = safe_prompt(_INITIAL_PROMPT_RAW)
51
-
52
- # ---------- Servidor ----------
53
- app = Flask(__name__)
54
- CORS(app)
55
- logging.basicConfig(
56
- level=logging.INFO,
57
- format="%(asctime)s - %(levelname)s - %(message)s"
58
- )
59
-
60
- logging.info("Carregando modelos…")
61
-
62
- # Gemini
63
  gemini_model = None
64
- if GOOGLE_API_KEY:
 
 
 
 
65
  try:
66
- genai.configure(api_key=GOOGLE_API_KEY)
67
- gemini_model = genai.GenerativeModel("gemini-1.5-flash")
68
- logging.info("Gemini pronto.")
69
  except Exception as e:
70
- logging.error("Falha no Gemini: %s", e)
71
-
72
- # Whisper
73
- try:
74
- whisper_model = WhisperModel(
75
- "small", device="cpu", compute_type="int8"
76
- )
77
- logging.info("Whisper pronto.")
78
- except Exception as e:
79
- whisper_model = None
80
- logging.error("Falha no Whisper: %s", e)
 
 
81
 
82
  # ---------- Utilidades ----------
83
  def ask_gemini(question: str) -> str:
84
  if not gemini_model:
85
- return ("Desculpe, o serviço de IA não está disponível.")
86
- system_prompt = (
87
- "Você é 'SintonIA', assistente de saúde bucal. "
88
- "Responda em 2–3 frases, com empatia, sem diagnosticar, "
89
- "e incentive consulta presencial ao dentista."
90
- )
91
  try:
92
- resp = gemini_model.generate_content([system_prompt, question])
93
- return resp.text
94
  except Exception as e:
95
- logging.error("Erro no Gemini: %s", e)
96
- return ("Desculpe, ocorreu um erro ao gerar a resposta.")
97
 
98
  VOICE = "pt-BR-AntonioNeural"
99
  async def synthesize(text: str) -> bytes | None:
100
  try:
101
- audio = b""
102
  communicate = edge_tts.Communicate(text, VOICE)
103
  async for chunk in communicate.stream():
104
  if chunk["type"] == "audio":
105
- audio += chunk["data"]
106
- return audio
107
  except Exception as e:
108
- logging.error("Erro no TTS: %s", e)
109
  return None
110
 
111
- # ---------- Rotas ----------
112
- @app.route("/")
113
- def index():
114
- return send_from_directory(".", "index.html")
115
 
116
- @app.route("/login", methods=["POST"])
117
- def login():
118
  if not LOGIN_PASSWORDS:
119
- return jsonify(success=True)
120
- valid = [p.strip() for p in LOGIN_PASSWORDS.split(",")]
121
- ok = (request.json or {}).get("password", "") in valid
122
- return jsonify(success=ok), (200 if ok else 401)
123
-
124
- @app.route("/process-audio", methods=["POST"])
125
- async def process_audio():
126
- if "audio" not in request.files:
127
- return jsonify(error="Nenhum áudio enviado."), 400
128
- if not whisper_model:
129
- return jsonify(error="ASR indisponível."), 500
130
-
131
- audio_file = request.files["audio"]
132
- with tempfile.NamedTemporaryFile(suffix=".webm", delete=True) as tmp:
133
- audio_file.save(tmp.name)
134
-
135
- # 1ª tentativa — com initial_prompt
136
- try:
137
- segments, _ = whisper_model.transcribe(
138
- tmp.name, language="pt", initial_prompt=INITIAL_PROMPT
139
- )
140
- text = "".join(s.text for s in segments).strip()
141
- except Exception as e:
142
- logging.warning("Falha com prompt: %s; tentando sem.", e)
143
- text = ""
144
-
145
- # Fallback sem prompt
146
- if not text:
147
- try:
148
- segments, _ = whisper_model.transcribe(
149
- tmp.name, language="pt"
150
- )
151
- text = "".join(s.text for s in segments).strip()
152
- except Exception as e:
153
- logging.error("Falha sem prompt: %s", e)
154
- return jsonify(error="Erro na transcrição."), 500
155
-
156
- # Se ainda vazio, devolve pedido de repetição
157
  if not text:
158
- ai_text = ("Desculpe, não entendi o que foi dito. "
159
- "Você poderia repetir, por favor?")
160
  else:
161
  ai_text = ask_gemini(text)
162
 
163
- audio_bytes = await synthesize(ai_text) if ai_text else None
164
-
165
- return jsonify(
166
- user_question=text,
167
- ai_answer=ai_text,
168
- audio_base64=(
169
- base64.b64encode(audio_bytes).decode()
170
- if audio_bytes else None
171
- )
172
- )
173
 
174
- @app.route("/healthz")
175
- def healthz():
176
- return "OK", 200
 
 
177
 
178
- # ---------- ASGI ----------
179
- asgi_app = WsgiToAsgi(app)
 
 
1
+ import os, io, base64, tempfile, logging, json, asyncio
2
+ from fastapi import FastAPI, File, UploadFile, HTTPException
3
+ from fastapi.responses import FileResponse, JSONResponse
4
+ from dotenv import load_dotenv
5
  from faster_whisper import WhisperModel
 
6
  import google.generativeai as genai
 
 
 
7
  import edge_tts
 
8
 
9
+ # ---------- Configuração Inicial ----------
10
  load_dotenv()
11
+
12
  CACHE_DIR = os.getenv("HF_HUB_CACHE", "./models_cache")
13
  os.environ["MPLCONFIGDIR"] = os.path.join(CACHE_DIR, "matplotlib")
14
 
15
  LOGIN_PASSWORDS = os.getenv("LOGIN_PASSWORDS")
16
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
17
+
18
+ # ---------- Aplicação FastAPI ----------
19
+ app = FastAPI()
20
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
21
+
22
+ # ---------- Carregamento de Modelos (no arranque) ----------
23
+ whisper_model = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  gemini_model = None
25
+
26
+ @app.on_event("startup")
27
+ def load_models():
28
+ global whisper_model, gemini_model
29
+ logging.info("A carregar modelos e clientes de API...")
30
  try:
31
+ model_name = "medium"
32
+ whisper_model = WhisperModel(model_name, device="cpu", compute_type="int8")
33
+ logging.info(f"Modelo faster-whisper '{model_name}' (int8) pronto.")
34
  except Exception as e:
35
+ logging.error(f"Falha ao iniciar o modelo faster-whisper: {e}")
36
+ raise RuntimeError("Não foi possível carregar o modelo Whisper.") from e
37
+
38
+ if GOOGLE_API_KEY:
39
+ try:
40
+ genai.configure(api_key=GOOGLE_API_KEY)
41
+ gemini_model = genai.GenerativeModel("gemini-1.5-flash")
42
+ logging.info("Gemini pronto.")
43
+ except Exception as e:
44
+ logging.error(f"Falha ao iniciar Gemini: {e}")
45
+ raise RuntimeError("Não foi possível carregar o modelo Gemini.") from e
46
+ logging.info("Modelos carregados com sucesso.")
47
+
48
 
49
  # ---------- Utilidades ----------
50
  def ask_gemini(question: str) -> str:
51
  if not gemini_model:
52
+ raise HTTPException(status_code=503, detail="Modelo de linguagem não está disponível.")
53
+ prompt = ("Você é 'SintonIA', um assistente de IA por voz para saúde bucal. "
54
+ "Responda de forma empática, clara e segura, em 2-3 frases. "
55
+ "NUNCA diagnóstico e sempre recomende consulta presencial a um dentista.")
 
 
56
  try:
57
+ response = gemini_model.generate_content([prompt, question])
58
+ return response.text
59
  except Exception as e:
60
+ logging.error(f"Erro no Gemini: {e}")
61
+ raise HTTPException(status_code=500, detail="Erro ao gerar a resposta de IA.")
62
 
63
  VOICE = "pt-BR-AntonioNeural"
64
  async def synthesize(text: str) -> bytes | None:
65
  try:
66
+ audio_bytes = b""
67
  communicate = edge_tts.Communicate(text, VOICE)
68
  async for chunk in communicate.stream():
69
  if chunk["type"] == "audio":
70
+ audio_bytes += chunk["data"]
71
+ return audio_bytes
72
  except Exception as e:
73
+ logging.error(f"Erro ao sintetizar áudio com Edge TTS: {e}")
74
  return None
75
 
76
+ # ---------- Rotas (Endpoints) ----------
77
+ @app.get("/")
78
+ async def read_index():
79
+ return FileResponse('index.html')
80
 
81
+ @app.post("/login")
82
+ async def login(request: dict):
83
  if not LOGIN_PASSWORDS:
84
+ return {"success": True}
85
+ valid_passwords = [p.strip() for p in LOGIN_PASSWORDS.split(',')]
86
+ pwd_received = request.get("password", "")
87
+ is_ok = pwd_received in valid_passwords
88
+ if not is_ok:
89
+ raise HTTPException(status_code=401, detail="Senha incorreta.")
90
+ return {"success": True}
91
+
92
+ @app.post("/process-audio")
93
+ async def process_audio(audio: UploadFile = File(...)):
94
+ if not all([whisper_model, gemini_model]):
95
+ raise HTTPException(status_code=503, detail="Um serviço de IA não está disponível.")
96
+
97
+ try:
98
+ with tempfile.NamedTemporaryFile(delete=True, suffix=".webm") as tmp_file:
99
+ content = await audio.read()
100
+ tmp_file.write(content)
101
+ tmp_file.seek(0)
102
+
103
+ if os.path.getsize(tmp_file.name) > 1000:
104
+ segments, _ = whisper_model.transcribe(tmp_file.name, language="pt")
105
+ transcribed_parts = [segment.text for segment in segments]
106
+ text = "".join(transcribed_parts).strip()
107
+ logging.info(f"Texto transcrito: '{text}'")
108
+ else:
109
+ text = ""
110
+ except Exception as e:
111
+ logging.error(f"Erro na transcrição do faster-whisper: {e}")
112
+ text = ""
113
+
 
 
 
 
 
 
 
 
114
  if not text:
115
+ ai_text = "Desculpe, não entendi o que foi dito. Você poderia repetir, por favor?"
 
116
  else:
117
  ai_text = ask_gemini(text)
118
 
119
+ audio_bytes = None
120
+ if ai_text:
121
+ audio_bytes = await synthesize(ai_text)
 
 
 
 
 
 
 
122
 
123
+ return JSONResponse(content={
124
+ "user_question": text,
125
+ "ai_answer": ai_text,
126
+ "audio_base64": base64.b64encode(audio_bytes).decode('utf-8') if audio_bytes else None
127
+ })
128
 
129
+ @app.get("/healthz")
130
+ async def health_check():
131
+ return {"status": "OK"}