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

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +133 -93
app.py CHANGED
@@ -1,9 +1,11 @@
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
8
  import time
9
  import requests
@@ -11,12 +13,12 @@ import streamlit as st
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,19 +28,16 @@ try:
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",
@@ -48,6 +47,7 @@ LANGS = {
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",
@@ -55,7 +55,7 @@ LANGS = {
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.",
@@ -63,15 +63,20 @@ LANGS = {
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",
@@ -81,6 +86,7 @@ LANGS = {
81
  "paste_url": "Audio URL (optional)",
82
  "analyze": "Analyze",
83
  "reset": "Reset",
 
84
  "result": "Result",
85
  "commentary": "Summary & Recommendations",
86
  "json": "Detailed Findings",
@@ -88,7 +94,7 @@ LANGS = {
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.",
@@ -97,44 +103,40 @@ LANGS = {
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),
@@ -142,7 +144,7 @@ with c2:
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"])
@@ -152,24 +154,31 @@ if DEV_MODE:
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)
@@ -186,39 +195,42 @@ def build_session(auto_retry: bool) -> requests.Session:
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
 
203
  def fetch_bytes(sess: requests.Session, base: str, url: str) -> bytes:
204
  full = url if url.startswith("http") else f"{base.rstrip('/')}{url}"
205
- r = sess.get(full, timeout=60)
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
 
@@ -227,28 +239,30 @@ def post_analyze(sess: requests.Session, base: str, _file, _url: str) -> dict:
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
 
@@ -256,18 +270,45 @@ 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"):
@@ -279,45 +320,44 @@ if res:
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")
 
1
  # app.py — Diag Studio (Customer UI)
2
+ # AI Destekli Araç Motor Ses Analizi — sade ticari arayüz
3
+ # İstekler: Ses oynatıcı, ilerleme çubuğu, kullanıcı dostu akış.
4
+ # Teknik sağlayıcı/kitaplık adları ve loglar UI’da gösterilmez.
5
+ # Yönetici ayarları yalnızca DEV_MODE=1 iken görünür.
6
 
7
  import os
8
+ import io
9
  import json
10
  import time
11
  import requests
 
13
  from requests.adapters import HTTPAdapter
14
  from urllib3.util.retry import Retry
15
 
16
+ # ---------------- Safe defaults ----------------
17
  os.environ.setdefault("HOME", "/tmp")
18
  os.environ.setdefault("STREAMLIT_BROWSER_GATHER_USAGE_STATS", "false")
19
  os.environ.setdefault("STREAMLIT_SERVER_HEADLESS", "true")
20
 
21
+ # ---------------- Theme ----------------
22
  try:
23
  st._config.set_option("theme.base", "dark")
24
  st._config.set_option("theme.primaryColor", "#ff7a1a")
 
28
  except Exception:
29
  pass
30
 
31
+ # ---------------- Env helpers ----------------
32
  def _env(k: str) -> str:
33
  return (os.getenv(k) or "").strip().strip("'\"")
34
 
35
  DEV_MODE = (_env("DEV_MODE") in {"1", "true", "yes", "on"})
36
+ API_BASE_DEFAULT = (_env("CARDIAG_AI_API") or _env("APP_API") or "http://127.0.0.1:7860").rstrip("/")
 
 
 
37
  TOKEN = _env("CARDIAG_AI_TOKEN") or _env("APP_TOKEN")
38
  HEADERS = {"Authorization": f"Bearer {TOKEN}"} if TOKEN else {}
39
 
40
+ # ---------------- i18n ----------------
41
  LANGS = {
42
  "tr": {
43
  "title": "AI Destekli Araç Motor Ses Analizi",
 
47
  "paste_url": "Ses bağlantısı (opsiyonel)",
48
  "analyze": "Analiz Et",
49
  "reset": "Sıfırla",
50
+ "preview": "Önizleme",
51
  "result": "Sonuç",
52
  "commentary": "Özet ve Öneriler",
53
  "json": "Detaylı Bulgular",
 
55
  "download_summary": "Özet indir",
56
  "download_parsed": "Detay indir (JSON)",
57
  "download_raw": "Ham metin indir",
58
+ "upload_hint": "mp3 / wav / ogg önerilir. İsterseniz herkese açık bir bağlantı da girebilirsiniz.",
59
  "warming": "Hazırlanıyor…",
60
  "analyzing": "Analiz ediliyor…",
61
  "health_ok": "Sistem hazır.",
 
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": "Yoğunluk olabilir. Biraz sonra tekrar deneyin.",
67
+ "timeline_title": "İşlem Durumu",
68
+ "step_prepare": "Hazırlık",
69
+ "step_upload": "Yükleme",
70
+ "step_process": "Analiz",
71
+ "step_summarize": "Özet oluşturma",
72
  "admin": "Yönetici",
73
  "admin_desc": "Bu bölüm yalnızca yetkili kullanım içindir.",
74
  "backend_url": "Sistem adresi",
75
+ "use_data_uri": "Gizli yükleme",
76
+ "warmup": "Ön ısıtma",
77
  "auto_retry": "Otomatik yeniden dene",
78
  "max_wait": "Azami bekleme (sn)",
79
+ "show_debug": "Hata ayıklama bilgileri",
80
  },
81
  "en": {
82
  "title": "AI-Powered Engine Sound Analysis",
 
86
  "paste_url": "Audio URL (optional)",
87
  "analyze": "Analyze",
88
  "reset": "Reset",
89
+ "preview": "Preview",
90
  "result": "Result",
91
  "commentary": "Summary & Recommendations",
92
  "json": "Detailed Findings",
 
94
  "download_summary": "Download summary",
95
  "download_parsed": "Download details (JSON)",
96
  "download_raw": "Download raw text",
97
+ "upload_hint": "mp3 / wav / ogg recommended. You may paste a public URL instead of uploading.",
98
  "warming": "Preparing…",
99
  "analyzing": "Analyzing…",
100
  "health_ok": "Service is ready.",
 
103
  "need_input": "Please upload a file or enter a URL.",
104
  "error_generic": "Request failed. Please try again.",
105
  "try_again": "Service may be busy. Try again shortly.",
106
+ "timeline_title": "Progress",
107
+ "step_prepare": "Preparing",
108
+ "step_upload": "Uploading",
109
+ "step_process": "Processing",
110
+ "step_summarize": "Summarizing",
111
  "admin": "Admin",
112
  "admin_desc": "This section is for authorized use only.",
113
  "backend_url": "Service URL",
114
  "use_data_uri": "Private upload",
115
+ "warmup": "Warm up",
116
  "auto_retry": "Auto-retry",
117
  "max_wait": "Max wait (sec)",
118
  "show_debug": "Show debug info",
119
  },
120
  }
121
 
122
+ # ---------------- Session ----------------
123
+ if "lang" not in st.session_state: st.session_state.lang = "tr"
124
+ if "api_base" not in st.session_state: st.session_state.api_base = API_BASE_DEFAULT
125
+ if "use_data_uri" not in st.session_state: st.session_state.use_data_uri = True
126
+ if "warmup" not in st.session_state: st.session_state.warmup = True
127
+ if "auto_retry" not in st.session_state: st.session_state.auto_retry = True
128
+ if "max_wait" not in st.session_state: st.session_state.max_wait = 60
129
+ if "show_debug" not in st.session_state: st.session_state.show_debug = False
130
+ if "prompt_override" not in st.session_state: st.session_state.prompt_override = ""
131
+ if "res" not in st.session_state: st.session_state.res = None
 
 
 
 
 
 
 
 
 
132
 
133
  T = LANGS[st.session_state.lang]
134
 
135
+ # ---------------- Header ----------------
136
+ left, right = st.columns([0.7, 0.3])
137
+ with left:
138
  st.title(T["title"])
139
+ with right:
140
  st.session_state.lang = st.selectbox(
141
  "Language / Dil", ["tr", "en"],
142
  index=(0 if st.session_state.lang == "tr" else 1),
 
144
  T = LANGS[st.session_state.lang]
145
  st.caption(T["about"])
146
 
147
+ # ---------------- Admin (DEV only) ----------------
148
  if DEV_MODE:
149
  with st.expander(f"🛠 {T['admin']}", expanded=False):
150
  st.write(T["admin_desc"])
 
154
  st.session_state.auto_retry = st.checkbox(T["auto_retry"], value=st.session_state.auto_retry)
155
  st.session_state.max_wait = st.slider(T["max_wait"], 10, 240, st.session_state.max_wait, step=10)
156
  st.session_state.show_debug = st.checkbox(T["show_debug"], value=st.session_state.show_debug)
 
157
  st.session_state.prompt_override = st.text_area("Advanced prompt (optional)", value=st.session_state.prompt_override, height=120)
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
  st.write(f"**{T['or']}**")
165
  audio_url = st.text_input(T["paste_url"], value="", placeholder="https://...")
166
 
167
+ # Audio preview block (player)
168
+ if file is not None:
169
+ st.markdown(f"**{T['preview']}**")
170
+ st.audio(file, format=file.type or "audio/mpeg")
171
+ elif audio_url.strip():
172
+ st.markdown(f"**{T['preview']}**")
173
+ st.audio(audio_url)
174
+
175
+ # ---------------- Controls ----------------
176
+ c1, c2, c3 = st.columns([0.22, 0.22, 0.56])
177
+ go = c1.button(f"▶ {T['analyze']}", type="primary")
178
+ reset = c2.button(f"↩ {T['reset']}")
179
+ health = c3.button("🔄 Health")
180
+
181
+ # ---------------- HTTP helpers ----------------
182
  def build_session(auto_retry: bool) -> requests.Session:
183
  s = requests.Session()
184
  s.headers.update(HEADERS)
 
195
  s.mount("https://", adapter)
196
  return s
197
 
198
+ def warm_up(sess: requests.Session, base: str, progress_cb=None, total_budget: int = 25):
199
  start = time.time()
200
+ paths = ["/", "/health"]
201
+ for i, path in enumerate(paths, 1):
202
+ try:
203
+ _ = sess.get(f"{base.rstrip('/')}{path}", timeout=8)
204
+ except Exception:
205
+ pass
206
+ if progress_cb:
207
+ progress_cb(min(int(i/len(paths)*18), 18)) # 0-18%
208
+ if time.time() - start > total_budget:
209
+ break
210
+ time.sleep(0.4)
211
 
212
  def fetch_bytes(sess: requests.Session, base: str, url: str) -> bytes:
213
  full = url if url.startswith("http") else f"{base.rstrip('/')}{url}"
214
+ r = sess.get(full, timeout=90)
215
  r.raise_for_status()
216
  return r.content
217
 
218
+ def post_analyze(sess: requests.Session, base: str, _file, _url: str, progress_cb=None) -> dict:
219
  data = {
220
  "use_data_uri": "true" if st.session_state.use_data_uri else "false",
221
  "lang": st.session_state.lang,
222
  }
 
223
  if DEV_MODE and st.session_state.prompt_override.strip():
224
  data["prompt"] = st.session_state.prompt_override.strip()
225
 
226
  files = None
227
  if _file is not None:
228
+ # read into BytesIO for repeatable upload
229
+ content = _file.read()
230
+ buf = io.BytesIO(content)
231
  mime = getattr(_file, "type", None) or "application/octet-stream"
232
+ files = {"file": (_file.name, buf, mime)}
233
+ if progress_cb: progress_cb(30) # after upload prep
234
  elif _url.strip():
235
  data["audio_url"] = _url.strip()
236
 
 
239
 
240
  url = f"{base.rstrip('/')}/analyze"
241
  deadline = time.time() + st.session_state.max_wait
242
+ last_err = None
243
  while True:
244
  try:
245
+ if progress_cb: progress_cb(35) # request start
246
+ resp = sess.post(url, data=data, files=files, timeout=240)
247
+ if progress_cb: progress_cb(78) # server responded
248
  resp.raise_for_status()
249
+ if progress_cb: progress_cb(92) # parse
250
  return resp.json()
251
  except Exception as e:
252
+ last_err = e
253
+ if time.time() > deadline or not st.session_state.auto_retry:
254
+ return {"ok": False, "error": f"{T['error_generic']} ({last_err})"}
255
  time.sleep(1.2)
256
 
257
+ # ---------------- Actions ----------------
258
  sess = build_session(st.session_state.auto_retry)
259
 
260
  if health:
261
  try:
262
  r = sess.get(f"{st.session_state.api_base.rstrip('/')}/health", timeout=10)
263
+ st.success(T["health_ok"] if r.ok else T["health_fail"])
264
+ if DEV_MODE and r.ok:
265
+ st.code(json.dumps(r.json(), ensure_ascii=False, indent=2), language="json")
 
 
 
266
  except Exception:
267
  st.error(T["health_fail"])
268
 
 
270
  st.session_state.res = None
271
  st.rerun()
272
 
273
+ # Progress widgets (player stays above)
274
+ progress_box = st.container()
275
+ timeline = progress_box.expander(T["timeline_title"], expanded=True)
276
+ p_text = progress_box.empty()
277
+ p_bar = progress_box.progress(0)
278
+
279
+ def progress_update(pct: int, label: str = ""):
280
+ p_bar.progress(min(max(pct, 0), 100))
281
+ if label:
282
+ p_text.write(label)
283
+
284
  if go:
285
  if not st.session_state.api_base:
286
  st.error(T["need_backend"])
287
  elif not file and not audio_url.strip():
288
  st.error(T["need_input"])
289
  else:
290
+ # timeline
291
+ with timeline:
292
+ st.write(f"1) {T['step_prepare']}")
293
+ st.write(f"2) {T['step_upload']}")
294
+ st.write(f"3) {T['step_process']}")
295
+ st.write(f"4) {T['step_summarize']}")
296
+
297
+ # warmup (optional)
298
  if st.session_state.warmup:
299
+ progress_update(5, T["warming"])
300
+ warm_up(sess, st.session_state.api_base, progress_cb=lambda v: progress_update(v, T["warming"]))
301
+
302
+ # analyze
303
+ progress_update(22, T["analyzing"])
304
+ res = post_analyze(
305
+ sess, st.session_state.api_base, file, audio_url,
306
+ progress_cb=lambda v: progress_update(v, T["analyzing"])
307
+ )
308
+ st.session_state.res = res
309
+ progress_update(100, "")
310
 
311
+ # ---------------- Results ----------------
312
  res = st.session_state.res
313
  if res:
314
  if not res.get("ok", True) and res.get("error"):
 
320
  else:
321
  st.subheader(T["result"])
322
 
323
+ # Summary for drivers
324
  comm = (res.get("commentary") or "").strip()
325
  if comm:
326
  st.markdown(f"**{T['commentary']}**")
327
  st.write(comm)
328
 
329
+ # Detailed findings (JSON). This is the structured diagnosis no provider names.
330
  parsed = res.get("fal_parsed")
331
  if parsed:
332
  st.markdown(f"**{T['json']}**")
333
  st.json(parsed)
334
 
335
+ # Downloads
336
  dls = res.get("downloads") or {}
337
  if any(dls.get(k) for k in ("summary_txt", "fal_parsed_json", "fal_raw_txt")):
338
  st.markdown(f"**{T['artifacts']}**")
339
+ col_a, col_b, col_c = st.columns(3)
340
 
341
  if dls.get("summary_txt"):
342
  try:
343
  data = fetch_bytes(sess, st.session_state.api_base, dls["summary_txt"])
344
+ col_a.download_button(label=T["download_summary"], data=data, file_name="ozet.txt" if st.session_state.lang=="tr" else "summary.txt")
345
  except Exception:
346
+ col_a.error("")
347
 
348
  if dls.get("fal_parsed_json"):
349
  try:
350
  data = fetch_bytes(sess, st.session_state.api_base, dls["fal_parsed_json"])
351
+ col_b.download_button(label=T["download_parsed"], data=data, file_name="detay.json" if st.session_state.lang=="tr" else "details.json", mime="application/json")
352
  except Exception:
353
+ col_b.error("")
354
 
355
  if dls.get("fal_raw_txt"):
 
356
  try:
357
  data = fetch_bytes(sess, st.session_state.api_base, dls["fal_raw_txt"])
358
+ col_c.download_button(label=T["download_raw"], data=data, file_name="ham.txt" if st.session_state.lang=="tr" else "raw.txt")
359
  except Exception:
360
+ col_c.error("")
361
 
362
+ # ---------------- Footer ----------------
363
  st.caption("© 2025 Diag Studio — Araç Motor Ses Analizi")