renderfy commited on
Commit
2baa302
·
verified ·
1 Parent(s): dfda8d0

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +204 -225
app.py CHANGED
@@ -1,6 +1,7 @@
1
- # app.py — Diag Studio AI Frontend
2
- # Compatible with backend v1.8.0 (FAL open-world cue-rich GPT-4o hard-discriminator labeling)
3
- # TR/EN UI retries warm-up audio preview • artifact downloads
 
4
 
5
  import os
6
  import json
@@ -10,12 +11,12 @@ import streamlit as st
10
  from requests.adapters import HTTPAdapter
11
  from urllib3.util.retry import Retry
12
 
13
- # ---- container-safe defaults
14
  os.environ.setdefault("HOME", "/tmp")
15
  os.environ.setdefault("STREAMLIT_BROWSER_GATHER_USAGE_STATS", "false")
16
  os.environ.setdefault("STREAMLIT_SERVER_HEADLESS", "true")
17
 
18
- # ================= Theme =================
19
  try:
20
  st._config.set_option("theme.base", "dark")
21
  st._config.set_option("theme.primaryColor", "#ff7a1a")
@@ -25,163 +26,154 @@ try:
25
  except Exception:
26
  pass
27
 
28
- # ================= Env & API =================
29
  def _env(k: str) -> str:
30
  return (os.getenv(k) or "").strip().strip("'\"")
31
 
32
- # Allow reusing existing envs from your other apps
33
- API_BASE = (
34
- _env("CARDIAG_AI_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")
35
- ).rstrip("/") if (_env("CARDIAG_AI_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")) else ""
36
 
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
- # ================= i18n =================
41
- L = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  "en": {
43
- "title": "Diag Studio AI Engine Sound Diagnosis",
44
- "about": "Upload an engine sound (mp3/wav/ogg) or paste an audio URL. Backend extracts cue-rich acoustics with fal.ai, then GPT-4o applies HARD discriminators to avoid collapsing similar timbres.",
45
- "backend_url": "Backend URL",
46
- "lang": "Language",
47
  "upload": "Upload audio file",
48
  "or": "or",
49
- "paste_url": "Paste audio URL (optional)",
50
- "adv": "Advanced",
51
- "prompt": "Override FAL prompt (optional)",
52
- "use_data_uri": "Send as data:URI (safer for private backends)",
53
- "model": "OpenAI model (labeling/commentary)",
54
  "analyze": "Analyze",
55
  "reset": "Reset",
56
  "result": "Result",
57
- "commentary": "Commentary (driver-friendly)",
58
- "json": "Parsed JSON",
59
- "backend_artifacts": "Backend artifacts",
60
- "download_summary": "Download summary.txt",
61
- "download_parsed": "Download parsed.json",
62
- "download_raw": "Download raw.txt",
63
- "no_backend": "Enter a backend URL (e.g., http://127.0.0.1:7860)",
64
- "health_ok": "Backend is reachable.",
65
- "health_fail": "Backend not reachable.",
66
- "upload_hint": "Max ~200 MB per Streamlit; use mp3/wav/ogg.",
67
- "footer": "Tip: You can paste a public audio URL instead of uploading.",
68
- "auto_retry": "Auto-retry on 429/500/502/503/504",
69
- "warmup": "Warm up backend before analysis",
70
- "max_wait": "Max total wait (sec) for retries",
71
- "warming": "Warming up backend…",
72
  "analyzing": "Analyzing…",
73
- "lang_help": "Commentary language",
74
- "ok_false": "Backend returned a controlled error.",
75
- "try_again": "Service may be waking or rate-limited. Try again or increase ‘Max total wait’.",
76
- "preview": "Audio preview",
77
- "raw_text": "Raw model text",
78
- "prompt_used": "Prompt used (first 240 chars)",
79
- "api_used": "Backend audio URL (truncated)",
80
- "hard_note": "HARD discriminators active: half-crank vs full-crank, load vs RPM, cold-start confinement, tonal vs broadband, deep vs light."
 
 
 
 
 
 
81
  },
