Spaces:
Runtime error
Runtime error
| """ | |
| Health Voice Assistant | |
| - Whisper ASR (incremental) | |
| - GPT health guidance (NON-diagnostic) | |
| - Auto Text-to-Speech (autoplay, no button, no file saved) | |
| ⚠️ GENERAL guidance only – NOT medical advice | |
| """ | |
| # ================= IMPORTS ================= | |
| import time | |
| import traceback | |
| import re | |
| import textwrap | |
| import base64 | |
| from io import BytesIO | |
| import numpy as np | |
| import torch | |
| import requests | |
| import streamlit as st | |
| from transformers import pipeline | |
| from transformers.pipelines.audio_utils import ffmpeg_read | |
| from openai import OpenAI | |
| from gtts import gTTS | |
| # ================= PAGE CONFIG ================= | |
| st.set_page_config(page_title="Health Voice Assistant", layout="wide") | |
| # ================= SESSION STATE ================= | |
| if "transcript" not in st.session_state: | |
| st.session_state.transcript = None | |
| if "gpt_text" not in st.session_state: | |
| st.session_state.gpt_text = None | |
| if "audio_html" not in st.session_state: | |
| st.session_state.audio_html = None | |
| if "done" not in st.session_state: | |
| st.session_state.done = False | |
| # ================= CONFIG ================= | |
| MODEL_NAME = "openai/whisper-small.en" | |
| CHUNK_SEC = 6.0 | |
| OVERLAP_SEC = 0.05 | |
| MAX_CLIP_SEC = 59.0 | |
| CHATGPT_API = st.secrets["gpt_apikey"] | |
| CHATGPT_MODEL = st.secrets.get("gpt_model", "gpt-4.0-mini") | |
| FIREBASE_DB_URL = st.secrets["firebase_database_url"] | |
| device = 0 if torch.cuda.is_available() else -1 | |
| # ================= UI HEADER ================= | |
| st.title("🩺 Health Voice-Based Assistant") | |
| MEDICAL_DISCLAIMER = """ | |
| ⚠️ **IMPORTANT MEDICAL DISCLAIMER** | |
| This assistant is **NOT** a medical professional. | |
| It does **NOT** diagnose diseases or prescribe medication. | |
| Information provided is for **GENERAL guidance only**. | |
| """ | |
| # ================= SIDEBAR ================= | |
| st.sidebar.title("🎓 User Manual") | |
| st.sidebar.markdown( | |
| """ | |
| 1. Choose input method | |
| 2. Upload audio **or** record via web | |
| 3. App will automatically: | |
| - Transcribe (or read transcript) | |
| - Analyze with GPT | |
| - Speak the guidance | |
| """ | |
| ) | |
| st.sidebar.markdown("---") | |
| st.sidebar.markdown(MEDICAL_DISCLAIMER) | |
| # ================= LOAD ASR ================= | |
| def load_pipeline(): | |
| pipe = pipeline( | |
| task="automatic-speech-recognition", | |
| model=MODEL_NAME, | |
| chunk_length_s=CHUNK_SEC, | |
| device=device, | |
| ) | |
| try: | |
| forced_ids = pipe.tokenizer.get_decoder_prompt_ids( | |
| language="en", task="transcribe" | |
| ) | |
| pipe.model.config.forced_decoder_ids = forced_ids | |
| pipe._forced_decoder_ids = forced_ids | |
| except Exception: | |
| pass | |
| return pipe | |
| pipe = load_pipeline() | |
| # ================= GPT CLIENT ================= | |
| client = OpenAI(api_key=CHATGPT_API) | |
| # ================= HELPERS ================= | |
| def format_text(text: str, width: int = 90) -> str: | |
| text = re.sub(r"\s+", " ", text).strip() | |
| sentences = re.split(r"(?<=[.!?])\s+", text) | |
| paragraphs, buf = [], [] | |
| for s in sentences: | |
| buf.append(s) | |
| if len(buf) >= 3: | |
| paragraphs.append(" ".join(buf)) | |
| buf = [] | |
| if buf: | |
| paragraphs.append(" ".join(buf)) | |
| return "\n\n".join(textwrap.fill(p, width) for p in paragraphs) | |
| def merge_overlap(old: str, new: str, max_words: int = 10) -> str: | |
| if not old: | |
| return new | |
| a, b = old.split(), new.split() | |
| for k in range(min(max_words, len(a), len(b)), 0, -1): | |
| if a[-k:] == b[:k]: | |
| return " ".join(a + b[k:]) | |
| return old + " " + new | |
| def tts_bytes(text: str) -> BytesIO: | |
| mp3 = BytesIO() | |
| gTTS(text=text, lang="en").write_to_fp(mp3) | |
| mp3.seek(0) | |
| return mp3 | |
| # ================= FIREBASE ================= | |
| def fetch_firebase_transcript(): | |
| """ | |
| Read transcript text from Firebase Realtime Database | |
| Path: /transcribe | |
| """ | |
| try: | |
| url = f"{FIREBASE_DB_URL}/transcribe.json" | |
| res = requests.get(url, timeout=5) | |
| res.raise_for_status() | |
| data = res.json() | |
| if not data: | |
| return None | |
| text = data.get("text", "").strip() | |
| return text if text else None | |
| except Exception as e: | |
| st.error(f"Firebase read error: {e}") | |
| return None | |
| # ================= GPT ================= | |
| def run_gpt(transcript: str) -> str: | |
| prompt = f""" | |
| You are a NON-diagnostic health assistant. | |
| {MEDICAL_DISCLAIMER} | |
| Transcript: | |
| {transcript} | |
| TASK: | |
| - Summarize possible concern categories (NOT diagnosis) | |
| - Explain in simple language | |
| - Give safe general advice | |
| - Clearly state when to seek professional medical help | |
| """ | |
| res = client.chat.completions.create( | |
| model=CHATGPT_MODEL, | |
| messages=[ | |
| {"role": "system", "content": "Health assistant"}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| temperature=0.3, | |
| ) | |
| return res.choices[0].message.content.strip() | |
| # ================= ASR ================= | |
| def transcribe(audio_bytes: bytes): | |
| sr = pipe.feature_extractor.sampling_rate | |
| audio = ffmpeg_read(audio_bytes, sr) | |
| if audio.ndim > 1: | |
| audio = audio.mean(axis=1) | |
| if audio.shape[0] / sr > MAX_CLIP_SEC: | |
| raise ValueError("Audio too long (max 59s)") | |
| chunk_samples = int(CHUNK_SEC * sr) | |
| overlap_samples = int(OVERLAP_SEC * sr) | |
| total_chunks = max(1, (len(audio) + chunk_samples - 1) // chunk_samples) | |
| acc = "" | |
| for i in range(total_chunks): | |
| start = max(0, i * chunk_samples - overlap_samples) | |
| end = min(len(audio), (i + 1) * chunk_samples + overlap_samples) | |
| result = pipe( | |
| {"array": audio[start:end], "sampling_rate": sr}, | |
| generate_kwargs={ | |
| "forced_decoder_ids": getattr(pipe, "_forced_decoder_ids", None) | |
| }, | |
| ) | |
| acc = merge_overlap(acc, result["text"].strip()) | |
| yield acc, i + 1, total_chunks | |
| # ================= INPUT MODE ================= | |
| st.markdown("### 🎤 Input Mode") | |
| mode = st.radio( | |
| "Choose input method:", | |
| ["Upload audio file", "Live recording (via Firebase)"], | |
| ) | |
| # ================= MODE 2: FIREBASE ================= | |
| if mode == "Live recording (via Firebase)": | |
| st.info("Reading transcript from Firebase...") | |
| firebase_text = fetch_firebase_transcript() | |
| if firebase_text: | |
| st.success("Transcript found. Processing with AI...") | |
| st.session_state.audio_html = None | |
| st.session_state.transcript = firebase_text | |
| with st.spinner("Analyzing with GPT..."): | |
| st.session_state.gpt_text = run_gpt(firebase_text) | |
| voice = tts_bytes(st.session_state.gpt_text) | |
| b64 = base64.b64encode(voice.read()).decode() | |
| st.session_state.audio_html = f""" | |
| <audio autoplay> | |
| <source src="data:audio/mp3;base64,{b64}" type="audio/mp3"> | |
| </audio> | |
| """ | |
| st.session_state.done = True | |
| else: | |
| st.warning("No transcript found. Redirecting to recorder...") | |
| st.markdown( | |
| """ | |
| <meta http-equiv="refresh" content="0; url=https://nolist-ssp-recordoption.hf.space"> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| # ================= MODE 1: UPLOAD AUDIO ================= | |
| if mode == "Upload audio file": | |
| uploaded = st.file_uploader( | |
| "Upload audio (wav, mp3, m4a, ogg, flac)", | |
| type=["wav", "mp3", "m4a", "ogg", "flac"], | |
| ) | |
| if uploaded: | |
| audio_bytes = uploaded.read() | |
| st.audio(audio_bytes) | |
| if st.button("Transcribe"): | |
| try: | |
| st.session_state.audio_html = None | |
| placeholder = st.empty() | |
| progress = st.progress(0.0) | |
| final_text = "" | |
| for txt, i, total in transcribe(audio_bytes): | |
| final_text = txt | |
| placeholder.markdown( | |
| f"```text\n{format_text(txt)}\n```" | |
| ) | |
| progress.progress(i / total) | |
| st.session_state.transcript = final_text | |
| with st.spinner("Analyzing with GPT..."): | |
| st.session_state.gpt_text = run_gpt(final_text) | |
| voice = tts_bytes(st.session_state.gpt_text) | |
| b64 = base64.b64encode(voice.read()).decode() | |
| st.session_state.audio_html = f""" | |
| <audio autoplay> | |
| <source src="data:audio/mp3;base64,{b64}" type="audio/mp3"> | |
| </audio> | |
| """ | |
| st.session_state.done = True | |
| except Exception as e: | |
| st.error(f"Failed:\n{e}\n{traceback.format_exc()}") | |
| # ================= RENDER RESULT ================= | |
| if st.session_state.transcript: | |
| st.markdown("### 📝 Transcript") | |
| st.markdown(f"```text\n{format_text(st.session_state.transcript)}\n```") | |
| if st.session_state.gpt_text: | |
| st.markdown("### 🧠 Health Guidance") | |
| st.markdown(st.session_state.gpt_text) | |
| if st.session_state.audio_html: | |
| st.markdown(st.session_state.audio_html, unsafe_allow_html=True) | |
| st.markdown("---") | |
| st.markdown("⚠️ **NOT medical advice. Seek professional help when needed.**") | |