Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
| 1 |
# app.py — DiagStudio AI (Streamlit frontend)
|
| 2 |
-
#
|
| 3 |
-
# Configure the backend URL via:
|
| 4 |
-
# CARDIAG_AI_API (
|
| 5 |
-
# Optional auth
|
| 6 |
-
# CARDIAG_AI_TOKEN
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
import os, io, json, time, base64, requests, hashlib
|
| 9 |
-
|
|
|
|
| 10 |
os.environ.setdefault("HOME", "/tmp")
|
| 11 |
os.environ.setdefault("STREAMLIT_BROWSER_GATHER_USAGE_STATS", "false")
|
| 12 |
os.environ.setdefault("STREAMLIT_SERVER_HEADLESS", "true")
|
|
@@ -24,7 +28,8 @@ except Exception:
|
|
| 24 |
pass
|
| 25 |
|
| 26 |
# ================= Env & API =================
|
| 27 |
-
def _env(k
|
|
|
|
| 28 |
|
| 29 |
API_BASE = (
|
| 30 |
_env("CARDIAG_AI_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")
|
|
@@ -38,15 +43,15 @@ st.title("DiagStudio AI")
|
|
| 38 |
st.caption("Upload a short engine recording and get a clear diagnosis with visuals and next steps.")
|
| 39 |
|
| 40 |
# ================= Helpers =================
|
| 41 |
-
@st.cache_data(show_spinner=False, ttl=
|
| 42 |
-
def _health(url, headers):
|
| 43 |
try:
|
| 44 |
r = requests.get(f"{url}/health", headers=headers, timeout=10)
|
| 45 |
return r.status_code == 200
|
| 46 |
except Exception:
|
| 47 |
return False
|
| 48 |
|
| 49 |
-
def _nice_conf(x):
|
| 50 |
try:
|
| 51 |
return min(max(float(x), 0.0), 1.0)
|
| 52 |
except Exception:
|
|
@@ -58,7 +63,6 @@ def _post_analyze(audio_file, rpm_file, sr_target, n_fft, hop, env_band, run_llm
|
|
| 58 |
files["audio"] = (audio_file.name, audio_file.getvalue(), audio_file.type or "application/octet-stream")
|
| 59 |
if rpm_file is not None:
|
| 60 |
files["rpm_csv"] = (rpm_file.name, rpm_file.getvalue(), "text/csv")
|
| 61 |
-
|
| 62 |
data = {
|
| 63 |
"sr_target": str(sr_target),
|
| 64 |
"n_fft": str(n_fft),
|
|
@@ -66,14 +70,13 @@ def _post_analyze(audio_file, rpm_file, sr_target, n_fft, hop, env_band, run_llm
|
|
| 66 |
"env_band": env_band,
|
| 67 |
"run_llm": "true" if run_llm else "false",
|
| 68 |
"extra_text": notes or "",
|
| 69 |
-
# cache-bypass hint
|
| 70 |
-
"nonce": str(time.time())
|
| 71 |
}
|
| 72 |
r = requests.post(f"{API_BASE}/analyze", headers=HEADERS, files=files, data=data, timeout=240)
|
| 73 |
r.raise_for_status()
|
| 74 |
return r.json()
|
| 75 |
|
| 76 |
-
def _post_analyze_zip(audio_file, rpm_file, sr_target, n_fft, hop, env_band, run_llm, notes):
|
| 77 |
files = {}
|
| 78 |
if audio_file is not None:
|
| 79 |
files["audio"] = (audio_file.name, audio_file.getvalue(), audio_file.type or "application/octet-stream")
|
|
@@ -99,7 +102,7 @@ with st.sidebar:
|
|
| 99 |
rpm_up = st.file_uploader("Optional RPM CSV (time_sec,rpm)", type=["csv"])
|
| 100 |
|
| 101 |
st.header("Options")
|
| 102 |
-
run_llm = st.toggle("Explain with AI", value=True, help="
|
| 103 |
user_notes = st.text_area("Context (optional)", placeholder="Vehicle, engine type, driving condition, symptoms…")
|
| 104 |
|
| 105 |
with st.expander("Advanced (defaults are fine)"):
|
|
@@ -112,10 +115,10 @@ with st.sidebar:
|
|
| 112 |
st.error("Backend URL is not set. Define CARDIAG_AI_API (or AI_LIGHTBOX_API / LUXFIT_API).")
|
| 113 |
ok = _health(API_BASE, HEADERS) if API_BASE else False
|
| 114 |
if not ok and API_BASE:
|
| 115 |
-
st.warning("Could not reach the backend.
|
| 116 |
analyze_btn = st.button("Analyze now", type="primary", disabled=(audio_up is None or not ok))
|
| 117 |
|
| 118 |
-
# =================
|
| 119 |
if "last_audio_id" not in st.session_state:
|
| 120 |
st.session_state["last_audio_id"] = None
|
| 121 |
if audio_up is not None:
|
|
@@ -148,11 +151,10 @@ if res:
|
|
| 148 |
params = res.get("params", {})
|
| 149 |
llm = res.get("llm_report") or {}
|
| 150 |
|
| 151 |
-
# Model name is known from backend params (JSON mode removes auto-added model field)
|
| 152 |
model_name = params.get("vision_model", "gpt-4o")
|
| 153 |
st.caption(f"AI model: **{model_name}**")
|
| 154 |
|
| 155 |
-
#
|
| 156 |
if params.get("audio_id"):
|
| 157 |
st.caption(f"Audio ID: **{params['audio_id']}**")
|
| 158 |
if params.get("features"):
|
|
@@ -166,12 +168,11 @@ if res:
|
|
| 166 |
except Exception:
|
| 167 |
st.caption(f"Features — {feat}")
|
| 168 |
|
| 169 |
-
#
|
| 170 |
if isinstance(llm, dict) and llm.get("meta"):
|
| 171 |
meta = llm["meta"]
|
| 172 |
st.caption(f"Vision meta — seen_images: {meta.get('seen_images')}, views: {meta.get('views')}")
|
| 173 |
|
| 174 |
-
# Layout
|
| 175 |
col1, col2 = st.columns([1, 1])
|
| 176 |
diag_list = llm.get("diagnosis", [])
|
| 177 |
notes = llm.get("notes", "")
|
|
|
|
| 1 |
# app.py — DiagStudio AI (Streamlit frontend)
|
| 2 |
+
# Pairs with the FastAPI backend you built (composite image + JSON mode).
|
| 3 |
+
# Configure the backend URL via one of:
|
| 4 |
+
# CARDIAG_AI_API (preferred) | AI_LIGHTBOX_API | LUXFIT_API
|
| 5 |
+
# Optional bearer auth passthrough:
|
| 6 |
+
# CARDIAG_AI_TOKEN | AI_LIGHTBOX_TOKEN | LUXFIT_TOKEN
|
| 7 |
+
#
|
| 8 |
+
# Run (example):
|
| 9 |
+
# CARDIAG_AI_API="https://<your-private-backend>" streamlit run app.py --server.port 7860
|
| 10 |
|
| 11 |
import os, io, json, time, base64, requests, hashlib
|
| 12 |
+
|
| 13 |
+
# Avoid /.streamlit permission errors on containers/HF
|
| 14 |
os.environ.setdefault("HOME", "/tmp")
|
| 15 |
os.environ.setdefault("STREAMLIT_BROWSER_GATHER_USAGE_STATS", "false")
|
| 16 |
os.environ.setdefault("STREAMLIT_SERVER_HEADLESS", "true")
|
|
|
|
| 28 |
pass
|
| 29 |
|
| 30 |
# ================= Env & API =================
|
| 31 |
+
def _env(k: str) -> str:
|
| 32 |
+
return (os.getenv(k) or "").strip().strip("'\"")
|
| 33 |
|
| 34 |
API_BASE = (
|
| 35 |
_env("CARDIAG_AI_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")
|
|
|
|
| 43 |
st.caption("Upload a short engine recording and get a clear diagnosis with visuals and next steps.")
|
| 44 |
|
| 45 |
# ================= Helpers =================
|
| 46 |
+
@st.cache_data(show_spinner=False, ttl=30)
|
| 47 |
+
def _health(url: str, headers: dict) -> bool:
|
| 48 |
try:
|
| 49 |
r = requests.get(f"{url}/health", headers=headers, timeout=10)
|
| 50 |
return r.status_code == 200
|
| 51 |
except Exception:
|
| 52 |
return False
|
| 53 |
|
| 54 |
+
def _nice_conf(x) -> float:
|
| 55 |
try:
|
| 56 |
return min(max(float(x), 0.0), 1.0)
|
| 57 |
except Exception:
|
|
|
|
| 63 |
files["audio"] = (audio_file.name, audio_file.getvalue(), audio_file.type or "application/octet-stream")
|
| 64 |
if rpm_file is not None:
|
| 65 |
files["rpm_csv"] = (rpm_file.name, rpm_file.getvalue(), "text/csv")
|
|
|
|
| 66 |
data = {
|
| 67 |
"sr_target": str(sr_target),
|
| 68 |
"n_fft": str(n_fft),
|
|
|
|
| 70 |
"env_band": env_band,
|
| 71 |
"run_llm": "true" if run_llm else "false",
|
| 72 |
"extra_text": notes or "",
|
| 73 |
+
"nonce": str(time.time()) # cache-bypass hint; backend already sets no-store
|
|
|
|
| 74 |
}
|
| 75 |
r = requests.post(f"{API_BASE}/analyze", headers=HEADERS, files=files, data=data, timeout=240)
|
| 76 |
r.raise_for_status()
|
| 77 |
return r.json()
|
| 78 |
|
| 79 |
+
def _post_analyze_zip(audio_file, rpm_file, sr_target, n_fft, hop, env_band, run_llm, notes) -> bytes:
|
| 80 |
files = {}
|
| 81 |
if audio_file is not None:
|
| 82 |
files["audio"] = (audio_file.name, audio_file.getvalue(), audio_file.type or "application/octet-stream")
|
|
|
|
| 102 |
rpm_up = st.file_uploader("Optional RPM CSV (time_sec,rpm)", type=["csv"])
|
| 103 |
|
| 104 |
st.header("Options")
|
| 105 |
+
run_llm = st.toggle("Explain with AI", value=True, help="Get a readable diagnosis and next steps.")
|
| 106 |
user_notes = st.text_area("Context (optional)", placeholder="Vehicle, engine type, driving condition, symptoms…")
|
| 107 |
|
| 108 |
with st.expander("Advanced (defaults are fine)"):
|
|
|
|
| 115 |
st.error("Backend URL is not set. Define CARDIAG_AI_API (or AI_LIGHTBOX_API / LUXFIT_API).")
|
| 116 |
ok = _health(API_BASE, HEADERS) if API_BASE else False
|
| 117 |
if not ok and API_BASE:
|
| 118 |
+
st.warning("Could not reach the backend. Check your private Space URL.")
|
| 119 |
analyze_btn = st.button("Analyze now", type="primary", disabled=(audio_up is None or not ok))
|
| 120 |
|
| 121 |
+
# ================= Reset state on file change (prevents stale results) =================
|
| 122 |
if "last_audio_id" not in st.session_state:
|
| 123 |
st.session_state["last_audio_id"] = None
|
| 124 |
if audio_up is not None:
|
|
|
|
| 151 |
params = res.get("params", {})
|
| 152 |
llm = res.get("llm_report") or {}
|
| 153 |
|
|
|
|
| 154 |
model_name = params.get("vision_model", "gpt-4o")
|
| 155 |
st.caption(f"AI model: **{model_name}**")
|
| 156 |
|
| 157 |
+
# Input uniqueness proof + quick features
|
| 158 |
if params.get("audio_id"):
|
| 159 |
st.caption(f"Audio ID: **{params['audio_id']}**")
|
| 160 |
if params.get("features"):
|
|
|
|
| 168 |
except Exception:
|
| 169 |
st.caption(f"Features — {feat}")
|
| 170 |
|
| 171 |
+
# Show meta proving Vision read the image
|
| 172 |
if isinstance(llm, dict) and llm.get("meta"):
|
| 173 |
meta = llm["meta"]
|
| 174 |
st.caption(f"Vision meta — seen_images: {meta.get('seen_images')}, views: {meta.get('views')}")
|
| 175 |
|
|
|
|
| 176 |
col1, col2 = st.columns([1, 1])
|
| 177 |
diag_list = llm.get("diagnosis", [])
|
| 178 |
notes = llm.get("notes", "")
|