renderfy commited on
Commit
234d1ce
·
verified ·
1 Parent(s): 831e1da

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +77 -79
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # app.py — Streamlit Frontend (robust retries + warmup)
2
  import os
3
  import json
4
  import time
@@ -7,12 +7,12 @@ import streamlit as st
7
  from requests.adapters import HTTPAdapter
8
  from urllib3.util.retry import Retry
9
 
10
- # ---- container-safe defaults
11
  os.environ.setdefault("HOME", "/tmp")
12
  os.environ.setdefault("STREAMLIT_BROWSER_GATHER_USAGE_STATS", "false")
13
  os.environ.setdefault("STREAMLIT_SERVER_HEADLESS", "true")
14
 
15
- # ================= Theme (no extra files)
16
  try:
17
  st._config.set_option("theme.base", "dark")
18
  st._config.set_option("theme.primaryColor", "#ff7a1a")
@@ -22,23 +22,20 @@ try:
22
  except Exception:
23
  pass
24
 
25
- # ================= Env & API pickers
26
  def _env(k: str) -> str:
27
  return (os.getenv(k) or "").strip().strip("'\"")
28
 
29
- API_BASE_ENV = (
30
- _env("CARDIAG_AI_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")
31
- )
32
  API_BASE_ENV = API_BASE_ENV.rstrip("/") if API_BASE_ENV else ""
33
-
34
  HF_TOKEN = _env("CARDIAG_AI_TOKEN") or _env("AI_LIGHTBOX_TOKEN") or _env("LUXFIT_TOKEN")
35
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
36
 
