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

Upload 2 files

Browse files
Files changed (2) hide show
  1. Dockerfile +19 -7
  2. app.py +28 -37
Dockerfile CHANGED
@@ -1,21 +1,33 @@
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}"]
 
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}"]
app.py CHANGED
@@ -1,13 +1,17 @@
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
 
@@ -22,11 +26,11 @@ 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 (
@@ -41,25 +45,19 @@ 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)
@@ -83,8 +81,7 @@ def _post_analyze(audio_file, rpm_file, sr_target, n_fft, hop, env_band, run_llm
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
 
@@ -104,23 +101,19 @@ def _post_analyze_zip(audio_file, rpm_file, sr_target, n_fft, hop, env_band, run
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")
@@ -133,14 +126,18 @@ with st.sidebar:
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(
@@ -156,14 +153,11 @@ if analyze_btn and audio_up is not None:
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:
@@ -197,7 +191,6 @@ if res:
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",
@@ -206,7 +199,7 @@ if res:
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
 
@@ -238,7 +231,6 @@ if res:
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"):
@@ -253,9 +245,8 @@ if res:
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.")
 
1
+ # app.py — DiagStudio AI (Streamlit frontend, fixed)
2
+ # Root-cause fix: prevent Streamlit from writing to '/.streamlit'
3
+ # by defining a writable HOME and disabling usage stats before import.
 
 
4
 
5
  import os
6
+ # ----- MUST be set BEFORE importing streamlit -----
7
+ os.environ.setdefault("HOME", "/tmp")
8
+ os.environ.setdefault("STREAMLIT_BROWSER_GATHER_USAGE_STATS", "false")
9
+ os.environ.setdefault("STREAMLIT_SERVER_HEADLESS", "true")
10
+
11
  import io
12
  import json
13
  import time
14
+ import base64
15
  import requests
16
  import streamlit as st
17
 
 
26
  pass
27
 
28
  # ================= Env & API =================
29
+ def _env(k):
30
  return (os.getenv(k) or "").strip().strip("'\"")
31
 
32
  API_BASE = (
33
+ _env("CARDIAG_AI_API")
34
  or _env("AI_LIGHTBOX_API")
35
  or _env("LUXFIT_API")
36
  ).rstrip("/") if (
 
45
  st.title("DiagStudio AI")
46
  st.caption("Upload a short engine recording and get a clear, visual, and textual diagnosis report.")
47
 
 
48
  if not API_BASE:
49
  st.error("Backend URL is not set. Define CARDIAG_AI_API (or AI_LIGHTBOX_API / LUXFIT_API).")
50
  st.stop()
51
 
52
+ # ================= Helpers =================
53
  @st.cache_data(show_spinner=False, ttl=30)
54
+ def check_health(url: str, headers: dict):
55
  try:
56
+ r = requests.get(f"{url}/health", headers=headers, timeout=10)
57
  return r.status_code == 200
58
  except Exception:
59
  return False
60
 
 
 
 
 
 
61
  def _nice_conf(p):
62
  try:
63
  v = float(p)
 
81
  "extra_text": notes or "",
82
  }
83
 
84
+ r = requests.post(f"{API_BASE}/analyze", headers=HEADERS, files=files, data=data, timeout=180)
 
85
  r.raise_for_status()
86
  return r.json()
87
 
 
101
  "extra_text": notes or "",
102
  }
103
 
104
+ r = requests.post(f"{API_BASE}/analyze_zip", headers=HEADERS, files=files, data=data, timeout=240, stream=True)
105
  r.raise_for_status()
106
+ return r.content
107
 
108
+ def dataurl_to_bytes(data_url: str) -> bytes:
109
  # "data:image/png;base64,...."
110
  b64 = data_url.split(",", 1)[1]
 
 
 
 
111
  return base64.b64decode(b64)
112
 
113
  # ================= Sidebar =================
114
  with st.sidebar:
115
  st.header("Upload")
116
+ audio_up = st.file_uploader("Engine recording (WAV/MP3/M4A)", type=["wav", "mp3", "m4a"])
117
  rpm_up = st.file_uploader("Optional RPM CSV (time_sec,rpm)", type=["csv"])
118
 
119
  st.header("Options")
 
126
  hop = st.select_slider("Hop length", options=[128, 256, 512, 1024], value=512)
127
  env_band = st.text_input("Impact band (Hz low,high)", value="1000,8000")
128
 
129
+ ok = check_health(API_BASE, HEADERS)
130
+ if not ok:
131
+ st.warning("Could not reach the backend. Verify your private Space is running and reachable.")
132
+
133
+ disabled = (audio_up is None) or (not ok)
134
  analyze_btn = st.button("Analyze now", type="primary", disabled=disabled)
135
 
136
+ # ================= Main =================
137
  status_ph = st.empty()
138
 
139
  if analyze_btn and audio_up is not None:
140
+ status_ph.info("Analyzing…")
141
  t0 = time.time()
142
  try:
143
  data = _post_analyze(
 
153
  except Exception as e:
154
  status_ph.error(f"Request failed: {e}")
155
 
 
156
  res = st.session_state.get("last_result")
157
  if res:
 
158
  st.subheader("Result")
159
  col1, col2 = st.columns([1,1])
160
 
 
161
  llm = res.get("llm_report") or {}
162
  if isinstance(llm, str):
163
  try:
 
191
  st.markdown("**Suggested next checks**")
192
  st.write(next_tests)
193
 
 
194
  st.subheader("Visual checks")
195
  tabs = st.tabs([
196
  "Energy over time",
 
199
  "Signal envelope",
200
  "Detail view",
201
  "Order view"
202
+ ])
203
 
204
  imgs = res.get("images", {})
205
 
 
231
 
232
  st.divider()
233
 
 
234
  st.subheader("Download")
235
  st.caption("Get all visuals and the diagnosis as a single ZIP.")
236
  if st.button("Prepare ZIP"):
 
245
  data=zbytes,
246
  file_name="diagstudio_results.zip",
247
  mime="application/zip"
248
+ )
249
  except Exception as e:
250
  st.error(f"ZIP build failed: {e}")
 
251
  else:
252
  st.info("Upload an engine recording, optionally add RPM CSV, then click Analyze.")