Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
Health Voice Assistant
|
| 3 |
- Whisper ASR (incremental)
|
| 4 |
- GPT health guidance (NON-diagnostic)
|
| 5 |
-
- Auto Text-to-Speech (autoplay, no file saved)
|
| 6 |
|
| 7 |
⚠️ GENERAL guidance only – NOT medical advice
|
| 8 |
"""
|
|
@@ -14,11 +14,10 @@ import re
|
|
| 14 |
import textwrap
|
| 15 |
import base64
|
| 16 |
from io import BytesIO
|
| 17 |
-
from datetime import datetime
|
| 18 |
|
| 19 |
-
import requests
|
| 20 |
import numpy as np
|
| 21 |
import torch
|
|
|
|
| 22 |
import streamlit as st
|
| 23 |
|
| 24 |
from transformers import pipeline
|
|
@@ -26,13 +25,18 @@ from transformers.pipelines.audio_utils import ffmpeg_read
|
|
| 26 |
from openai import OpenAI
|
| 27 |
from gtts import gTTS
|
| 28 |
|
| 29 |
-
# ================= PAGE CONFIG
|
| 30 |
st.set_page_config(page_title="Health Voice Assistant", layout="wide")
|
| 31 |
|
| 32 |
# ================= SESSION STATE =================
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
# ================= CONFIG =================
|
| 38 |
MODEL_NAME = "openai/whisper-small.en"
|
|
@@ -51,10 +55,11 @@ device = 0 if torch.cuda.is_available() else -1
|
|
| 51 |
st.title("🩺 Health Voice-Based Assistant")
|
| 52 |
|
| 53 |
MEDICAL_DISCLAIMER = """
|
| 54 |
-
⚠️ IMPORTANT MEDICAL DISCLAIMER
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
| 58 |
"""
|
| 59 |
|
| 60 |
# ================= SIDEBAR =================
|
|
@@ -62,12 +67,11 @@ st.sidebar.title("🎓 User Manual")
|
|
| 62 |
st.sidebar.markdown(
|
| 63 |
"""
|
| 64 |
1. Choose input method
|
| 65 |
-
2. Upload audio **or** record
|
| 66 |
-
3.
|
| 67 |
-
|
| 68 |
-
- Analyze
|
| 69 |
-
-
|
| 70 |
-
- Speak response
|
| 71 |
"""
|
| 72 |
)
|
| 73 |
st.sidebar.markdown("---")
|
|
@@ -92,7 +96,6 @@ def load_pipeline():
|
|
| 92 |
pass
|
| 93 |
return pipe
|
| 94 |
|
| 95 |
-
|
| 96 |
pipe = load_pipeline()
|
| 97 |
|
| 98 |
# ================= GPT CLIENT =================
|
|
@@ -102,11 +105,15 @@ client = OpenAI(api_key=CHATGPT_API)
|
|
| 102 |
def format_text(text: str, width: int = 90) -> str:
|
| 103 |
text = re.sub(r"\s+", " ", text).strip()
|
| 104 |
sentences = re.split(r"(?<=[.!?])\s+", text)
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
def merge_overlap(old: str, new: str, max_words: int = 10) -> str:
|
| 112 |
if not old:
|
|
@@ -117,14 +124,35 @@ def merge_overlap(old: str, new: str, max_words: int = 10) -> str:
|
|
| 117 |
return " ".join(a + b[k:])
|
| 118 |
return old + " " + new
|
| 119 |
|
| 120 |
-
|
| 121 |
def tts_bytes(text: str) -> BytesIO:
|
| 122 |
mp3 = BytesIO()
|
| 123 |
gTTS(text=text, lang="en").write_to_fp(mp3)
|
| 124 |
mp3.seek(0)
|
| 125 |
return mp3
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
def run_gpt(transcript: str) -> str:
|
| 129 |
prompt = f"""
|
| 130 |
You are a NON-diagnostic health assistant.
|
|
@@ -150,34 +178,8 @@ TASK:
|
|
| 150 |
)
|
| 151 |
return res.choices[0].message.content.strip()
|
| 152 |
|
| 153 |
-
|
| 154 |
-
# ================= FIREBASE =================
|
| 155 |
-
def read_firebase_transcribe():
|
| 156 |
-
try:
|
| 157 |
-
url = f"{FIREBASE_DB_URL}/transcribe.json"
|
| 158 |
-
res = requests.get(url, timeout=5)
|
| 159 |
-
if res.status_code != 200:
|
| 160 |
-
return None
|
| 161 |
-
|
| 162 |
-
data = res.json()
|
| 163 |
-
if not data:
|
| 164 |
-
return None
|
| 165 |
-
|
| 166 |
-
text = data.get("text", "").strip()
|
| 167 |
-
if not text:
|
| 168 |
-
return None
|
| 169 |
-
|
| 170 |
-
return {
|
| 171 |
-
"text": text,
|
| 172 |
-
"length": data.get("length"),
|
| 173 |
-
"updated_at": data.get("updated_at"),
|
| 174 |
-
}
|
| 175 |
-
except Exception:
|
| 176 |
-
return None
|
| 177 |
-
|
| 178 |
-
|
| 179 |
# ================= ASR =================
|
| 180 |
-
def
|
| 181 |
sr = pipe.feature_extractor.sampling_rate
|
| 182 |
audio = ffmpeg_read(audio_bytes, sr)
|
| 183 |
|
|
@@ -190,10 +192,10 @@ def transcribe_audio(audio_bytes: bytes):
|
|
| 190 |
chunk_samples = int(CHUNK_SEC * sr)
|
| 191 |
overlap_samples = int(OVERLAP_SEC * sr)
|
| 192 |
|
|
|
|
| 193 |
acc = ""
|
| 194 |
-
total = max(1, len(audio) // chunk_samples + 1)
|
| 195 |
|
| 196 |
-
for i in range(
|
| 197 |
start = max(0, i * chunk_samples - overlap_samples)
|
| 198 |
end = min(len(audio), (i + 1) * chunk_samples + overlap_samples)
|
| 199 |
|
|
@@ -203,106 +205,98 @@ def transcribe_audio(audio_bytes: bytes):
|
|
| 203 |
"forced_decoder_ids": getattr(pipe, "_forced_decoder_ids", None)
|
| 204 |
},
|
| 205 |
)
|
| 206 |
-
acc = merge_overlap(acc, result["text"].strip())
|
| 207 |
-
yield acc, i + 1, total
|
| 208 |
|
|
|
|
|
|
|
| 209 |
|
| 210 |
# ================= INPUT MODE =================
|
| 211 |
-
st.markdown("## 🎤
|
| 212 |
|
| 213 |
-
|
| 214 |
"Choose input method:",
|
| 215 |
-
["Upload audio file", "
|
| 216 |
-
horizontal=True,
|
| 217 |
)
|
| 218 |
|
| 219 |
# ================= MODE 2: FIREBASE =================
|
| 220 |
-
if
|
| 221 |
-
st.info("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
st.markdown(
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
<a href="{record_url}" target="_blank" rel="noopener">Open recorder (new tab)</a>
|
| 233 |
-
<br><br>
|
| 234 |
-
The app will also attempt to redirect you automatically in 3 seconds.
|
| 235 |
-
<meta http-equiv="refresh" content="3; url={record_url}">
|
| 236 |
""",
|
| 237 |
unsafe_allow_html=True,
|
| 238 |
)
|
| 239 |
-
st.stop()
|
| 240 |
-
|
| 241 |
-
# Data exists -> show transcript and a Continue button to run GPT+TTS
|
| 242 |
-
st.success("✅ Data received from Firebase")
|
| 243 |
-
st.markdown("### 📝 Transcript (from Firebase)")
|
| 244 |
-
|
| 245 |
-
st.markdown(f"**Length:** {fb.get('length', 'N/A')}")
|
| 246 |
-
if fb.get("updated_at"):
|
| 247 |
-
st.markdown(f"**Updated at:** {fb['updated_at']}")
|
| 248 |
-
|
| 249 |
-
st.markdown(f"```text\n{format_text(fb['text'])}\n```")
|
| 250 |
-
|
| 251 |
-
# Continue button: use same behavior as Upload->Transcribe, but with existing text
|
| 252 |
-
if st.button("Continue"):
|
| 253 |
-
try:
|
| 254 |
-
st.session_state.transcript = fb["text"]
|
| 255 |
-
st.session_state.gpt_text = run_gpt(fb["text"])
|
| 256 |
-
|
| 257 |
-
voice = tts_bytes(st.session_state.gpt_text)
|
| 258 |
-
b64 = base64.b64encode(voice.read()).decode()
|
| 259 |
-
st.session_state.audio_html = f"""
|
| 260 |
-
<audio autoplay>
|
| 261 |
-
<source src="data:audio/mp3;base64,{b64}" type="audio/mp3">
|
| 262 |
-
</audio>
|
| 263 |
-
"""
|
| 264 |
-
st.success("Processed: GPT response generated and audio playing.")
|
| 265 |
-
except Exception as e:
|
| 266 |
-
st.error(f"Error processing Firebase transcript: {e}\n{traceback.format_exc()}")
|
| 267 |
-
|
| 268 |
-
st.stop()
|
| 269 |
|
| 270 |
# ================= MODE 1: UPLOAD AUDIO =================
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
|
|
|
| 275 |
|
| 276 |
-
if uploaded:
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
final_text =
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
-
|
| 303 |
-
|
| 304 |
|
| 305 |
-
# =================
|
| 306 |
if st.session_state.transcript:
|
| 307 |
st.markdown("### 📝 Transcript")
|
| 308 |
st.markdown(f"```text\n{format_text(st.session_state.transcript)}\n```")
|
|
@@ -315,4 +309,4 @@ if st.session_state.audio_html:
|
|
| 315 |
st.markdown(st.session_state.audio_html, unsafe_allow_html=True)
|
| 316 |
|
| 317 |
st.markdown("---")
|
| 318 |
-
st.markdown("⚠️ NOT medical advice. Seek
|
|
|
|
| 2 |
Health Voice Assistant
|
| 3 |
- Whisper ASR (incremental)
|
| 4 |
- GPT health guidance (NON-diagnostic)
|
| 5 |
+
- Auto Text-to-Speech (autoplay, no button, no file saved)
|
| 6 |
|
| 7 |
⚠️ GENERAL guidance only – NOT medical advice
|
| 8 |
"""
|
|
|
|
| 14 |
import textwrap
|
| 15 |
import base64
|
| 16 |
from io import BytesIO
|
|
|
|
| 17 |
|
|
|
|
| 18 |
import numpy as np
|
| 19 |
import torch
|
| 20 |
+
import requests
|
| 21 |
import streamlit as st
|
| 22 |
|
| 23 |
from transformers import pipeline
|
|
|
|
| 25 |
from openai import OpenAI
|
| 26 |
from gtts import gTTS
|
| 27 |
|
| 28 |
+
# ================= PAGE CONFIG =================
|
| 29 |
st.set_page_config(page_title="Health Voice Assistant", layout="wide")
|
| 30 |
|
| 31 |
# ================= SESSION STATE =================
|
| 32 |
+
if "transcript" not in st.session_state:
|
| 33 |
+
st.session_state.transcript = None
|
| 34 |
+
if "gpt_text" not in st.session_state:
|
| 35 |
+
st.session_state.gpt_text = None
|
| 36 |
+
if "audio_html" not in st.session_state:
|
| 37 |
+
st.session_state.audio_html = None
|
| 38 |
+
if "done" not in st.session_state:
|
| 39 |
+
st.session_state.done = False
|
| 40 |
|
| 41 |
# ================= CONFIG =================
|
| 42 |
MODEL_NAME = "openai/whisper-small.en"
|
|
|
|
| 55 |
st.title("🩺 Health Voice-Based Assistant")
|
| 56 |
|
| 57 |
MEDICAL_DISCLAIMER = """
|
| 58 |
+
⚠️ **IMPORTANT MEDICAL DISCLAIMER**
|
| 59 |
+
|
| 60 |
+
This assistant is **NOT** a medical professional.
|
| 61 |
+
It does **NOT** diagnose diseases or prescribe medication.
|
| 62 |
+
Information provided is for **GENERAL guidance only**.
|
| 63 |
"""
|
| 64 |
|
| 65 |
# ================= SIDEBAR =================
|
|
|
|
| 67 |
st.sidebar.markdown(
|
| 68 |
"""
|
| 69 |
1. Choose input method
|
| 70 |
+
2. Upload audio **or** record via web
|
| 71 |
+
3. App will automatically:
|
| 72 |
+
- Transcribe (or read transcript)
|
| 73 |
+
- Analyze with GPT
|
| 74 |
+
- Speak the guidance
|
|
|
|
| 75 |
"""
|
| 76 |
)
|
| 77 |
st.sidebar.markdown("---")
|
|
|
|
| 96 |
pass
|
| 97 |
return pipe
|
| 98 |
|
|
|
|
| 99 |
pipe = load_pipeline()
|
| 100 |
|
| 101 |
# ================= GPT CLIENT =================
|
|
|
|
| 105 |
def format_text(text: str, width: int = 90) -> str:
|
| 106 |
text = re.sub(r"\s+", " ", text).strip()
|
| 107 |
sentences = re.split(r"(?<=[.!?])\s+", text)
|
| 108 |
+
paragraphs, buf = [], []
|
| 109 |
+
for s in sentences:
|
| 110 |
+
buf.append(s)
|
| 111 |
+
if len(buf) >= 3:
|
| 112 |
+
paragraphs.append(" ".join(buf))
|
| 113 |
+
buf = []
|
| 114 |
+
if buf:
|
| 115 |
+
paragraphs.append(" ".join(buf))
|
| 116 |
+
return "\n\n".join(textwrap.fill(p, width) for p in paragraphs)
|
| 117 |
|
| 118 |
def merge_overlap(old: str, new: str, max_words: int = 10) -> str:
|
| 119 |
if not old:
|
|
|
|
| 124 |
return " ".join(a + b[k:])
|
| 125 |
return old + " " + new
|
| 126 |
|
|
|
|
| 127 |
def tts_bytes(text: str) -> BytesIO:
|
| 128 |
mp3 = BytesIO()
|
| 129 |
gTTS(text=text, lang="en").write_to_fp(mp3)
|
| 130 |
mp3.seek(0)
|
| 131 |
return mp3
|
| 132 |
|
| 133 |
+
# ================= FIREBASE =================
|
| 134 |
+
def fetch_firebase_transcript():
|
| 135 |
+
"""
|
| 136 |
+
Read transcript text from Firebase Realtime Database
|
| 137 |
+
Path: /transcribe
|
| 138 |
+
"""
|
| 139 |
+
try:
|
| 140 |
+
url = f"{FIREBASE_DB_URL}/transcribe.json"
|
| 141 |
+
res = requests.get(url, timeout=5)
|
| 142 |
+
res.raise_for_status()
|
| 143 |
+
data = res.json()
|
| 144 |
+
|
| 145 |
+
if not data:
|
| 146 |
+
return None
|
| 147 |
+
|
| 148 |
+
text = data.get("text", "").strip()
|
| 149 |
+
return text if text else None
|
| 150 |
|
| 151 |
+
except Exception as e:
|
| 152 |
+
st.error(f"Firebase read error: {e}")
|
| 153 |
+
return None
|
| 154 |
+
|
| 155 |
+
# ================= GPT =================
|
| 156 |
def run_gpt(transcript: str) -> str:
|
| 157 |
prompt = f"""
|
| 158 |
You are a NON-diagnostic health assistant.
|
|
|
|
| 178 |
)
|
| 179 |
return res.choices[0].message.content.strip()
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
# ================= ASR =================
|
| 182 |
+
def transcribe(audio_bytes: bytes):
|
| 183 |
sr = pipe.feature_extractor.sampling_rate
|
| 184 |
audio = ffmpeg_read(audio_bytes, sr)
|
| 185 |
|
|
|
|
| 192 |
chunk_samples = int(CHUNK_SEC * sr)
|
| 193 |
overlap_samples = int(OVERLAP_SEC * sr)
|
| 194 |
|
| 195 |
+
total_chunks = max(1, (len(audio) + chunk_samples - 1) // chunk_samples)
|
| 196 |
acc = ""
|
|
|
|
| 197 |
|
| 198 |
+
for i in range(total_chunks):
|
| 199 |
start = max(0, i * chunk_samples - overlap_samples)
|
| 200 |
end = min(len(audio), (i + 1) * chunk_samples + overlap_samples)
|
| 201 |
|
|
|
|
| 205 |
"forced_decoder_ids": getattr(pipe, "_forced_decoder_ids", None)
|
| 206 |
},
|
| 207 |
)
|
|
|
|
|
|
|
| 208 |
|
| 209 |
+
acc = merge_overlap(acc, result["text"].strip())
|
| 210 |
+
yield acc, i + 1, total_chunks
|
| 211 |
|
| 212 |
# ================= INPUT MODE =================
|
| 213 |
+
st.markdown("### 🎤 Input Mode")
|
| 214 |
|
| 215 |
+
mode = st.radio(
|
| 216 |
"Choose input method:",
|
| 217 |
+
["Upload audio file", "Live recording (via Firebase)"],
|
|
|
|
| 218 |
)
|
| 219 |
|
| 220 |
# ================= MODE 2: FIREBASE =================
|
| 221 |
+
if mode == "Live recording (via Firebase)":
|
| 222 |
+
st.info("Reading transcript from Firebase...")
|
| 223 |
+
|
| 224 |
+
firebase_text = fetch_firebase_transcript()
|
| 225 |
+
|
| 226 |
+
if firebase_text:
|
| 227 |
+
st.success("Transcript found. Processing with AI...")
|
| 228 |
+
|
| 229 |
+
st.session_state.audio_html = None
|
| 230 |
+
st.session_state.transcript = firebase_text
|
| 231 |
+
|
| 232 |
+
with st.spinner("Analyzing with GPT..."):
|
| 233 |
+
st.session_state.gpt_text = run_gpt(firebase_text)
|
| 234 |
|
| 235 |
+
voice = tts_bytes(st.session_state.gpt_text)
|
| 236 |
+
b64 = base64.b64encode(voice.read()).decode()
|
| 237 |
+
st.session_state.audio_html = f"""
|
| 238 |
+
<audio autoplay>
|
| 239 |
+
<source src="data:audio/mp3;base64,{b64}" type="audio/mp3">
|
| 240 |
+
</audio>
|
| 241 |
+
"""
|
| 242 |
|
| 243 |
+
st.session_state.done = True
|
| 244 |
+
|
| 245 |
+
else:
|
| 246 |
+
st.warning("No transcript found. Redirecting to recorder...")
|
| 247 |
st.markdown(
|
| 248 |
+
"""
|
| 249 |
+
<meta http-equiv="refresh" content="0; url=https://nolist-ssp-recordoption.hf.space">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
""",
|
| 251 |
unsafe_allow_html=True,
|
| 252 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
# ================= MODE 1: UPLOAD AUDIO =================
|
| 255 |
+
if mode == "Upload audio file":
|
| 256 |
+
uploaded = st.file_uploader(
|
| 257 |
+
"Upload audio (wav, mp3, m4a, ogg, flac)",
|
| 258 |
+
type=["wav", "mp3", "m4a", "ogg", "flac"],
|
| 259 |
+
)
|
| 260 |
|
| 261 |
+
if uploaded:
|
| 262 |
+
audio_bytes = uploaded.read()
|
| 263 |
+
st.audio(audio_bytes)
|
| 264 |
+
|
| 265 |
+
if st.button("Transcribe"):
|
| 266 |
+
try:
|
| 267 |
+
st.session_state.audio_html = None
|
| 268 |
+
|
| 269 |
+
placeholder = st.empty()
|
| 270 |
+
progress = st.progress(0.0)
|
| 271 |
+
|
| 272 |
+
final_text = ""
|
| 273 |
+
|
| 274 |
+
for txt, i, total in transcribe(audio_bytes):
|
| 275 |
+
final_text = txt
|
| 276 |
+
placeholder.markdown(
|
| 277 |
+
f"```text\n{format_text(txt)}\n```"
|
| 278 |
+
)
|
| 279 |
+
progress.progress(i / total)
|
| 280 |
+
|
| 281 |
+
st.session_state.transcript = final_text
|
| 282 |
+
|
| 283 |
+
with st.spinner("Analyzing with GPT..."):
|
| 284 |
+
st.session_state.gpt_text = run_gpt(final_text)
|
| 285 |
+
|
| 286 |
+
voice = tts_bytes(st.session_state.gpt_text)
|
| 287 |
+
b64 = base64.b64encode(voice.read()).decode()
|
| 288 |
+
st.session_state.audio_html = f"""
|
| 289 |
+
<audio autoplay>
|
| 290 |
+
<source src="data:audio/mp3;base64,{b64}" type="audio/mp3">
|
| 291 |
+
</audio>
|
| 292 |
+
"""
|
| 293 |
+
|
| 294 |
+
st.session_state.done = True
|
| 295 |
|
| 296 |
+
except Exception as e:
|
| 297 |
+
st.error(f"Failed:\n{e}\n{traceback.format_exc()}")
|
| 298 |
|
| 299 |
+
# ================= RENDER RESULT =================
|
| 300 |
if st.session_state.transcript:
|
| 301 |
st.markdown("### 📝 Transcript")
|
| 302 |
st.markdown(f"```text\n{format_text(st.session_state.transcript)}\n```")
|
|
|
|
| 309 |
st.markdown(st.session_state.audio_html, unsafe_allow_html=True)
|
| 310 |
|
| 311 |
st.markdown("---")
|
| 312 |
+
st.markdown("⚠️ **NOT medical advice. Seek professional help when needed.**")
|