37
- # ================= i18n
38
  L = {
39
  "en": {
40
  "title": "Diag Studio AI — Engine Sound Diagnosis",
41
- "about": "Upload an engine sound (mp3/wav/ogg) or paste an audio URL. Backend runs fal.ai audio-understanding with detailed analysis, then summarizes with GPT-4o.",
42
  "backend_url": "Backend URL",
43
  "lang": "Language",
44
  "upload": "Upload audio file",
@@ -46,8 +43,8 @@ L = {
46
  "paste_url": "Paste audio URL (optional)",
47
  "adv": "Advanced",
48
  "prompt": "Override prompt (optional)",
49
- "detailed": "Detailed analysis",
50
- "use_data_uri": "Send as data:URI (safer for private backends)",
51
  "model": "Commentary model (OpenAI)",
52
  "analyze": "Analyze",
53
  "reset": "Reset",
@@ -61,17 +58,19 @@ L = {
61
  "no_backend": "Enter a backend URL (e.g., http://127.0.0.1:7860)",
62
  "health_ok": "Backend is reachable.",
63
  "health_fail": "Backend not reachable.",
64
- "upload_hint": "Max ~200 MB per Streamlit docs; use mp3/wav/ogg.",
65
- "footer": "Tip: You can also paste a public audio URL if you don’t want to upload the file.",
66
  "auto_retry": "Auto-retry on 429/500/502/503/504",
67
  "warmup": "Warm up backend before analysis",
68
  "max_wait": "Max total wait (sec) for retries",
69
  "warming": "Warming up backend…",
70
- "analyzing": "Analyzing…"
 
 
71
  },
72
  "tr": {
73
  "title": "Diag Studio AI — Motor Sesi Teşhis",
74
- "about": "Motor sesini (mp3/wav/ogg) yükleyin veya bir ses URL’si verin. Backend fal.ai detailed analysis ile çalışır, sonra GPT-4o özetler.",
75
  "backend_url": "Backend URL",
76
  "lang": "Dil",
77
  "upload": "Ses dosyası yükle",
@@ -79,8 +78,8 @@ L = {
79
  "paste_url": "Ses URL’si (opsiyonel)",
80
  "adv": "Gelişmiş",
81
  "prompt": "Prompt geçersiz kıl (opsiyonel)",
82
- "detailed": "Ayrıntılı analiz",
83
- "use_data_uri": "data:URI olarak gönder (özel backend için güvenli)",
84
  "model": "Yorum modeli (OpenAI)",
85
  "analyze": "Analiz Et",
86
  "reset": "Sıfırla",
@@ -100,17 +99,19 @@ L = {
100
  "warmup": "Analizden önce backend’i ısıt",
101
  "max_wait": "Retry için maksimum toplam bekleme (sn)",
102
  "warming": "Backend ısıtılıyor…",
103
- "analyzing": "Analiz ediliyor…"
 
 
104
  }
105
  }
106
 
107
- # ================= Session State
108
  if "lang" not in st.session_state:
109
  st.session_state.lang = "tr"
110
  if "last_response" not in st.session_state:
111
  st.session_state.last_response = None
112
 
113
- # ================= UI
114
  col1, col2 = st.columns([0.65, 0.35])
115
  with col1:
116
  st.title(L[st.session_state.lang]["title"])
@@ -124,11 +125,29 @@ with col2:
124
  t = L[st.session_state.lang]
125
  st.caption(t["about"])
126
 
127
- backend_default = API_BASE_ENV or "https://renderfy-diagstudio-ai-backend.hf.space"
128
- api_base = st.text_input(t["backend_url"], value=backend_default)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
 
130
  with st.expander(t["adv"], expanded=False):
131
- detailed = st.checkbox(t["detailed"], value=True)
132
  use_data_uri = st.checkbox(t["use_data_uri"], value=True)
133
  openai_model = st.text_input(t["model"], value="gpt-4o")
134
  prompt_override = st.text_area(t["prompt"], value="", height=120)
@@ -137,13 +156,8 @@ with st.expander(t["adv"], expanded=False):
137
  max_wait = st.slider(t["max_wait"], 10, 180, 60, step=10)
138
  st.caption(t["footer"])
139
 
140
- st.markdown("---")
141
- st.write(t["upload_hint"])
142
- file = st.file_uploader(t["upload"], type=["mp3", "wav", "ogg"]) # 200 MB default limit. :contentReference[oaicite:3]{index=3}
143
- st.write(f"**{t['or']}**")
144
- audio_url = st.text_input(t["paste_url"], value="", placeholder="https://...")
145
-
146
- c1, c2, c3 = st.columns([0.2, 0.2, 0.6])
147
  go = c1.button(f"▶ {t['analyze']}", type="primary")
148
  reset = c2.button(f"↩ {t['reset']}")
149
 
@@ -151,16 +165,13 @@ if reset:
151
  st.session_state.last_response = None
152
  st.rerun()
153
 
154
- # ================= HTTP session with retries
155
  def build_session(_auto_retry: bool) -> requests.Session:
156
  s = requests.Session()
157
  s.headers.update(HEADERS)
158
  if _auto_retry:
159
- # Retry on common transient errors: 429, 500, 502, 503, 504. Backoff = 0.8s, exponential. :contentReference[oaicite:4]{index=4}
160
  retry = Retry(
161
- total=6,
162
- connect=6,
163
- read=6,
164
  backoff_factor=0.8,
165
  status_forcelist=[429, 500, 502, 503, 504],
166
  allowed_methods=frozenset(["GET", "POST"]),
@@ -174,7 +185,6 @@ def build_session(_auto_retry: bool) -> requests.Session:
174
  def warm_up_backend(s: requests.Session, base: str, total_budget: int = 30):
175
  start = time.time()
176
  with st.spinner(t["warming"]):
177
- # Many Spaces need a wake-up; hit GET / then /health with short timeouts. Cold boot ~1–2 min typical. :contentReference[oaicite:5]{index=5}
178
  for path in ["/", "/health"]:
179
  try:
180
  r = s.get(f"{base.rstrip('/')}{path}", timeout=8)
@@ -186,9 +196,11 @@ def warm_up_backend(s: requests.Session, base: str, total_budget: int = 30):
186
  break
187
  time.sleep(2)
188
 
 
189
  def post_analyze(s: requests.Session, base: str, _file, _audio_url: str, timeout_sec: int = 120) -> dict:
190
  url = f"{base.rstrip('/')}/analyze"
191
  data = {
 
192
  "detailed_analysis": "true" if detailed else "false",
193
  "use_data_uri": "true" if use_data_uri else "false",
194
  "openai_model": openai_model or "gpt-4o",
@@ -203,33 +215,25 @@ def post_analyze(s: requests.Session, base: str, _file, _audio_url: str, timeout
203
  elif _audio_url.strip():
204
  data["audio_url"] = _audio_url.strip()
205
 
206
- # Guard: nothing to send
207
- if not files and "audio_url" not in data:
208
- raise RuntimeError("No file or URL provided")
209
-
210
- # Try within a total budget (for bad gateway/cold start); break if succeeded
211
- deadline = time.time() + max_wait
212
- last_err = None
213
- attempts = 0
214
- while True:
215
- attempts += 1
216
- try:
217
- resp = s.post(url, data=data, files=files, timeout=timeout_sec)
218
- # Accept 200 only; other statuses will be retried by the Session if enabled
219
- resp.raise_for_status()
220
- return resp.json()
221
- except Exception as e:
222
- last_err = e
223
- if not auto_retry or time.time() > deadline:
224
- # Return controlled error payload instead of exploding UI
225
- return {
226
- "ok": False,
227
- "error": f"All attempts failed. Last error: {e}",
228
- "attempts": attempts,
229
- "api_base": base,
230
- }
231
- time.sleep(1.2) # small manual delay between loops; Session handles backoff too
232
 
 
233
  if go:
234
  if not api_base:
235
  st.error(t["no_backend"])
@@ -243,36 +247,32 @@ if go:
243
  res = post_analyze(sess, api_base, file, audio_url, timeout_sec=180)
244
  st.session_state.last_response = res
245
 
246
- # ================= Results
247
  res = st.session_state.last_response
248
  if res:
249
- # If backend returned controlled error (instead of raising), show nice message
250
  if not res.get("ok", True) and res.get("error"):
251
  st.error(res["error"])
252
- st.info("Backend might be waking up or rate-limited. Try Analyze again, or increase ‘Max total wait’.")
253
  else:
254
  st.subheader(t["result"])
255
 
256
- # Commentary
257
  comm = res.get("commentary") or ""
258
  if comm:
259
  st.markdown(f"**{t['commentary']}**")
260
  st.write(comm)
261
 
262
- # JSON pretty
263
  parsed = res.get("fal_parsed")
264
  if parsed:
265
  st.markdown(f"**{t['json']}**")
266
  st.json(parsed)
267
 
268
- # Backend artifacts
269
- st.markdown(f"**{t['backend_artifacts']}**")
270
  dls = res.get("downloads") or {}
271
  col_a, col_b, col_c = st.columns(3)
272
 
273
  def fetch_bytes(url: str) -> bytes:
274
- if not url:
275
- raise RuntimeError("empty url")
276
  full = url if url.startswith("http") else f"{api_base.rstrip('/')}{url}"
277
  r = requests.get(full, headers=HEADERS, timeout=60)
278
  r.raise_for_status()
@@ -280,23 +280,21 @@ if res:
280
 
281
  if dls.get("summary_txt"):
282
  try:
283
- data = fetch_bytes(dls["summary_txt"])
284
- col_a.download_button(label=t["download_summary"], data=data, file_name="summary.txt")
285
  except Exception:
286
  col_a.error("summary.txt ✖")
287
 
288
  if dls.get("fal_parsed_json"):
289
  try:
290
- data = fetch_bytes(dls["fal_parsed_json"])
291
- col_b.download_button(label=t["download_parsed"], data=data, file_name="parsed.json", mime="application/json")
292
  except Exception:
293
  col_b.error("parsed.json ✖")
294
 
295
  if dls.get("fal_raw_txt"):
296
  try:
297
- data = fetch_bytes(dls["fal_raw_txt"])
298
- col_c.download_button(label=t["download_raw"], data=data, file_name="raw.txt")
299
  except Exception:
300
  col_c.error("raw.txt ✖")
301
 
302
- st.caption("© 2025 Diag Studio AI — Streamlit frontend (robust)")
 
1
+ # app.py — Streamlit Frontend for Diag Studio AI (TR/EN, robust retries, warmup, downloads)
2
  import os
3
  import json
4
  import time
 
7
  from requests.adapters import HTTPAdapter
8
  from urllib3.util.retry import Retry
9
 
10
+ # ---------------- Container-safe defaults ----------------
11
  os.environ.setdefault("HOME", "/tmp")
12
  os.environ.setdefault("STREAMLIT_BROWSER_GATHER_USAGE_STATS", "false")
13
  os.environ.setdefault("STREAMLIT_SERVER_HEADLESS", "true")
14
 
15
+ # ---------------- Theme ----------------
16
  try:
17
  st._config.set_option("theme.base", "dark")
18
  st._config.set_option("theme.primaryColor", "#ff7a1a")
 
22
  except Exception:
23
  pass
24
 
25
+ # ---------------- Env helpers ----------------
26
  def _env(k: str) -> str:
27
  return (os.getenv(k) or "").strip().strip("'\"")
28
 
29
+ API_BASE_ENV = _env("CARDIAG_AI_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")
 
 
30
  API_BASE_ENV = API_BASE_ENV.rstrip("/") if API_BASE_ENV else ""
 
31
  HF_TOKEN = _env("CARDIAG_AI_TOKEN") or _env("AI_LIGHTBOX_TOKEN") or _env("LUXFIT_TOKEN")
32
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
33
 
34
+ # ---------------- i18n ----------------
35
  L = {
36
  "en": {
37
  "title": "Diag Studio AI — Engine Sound Diagnosis",
38
+ "about": "Upload an engine sound (mp3/wav/ogg) or paste a URL. Backend runs fal.ai detailed analysis, then summarizes with GPT-4o.",
39
  "backend_url": "Backend URL",
40
  "lang": "Language",
41
  "upload": "Upload audio file",
 
43
  "paste_url": "Paste audio URL (optional)",
44
  "adv": "Advanced",
45
  "prompt": "Override prompt (optional)",
46
+ "detailed": "Detailed analysis (fal)",
47
+ "use_data_uri": "Send as data:URI",
48
  "model": "Commentary model (OpenAI)",
49
  "analyze": "Analyze",
50
  "reset": "Reset",
 
58
  "no_backend": "Enter a backend URL (e.g., http://127.0.0.1:7860)",
59
  "health_ok": "Backend is reachable.",
60
  "health_fail": "Backend not reachable.",
61
+ "upload_hint": "Default upload limit ~200 MB; use mp3/wav/ogg.",
62
+ "footer": "Tip: You can also paste a public audio URL.",
63
  "auto_retry": "Auto-retry on 429/500/502/503/504",
64
  "warmup": "Warm up backend before analysis",
65
  "max_wait": "Max total wait (sec) for retries",
66
  "warming": "Warming up backend…",
67
+ "analyzing": "Analyzing…",
68
+ "req_failed": "Request failed",
69
+ "try_again_info": "Backend might be waking up or rate-limited. Try Analyze again, or increase ‘Max total wait’."
70
  },
71
  "tr": {
72
  "title": "Diag Studio AI — Motor Sesi Teşhis",
73
+ "about": "Motor sesini (mp3/wav/ogg) yükleyin veya bir URL verin. Backend fal.ai ayrıntılı analizi çalıştırır, sonra GPT-4o ile Türkçe özetler.",
74
  "backend_url": "Backend URL",
75
  "lang": "Dil",
76
  "upload": "Ses dosyası yükle",
 
78
  "paste_url": "Ses URL’si (opsiyonel)",
79
  "adv": "Gelişmiş",
80
  "prompt": "Prompt geçersiz kıl (opsiyonel)",
81
+ "detailed": "Ayrıntılı analiz (fal)",
82
+ "use_data_uri": "data:URI olarak gönder",
83
  "model": "Yorum modeli (OpenAI)",
84
  "analyze": "Analiz Et",
85
  "reset": "Sıfırla",
 
99
  "warmup": "Analizden önce backend’i ısıt",
100
  "max_wait": "Retry için maksimum toplam bekleme (sn)",
101
  "warming": "Backend ısıtılıyor…",
102
+ "analyzing": "Analiz ediliyor…",
103
+ "req_failed": "İstek başarısız",
104
+ "try_again_info": "Backend uyanıyor veya sınırda olabilir. Analiz’i yeniden deneyin ya da ‘Maksimum bekleme’ değerini artırın."
105
  }
106
  }
107
 
108
+ # ---------------- Session State ----------------
109
  if "lang" not in st.session_state:
110
  st.session_state.lang = "tr"
111
  if "last_response" not in st.session_state:
112
  st.session_state.last_response = None
113
 
114
+ # ---------------- Header ----------------
115
  col1, col2 = st.columns([0.65, 0.35])
116
  with col1:
117
  st.title(L[st.session_state.lang]["title"])
 
125
  t = L[st.session_state.lang]
126
  st.caption(t["about"])
127
 
128
+ api_base = st.text_input(t["backend_url"], value=(API_BASE_ENV or "http://127.0.0.1:7860"))
129
+
130
+ # Health check (isteğe bağlı)
131
+ hc1, _ = st.columns([0.28, 0.72])
132
+ with hc1:
133
+ if st.button("🔄 Health"):
134
+ try:
135
+ r = requests.get(f"{api_base.rstrip('/')}/health", headers=HEADERS, timeout=10)
136
+ st.success(t["health_ok"] if r.ok else t["health_fail"])
137
+ except Exception:
138
+ st.error(t["health_fail"])
139
+
140
+ st.markdown("---")
141
+
142
+ # ---------------- Upload or URL ----------------
143
+ st.write(t["upload_hint"])
144
+ file = st.file_uploader(t["upload"], type=["mp3", "wav", "ogg"])
145
+ st.write(f"**{t['or']}**")
146
+ audio_url = st.text_input(t["paste_url"], value="", placeholder="https://...")
147
 
148
+ # ---------------- Advanced ----------------
149
  with st.expander(t["adv"], expanded=False):
150
+ detailed = st.checkbox(t["detailed"], value=True) # fal detailed
151
  use_data_uri = st.checkbox(t["use_data_uri"], value=True)
152
  openai_model = st.text_input(t["model"], value="gpt-4o")
153
  prompt_override = st.text_area(t["prompt"], value="", height=120)
 
156
  max_wait = st.slider(t["max_wait"], 10, 180, 60, step=10)
157
  st.caption(t["footer"])
158
 
159
+ # ---------------- Controls ----------------
160
+ c1, c2 = st.columns([0.25, 0.75])
 
 
 
 
 
161
  go = c1.button(f"▶ {t['analyze']}", type="primary")
162
  reset = c2.button(f"↩ {t['reset']}")
163
 
 
165
  st.session_state.last_response = None
166
  st.rerun()
167
 
168
+ # ---------------- HTTP session with retries ----------------
169
  def build_session(_auto_retry: bool) -> requests.Session:
170
  s = requests.Session()
171
  s.headers.update(HEADERS)
172
  if _auto_retry:
 
173
  retry = Retry(
174
+ total=6, connect=6, read=6,
 
 
175
  backoff_factor=0.8,
176
  status_forcelist=[429, 500, 502, 503, 504],
177
  allowed_methods=frozenset(["GET", "POST"]),
 
185
  def warm_up_backend(s: requests.Session, base: str, total_budget: int = 30):
186
  start = time.time()
187
  with st.spinner(t["warming"]):
 
188
  for path in ["/", "/health"]:
189
  try:
190
  r = s.get(f"{base.rstrip('/')}{path}", timeout=8)
 
196
  break
197
  time.sleep(2)
198
 
199
+ # ---------------- Core request ----------------
200
  def post_analyze(s: requests.Session, base: str, _file, _audio_url: str, timeout_sec: int = 120) -> dict:
201
  url = f"{base.rstrip('/')}/analyze"
202
  data = {
203
+ "lang": st.session_state.lang, # <<— UI dilini backend’e gönder
204
  "detailed_analysis": "true" if detailed else "false",
205
  "use_data_uri": "true" if use_data_uri else "false",
206
  "openai_model": openai_model or "gpt-4o",
 
215
  elif _audio_url.strip():
216
  data["audio_url"] = _audio_url.strip()
217
 
218
+ # Gönder ve hataları kontrollü ele al
219
+ try:
220
+ resp = s.post(url, data=data, files=files, timeout=timeout_sec)
221
+ # Backend çoğu hatayı 200 + {"ok": False, ...} ile döndürüyor; yine de .ok değilse yakalayalım
222
+ if not resp.ok:
223
+ # JSON gövdesi varsa gösterelim
224
+ try:
225
+ j = resp.json()
226
+ j.setdefault("ok", False)
227
+ j.setdefault("error", f"{t['req_failed']}: HTTP {resp.status_code}")
228
+ return j
229
+ except Exception:
230
+ return {"ok": False, "error": f"{t['req_failed']}: HTTP {resp.status_code}"}
231
+ # OK ise normal JSON
232
+ return resp.json()
233
+ except requests.RequestException as e:
234
+ return {"ok": False, "error": f"{t['req_failed']}: {e}"}
 
 
 
 
 
 
 
 
 
235
 
236
+ # ---------------- Run analysis ----------------
237
  if go:
238
  if not api_base:
239
  st.error(t["no_backend"])
 
247
  res = post_analyze(sess, api_base, file, audio_url, timeout_sec=180)
248
  st.session_state.last_response = res
249
 
250
+ # ---------------- Results ----------------
251
  res = st.session_state.last_response
252
  if res:
 
253
  if not res.get("ok", True) and res.get("error"):
254
  st.error(res["error"])
255
+ st.info(t["try_again_info"])
256
  else:
257
  st.subheader(t["result"])
258
 
259
+ # Driver-friendly commentary (TR/EN)
260
  comm = res.get("commentary") or ""
261
  if comm:
262
  st.markdown(f"**{t['commentary']}**")
263
  st.write(comm)
264
 
265
+ # Parsed JSON
266
  parsed = res.get("fal_parsed")
267
  if parsed:
268
  st.markdown(f"**{t['json']}**")
269
  st.json(parsed)
270
 
271
+ # Artifacts (download)
 
272
  dls = res.get("downloads") or {}
273
  col_a, col_b, col_c = st.columns(3)
274
 
275
  def fetch_bytes(url: str) -> bytes:
 
 
276
  full = url if url.startswith("http") else f"{api_base.rstrip('/')}{url}"
277
  r = requests.get(full, headers=HEADERS, timeout=60)
278
  r.raise_for_status()
 
280
 
281
  if dls.get("summary_txt"):
282
  try:
283
+ col_a.download_button(label=t["download_summary"], data=fetch_bytes(dls["summary_txt"]), file_name="summary.txt")
 
284
  except Exception:
285
  col_a.error("summary.txt ✖")
286
 
287
  if dls.get("fal_parsed_json"):
288
  try:
289
+ col_b.download_button(label=t["download_parsed"], data=fetch_bytes(dls["fal_parsed_json"]),
290
+ file_name="parsed.json", mime="application/json")
291
  except Exception:
292
  col_b.error("parsed.json ✖")
293
 
294
  if dls.get("fal_raw_txt"):
295
  try:
296
+ col_c.download_button(label=t["download_raw"], data=fetch_bytes(dls["fal_raw_txt"]), file_name="raw.txt")
 
297
  except Exception:
298
  col_c.error("raw.txt ✖")
299
 
300
+ st.caption("© 2025 Diag Studio AI — Streamlit frontend (language-aware, robust)")