Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
-
# app.py — Diag Studio
|
| 2 |
-
#
|
| 3 |
-
#
|
|
|
|
| 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 |
-
#
|
| 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 |
-
# ==========
|
| 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 |
-
# ==========
|
| 29 |
def _env(k: str) -> str:
|
| 30 |
return (os.getenv(k) or "").strip().strip("'\"")
|
| 31 |
|
| 32 |
-
|
| 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 |
-
|
| 38 |
-
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
"en": {
|
| 43 |
-
"title": "
|
| 44 |
-
"about": "Upload an engine sound
|
| 45 |
-
"backend_url": "Backend URL",
|
| 46 |
-
"lang": "Language",
|
| 47 |
"upload": "Upload audio file",
|
| 48 |
"or": "or",
|
| 49 |
-
"paste_url": "
|
| 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": "
|
| 58 |
-
"json": "
|
| 59 |
-
"
|
| 60 |
-
"download_summary": "Download summary
|
| 61 |
-
"download_parsed": "Download
|
| 62 |
-
"download_raw": "Download raw
|
| 63 |
-
"
|
| 64 |
-
"
|
| 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 |
-
"
|
| 74 |
-
"
|
| 75 |
-
"
|
| 76 |
-
"
|
| 77 |
-
"
|
| 78 |
-
"
|
| 79 |
-
"
|
| 80 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
# ==========
|
| 125 |
if "lang" not in st.session_state:
|
| 126 |
st.session_state.lang = "tr"
|
| 127 |
-
if "
|
| 128 |
-
st.session_state.
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
st.session_state.lang = st.selectbox(
|
| 136 |
-
|
| 137 |
-
["tr", "en"],
|
| 138 |
index=(0 if st.session_state.lang == "tr" else 1),
|
| 139 |
-
key="lang_selector",
|
| 140 |
)
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
max_wait = st.slider(t["max_wait"], 10, 240, 60, step=10)
|
| 157 |
-
st.caption(t["footer"])
|
| 158 |
|
| 159 |
st.markdown("---")
|
| 160 |
|
| 161 |
-
# ==========
|
| 162 |
-
st.write(
|
| 163 |
-
file = st.file_uploader(
|
| 164 |
-
|
| 165 |
-
|
| 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 |
-
# ==========
|
| 175 |
-
|
| 176 |
-
go =
|
| 177 |
-
reset =
|
| 178 |
-
|
| 179 |
|
| 180 |
-
# ==========
|
| 181 |
-
def build_session(
|
| 182 |
s = requests.Session()
|
| 183 |
s.headers.update(HEADERS)
|
| 184 |
-
if
|
| 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
|
| 198 |
start = time.time()
|
| 199 |
-
with st.spinner(
|
| 200 |
for path in ["/", "/health"]:
|
| 201 |
try:
|
| 202 |
-
r =
|
| 203 |
if r.ok:
|
| 204 |
continue
|
| 205 |
except Exception:
|
| 206 |
pass
|
| 207 |
-
if time.time() - start >
|
| 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,
|
| 218 |
-
url = f"{base.rstrip('/')}/analyze"
|
| 219 |
data = {
|
| 220 |
-
"use_data_uri": "true" if use_data_uri else "false",
|
| 221 |
-
"
|
| 222 |
-
"lang": lang, # "tr" or "en"
|
| 223 |
-
# backend pins detailed_analysis=True
|
| 224 |
}
|
| 225 |
-
|
| 226 |
-
|
|
|
|
| 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
|
| 233 |
-
data["audio_url"] =
|
| 234 |
|
| 235 |
if not files and "audio_url" not in data:
|
| 236 |
-
return {"ok": False, "error": "
|
| 237 |
|
| 238 |
-
|
| 239 |
-
|
| 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 |
-
|
| 249 |
-
|
| 250 |
-
return {"ok": False, "error": f"All attempts failed. Last error: {e}", "attempts": attempts, "api_base": base}
|
| 251 |
time.sleep(1.2)
|
| 252 |
|
| 253 |
-
# ==========
|
| 254 |
-
sess = build_session(auto_retry)
|
| 255 |
|
| 256 |
-
if
|
| 257 |
try:
|
| 258 |
-
r = sess.get(f"{api_base.rstrip('/')}/health", timeout=10)
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
|
|
|
|
|
|
| 263 |
except Exception:
|
| 264 |
-
st.error(
|
| 265 |
|
| 266 |
if reset:
|
| 267 |
-
st.session_state.
|
| 268 |
st.rerun()
|
| 269 |
|
| 270 |
if go:
|
| 271 |
-
if not api_base:
|
| 272 |
-
st.error(
|
| 273 |
elif not file and not audio_url.strip():
|
| 274 |
-
st.error(
|
| 275 |
else:
|
| 276 |
-
if warmup:
|
| 277 |
-
|
| 278 |
-
with st.spinner(
|
| 279 |
-
res = post_analyze(sess, api_base, file, audio_url
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
res = st.session_state.last_response
|
| 284 |
if res:
|
| 285 |
if not res.get("ok", True) and res.get("error"):
|
| 286 |
-
st.error(
|
| 287 |
st.write(res.get("error"))
|
| 288 |
-
st.info(
|
| 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(
|
| 293 |
|
| 294 |
-
#
|
| 295 |
-
comm = res.get("commentary") or ""
|
| 296 |
if comm:
|
| 297 |
-
st.markdown(f"**{
|
| 298 |
st.write(comm)
|
| 299 |
|
| 300 |
-
#
|
| 301 |
parsed = res.get("fal_parsed")
|
| 302 |
if parsed:
|
| 303 |
-
st.markdown(f"**{
|
| 304 |
st.json(parsed)
|
| 305 |
|
| 306 |
-
#
|
| 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 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 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")
|