renderfy commited on
Commit
e31837d
·
verified ·
1 Parent(s): cd06179

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +21 -0
  2. app.py +261 -0
  3. requirements.txt +2 -0
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---------- Public HF Space: Streamlit frontend ----------
2
+ FROM python:3.11-slim
3
+
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ ca-certificates \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ WORKDIR /app
9
+
10
+ COPY requirements.txt /app/requirements.txt
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copy frontend app
14
+ COPY app.py /app/app.py
15
+
16
+ # Expose default Spaces port
17
+ EXPOSE 7860
18
+
19
+ # Streamlit must bind to 0.0.0.0 and the provided port
20
+ # Use ${PORT} if set by the platform, otherwise fall back to 7860.
21
+ CMD ["sh", "-c", "streamlit run app.py --server.address=0.0.0.0 --server.port=${PORT:-7860}"]
app.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py — DiagStudio AI (Streamlit frontend for public HF Space)
2
+ # - Talks to your private FastAPI backend:
3
+ # POST {API_BASE}/analyze
4
+ # POST {API_BASE}/analyze_zip
5
+ # - Friendly UI for non-technical users; no DSP jargon on the surface.
6
+
7
+ import os
8
+ import io
9
+ import json
10
+ import time
11
+ import requests
12
+ import streamlit as st
13
+
14
+ # ================= Theme (no extra files) =================
15
+ try:
16
+ st._config.set_option("theme.base", "dark")
17
+ st._config.set_option("theme.primaryColor", "#ff7a1a")
18
+ st._config.set_option("theme.backgroundColor", "#0e0f12")
19
+ st._config.set_option("theme.secondaryBackgroundColor", "#171a1f")
20
+ st._config.set_option("theme.textColor", "#e6e8ea")
21
+ except Exception:
22
+ pass
23
+
24
+ # ================= Env & API =================
25
+ def _env(k):
26
+ return (os.getenv(k) or "").strip().strip("'\"")
27
+
28
+ API_BASE = (
29
+ _env("CARDIAG_AI_API")
30
+ or _env("AI_LIGHTBOX_API")
31
+ or _env("LUXFIT_API")
32
+ ).rstrip("/") if (
33
+ _env("CARDIAG_AI_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")
34
+ ) else ""
35
+
36
+ HF_TOKEN = _env("CARDIAG_AI_TOKEN") or _env("AI_LIGHTBOX_TOKEN") or _env("LUXFIT_TOKEN")
37
+ HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
38
+
39
+ # ================= App Header =================
40
+ st.set_page_config(page_title="DiagStudio AI", layout="wide")
41
+ st.title("DiagStudio AI")
42
+ st.caption("Upload a short engine recording and get a clear, visual, and textual diagnosis report.")
43
+
44
+ # Simple validation for API base
45
+ if not API_BASE:
46
+ st.error("Backend URL is not set. Define CARDIAG_AI_API (or AI_LIGHTBOX_API / LUXFIT_API).")
47
+ st.stop()
48
+
49
+ # Health check
50
+ @st.cache_data(show_spinner=False, ttl=30)
51
+ def check_health():
52
+ try:
53
+ r = requests.get(f"{API_BASE}/health", headers=HEADERS, timeout=10)
54
+ return r.status_code == 200
55
+ except Exception:
56
+ return False
57
+
58
+ ok = check_health()
59
+ if not ok:
60
+ st.warning("Could not reach the backend. Please verify your private Space is running and accessible.")
61
+
62
+ # ================= Helpers =================
63
+ def _nice_conf(p):
64
+ try:
65
+ v = float(p)
66
+ return min(max(v, 0.0), 1.0)
67
+ except Exception:
68
+ return 0.0
69
+
70
+ def _post_analyze(audio_file, rpm_file, sr_target, n_fft, hop, env_band, run_llm, notes):
71
+ files = {}
72
+ if audio_file is not None:
73
+ files["audio"] = (audio_file.name, audio_file.getvalue(), audio_file.type or "application/octet-stream")
74
+ if rpm_file is not None:
75
+ files["rpm_csv"] = (rpm_file.name, rpm_file.getvalue(), "text/csv")
76
+
77
+ data = {
78
+ "sr_target": str(sr_target),
79
+ "n_fft": str(n_fft),
80
+ "hop": str(hop),
81
+ "env_band": env_band,
82
+ "run_llm": "true" if run_llm else "false",
83
+ "extra_text": notes or "",
84
+ }
85
+
86
+ # Multipart form-data via requests.files -> backend expects UploadFile/File. :contentReference[oaicite:1]{index=1}
87
+ r = requests.post(f"{API_BASE}/analyze", headers=HEADERS, files=files, data=data, timeout=120)
88
+ r.raise_for_status()
89
+ return r.json()
90
+
91
+ def _post_analyze_zip(audio_file, rpm_file, sr_target, n_fft, hop, env_band, run_llm, notes):
92
+ files = {}
93
+ if audio_file is not None:
94
+ files["audio"] = (audio_file.name, audio_file.getvalue(), audio_file.type or "application/octet-stream")
95
+ if rpm_file is not None:
96
+ files["rpm_csv"] = (rpm_file.name, rpm_file.getvalue(), "text/csv")
97
+
98
+ data = {
99
+ "sr_target": str(sr_target),
100
+ "n_fft": str(n_fft),
101
+ "hop": str(hop),
102
+ "env_band": env_band,
103
+ "run_llm": "true" if run_llm else "false",
104
+ "extra_text": notes or "",
105
+ }
106
+
107
+ r = requests.post(f"{API_BASE}/analyze_zip", headers=HEADERS, files=files, data=data, timeout=180, stream=True)
108
+ r.raise_for_status()
109
+ return r.content # bytes for st.download_button. :contentReference[oaicite:2]{index=2}
110
+
111
+ def _dataurl_to_bytes(data_url: str) -> bytes:
112
+ # "data:image/png;base64,...."
113
+ b64 = data_url.split(",", 1)[1]
114
+ return base64_to_bytes(b64)
115
+
116
+ def base64_to_bytes(b64: str) -> bytes:
117
+ import base64
118
+ return base64.b64decode(b64)
119
+
120
+ # ================= Sidebar =================
121
+ with st.sidebar:
122
+ st.header("Upload")
123
+ audio_up = st.file_uploader("Engine recording (WAV/MP3/M4A)", type=["wav", "mp3", "m4a"]) # st.file_uploader returns UploadedFile. :contentReference[oaicite:3]{index=3}
124
+ rpm_up = st.file_uploader("Optional RPM CSV (time_sec,rpm)", type=["csv"])
125
+
126
+ st.header("Options")
127
+ run_llm = st.toggle("Explain with AI", value=True, help="Adds an easy-to-read diagnosis and next steps.")
128
+ user_notes = st.text_area("Context (optional)", placeholder="Vehicle, engine type, driving condition, symptoms…")
129
+
130
+ with st.expander("Advanced (defaults are fine)"):
131
+ sr_target = st.select_slider("Sample rate (Hz)", options=[16000, 22050, 32000, 44100, 48000], value=22050)
132
+ n_fft = st.select_slider("Analysis window (n_fft)", options=[1024, 2048, 4096], value=2048)
133
+ hop = st.select_slider("Hop length", options=[128, 256, 512, 1024], value=512)
134
+ env_band = st.text_input("Impact band (Hz low,high)", value="1000,8000")
135
+
136
+ disabled = audio_up is None or not ok
137
+ analyze_btn = st.button("Analyze now", type="primary", disabled=disabled)
138
+
139
+ # ================= Main UI =================
140
+ status_ph = st.empty()
141
+
142
+ if analyze_btn and audio_up is not None:
143
+ status_ph.info("Analyzing… This usually takes a few seconds.")
144
+ t0 = time.time()
145
+ try:
146
+ data = _post_analyze(
147
+ audio_file=audio_up, rpm_file=rpm_up,
148
+ sr_target=sr_target, n_fft=n_fft, hop=hop,
149
+ env_band=env_band, run_llm=run_llm, notes=user_notes
150
+ )
151
+ elapsed = time.time() - t0
152
+ status_ph.success(f"Done in {elapsed:.1f}s")
153
+ st.session_state["last_result"] = data
154
+ except requests.HTTPError as e:
155
+ status_ph.error(f"Backend error: {e.response.status_code} — {e.response.text[:300]}")
156
+ except Exception as e:
157
+ status_ph.error(f"Request failed: {e}")
158
+
159
+ # Show results if present
160
+ res = st.session_state.get("last_result")
161
+ if res:
162
+ # -------- Summary cards (user-friendly text) --------
163
+ st.subheader("Result")
164
+ col1, col2 = st.columns([1,1])
165
+
166
+ # AI diagnosis (JSON). We tolerate either dict or string.
167
+ llm = res.get("llm_report") or {}
168
+ if isinstance(llm, str):
169
+ try:
170
+ llm = json.loads(llm)
171
+ except Exception:
172
+ llm = {"raw": llm}
173
+
174
+ diag_list = llm.get("diagnosis", [])
175
+ notes = llm.get("notes", "")
176
+ next_tests = llm.get("next_tests", "")
177
+
178
+ with col1:
179
+ st.markdown("**Likely issues**")
180
+ if diag_list:
181
+ for item in diag_list:
182
+ label = str(item.get("label", "Issue"))
183
+ conf = _nice_conf(item.get("likelihood", 0))
184
+ evidence = str(item.get("evidence", ""))
185
+ st.text(label)
186
+ st.progress(conf)
187
+ if evidence:
188
+ st.caption(evidence)
189
+ else:
190
+ st.info("No specific faults detected with high confidence.")
191
+
192
+ with col2:
193
+ if notes:
194
+ st.markdown("**Mechanic-friendly notes**")
195
+ st.write(notes)
196
+ if next_tests:
197
+ st.markdown("**Suggested next checks**")
198
+ st.write(next_tests)
199
+
200
+ # -------- Visuals in tabs (friendly labels, no DSP jargon) --------
201
+ st.subheader("Visual checks")
202
+ tabs = st.tabs([
203
+ "Energy over time",
204
+ "Perceptual view",
205
+ "Impact pattern",
206
+ "Signal envelope",
207
+ "Detail view",
208
+ "Order view"
209
+ ]) # st.tabs API. :contentReference[oaicite:4]{index=4}
210
+
211
+ imgs = res.get("images", {})
212
+
213
+ with tabs[0]:
214
+ if "stft" in imgs:
215
+ st.image(imgs["stft"], caption="Energy distribution across time.")
216
+
217
+ with tabs[1]:
218
+ if "mel" in imgs:
219
+ st.image(imgs["mel"], caption="Perceptually-weighted overview.")
220
+
221
+ with tabs[2]:
222
+ if "envelope_spectrum" in imgs:
223
+ st.image(imgs["envelope_spectrum"], caption="Repetitive impact signature.")
224
+
225
+ with tabs[3]:
226
+ if "envelope_trace" in imgs:
227
+ st.image(imgs["envelope_trace"], caption="Overall intensity trace.")
228
+
229
+ with tabs[4]:
230
+ if "cwt" in imgs:
231
+ st.image(imgs["cwt"], caption="Fine details and short events.")
232
+
233
+ with tabs[5]:
234
+ if "order_map" in imgs:
235
+ st.image(imgs["order_map"], caption="Patterns that track engine speed.")
236
+ else:
237
+ st.caption("Order view requires an RPM CSV (time_sec,rpm).")
238
+
239
+ st.divider()
240
+
241
+ # -------- Download ZIP --------
242
+ st.subheader("Download")
243
+ st.caption("Get all visuals and the diagnosis as a single ZIP.")
244
+ if st.button("Prepare ZIP"):
245
+ try:
246
+ zbytes = _post_analyze_zip(
247
+ audio_file=audio_up, rpm_file=rpm_up,
248
+ sr_target=sr_target, n_fft=n_fft, hop=hop,
249
+ env_band=env_band, run_llm=run_llm, notes=user_notes
250
+ )
251
+ st.download_button(
252
+ "Download results (ZIP)",
253
+ data=zbytes,
254
+ file_name="diagstudio_results.zip",
255
+ mime="application/zip"
256
+ ) # st.download_button for binary files. :contentReference[oaicite:5]{index=5}
257
+ except Exception as e:
258
+ st.error(f"ZIP build failed: {e}")
259
+
260
+ else:
261
+ st.info("Upload an engine recording, optionally add RPM CSV, then click Analyze.")
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ streamlit>=1.36
2
+ requests>=2.32