82
- "tr": {
83
- "title": "Diag Studio AI — Motor Sesi Teşhis",
84
- "about": "Motor sesini (mp3/wav/ogg) yükleyin veya bir ses URL’si girin. Backend fal.ai ile ipucu-zengin akustik özet çıkarır; GPT-4o benzer tınıları karıştırmamak için SERT ayraçlar uygular.",
85
- "backend_url": "Backend URL",
86
- "lang": "Dil",
87
- "upload": "Ses dosyası yükle",
88
- "or": "veya",
89
- "paste_url": "Ses URL’si (opsiyonel)",
90
- "adv": "Gelişmiş",
91
- "prompt": "FAL prompt’u geçersiz kıl (opsiyonel)",
92
- "use_data_uri": "data:URI olarak gönder (özel backend için güvenli)",
93
- "model": "OpenAI modeli (label/yorum)",
94
- "analyze": "Analiz Et",
95
- "reset": "Sıfırla",
96
- "result": "Sonuç",
97
- "commentary": "Yorum (sürücü dostu)",
98
- "json": "Ayrıştırılmış JSON",
99
- "backend_artifacts": "Backend çıktıları",
100
- "download_summary": "summary.txt indir",
101
- "download_parsed": "parsed.json indir",
102
- "download_raw": "raw.txt indir",
103
- "no_backend": "Bir backend URL girin (örn: http://127.0.0.1:7860)",
104
- "health_ok": "Backend erişilebilir.",
105
- "health_fail": "Backend’e erişilemedi.",
106
- "upload_hint": "Streamlit yükleme sınırı ~200 MB; mp3/wav/ogg kullanın.",
107
- "footer": "İpucu: Dosya yüklemek istemezseniz herkese açık bir ses URL’si girebilirsiniz.",
108
- "auto_retry": "429/500/502/503/504 için otomatik yeniden dene",
109
- "warmup": "Analizden önce backend’i ısıt",
110
- "max_wait": "Retry için azami bekleme (sn)",
111
- "warming": "Backend ısıtılıyor…",
112
- "analyzing": "Analiz ediliyor…",
113
- "lang_help": "Yorum dilini seçin",
114
- "ok_false": "Backend kontrollü bir hata döndürdü.",
115
- "try_again": "Servis uyanıyor veya sınırlandı. Tekrar deneyin veya ‘Azami bekleme’ süresini artırın.",
116
- "preview": "Ses önizleme",
117
- "raw_text": "Ham model metni",
118
- "prompt_used": "Kullanılan prompt (ilk 240 karakter)",
119
- "api_used": "Backend ses URL’si (kısaltılmış)",
120
- "hard_note": "SERT ayraçlar aktif: yarım-krank vs tam-krank, yük vs RPM, soğuk-başlangıç sınırı, tonal vs geniş bant, derin vs hafif."
121
- }
122
  }
123
 
124
- # ================= Session State =================
125
  if "lang" not in st.session_state:
126
  st.session_state.lang = "tr"
127
- if "last_response" not in st.session_state:
128
- st.session_state.last_response = None
129
-
130
- # ================= Header =================
131
- col1, col2 = st.columns([0.65, 0.35])
132
- with col1:
133
- st.title(L[st.session_state.lang]["title"])
134
- with col2:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  st.session_state.lang = st.selectbox(
136
- L[st.session_state.lang]["lang"],
137
- ["tr", "en"],
138
  index=(0 if st.session_state.lang == "tr" else 1),
139
- key="lang_selector",
140
  )
