renderfy commited on
Commit
2a0f99d
·
verified ·
1 Parent(s): 1c0f876

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +13 -18
  2. app.py +175 -217
  3. requirements.txt +2 -2
Dockerfile CHANGED
@@ -1,33 +1,28 @@
1
- # ---------- Public HF Space: Streamlit frontend (DiagStudio AI) ----------
2
  FROM python:3.11-slim
3
 
4
- # System deps
 
 
 
 
 
 
5
  RUN apt-get update && apt-get install -y --no-install-recommends \
6
  ca-certificates curl && \
7
  rm -rf /var/lib/apt/lists/*
8
 
9
- # Prevent Python from writing .pyc; flush stdout
10
- ENV PYTHONDONTWRITEBYTECODE=1 \
11
- PYTHONUNBUFFERED=1
12
-
13
- # Streamlit needs a writable HOME; disable usage stats; headless mode
14
- ENV HOME=/tmp \
15
- STREAMLIT_BROWSER_GATHER_USAGE_STATS=false \
16
- STREAMLIT_SERVER_HEADLESS=true
17
-
18
- # Workdir
19
  WORKDIR /app
20
 
21
  # Install deps first (better layer caching)
22
  COPY requirements.txt /app/requirements.txt
23
- RUN pip install --no-cache-dir -r requirements.txt
24
 
25
- # Copy app
26
  COPY app.py /app/app.py
27
 
28
- # Spaces Docker expects the app to listen on 7860
29
  EXPOSE 7860
30
 
31
- # Bind to 0.0.0.0 and honor $PORT if provided by platform
32
- # Using sh -c form to expand ${PORT}; recommended for passing env to CMD
33
- CMD ["sh", "-c", "streamlit run app.py --server.address=0.0.0.0 --server.port=${PORT:-7860}"]
 
1
+ # Streamlit frontend (TR/EN) — containerized
2
  FROM python:3.11-slim
3
 
4
+ ENV PYTHONDONTWRITEBYTECODE=1 \
5
+ PYTHONUNBUFFERED=1 \
6
+ STREAMLIT_SERVER_HEADLESS=true \
7
+ STREAMLIT_BROWSER_GATHER_USAGE_STATS=false \
8
+ PORT=7860
9
+
10
+ # (Optional) useful utils
11
  RUN apt-get update && apt-get install -y --no-install-recommends \
12
  ca-certificates curl && \
13
  rm -rf /var/lib/apt/lists/*
14
 
 
 
 
 
 
 
 
 
 
 
15
  WORKDIR /app
16
 
17
  # Install deps first (better layer caching)
18
  COPY requirements.txt /app/requirements.txt
19
+ RUN pip install --no-cache-dir -r /app/requirements.txt
20
 
21
+ # Copy app last
22
  COPY app.py /app/app.py
23
 
24
+ # If you mount envs in HF Space, they’ll be visible at runtime (no bake-in here)
25
  EXPOSE 7860
26
 
27
+ # Streamlit CLI with explicit address/port (headless) :contentReference[oaicite:3]{index=3}
28
+ CMD ["streamlit", "run", "app.py", "--server.address=0.0.0.0", "--server.port=7860"]
 
app.py CHANGED
@@ -1,14 +1,5 @@
1
- # app.py — DiagStudio AI (Streamlit frontend)
2
- # Pairs with the FastAPI backend (composite image + JSON mode).
3
- # Backend URL via one of:
4
- # CARDIAG_AI_API | AI_LIGHTBOX_API | LUXFIT_API
5
- # Optional bearer token passthrough:
6
- # CARDIAG_AI_TOKEN | AI_LIGHTBOX_TOKEN | LUXFIT_TOKEN
7
- #
8
- # Run:
9
- # CARDIAG_AI_API="https://<your-private-backend>" streamlit run app.py --server.port 7860
10
-
11
- import os, json, time, hashlib, requests, io, base64
12
  import streamlit as st
13
 
14
  # ---- container-safe defaults to avoid '/.streamlit' permission errors
@@ -24,6 +15,8 @@ try:
24
  st._config.set_option("theme.secondaryBackgroundColor", "#171a1f")
25
  st._config.set_option("theme.textColor", "#e6e8ea")
26
  except Exception:
 
 
27
  pass
28
 
29
  # ================= Env & API =================
@@ -37,215 +30,180 @@ API_BASE = (
37
  HF_TOKEN = _env("CARDIAG_AI_TOKEN") or _env("AI_LIGHTBOX_TOKEN") or _env("LUXFIT_TOKEN")
38
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
39
 
40
- # ================= Page =================
41
- st.set_page_config(page_title="DiagStudio AI", layout="wide")
42
- st.title("DiagStudio AI")
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=20)
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:
58
- return 0.0
59
-
60
- def _post_analyze(audio_file, rpm_file, sr_target, n_fft, hop, env_band, run_llm, notes):
61
- files = {}
62
- if audio_file is not None:
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),
69
- "hop": str(hop),
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")
83
- if rpm_file is not None:
84
- files["rpm_csv"] = (rpm_file.name, rpm_file.getvalue(), "text/csv")
85
- data = {
86
- "sr_target": str(sr_target),
87
- "n_fft": str(n_fft),
88
- "hop": str(hop),
89
- "env_band": env_band,
90
- "run_llm": "true" if run_llm else "false",
91
- "extra_text": notes or "",
92
- "nonce": str(time.time())
93
- }
94
- r = requests.post(f"{API_BASE}/analyze_zip", headers=HEADERS, files=files, data=data, timeout=300, stream=True)
95
- r.raise_for_status()
96
- return r.content
97
-
98
- # ================= Sidebar =================
99
- with st.sidebar:
100
- st.header("Upload")
101
- audio_up = st.file_uploader("Engine recording (WAV/MP3/M4A)", type=["wav", "mp3", "m4a"])
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)"):
109
- sr_target = st.select_slider("Sample rate (Hz)", options=[16000, 22050, 32000, 44100, 48000], value=22050)
110
- n_fft = st.select_slider("Analysis window (n_fft)", options=[1024, 2048, 4096], value=2048)
111
- hop = st.select_slider("Hop length", options=[128, 256, 512, 1024], value=512)
112
- env_band = st.text_input("Impact band (Hz low,high or 'auto')", value="auto")
113
-
114
- # Allow manual URL override if env not set
115
- if not API_BASE:
116
- API_BASE = st.text_input("Backend URL", value="", placeholder="https://<your-private-backend>").strip().rstrip("/")
117
- ok = _health(API_BASE, HEADERS) if API_BASE else False
118
- if not API_BASE:
119
- st.error("Backend URL missing. Set CARDIAG_AI_API env or enter it above.")
120
- elif not ok:
121
- st.warning("Backend not reachable. Check URL or token.")
122
- analyze_btn = st.button("Analyze now", type="primary", disabled=(audio_up is None or not ok))
123
-
124
- # ================= Reset state on file change =================
125
- if "last_audio_id" not in st.session_state:
126
- st.session_state["last_audio_id"] = None
127
- if audio_up is not None:
128
- _aid = hashlib.sha256(audio_up.getvalue()).hexdigest()[:12]
129
- if st.session_state["last_audio_id"] != _aid:
130
- st.session_state["last_audio_id"] = _aid
131
- st.session_state.pop("last_result", None)
132
-
133
- status_ph = st.empty()
134
-
135
- # ================= Action =================
136
- if analyze_btn and audio_up is not None:
137
- status_ph.info("Analyzing…")
138
- t0 = time.time()
139
- try:
140
- data = _post_analyze(audio_up, rpm_up, sr_target, n_fft, hop, env_band, run_llm, user_notes)
141
- st.session_state["last_result"] = data
142
- status_ph.success(f"Done in {time.time()-t0:.1f}s")
143
- except requests.HTTPError as e:
144
- status_ph.error(f"Backend error: {e.response.status_code} — {e.response.text[:300]}")
145
- except Exception as e:
146
- status_ph.error(f"Request failed: {e}")
147
-
148
- res = st.session_state.get("last_result")
149
-
150
- # ================= Result =================
151
  if res:
152
- st.subheader("Result")
153
-
154
- params = res.get("params", {})
155
- llm = res.get("llm_report") or {}
156
- imgs = res.get("images", {}) if isinstance(res.get("images", {}), dict) else {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
- st.caption(f"AI model: **{params.get('vision_model','gpt-4o')}**")
159
- if params.get("audio_id"):
160
- st.caption(f"Audio ID: **{params['audio_id']}**")
161
- if params.get("features"):
162
- feat = params["features"]
163
  try:
164
- st.caption(
165
- f"Features — rms: {feat.get('rms'):.4f}, zcr: {feat.get('zcr'):.4f}, "
166
- f"centroid: {feat.get('centroid'):.1f} Hz, rolloff: {feat.get('rolloff'):.1f} Hz, "
167
- f"flux: {feat.get('flux'):.2f}"
168
- )
169
- except Exception:
170
- st.caption(f"Features — {feat}")
171
-
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", "")
179
- next_tests = llm.get("next_tests", "")
180
-
181
- with col1:
182
- st.markdown("**Likely issues**")
183
- if isinstance(diag_list, list) and len(diag_list) > 0:
184
- for item in diag_list:
185
- label = str(item.get("label", "Issue"))
186
- conf = _nice_conf(item.get("likelihood", 0))
187
- st.text(label)
188
- st.progress(conf)
189
- if item.get("evidence"):
190
- st.caption(item["evidence"])
191
- else:
192
- st.info("No specific faults detected with high confidence.")
193
-
194
- with col2:
195
- if notes:
196
- st.markdown("**Mechanic-friendly notes**")
197
- st.write(notes)
198
- if next_tests:
199
- st.markdown("**Suggested next checks**")
200
- st.write(next_tests)
201
-
202
- st.subheader("Visual checks")
203
- tabs = st.tabs([
204
- "Composite",
205
- "Energy over time",
206
- "Perceptual view",
207
- "Impact pattern",
208
- "Signal envelope",
209
- "Order view",
210
- ])
211
-
212
- with tabs[0]:
213
- if imgs.get("composite"): st.image(imgs["composite"], caption="Composite diagnostic figure")
214
- else: st.caption("Composite image is not available.")
215
-
216
- with tabs[1]:
217
- if imgs.get("stft"): st.image(imgs["stft"], caption="Energy distribution across time.")
218
- else: st.caption("Shown in composite.")
219
-
220
- with tabs[2]:
221
- if imgs.get("mel"): st.image(imgs["mel"], caption="Perceptually-weighted overview.")
222
- else: st.caption("Shown in composite.")
223
-
224
- with tabs[3]:
225
- if imgs.get("envelope_spectrum"): st.image(imgs["envelope_spectrum"], caption="Repetitive impact signature.")
226
- else: st.caption("Shown in composite.")
227
-
228
- with tabs[4]:
229
- if imgs.get("envelope_trace"): st.image(imgs["envelope_trace"], caption="Overall intensity trace.")
230
- else: st.caption("Shown in composite.")
231
-
232
- with tabs[5]:
233
- if imgs.get("order_map"): st.image(imgs["order_map"], caption="Patterns that track engine speed.")
234
- else: st.caption("Order view requires an RPM CSV (time_sec,rpm).")
235
-
236
- st.divider()
237
- st.subheader("Download")
238
- st.caption("Get all visuals and the diagnosis as a single ZIP.")
239
- if st.button("Prepare ZIP"):
240
  try:
241
- zbytes = _post_analyze_zip(audio_up, rpm_up, sr_target, n_fft, hop, env_band, run_llm, user_notes)
242
- st.download_button(
243
- "Download results (ZIP)",
244
- data=zbytes,
245
- file_name="diagstudio_results.zip",
246
- mime="application/zip"
247
- )
248
  except Exception as e:
249
- st.error(f"ZIP build failed: {e}")
250
- else:
251
- st.info("Upload an engine recording, optionally add an RPM CSV, then click Analyze.")
 
 
1
+ # app.py — Streamlit Frontend for Diag Studio AI
2
+ import os, json, time, io, requests, traceback
 
 
 
 
 
 
 
 
 
3
  import streamlit as st
4
 
5
  # ---- container-safe defaults to avoid '/.streamlit' permission errors
 
15
  st._config.set_option("theme.secondaryBackgroundColor", "#171a1f")
16
  st._config.set_option("theme.textColor", "#e6e8ea")
17
  except Exception:
18
+ # Themes usually set via config.toml, but programmatic options apply at runtime.
19
+ # See Streamlit theming docs. # docs: https://docs.streamlit.io/develop/concepts/configuration/theming
20
  pass
21
 
22
  # ================= Env & API =================
 
30
  HF_TOKEN = _env("CARDIAG_AI_TOKEN") or _env("AI_LIGHTBOX_TOKEN") or _env("LUXFIT_TOKEN")
31
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
32
 
33
+ # ================= i18n =================
34
+ L = {
35
+ "en": {
36
+ "title": "Diag Studio AI — Engine Sound Diagnosis",
37
+ "about": "Upload an engine sound (mp3/wav/ogg) or paste an audio URL. The backend runs fal.ai audio-understanding with detailed analysis, then summarizes with GPT-4o.",
38
+ "backend_url": "Backend URL",
39
+ "lang": "Language",
40
+ "upload": "Upload audio file",
41
+ "or": "or",
42
+ "paste_url": "Paste audio URL (optional)",
43
+ "adv": "Advanced",
44
+ "prompt": "Override prompt (optional)",
45
+ "detailed": "Detailed analysis",
46
+ "use_data_uri": "Send as data:URI (safer for private backends)",
47
+ "model": "Commentary model (OpenAI)",
48
+ "analyze": "Analyze",
49
+ "reset": "Reset",
50
+ "result": "Result",
51
+ "commentary": "Commentary (driver-friendly)",
52
+ "json": "Parsed JSON",
53
+ "backend_artifacts": "Backend artifacts",
54
+ "download_summary": "Download summary.txt",
55
+ "download_parsed": "Download parsed.json",
56
+ "download_raw": "Download raw.txt",
57
+ "no_backend": "Enter a backend URL (e.g., http://127.0.0.1:7860)",
58
+ "health_ok": "Backend is reachable.",
59
+ "health_fail": "Backend not reachable.",
60
+ "upload_hint": "Max ~200 MB per Streamlit docs; use mp3/wav/ogg.",
61
+ "footer": "Tip: You can also paste a public audio URL if you don’t want to upload the file."
62
+ },
63
+ "tr": {
64
+ "title": "Diag Studio AI — Motor Sesi Teşhis",
65
+ "about": "Motor sesini (mp3/wav/ogg) yükleyin veya bir ses URL’si verin. Backend fal.ai audio-understanding’ı ayrıntılı analizle çalıştırır, ardından GPT-4o ile özetler.",
66
+ "backend_url": "Backend URL",
67
+ "lang": "Dil",
68
+ "upload": "Ses dosyası yükle",
69
+ "or": "veya",
70
+ "paste_url": "Ses URL’si (opsiyonel)",
71
+ "adv": "Gelişmiş",
72
+ "prompt": "Prompt geçersiz kıl (opsiyonel)",
73
+ "detailed": "Ayrıntılı analiz",
74
+ "use_data_uri": "data:URI olarak gönder (özel backend için güvenli)",
75
+ "model": "Yorum modeli (OpenAI)",
76
+ "analyze": "Analiz Et",
77
+ "reset": "Sıfırla",
78
+ "result": "Sonuç",
79
+ "commentary": "Yorum (sürücü dostu)",
80
+ "json": "Ayrıştırılmış JSON",
81
+ "backend_artifacts": "Backend çıktıları",
82
+ "download_summary": "summary.txt indir",
83
+ "download_parsed": "parsed.json indir",
84
+ "download_raw": "raw.txt indir",
85
+ "no_backend": "Bir backend URL girin (örn: http://127.0.0.1:7860)",
86
+ "health_ok": "Backend erişilebilir.",
87
+ "health_fail": "Backend’e erişilemedi.",
88
+ "upload_hint": "Streamlit varsayılan üst sınır ~200 MB; mp3/wav/ogg kullanın.",
89
+ "footer": "İpucu: Dosya yüklemek istemezseniz herkese açık bir ses URL’si de girebilirsiniz."
90
  }
91
+ }
92
+
93
+ # ================= Session State =================
94
+ if "lang" not in st.session_state:
95
+ st.session_state.lang = "tr"
96
+ if "last_response" not in st.session_state:
97
+ st.session_state.last_response = None
98
+
99
+ # ================= UI =================
100
+ col1, col2 = st.columns([0.65, 0.35])
101
+ with col1:
102
+ st.title(L[st.session_state.lang]["title"])
103
+ with col2:
104
+ st.session_state.lang = st.selectbox(
105
+ L[st.session_state.lang]["lang"],
106
+ ["tr", "en"],
107
+ index=(0 if st.session_state.lang == "tr" else 1),
108
+ key="lang_selector",
109
+ )
110
+
111
+ t = L[st.session_state.lang]
112
+ st.caption(t["about"])
113
+
114
+ backend_default = API_BASE or "http://127.0.0.1:7860"
115
+ api_base = st.text_input(t["backend_url"], value=backend_default)
116
+
117
+ # Health check
118
+ health_col, spacer = st.columns([0.28, 0.72])
119
+ with health_col:
120
+ if st.button("🔄 Health"):
121
+ try:
122
+ r = requests.get(f"{api_base}/health", headers=HEADERS, timeout=10)
123
+ if r.ok:
124
+ st.success(t["health_ok"])
125
+ else:
126
+ st.error(t["health_fail"])
127
+ except Exception:
128
+ st.error(t["health_fail"])
129
+
130
+ st.markdown("---")
131
+
132
+ # Upload or URL input
133
+ st.write(t["upload_hint"])
134
+ file = st.file_uploader(t["upload"], type=["mp3", "wav", "ogg"]) # accepts file-like objects; returns UploadedFile. Docs show in-memory behavior. #
135
+ files = {"file": (_file.name, _file, _file.type or "application/octet-stream")}
136
+ elif _audio_url.strip():
137
+ data["audio_url"] = _audio_url.strip()
138
+
139
+ resp = requests.post(url, headers=_headers, data=data, files=files, timeout=180)
140
+ resp.raise_for_status()
141
+ return resp.json()
142
+
143
+ if go:
144
+ if not api_base:
145
+ st.error(t["no_backend"])
146
+ elif not file and not audio_url.strip():
147
+ st.error(t["upload"])
148
+ else:
149
+ with st.spinner("Analyzing..."):
150
+ try:
151
+ res = post_analyze(api_base, HEADERS, file, audio_url)
152
+ st.session_state.last_response = res
153
+ except Exception as e:
154
+ st.error(f"Request failed: {e}")
155
+ st.stop()
156
+
157
+ # ============== Results ==============
158
+ res = st.session_state.last_response
 
 
 
 
 
 
 
 
159
  if res:
160
+ st.subheader(t["result"])
161
+
162
+ # Commentary
163
+ comm = res.get("commentary") or ""
164
+ if comm:
165
+ st.markdown(f"**{t['commentary']}**")
166
+ st.write(comm)
167
+
168
+ # JSON pretty
169
+ parsed = res.get("fal_parsed")
170
+ if parsed:
171
+ st.markdown(f"**{t['json']}**")
172
+ st.json(parsed) # renders a collapsible JSON tree
173
+
174
+ # Backend artifacts — download via Streamlit
175
+ st.markdown(f"**{t['backend_artifacts']}**")
176
+ dls = res.get("downloads") or {}
177
+ col_a, col_b, col_c = st.columns(3)
178
+
179
+ def fetch_bytes(url: str) -> bytes:
180
+ r = requests.get(url if url.startswith("http") else f"{api_base}{url}", headers=HEADERS, timeout=60)
181
+ r.raise_for_status()
182
+ return r.content
183
+
184
+ # summary
185
+ if dls.get("summary_txt"):
186
+ try:
187
+ data = fetch_bytes(dls["summary_txt"])
188
+ col_a.download_button(label=t["download_summary"], data=data, file_name="summary.txt")
189
+ except Exception as e:
190
+ col_a.error("summary.txt ✖")
191
 
192
+ # parsed
193
+ if dls.get("fal_parsed_json"):
 
 
 
194
  try:
195
+ data = fetch_bytes(dls["fal_parsed_json"])
196
+ col_b.download_button(label=t["download_parsed"], data=data, file_name="parsed.json", mime="application/json")
197
+ except Exception as e:
198
+ col_b.error("parsed.json ✖")
199
+
200
+ # raw
201
+ if dls.get("fal_raw_txt"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  try:
203
+ data = fetch_bytes(dls["fal_raw_txt"])
204
+ col_c.download_button(label=t["download_raw"], data=data, file_name="raw.txt")
 
 
 
 
 
205
  except Exception as e:
206
+ col_c.error("raw.txt ✖")
207
+
208
+ # ===== Footer =====
209
+ st.caption("© 2025 Diag Studio AI — Streamlit frontend")
requirements.txt CHANGED
@@ -1,2 +1,2 @@
1
- streamlit>=1.36
2
- requests>=2.32
 
1
+ streamlit==1.40.0
2
+ requests>=2.31.0