141
-
142
- t = L[st.session_state.lang]
143
- st.caption(t["about"])
144
- st.info(t["hard_note"])
145
-
146
- backend_default = API_BASE or "http://127.0.0.1:7860"
147
- api_base = st.text_input(t["backend_url"], value=backend_default)
148
-
149
- # ================= Advanced options =================
150
- with st.expander(t["adv"], expanded=False):
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=140, help="Leave empty to use backend’s cue-rich default")
154
- auto_retry = st.checkbox(t["auto_retry"], value=True)
155
- warmup = st.checkbox(t["warmup"], value=True)
156
- max_wait = st.slider(t["max_wait"], 10, 240, 60, step=10)
157
- st.caption(t["footer"])
158
 
159
  st.markdown("---")
160
 
161
- # ================= Upload / URL =================
162
- st.write(t["upload_hint"])
163
- file = st.file_uploader(t["upload"], type=["mp3", "wav", "ogg"])
164
- if file is not None:
165
- try:
166
- st.markdown(f"**{t['preview']}**")
167
- st.audio(file, format=file.type or "audio/mpeg")
168
- except Exception:
169
- pass
170
-
171
- st.write(f"**{t['or']}**")
172
- audio_url = st.text_input(t["paste_url"], value="", placeholder="https://...")
173
 
174
- # ================= Controls =================
175
- c1, c2, c3 = st.columns([0.2, 0.2, 0.6])
176
- go = c1.button(f"▶ {t['analyze']}", type="primary")
177
- reset = c2.button(f"↩ {t['reset']}")
178
- health_btn = c3.button("🔄 Health")
179
 
180
- # ================= HTTP session with retries =================
181
- def build_session(_auto_retry: bool) -> requests.Session:
182
  s = requests.Session()
183
  s.headers.update(HEADERS)
184
- if _auto_retry:
185
  retry = Retry(
186
  total=6, connect=6, read=6,
187
  backoff_factor=0.8,
@@ -194,17 +186,17 @@ def build_session(_auto_retry: bool) -> requests.Session:
194
  s.mount("https://", adapter)
195
  return s
196
 
197
- def warm_up_backend(s: requests.Session, base: str, total_budget: int = 30):
198
  start = time.time()
199
- with st.spinner(t["warming"]):
200
  for path in ["/", "/health"]:
201
  try:
202
- r = s.get(f"{base.rstrip('/')}{path}", timeout=8)
203
  if r.ok:
204
  continue
205
  except Exception:
206
  pass
207
- if time.time() - start > total_budget:
208
  break
209
  time.sleep(2)
210
 
@@ -214,131 +206,118 @@ def fetch_bytes(sess: requests.Session, base: str, url: str) -> bytes:
214
  r.raise_for_status()
215
  return r.content
216
 
217
- def post_analyze(sess: requests.Session, base: str, _file, _audio_url: str, lang: str) -> dict:
218
- url = f"{base.rstrip('/')}/analyze"
219
  data = {
220
- "use_data_uri": "true" if use_data_uri else "false",
221
- "openai_model": openai_model or "gpt-4o",
222
- "lang": lang, # "tr" or "en"
223
- # backend pins detailed_analysis=True
224
  }
225
- if prompt_override.strip():
226
- data["prompt"] = prompt_override.strip()
 
227
 
228
  files = None
229
  if _file is not None:
230
  mime = getattr(_file, "type", None) or "application/octet-stream"
231
  files = {"file": (_file.name, _file, mime)}
232
- elif _audio_url.strip():
233
- data["audio_url"] = _audio_url.strip()
234
 
235
  if not files and "audio_url" not in data:
236
- return {"ok": False, "error": "No file or URL provided."}
237
 
238
- deadline = time.time() + max_wait
239
- attempts = 0
240
- last_err = None
241
  while True:
242
- attempts += 1
243
  try:
244
  resp = sess.post(url, data=data, files=files, timeout=180)
245
  resp.raise_for_status()
246
  return resp.json()
247
  except Exception as e:
248
- last_err = e
249
- if not auto_retry or time.time() > deadline:
250
- return {"ok": False, "error": f"All attempts failed. Last error: {e}", "attempts": attempts, "api_base": base}
251
  time.sleep(1.2)
252
 
253
- # ================= Actions =================
254
- sess = build_session(auto_retry)
255
 
256
- if health_btn:
257
  try:
258
- r = sess.get(f"{api_base.rstrip('/')}/health", timeout=10)
259
- st.success(t["health_ok"] if r.ok else t["health_fail"])
260
- if r.ok and r.headers.get("content-type","").startswith("application/json"):
261
- payload = r.json()
262
- st.code(json.dumps(payload, ensure_ascii=False, indent=2), language="json")
 
 
263
  except Exception:
264
- st.error(t["health_fail"])
265
 
266
  if reset:
267
- st.session_state.last_response = None
268
  st.rerun()
269
 
270
  if go:
271
- if not api_base:
272
- st.error(t["no_backend"])
273
  elif not file and not audio_url.strip():
274
- st.error(t["upload"])
275
  else:
276
- if warmup:
277
- warm_up_backend(sess, api_base, total_budget=min(max_wait, 40))
278
- with st.spinner(t["analyzing"]):
279
- res = post_analyze(sess, api_base, file, audio_url, st.session_state.lang)
280
- st.session_state.last_response = res
281
-
282
- # ================= Results =================
283
- res = st.session_state.last_response
284
  if res:
285
  if not res.get("ok", True) and res.get("error"):
286
- st.error(t["ok_false"])
287
  st.write(res.get("error"))
288
- st.info(t["try_again"])
289
- if res.get("fal_debug"):
290
  st.expander("Debug").code(json.dumps(res["fal_debug"], ensure_ascii=False, indent=2), language="json")
291
  else:
292
- st.subheader(t["result"])
293
 
294
- # Commentary
295
- comm = res.get("commentary") or ""
296
  if comm:
297
- st.markdown(f"**{t['commentary']}**")
298
  st.write(comm)
299
 
300
- # Parsed JSON
301
  parsed = res.get("fal_parsed")
302
  if parsed:
303
- st.markdown(f"**{t['json']}**")
304
  st.json(parsed)
305
 
306
- # Prompt used + fal audio url
307
- extras_col1, extras_col2 = st.columns(2)
308
- if res.get("prompt_used"):
309
- with extras_col1:
310
- st.caption(t["prompt_used"])
311
- st.code(res["prompt_used"], language="text")
312
- if res.get("fal_audio_url"):
313
- with extras_col2:
314
- st.caption(t["api_used"])
315
- st.code(res["fal_audio_url"], language="text")
316
-
317
- # Backend artifacts
318
- st.markdown(f"**{t['backend_artifacts']}**")
319
  dls = res.get("downloads") or {}
320
- col_a, col_b, col_c = st.columns(3)
321
-
322
- if dls.get("summary_txt"):
323
- try:
324
- data = fetch_bytes(sess, api_base, dls["summary_txt"])
325
- col_a.download_button(label=t["download_summary"], data=data, file_name="summary.txt")
326
- except Exception:
327
- col_a.error("summary.txt")
328
-
329
- if dls.get("fal_parsed_json"):
330
- try:
331
- data = fetch_bytes(sess, api_base, dls["fal_parsed_json"])
332
- col_b.download_button(label=t["download_parsed"], data=data, file_name="parsed.json", mime="application/json")
333
- except Exception:
334
- col_b.error("parsed.json ")
335
-
336
- if dls.get("fal_raw_txt"):
337
- try:
338
- data = fetch_bytes(sess, api_base, dls["fal_raw_txt"])
339
- col_c.download_button(label=t["download_raw"], data=data, file_name="raw.txt")
340
- except Exception:
341
- col_c.error("raw.txt ")
342
-
343
- # ===== Footer =====
344
- st.caption("© 2025 Diag Studio AI — Streamlit frontend (TR/EN, retries, warm-up, cue-rich + hard-discriminator flow)")
 
 
 
 
1
+ # app.py — Diag Studio (Customer UI)
2
+ # “AI Destekli Araç Motor Ses Analizi” sade, ticari arayüz.
3
+ # Not: Teknik terimler, model/sağlayıcı adları, loglar UI’da gösterilmez.
4
+ # İdari/teknik ayarlar yalnızca DEV_MODE=1 iken “Yönetici” bölümünde görünür.
5
 
6
  import os
7
  import json
 
11
  from requests.adapters import HTTPAdapter
12
  from urllib3.util.retry import Retry
13
 
14
+ # ========== güvenli varsayılanlar ==========
15
  os.environ.setdefault("HOME", "/tmp")
16
  os.environ.setdefault("STREAMLIT_BROWSER_GATHER_USAGE_STATS", "false")
17
  os.environ.setdefault("STREAMLIT_SERVER_HEADLESS", "true")
18
 
19
+ # ========== tema ==========
20
  try:
21
  st._config.set_option("theme.base", "dark")
22
  st._config.set_option("theme.primaryColor", "#ff7a1a")
 
26
  except Exception:
27
  pass
28
 
29
+ # ========== ortam & yetki ==========
30
  def _env(k: str) -> str:
31
  return (os.getenv(k) or "").strip().strip("'\"")
32
 
33
+ DEV_MODE = (_env("DEV_MODE") in {"1", "true", "yes", "on"})
 
 
 
34
 
35
+ # Tek bir sabit backend anahtarı (kullanıcıya gösterilmez)
36
+ API_BASE = (_env("CARDIAG_AI_API") or _env("APP_API") or "http://127.0.0.1:7860").rstrip("/")
37
 
38
+ TOKEN = _env("CARDIAG_AI_TOKEN") or _env("APP_TOKEN")
39
+ HEADERS = {"Authorization": f"Bearer {TOKEN}"} if TOKEN else {}
40
+
41
+ # ========== çok dilli metin ==========
42
+ LANGS = {
43
+ "tr": {
44
+ "title": "AI Destekli Araç Motor Ses Analizi",
45
+ "about": "Motor ses kaydını yükleyin veya bağlantı girin. Sistem sesi analiz eder, olası arızaları ve önerileri özetler.",
46
+ "upload": "Ses dosyası yükle",
47
+ "or": "veya",
48
+ "paste_url": "Ses bağlantısı (opsiyonel)",
49
+ "analyze": "Analiz Et",
50
+ "reset": "Sıfırla",
51
+ "result": "Sonuç",
52
+ "commentary": "Özet ve Öneriler",
53
+ "json": "Detaylı Bulgular",
54
+ "artifacts": "Çıktılar",
55
+ "download_summary": "Özet indir",
56
+ "download_parsed": "Detay indir (JSON)",
57
+ "download_raw": "Ham metin indir",
58
+ "upload_hint": "mp3/wav/ogg biçimi önerilir. Dosya yerine herkese açık bir bağlantı da kullanabilirsiniz.",
59
+ "warming": "Hazırlanıyor…",
60
+ "analyzing": "Analiz ediliyor…",
61
+ "health_ok": "Sistem hazır.",
62
+ "health_fail": "Sisteme erişilemedi.",
63
+ "need_backend": "Sistem adresi yapılandırılmadı.",
64
+ "need_input": "Lütfen bir dosya yükleyin veya bağlantı girin.",
65
+ "error_generic": "İşlem başarısız. Lütfen tekrar deneyin.",
66
+ "try_again": "Hizmet yoğun olabilir. Biraz sonra tekrar deneyin.",
67
+ "admin": "Yönetici",
68
+ "admin_desc": "Bu bölüm yalnızca yetkili kullanım içindir.",
69
+ "backend_url": "Sistem adresi",
70
+ "use_data_uri": "Dosyayı gizli yükle",
71
+ "warmup": "İşlem öncesi ısıt",
72
+ "auto_retry": "Otomatik yeniden dene",
73
+ "max_wait": "Azami bekleme (sn)",
74
+ "show_debug": "Hata ayıklama bilgilerini göster",
75
+ },
76
  "en": {
77
+ "title": "AI-Powered Engine Sound Analysis",
78
+ "about": "Upload an engine sound or provide a public URL. The system analyzes the audio and summarizes likely issues and next steps.",
 
 
79
  "upload": "Upload audio file",
80
  "or": "or",
81
+ "paste_url": "Audio URL (optional)",
 
 
 
 
82
  "analyze": "Analyze",
83
  "reset": "Reset",
84
  "result": "Result",
85
+ "commentary": "Summary & Recommendations",
86
+ "json": "Detailed Findings",
87
+ "artifacts": "Downloads",
88
+ "download_summary": "Download summary",
89
+ "download_parsed": "Download details (JSON)",
90
+ "download_raw": "Download raw text",
91
+ "upload_hint": "Use mp3/wav/ogg. You may also paste a public URL instead of uploading.",
92
+ "warming": "Preparing…",
 
 
 
 
 
 
 
93
  "analyzing": "Analyzing…",
94
+ "health_ok": "Service is ready.",
95
+ "health_fail": "Cannot reach the service.",
96
+ "need_backend": "Service URL is not configured.",
97
+ "need_input": "Please upload a file or enter a URL.",
98
+ "error_generic": "Request failed. Please try again.",
99
+ "try_again": "Service may be busy. Try again shortly.",
100
+ "admin": "Admin",
101
+ "admin_desc": "This section is for authorized use only.",
102
+ "backend_url": "Service URL",
103
+ "use_data_uri": "Private upload",
104
+ "warmup": "Warm up before analyze",
105
+ "auto_retry": "Auto-retry",
106
+ "max_wait": "Max wait (sec)",
107
+ "show_debug": "Show debug info",
108
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  }
110
 
111
+ # ========== oturum durumu ==========
112
  if "lang" not in st.session_state:
113
  st.session_state.lang = "tr"
114
+ if "res" not in st.session_state:
115
+ st.session_state.res = None
116
+ if "api_base" not in st.session_state:
117
+ st.session_state.api_base = API_BASE
118
+ if "use_data_uri" not in st.session_state:
119
+ st.session_state.use_data_uri = True
120
+ if "warmup" not in st.session_state:
121
+ st.session_state.warmup = True
122
+ if "auto_retry" not in st.session_state:
123
+ st.session_state.auto_retry = True
124
+ if "max_wait" not in st.session_state:
125
+ st.session_state.max_wait = 60
126
+ if "show_debug" not in st.session_state:
127
+ st.session_state.show_debug = False
128
+ if "prompt_override" not in st.session_state:
129
+ st.session_state.prompt_override = "" # UI’da gösterilmez; sadece admin
130
+
131
+ T = LANGS[st.session_state.lang]
132
+
133
+ # ========== başlık ==========
134
+ c1, c2 = st.columns([0.7, 0.3])
135
+ with c1:
136
+ st.title(T["title"])
137
+ with c2:
138
  st.session_state.lang = st.selectbox(
139
+ "Language / Dil", ["tr", "en"],
 
140
  index=(0 if st.session_state.lang == "tr" else 1),
 
141
  )
142
+ T = LANGS[st.session_state.lang]
143
+ st.caption(T["about"])
144
+
145
+ # ========== admin (yalnızca DEV_MODE) ==========
146
+ if DEV_MODE:
147
+ with st.expander(f"🛠 {T['admin']}", expanded=False):
148
+ st.write(T["admin_desc"])
149
+ st.session_state.api_base = st.text_input(T["backend_url"], value=st.session_state.api_base)
150
+ st.session_state.use_data_uri = st.checkbox(T["use_data_uri"], value=st.session_state.use_data_uri)
151
+ st.session_state.warmup = st.checkbox(T["warmup"], value=st.session_state.warmup)
152
+ st.session_state.auto_retry = st.checkbox(T["auto_retry"], value=st.session_state.auto_retry)
153
+ st.session_state.max_wait = st.slider(T["max_wait"], 10, 240, st.session_state.max_wait, step=10)
154
+ st.session_state.show_debug = st.checkbox(T["show_debug"], value=st.session_state.show_debug)
155
+ # İsteğe bağlı gelişmiş prompt (son kullanıcıya görünmez)
156
+ st.session_state.prompt_override = st.text_area("Advanced prompt (optional)", value=st.session_state.prompt_override, height=120)
 
 
157
 
158
  st.markdown("---")
159
 
160
+ # ========== yükleme / URL ==========
161
+ st.write(T["upload_hint"])
162
+ file = st.file_uploader(T["upload"], type=["mp3", "wav", "ogg"])
163
+ st.write(f"**{T['or']}**")
164
+ audio_url = st.text_input(T["paste_url"], value="", placeholder="https://...")
 
 
 
 
 
 
 
165
 
166
+ # ========== butonlar ==========
167
+ b1, b2, b3 = st.columns([0.22, 0.22, 0.56])
168
+ go = b1.button(f"▶ {T['analyze']}", type="primary")
169
+ reset = b2.button(f"↩ {T['reset']}")
170
+ health = b3.button("🔄 Health")
171
 
172
+ # ========== HTTP oturumu (retry) ==========
173
+ def build_session(auto_retry: bool) -> requests.Session:
174
  s = requests.Session()
175
  s.headers.update(HEADERS)
176
+ if auto_retry:
177
  retry = Retry(
178
  total=6, connect=6, read=6,
179
  backoff_factor=0.8,
 
186
  s.mount("https://", adapter)
187
  return s
188
 
189
+ def warm_up(sess: requests.Session, base: str, budget: int = 30):
190
  start = time.time()
191
+ with st.spinner(T["warming"]):
192
  for path in ["/", "/health"]:
193
  try:
194
+ r = sess.get(f"{base.rstrip('/')}{path}", timeout=8)
195
  if r.ok:
196
  continue
197
  except Exception:
198
  pass
199
+ if time.time() - start > budget:
200
  break
201
  time.sleep(2)
202
 
 
206
  r.raise_for_status()
207
  return r.content
208
 
209
+ def post_analyze(sess: requests.Session, base: str, _file, _url: str) -> dict:
 
210
  data = {
211
+ "use_data_uri": "true" if st.session_state.use_data_uri else "false",
212
+ "lang": st.session_state.lang,
 
 
213
  }
214
+ # prompt_override yalnızca admin kullanımında backend’e gönderilir
215
+ if DEV_MODE and st.session_state.prompt_override.strip():
216
+ data["prompt"] = st.session_state.prompt_override.strip()
217
 
218
  files = None
219
  if _file is not None:
220
  mime = getattr(_file, "type", None) or "application/octet-stream"
221
  files = {"file": (_file.name, _file, mime)}
222
+ elif _url.strip():
223
+ data["audio_url"] = _url.strip()
224
 
225
  if not files and "audio_url" not in data:
226
+ return {"ok": False, "error": T["need_input"]}
227
 
228
+ url = f"{base.rstrip('/')}/analyze"
229
+ deadline = time.time() + st.session_state.max_wait
 
230
  while True:
 
231
  try:
232
  resp = sess.post(url, data=data, files=files, timeout=180)
233
  resp.raise_for_status()
234
  return resp.json()
235
  except Exception as e:
236
+ if not st.session_state.auto_retry or time.time() > deadline:
237
+ return {"ok": False, "error": f"{T['error_generic']} ({e})"}
 
238
  time.sleep(1.2)
239
 
240
+ # ========== işlemler ==========
241
+ sess = build_session(st.session_state.auto_retry)
242
 
243
+ if health:
244
  try:
245
+ r = sess.get(f"{st.session_state.api_base.rstrip('/')}/health", timeout=10)
246
+ if r.ok:
247
+ st.success(T["health_ok"])
248
+ if DEV_MODE:
249
+ st.code(json.dumps(r.json(), ensure_ascii=False, indent=2), language="json")
250
+ else:
251
+ st.error(T["health_fail"])
252
  except Exception:
253
+ st.error(T["health_fail"])
254
 
255
  if reset:
256
+ st.session_state.res = None
257
  st.rerun()
258
 
259
  if go:
260
+ if not st.session_state.api_base:
261
+ st.error(T["need_backend"])
262
  elif not file and not audio_url.strip():
263
+ st.error(T["need_input"])
264
  else:
265
+ if st.session_state.warmup:
266
+ warm_up(sess, st.session_state.api_base, budget=min(st.session_state.max_wait, 40))
267
+ with st.spinner(T["analyzing"]):
268
+ st.session_state.res = post_analyze(sess, st.session_state.api_base, file, audio_url)
269
+
270
+ # ========== sonuçlar ==========
271
+ res = st.session_state.res
 
272
  if res:
273
  if not res.get("ok", True) and res.get("error"):
274
+ st.error(T["error_generic"])
275
  st.write(res.get("error"))
276
+ st.info(T["try_again"])
277
+ if DEV_MODE and st.session_state.show_debug and res.get("fal_debug"):
278
  st.expander("Debug").code(json.dumps(res["fal_debug"], ensure_ascii=False, indent=2), language="json")
279
  else:
280
+ st.subheader(T["result"])
281
 
282
+ # Son kullanıcıya uygun kısa özet
283
+ comm = (res.get("commentary") or "").strip()
284
  if comm:
285
+ st.markdown(f"**{T['commentary']}**")
286
  st.write(comm)
287
 
288
+ # Detaylı bulgular (JSON) — teknik isimler içerebilir; ama sağlayıcı/altyapı adı içermez
289
  parsed = res.get("fal_parsed")
290
  if parsed:
291
+ st.markdown(f"**{T['json']}**")
292
  st.json(parsed)
293
 
294
+ # İndirilebilir çıktılar (dosya adlarını sade tutuyoruz)
 
 
 
 
 
 
 
 
 
 
 
 
295
  dls = res.get("downloads") or {}
296
+ if any(dls.get(k) for k in ("summary_txt", "fal_parsed_json", "fal_raw_txt")):
297
+ st.markdown(f"**{T['artifacts']}**")
298
+ ca, cb, cc = st.columns(3)
299
+
300
+ if dls.get("summary_txt"):
301
+ try:
302
+ data = fetch_bytes(sess, st.session_state.api_base, dls["summary_txt"])
303
+ ca.download_button(label=T["download_summary"], data=data, file_name="ozet.txt")
304
+ except Exception:
305
+ ca.error("Özet indirilemedi")
306
+
307
+ if dls.get("fal_parsed_json"):
308
+ try:
309
+ data = fetch_bytes(sess, st.session_state.api_base, dls["fal_parsed_json"])
310
+ cb.download_button(label=T["download_parsed"], data=data, file_name="detay.json", mime="application/json")
311
+ except Exception:
312
+ cb.error("Detay indirilemedi")
313
+
314
+ if dls.get("fal_raw_txt"):
315
+ # “Ham metin” geliştirici içindir; kullanıcı isterse indirebilir.
316
+ try:
317
+ data = fetch_bytes(sess, st.session_state.api_base, dls["fal_raw_txt"])
318
+ cc.download_button(label=T["download_raw"], data=data, file_name="ham.txt")
319
+ except Exception:
320
+ cc.error("Ham metin indirilemedi")
321
+
322
+ # ========== alt bilgi ==========
323
+ st.caption("© 2025 Diag Studio — Araç Motor Ses Analizi")