renderfy commited on
Commit
950b45a
·
verified ·
1 Parent(s): d82dcdf

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +216 -132
streamlit_app.py CHANGED
@@ -1,4 +1,4 @@
1
- # streamlit_app.py — AI LightBox · Beauty (v0.3 UI + Minimal Demo: input.jpg & tryon.jpg)
2
  # Packshot → Padding (square/canvas) → Try-on (lips/eyes/face/nails/hair/brow/body/lifestyle) · SR · (optional) Video
3
 
4
  import os, io, zipfile, requests, streamlit as st
@@ -6,9 +6,10 @@ from PIL import Image
6
  from pathlib import Path
7
 
8
  # ================= Env & API (robust) =================
9
- def _env(k):
10
  return (os.getenv(k) or "").strip().strip("'\"")
11
 
 
12
  API_BASE = (
13
  _env("AI_BEAUTYBOX_API") or
14
  _env("AI_LIGHTBOX_API") or
@@ -17,8 +18,8 @@ API_BASE = (
17
  ).rstrip("/")
18
 
19
  HF_TOKEN = (
20
- _env("AI_BEAUTYBOX_TOKEN") or
21
- _env("AI_LIGHTBOX_TOKEN") or
22
  _env("BeautyBoxAI_TOKEN") or
23
  _env("AI_BEAUTY_TOKEN")
24
  )
@@ -29,19 +30,12 @@ if not API_BASE:
29
 
30
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
31
  PREVIEW_SIZE = int(os.getenv("PREVIEW_SIZE", "512"))
32
- APP_DIR = Path(__file__).parent
33
-
34
- # ================= Demo dosyaları (yalnızca yerel klasör) =================
35
- DEMO_INPUT = str(APP_DIR / "input.jpg")
36
- DEMO_TRYON = str(APP_DIR / "tryon.jpg")
37
 
38
  # ================= Session State (ilk yükte) =================
39
  defaults = {
40
  "results": None,
41
  "job_counter": 0,
42
  "duration": "6",
43
- # input.jpg ve tryon.jpg varsa demo otomatik açık başlar
44
- "demo_on": (Path(DEMO_INPUT).is_file() and Path(DEMO_TRYON).is_file()),
45
  }
46
  for k, v in defaults.items():
47
  if k not in st.session_state:
@@ -58,6 +52,7 @@ HAIR = {"hair_color"}
58
  REGIONS = ["auto","lips","eyes","face","nails","hand","hair","brow","body","lifestyle"]
59
 
60
  def tryon_label(category: str, region_override: str, sr: bool) -> str:
 
61
  r = (region_override or "auto").lower()
62
  if r != "auto":
63
  base = {
@@ -66,6 +61,7 @@ def tryon_label(category: str, region_override: str, sr: bool) -> str:
66
  "lifestyle":"Lifestyle"
67
  }.get(r, "Try-on")
68
  return f"{base} (SR)" if sr else base
 
69
  c = (category or "auto").lower()
70
  if c in LIPS: base = "On-Lips"
71
  elif c in EYES: base = "On-Eyes"
@@ -80,6 +76,25 @@ def tryon_label(category: str, region_override: str, sr: bool) -> str:
80
  def _needs_auth(url: str) -> bool:
81
  return url.startswith(API_BASE) or url.startswith("outputs/")
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  def backend_ok() -> bool:
84
  try:
85
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
@@ -96,34 +111,22 @@ def post_chain(data: dict, files_payload):
96
  return r.json()
97
 
98
  def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=False):
99
- payload = {"image_url": image_url, "duration": duration, "resolution": resolution,
100
- "prompt_optimizer": "true" if prompt_optimizer else "false"}
 
 
 
 
101
  r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload,
102
  headers=HEADERS if HF_TOKEN else None, timeout=600)
103
  r.raise_for_status()
104
  return r.json()
105
 
106
- @st.cache_data(ttl=300, show_spinner=False)
107
- def fetch_bytes(url_or_path: str):
108
- if not url_or_path: return None
109
- p = Path(url_or_path)
110
- if p.is_file():
111
- try: return p.read_bytes()
112
- except Exception: return None
113
- # outputs/ veya URL
114
- try:
115
- url = f"{API_BASE}/{url_or_path}" if url_or_path.startswith("outputs/") else url_or_path
116
- headers = (HEADERS if (_needs_auth(url) and HF_TOKEN) else None)
117
- r = requests.get(url, headers=headers, timeout=180)
118
- r.raise_for_status()
119
- return r.content
120
- except Exception:
121
- return None
122
-
123
  @st.cache_data(ttl=300, show_spinner=False)
124
  def image_size_from_bytes(b: bytes):
125
  try:
126
- im = Image.open(io.BytesIO(b)); return im.size
 
127
  except Exception:
128
  return None
129
 
@@ -131,14 +134,15 @@ def infer_ext(data: bytes) -> str:
131
  try:
132
  im = Image.open(io.BytesIO(data))
133
  fmt = (im.format or "JPEG").lower()
134
- return "." + {"jpeg":"jpg","jpg":"jpg","png":"png","webp":"webp"}.get(fmt, "jpg")
135
  except Exception:
136
  return ".jpg"
137
 
138
  @st.cache_data(ttl=300, show_spinner=False)
139
  def square_preview_bytes(img_bytes: bytes, size: int = PREVIEW_SIZE, bg_rgb=(255, 255, 255)) -> bytes:
140
  im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")
141
- w, h = im.size; scale = min(size / w, size / h)
 
142
  new_w, new_h = max(1, int(w * scale)), max(1, int(h * scale))
143
  im_resized = im.resize((new_w, new_h), Image.LANCZOS)
144
  canvas = Image.new("RGBA", (size, size), (*bg_rgb, 255))
@@ -147,40 +151,57 @@ def square_preview_bytes(img_bytes: bytes, size: int = PREVIEW_SIZE, bg_rgb=(255
147
  buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
148
 
149
  def clamp(v: float, lo: float, hi: float) -> float:
150
- try: v = float(v)
151
- except Exception: v = (lo + hi) / 2.0
 
 
152
  return max(lo, min(hi, v))
153
 
154
  def first_present(d: dict, keys: list, default=None):
155
  for k in keys:
156
  v = d.get(k)
157
- if v: return v
 
158
  return default
159
 
160
  def show_tile(col, title, url: str | None, bg_rgb=(255,255,255), filename_stub="image", key_prefix=""):
161
  col.subheader(title)
162
  if not url:
163
- placeholder = Image.new("RGBA", (PREVIEW_SIZE, PREVIEW_SIZE), (0,0,0,0))
164
- col.image(placeholder, use_container_width=True); col.caption("—"); return None, None
 
 
 
165
  b = fetch_bytes(url)
166
  if not b:
167
- col.warning("Görsel yüklenemedi"); return None, None
 
 
168
  pv = square_preview_bytes(b, size=PREVIEW_SIZE, bg_rgb=bg_rgb)
169
  col.image(pv, use_container_width=True)
170
  sz = image_size_from_bytes(b)
171
- if sz: col.caption(f"Gerçek çözünürlük: {sz[0]}×{sz[1]} px")
 
 
172
  ext = infer_ext(b)
173
- mime = "image/jpeg" if ext.lower() in [".jpg",".jpeg"] else ("image/png" if ext.lower()==".png" else "image/webp")
174
- col.download_button("İndir (tam kalite)", data=b, file_name=f"{filename_stub}{ext}",
175
- mime=mime, key=f"{key_prefix}_{filename_stub}_dl")
 
 
 
 
 
176
  return url, b
177
 
178
  def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
179
  buf = io.BytesIO()
180
  with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
181
  for fname, b in named_bytes:
182
- if b: z.writestr(fname, b)
183
- buf.seek(0); return buf.read()
 
 
184
 
185
  # ================= Sayfa =================
186
  st.set_page_config(page_title="AI LightBox · Beauty", layout="wide", page_icon="💄")
@@ -190,20 +211,37 @@ st.caption("Packshot → Padding (canvas) → Try-on (lips/eyes/face/nails/hair/
190
  # ================= Üst Kontrol Şeridi =================
191
  ok = backend_ok()
192
  col1, col2, col3, col4, col5, col6, col7 = st.columns([1.8, 1.6, 1.4, 1.3, 0.9, 1.1, 0.9])
 
193
  with col1:
194
  file_list = st.file_uploader("Product image (upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
195
  with col2:
196
  image_url = st.text_input("or Product URL", key="image_url")
197
  with col3:
198
- category = st.selectbox("Category",
199
- ["auto","lipstick","lipgloss","lipliner",
200
- "eyeshadow","eyeliner","mascara","brow","false_lashes",
201
- "foundation","concealer","blush","bronzer","contour","highlighter","body_makeup",
202
- "nail_polish","skincare","perfume","tools","set",
203
- "hair_color","haircare","shampoo","conditioner","body_lotion","body_care"], index=0, key="category")
 
 
 
 
 
 
 
 
 
 
 
 
204
  with col4:
205
- padding_ratio_val = st.number_input("Content scale (0.30–0.95)", min_value=0.30, max_value=0.95, value=0.50, step=0.01,
206
- help="Kare tuvalde ürünün uzun kenarı = tuval × bu oran.", key="padding_ratio")
 
 
 
 
207
  with col5:
208
  upscale = st.checkbox("Super Resolution", False, key="upscale")
209
  with col6:
@@ -211,6 +249,7 @@ with col6:
211
  with col7:
212
  upscale_factor = st.selectbox("SR factor", ["2","4"], index=0, key="upscale_factor")
213
 
 
214
  t1, t2, t3, t4 = st.columns([1.15, 1.15, 1.2, 1.5])
215
  with t1:
216
  canvas_policy = st.selectbox("Canvas", ["fixed_1200","match_long_edge","keep_input"], index=0, key="canvas_policy")
@@ -229,95 +268,105 @@ with b2:
229
  with b3:
230
  identity_lock = st.checkbox("Identity lock", True, key="identity_lock")
231
 
232
- # ============ DEMO PREVIEW (input.jpg & tryon.jpg; backend yok) ============
233
  st.markdown("---")
234
- d1, d2 = st.columns([1.0, 1.0])
235
- with d1:
236
- st.checkbox("Demo preview (çalıştırmadan örnek göster)", value=st.session_state["demo_on"], key="demo_on")
237
- with d2:
238
- if st.session_state["demo_on"]:
239
- st.info("Bu sadece örnek ön izleme. Run butonu demo açıkken pasif.", icon="👀")
240
-
241
- if st.session_state["demo_on"]:
242
- c_in_demo, c_try_demo = st.columns(2)
243
-
244
- # Input.jpg
245
- ib = fetch_bytes(DEMO_INPUT)
246
- c_in_demo.subheader("Input")
247
- if ib:
248
- c_in_demo.image(square_preview_bytes(ib, size=PREVIEW_SIZE, bg_rgb=(255,255,255)), use_container_width=True)
249
- try:
250
- wsz = Image.open(io.BytesIO(ib)).size
251
- c_in_demo.caption(f"Input resolution: {wsz[0]}×{wsz[1]} px")
252
- except Exception:
253
- c_in_demo.caption("")
254
- c_in_demo.download_button("İndir (tam kalite)", data=ib, file_name=f"demo_input{infer_ext(ib)}",
255
- mime="image/jpeg", key="demo_input_dl")
256
- else:
257
- c_in_demo.warning("input.jpg bulunamadı.")
258
-
259
- # Tryon.jpg
260
- show_tile(c_try_demo, "On-Hair (örnek)", DEMO_TRYON, filename_stub="demo_tryon", key_prefix="demo")
261
-
262
- if st.button("Bu örneği forma aktar ve demoyu kapat", use_container_width=True, key="use_demo_btn"):
263
- st.session_state["image_url"] = DEMO_INPUT # yerel dosyayı kullan
264
- if st.session_state.get("category") == "auto":
265
- st.session_state["category"] = "hair_color"
266
- st.session_state["apply_region"] = "hair"
267
- st.session_state["demo_on"] = False
268
- st.experimental_rerun()
 
 
 
 
 
269
 
270
  # ================= Alt kontrol şeridi =================
271
  cA, cB = st.columns([1.0, 1.0])
272
  with cA:
273
  st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
274
  with cB:
275
- run = st.button("Run", type="primary", use_container_width=True, key="run_btn", disabled=st.session_state["demo_on"])
276
 
277
- # ================= Girdi Önizleme (form girdisi) =================
278
- c_in2, c_pack, c_pad, c_try = st.columns(4)
279
  white_bg = (255, 255, 255)
280
 
281
  input_preview_bytes = None
282
  if file_list:
283
- try: input_preview_bytes = file_list[0].getvalue()
284
- except Exception: input_preview_bytes = None
285
- elif st.session_state.get("image_url"):
286
- input_preview_bytes = fetch_bytes(st.session_state["image_url"])
 
 
287
 
288
  if input_preview_bytes:
289
- c_in2.subheader("Input")
290
- c_in2.image(square_preview_bytes(input_preview_bytes, size=PREVIEW_SIZE, bg_rgb=white_bg), use_container_width=True)
291
  try:
292
  sz = Image.open(io.BytesIO(input_preview_bytes)).size
293
- c_in2.caption(f"Input resolution: {sz[0]}×{sz[1]} px")
294
  except Exception:
295
- c_in2.caption("—")
296
  else:
297
- show_tile(c_in2, "Input", None)
298
 
299
  # ================ Koş & Sonuçları Kaydet ================
300
  if run:
301
  if not ok:
302
  st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
303
- elif not (file_list or st.session_state.get("image_url")):
304
  st.error("En az bir product görseli girin (upload veya URL).")
305
  else:
306
  try:
307
  padding_ratio = clamp(padding_ratio_val, 0.30, 0.95)
308
  data = {
309
- "category": st.session_state["category"],
310
  "edit_prompt": "",
311
  "num_images": "1",
312
- "image_urls": (st.session_state.get("image_url") or ""),
313
  "mannequin_image_url": (model_ref_url or ""),
314
  "padding_ratio": str(padding_ratio),
315
  "identity_lock": "true" if identity_lock else "false",
316
- "canvas_policy": st.session_state["canvas_policy"],
 
317
  "canvas_size": str(int(canvas_size or 1200)),
 
318
  "shade_hex": (shade_hex or "").strip() or None,
319
  "shade_swatch_url": (shade_swatch_url or "").strip() or None,
320
- "apply_region": st.session_state["apply_region"],
 
321
  "upscale": "true" if upscale else "false",
322
  "upscale_factor": st.session_state["upscale_factor"],
323
  "upscale_stage": st.session_state["upscale_stage"],
@@ -334,16 +383,20 @@ if run:
334
  packshot_url = None
335
  try:
336
  imgs = out["packshot_step"]["result"]["images"]
337
- if imgs: packshot_url = imgs[0].get("url")
338
- except Exception: pass
 
 
339
 
340
  padded_url = None
341
  try:
342
  padded_url = out.get("padding_step", {}).get("saved_file")
343
  if not padded_url:
344
  pads = out.get("packshot_step", {}).get("padded_urls") or out.get("padded_urls") or []
345
- if pads: padded_url = pads[0]
346
- except Exception: pass
 
 
347
 
348
  packshot_sr_list = first_present(out.get("packshot_step", {}), [
349
  "upscaled_packshot_urls", "upscaled_packshot_files"
@@ -353,8 +406,10 @@ if run:
353
  placed_url = None
354
  try:
355
  imgs = out["image_step"]["result"]["images"]
356
- if imgs: placed_url = imgs[0].get("url")
357
- except Exception: pass
 
 
358
 
359
  final_sr_list = first_present(out.get("image_step", {}), [
360
  "upscaled_final_urls", "upscaled_final_files"
@@ -377,8 +432,9 @@ if run:
377
  "video_url": None,
378
  "api_base": API_BASE,
379
  "has_auth_header": bool(HEADERS),
380
- "category": st.session_state["category"],
381
- "apply_region": st.session_state["apply_region"],
 
382
  }
383
 
384
  except requests.HTTPError as e:
@@ -393,15 +449,22 @@ if res:
393
  key_prefix = f"job{st.session_state['job_counter']}"
394
 
395
  p_title = "Packshot (SR)" if res.get("packshot_sr_url") else "Packshot"
396
- p_url, p_bytes = show_tile(c_pack, p_title, res.get("packshot_sr_url") or res.get("packshot_url"),
397
- bg_rgb=(255,255,255), filename_stub="packshot", key_prefix=key_prefix)
398
- pad_url, pad_bytes = show_tile(c_pad, "Padded", res.get("padded_url"),
399
- bg_rgb=(255,255,255), filename_stub="padded", key_prefix=key_prefix)
 
 
 
 
 
400
 
401
  shown_final_url = res.get("placed_sr_url") or res.get("placed_url")
402
  m_title = tryon_label(res.get("category","auto"), res.get("apply_region","auto"), bool(res.get("placed_sr_url")))
403
- m_url, m_bytes = show_tile(c_try, m_title, shown_final_url,
404
- bg_rgb=(255,255,255), filename_stub="tryon", key_prefix=key_prefix)
 
 
405
 
406
  # ---- Video (final görselden) ----
407
  st.markdown("---")
@@ -419,11 +482,17 @@ if res:
419
  st.warning("Video için final görsel yok.")
420
  else:
421
  with st.spinner("Generating video…"):
422
- v = post_video(src_for_video, duration=st.session_state.get("duration_select","6"),
423
- resolution="768P", prompt_optimizer=False)
 
 
 
 
424
  vurl = (v.get("result") or {}).get("video", {}).get("url")
425
- if vurl: st.session_state["results"]["video_url"] = vurl
426
- else: st.info("Video URL gelmedi.")
 
 
427
 
428
  vurl = st.session_state["results"].get("video_url")
429
  if vurl:
@@ -432,8 +501,13 @@ if res:
432
  vb = fetch_bytes(vurl)
433
  if vb:
434
  vbytes = vb
435
- st.download_button("Download video (MP4)", data=vb, file_name="ai_lightbox_video.mp4",
436
- mime="video/mp4", key=f"{key_prefix}_video_dl")
 
 
 
 
 
437
  else:
438
  st.info("Video URL var ama içerik indirilemedi.")
439
 
@@ -445,9 +519,15 @@ if res:
445
  if vbytes: named.append(("video.mp4", vbytes))
446
  if named:
447
  zip_bytes = make_zip(named)
448
- st.download_button("Download all (ZIP)", data=zip_bytes, file_name="ai_lightbox_beauty_outputs.zip",
449
- mime="application/zip", key=f"{key_prefix}_zip_dl")
450
-
 
 
 
 
 
 
451
  with st.expander("Debug"):
452
  st.json({
453
  "job_id": res.get("job_id"),
@@ -471,10 +551,14 @@ with st.sidebar:
471
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
472
  if st.button("Ping /health"):
473
  try:
474
- r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
 
 
475
  st.write(r.status_code)
476
- try: st.json(r.json())
477
- except Exception: st.code((r.text or "")[:1000])
 
 
478
  except Exception as e:
479
  st.error(f"Health error: {e}")
480
  if st.button("Clear cache"):
 
1
+ # streamlit_app.py — AI LightBox · Beauty (v0.3 UI)
2
  # Packshot → Padding (square/canvas) → Try-on (lips/eyes/face/nails/hair/brow/body/lifestyle) · SR · (optional) Video
3
 
4
  import os, io, zipfile, requests, streamlit as st
 
6
  from pathlib import Path
7
 
8
  # ================= Env & API (robust) =================
9
+ def _env(k):
10
  return (os.getenv(k) or "").strip().strip("'\"")
11
 
12
+ # Desteklenen env anahtarları (öncelik sırası)
13
  API_BASE = (
14
  _env("AI_BEAUTYBOX_API") or
15
  _env("AI_LIGHTBOX_API") or
 
18
  ).rstrip("/")
19
 
20
  HF_TOKEN = (
21
+ _env("AI_BEAUTYBOX_TOKEN") or
22
+ _env("AI_LIGHTBOX_TOKEN") or
23
  _env("BeautyBoxAI_TOKEN") or
24
  _env("AI_BEAUTY_TOKEN")
25
  )
 
30
 
31
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
32
  PREVIEW_SIZE = int(os.getenv("PREVIEW_SIZE", "512"))
 
 
 
 
 
33
 
34
  # ================= Session State (ilk yükte) =================
35
  defaults = {
36
  "results": None,
37
  "job_counter": 0,
38
  "duration": "6",
 
 
39
  }
40
  for k, v in defaults.items():
41
  if k not in st.session_state:
 
52
  REGIONS = ["auto","lips","eyes","face","nails","hand","hair","brow","body","lifestyle"]
53
 
54
  def tryon_label(category: str, region_override: str, sr: bool) -> str:
55
+ # Region override önce gelir
56
  r = (region_override or "auto").lower()
57
  if r != "auto":
58
  base = {
 
61
  "lifestyle":"Lifestyle"
62
  }.get(r, "Try-on")
63
  return f"{base} (SR)" if sr else base
64
+
65
  c = (category or "auto").lower()
66
  if c in LIPS: base = "On-Lips"
67
  elif c in EYES: base = "On-Eyes"
 
76
  def _needs_auth(url: str) -> bool:
77
  return url.startswith(API_BASE) or url.startswith("outputs/")
78
 
79
+ @st.cache_data(ttl=300, show_spinner=False)
80
+ def fetch_bytes(url_or_path: str):
81
+ """URL, outputs/ yolu ya da mevcut klasördeki yerel dosyayı okuyabilir."""
82
+ if not url_or_path:
83
+ return None
84
+ try:
85
+ # 1) Mevcut klasörde yerel dosya?
86
+ p = Path(url_or_path)
87
+ if p.is_file():
88
+ return p.read_bytes()
89
+ # 2) outputs/ için API_BASE ön ekini uygula
90
+ url = f"{API_BASE}/{url_or_path}" if url_or_path.startswith("outputs/") else url_or_path
91
+ headers = (HEADERS if (_needs_auth(url) and HF_TOKEN) else None)
92
+ r = requests.get(url, headers=headers, timeout=180)
93
+ r.raise_for_status()
94
+ return r.content
95
+ except Exception:
96
+ return None
97
+
98
  def backend_ok() -> bool:
99
  try:
100
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
 
111
  return r.json()
112
 
113
  def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=False):
114
+ payload = {
115
+ "image_url": image_url,
116
+ "duration": duration,
117
+ "resolution": resolution,
118
+ "prompt_optimizer": "true" if prompt_optimizer else "false",
119
+ }
120
  r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload,
121
  headers=HEADERS if HF_TOKEN else None, timeout=600)
122
  r.raise_for_status()
123
  return r.json()
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  @st.cache_data(ttl=300, show_spinner=False)
126
  def image_size_from_bytes(b: bytes):
127
  try:
128
+ im = Image.open(io.BytesIO(b))
129
+ return im.size
130
  except Exception:
131
  return None
132
 
 
134
  try:
135
  im = Image.open(io.BytesIO(data))
136
  fmt = (im.format or "JPEG").lower()
137
+ return "." + {"jpeg": "jpg", "jpg": "jpg", "png": "png", "webp": "webp"}.get(fmt, "jpg")
138
  except Exception:
139
  return ".jpg"
140
 
141
  @st.cache_data(ttl=300, show_spinner=False)
142
  def square_preview_bytes(img_bytes: bytes, size: int = PREVIEW_SIZE, bg_rgb=(255, 255, 255)) -> bytes:
143
  im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")
144
+ w, h = im.size
145
+ scale = min(size / w, size / h)
146
  new_w, new_h = max(1, int(w * scale)), max(1, int(h * scale))
147
  im_resized = im.resize((new_w, new_h), Image.LANCZOS)
148
  canvas = Image.new("RGBA", (size, size), (*bg_rgb, 255))
 
151
  buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
152
 
153
  def clamp(v: float, lo: float, hi: float) -> float:
154
+ try:
155
+ v = float(v)
156
+ except Exception:
157
+ v = (lo + hi) / 2.0
158
  return max(lo, min(hi, v))
159
 
160
  def first_present(d: dict, keys: list, default=None):
161
  for k in keys:
162
  v = d.get(k)
163
+ if v:
164
+ return v
165
  return default
166
 
167
  def show_tile(col, title, url: str | None, bg_rgb=(255,255,255), filename_stub="image", key_prefix=""):
168
  col.subheader(title)
169
  if not url:
170
+ placeholder = Image.new("RGBA", (PREVIEW_SIZE, PREVIEW_SIZE), (0, 0, 0, 0))
171
+ col.image(placeholder, use_container_width=True)
172
+ col.caption("—")
173
+ return None, None
174
+
175
  b = fetch_bytes(url)
176
  if not b:
177
+ col.warning("Görsel yüklenemedi")
178
+ return None, None
179
+
180
  pv = square_preview_bytes(b, size=PREVIEW_SIZE, bg_rgb=bg_rgb)
181
  col.image(pv, use_container_width=True)
182
  sz = image_size_from_bytes(b)
183
+ if sz:
184
+ col.caption(f"Gerçek çözünürlük: {sz[0]}×{sz[1]} px")
185
+
186
  ext = infer_ext(b)
187
+ mime = "image/jpeg" if ext.lower() in [".jpg", ".jpeg"] else ("image/png" if ext.lower()==".png" else "image/webp")
188
+ col.download_button(
189
+ "İndir (tam kalite)",
190
+ data=b,
191
+ file_name=f"{filename_stub}{ext}",
192
+ mime=mime,
193
+ key=f"{key_prefix}_{filename_stub}_dl"
194
+ )
195
  return url, b
196
 
197
  def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
198
  buf = io.BytesIO()
199
  with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
200
  for fname, b in named_bytes:
201
+ if b:
202
+ z.writestr(fname, b)
203
+ buf.seek(0)
204
+ return buf.read()
205
 
206
  # ================= Sayfa =================
207
  st.set_page_config(page_title="AI LightBox · Beauty", layout="wide", page_icon="💄")
 
211
  # ================= Üst Kontrol Şeridi =================
212
  ok = backend_ok()
213
  col1, col2, col3, col4, col5, col6, col7 = st.columns([1.8, 1.6, 1.4, 1.3, 0.9, 1.1, 0.9])
214
+
215
  with col1:
216
  file_list = st.file_uploader("Product image (upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
217
  with col2:
218
  image_url = st.text_input("or Product URL", key="image_url")
219
  with col3:
220
+ category = st.selectbox(
221
+ "Category",
222
+ [
223
+ # Lips
224
+ "auto","lipstick","lipgloss","lipliner",
225
+ # Eyes
226
+ "eyeshadow","eyeliner","mascara","brow","false_lashes",
227
+ # Face/complexion
228
+ "foundation","concealer","blush","bronzer","contour","highlighter","body_makeup",
229
+ # Nails
230
+ "nail_polish",
231
+ # Lifestyle
232
+ "skincare","perfume","tools","set",
233
+ # Hair/Body care
234
+ "hair_color","haircare","shampoo","conditioner","body_lotion","body_care",
235
+ ],
236
+ index=0, key="category"
237
+ )
238
  with col4:
239
+ padding_ratio_val = st.number_input(
240
+ "Content scale (0.30–0.95)",
241
+ min_value=0.30, max_value=0.95, value=0.50, step=0.01,
242
+ help="Kare tuvalde ürünün uzun kenarı = tuval × bu oran.",
243
+ key="padding_ratio"
244
+ )
245
  with col5:
246
  upscale = st.checkbox("Super Resolution", False, key="upscale")
247
  with col6:
 
249
  with col7:
250
  upscale_factor = st.selectbox("SR factor", ["2","4"], index=0, key="upscale_factor")
251
 
252
+ # == Tuval & Try-on kontrolleri ==
253
  t1, t2, t3, t4 = st.columns([1.15, 1.15, 1.2, 1.5])
254
  with t1:
255
  canvas_policy = st.selectbox("Canvas", ["fixed_1200","match_long_edge","keep_input"], index=0, key="canvas_policy")
 
268
  with b3:
269
  identity_lock = st.checkbox("Identity lock", True, key="identity_lock")
270
 
271
+ # ============ DEMO PREVIEW (mevcut klasörden input.jpg & tryon.jpg) ============
272
  st.markdown("---")
273
+ st.subheader("Demo preview (çalıştırmadan önce örnek)")
274
+ st.caption("Bu bölüm sadece örnektir; aşağıdaki iki görsel mevcut klasörden okunur: input.jpg ve tryon.jpg.")
275
+
276
+ d_in, d_pack, d_pad, d_try = st.columns(4)
277
+
278
+ # Input preview
279
+ in_bytes = fetch_bytes("input.jpg")
280
+ if in_bytes:
281
+ d_in.subheader("Input")
282
+ d_in.image(square_preview_bytes(in_bytes, size=PREVIEW_SIZE, bg_rgb=(255,255,255)), use_container_width=True)
283
+ sz = image_size_from_bytes(in_bytes)
284
+ if sz:
285
+ d_in.caption(f"Input resolution: {sz[0]}×{sz[1]} px")
286
+ d_in.download_button(
287
+ "İndir (tam kalite)", data=in_bytes,
288
+ file_name=f"input{infer_ext(in_bytes)}",
289
+ mime="image/jpeg", key="demo_input_dl"
290
+ )
291
+ else:
292
+ show_tile(d_in, "Input", None)
293
+
294
+ # Packshot & Padded columnları bu demoda boş
295
+ show_tile(d_pack, "Packshot", None)
296
+ show_tile(d_pad, "Padded", None)
297
+
298
+ # On-Hair / final try-on preview
299
+ try_bytes = fetch_bytes("tryon.jpg")
300
+ if try_bytes:
301
+ d_try.subheader("On-Hair")
302
+ d_try.image(square_preview_bytes(try_bytes, size=PREVIEW_SIZE, bg_rgb=(255,255,255)), use_container_width=True)
303
+ sz2 = image_size_from_bytes(try_bytes)
304
+ if sz2:
305
+ d_try.caption(f"Gerçek çözünürlük: {sz2[0]}×{sz2[1]} px")
306
+ d_try.download_button(
307
+ "İndir (tam kalite)", data=try_bytes,
308
+ file_name=f"tryon{infer_ext(try_bytes)}",
309
+ mime="image/jpeg", key="demo_tryon_dl"
310
+ )
311
+ else:
312
+ show_tile(d_try, "On-Hair", None)
313
 
314
  # ================= Alt kontrol şeridi =================
315
  cA, cB = st.columns([1.0, 1.0])
316
  with cA:
317
  st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
318
  with cB:
319
+ run = st.button("Run", type="primary", use_container_width=True, key="run_btn")
320
 
321
+ # ================= Girdi Önizleme =================
322
+ c_in, c_pack, c_pad, c_try = st.columns(4)
323
  white_bg = (255, 255, 255)
324
 
325
  input_preview_bytes = None
326
  if file_list:
327
+ try:
328
+ input_preview_bytes = file_list[0].getvalue()
329
+ except Exception:
330
+ input_preview_bytes = None
331
+ elif image_url:
332
+ input_preview_bytes = fetch_bytes(image_url)
333
 
334
  if input_preview_bytes:
335
+ c_in.subheader("Input")
336
+ c_in.image(square_preview_bytes(input_preview_bytes, size=PREVIEW_SIZE, bg_rgb=white_bg), use_container_width=True)
337
  try:
338
  sz = Image.open(io.BytesIO(input_preview_bytes)).size
339
+ c_in.caption(f"Input resolution: {sz[0]}×{sz[1]} px")
340
  except Exception:
341
+ c_in.caption("—")
342
  else:
343
+ show_tile(c_in, "Input", None)
344
 
345
  # ================ Koş & Sonuçları Kaydet ================
346
  if run:
347
  if not ok:
348
  st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
349
+ elif not (file_list or image_url):
350
  st.error("En az bir product görseli girin (upload veya URL).")
351
  else:
352
  try:
353
  padding_ratio = clamp(padding_ratio_val, 0.30, 0.95)
354
  data = {
355
+ "category": category,
356
  "edit_prompt": "",
357
  "num_images": "1",
358
+ "image_urls": (image_url or ""),
359
  "mannequin_image_url": (model_ref_url or ""),
360
  "padding_ratio": str(padding_ratio),
361
  "identity_lock": "true" if identity_lock else "false",
362
+ # Canvas
363
+ "canvas_policy": canvas_policy,
364
  "canvas_size": str(int(canvas_size or 1200)),
365
+ # Shade / Region
366
  "shade_hex": (shade_hex or "").strip() or None,
367
  "shade_swatch_url": (shade_swatch_url or "").strip() or None,
368
+ "apply_region": apply_region,
369
+ # SR
370
  "upscale": "true" if upscale else "false",
371
  "upscale_factor": st.session_state["upscale_factor"],
372
  "upscale_stage": st.session_state["upscale_stage"],
 
383
  packshot_url = None
384
  try:
385
  imgs = out["packshot_step"]["result"]["images"]
386
+ if imgs:
387
+ packshot_url = imgs[0].get("url")
388
+ except Exception:
389
+ pass
390
 
391
  padded_url = None
392
  try:
393
  padded_url = out.get("padding_step", {}).get("saved_file")
394
  if not padded_url:
395
  pads = out.get("packshot_step", {}).get("padded_urls") or out.get("padded_urls") or []
396
+ if pads:
397
+ padded_url = pads[0]
398
+ except Exception:
399
+ pass
400
 
401
  packshot_sr_list = first_present(out.get("packshot_step", {}), [
402
  "upscaled_packshot_urls", "upscaled_packshot_files"
 
406
  placed_url = None
407
  try:
408
  imgs = out["image_step"]["result"]["images"]
409
+ if imgs:
410
+ placed_url = imgs[0].get("url")
411
+ except Exception:
412
+ pass
413
 
414
  final_sr_list = first_present(out.get("image_step", {}), [
415
  "upscaled_final_urls", "upscaled_final_files"
 
432
  "video_url": None,
433
  "api_base": API_BASE,
434
  "has_auth_header": bool(HEADERS),
435
+ # UI state snapshot (etiket/isimlendirme için)
436
+ "category": category,
437
+ "apply_region": apply_region,
438
  }
439
 
440
  except requests.HTTPError as e:
 
449
  key_prefix = f"job{st.session_state['job_counter']}"
450
 
451
  p_title = "Packshot (SR)" if res.get("packshot_sr_url") else "Packshot"
452
+ p_url, p_bytes = show_tile(
453
+ c_pack, p_title, res.get("packshot_sr_url") or res.get("packshot_url"),
454
+ bg_rgb=white_bg, filename_stub="packshot", key_prefix=key_prefix
455
+ )
456
+
457
+ pad_url, pad_bytes = show_tile(
458
+ c_pad, "Padded", res.get("padded_url"),
459
+ bg_rgb=white_bg, filename_stub="padded", key_prefix=key_prefix
460
+ )
461
 
462
  shown_final_url = res.get("placed_sr_url") or res.get("placed_url")
463
  m_title = tryon_label(res.get("category","auto"), res.get("apply_region","auto"), bool(res.get("placed_sr_url")))
464
+ m_url, m_bytes = show_tile(
465
+ c_try, m_title, shown_final_url,
466
+ bg_rgb=white_bg, filename_stub="tryon", key_prefix=key_prefix
467
+ )
468
 
469
  # ---- Video (final görselden) ----
470
  st.markdown("---")
 
482
  st.warning("Video için final görsel yok.")
483
  else:
484
  with st.spinner("Generating video…"):
485
+ v = post_video(
486
+ src_for_video,
487
+ duration=st.session_state.get("duration_select","6"),
488
+ resolution="768P",
489
+ prompt_optimizer=False
490
+ )
491
  vurl = (v.get("result") or {}).get("video", {}).get("url")
492
+ if vurl:
493
+ st.session_state["results"]["video_url"] = vurl
494
+ else:
495
+ st.info("Video URL gelmedi.")
496
 
497
  vurl = st.session_state["results"].get("video_url")
498
  if vurl:
 
501
  vb = fetch_bytes(vurl)
502
  if vb:
503
  vbytes = vb
504
+ st.download_button(
505
+ "Download video (MP4)",
506
+ data=vb,
507
+ file_name="ai_lightbox_video.mp4",
508
+ mime="video/mp4",
509
+ key=f"{key_prefix}_video_dl"
510
+ )
511
  else:
512
  st.info("Video URL var ama içerik indirilemedi.")
513
 
 
519
  if vbytes: named.append(("video.mp4", vbytes))
520
  if named:
521
  zip_bytes = make_zip(named)
522
+ st.download_button(
523
+ "Download all (ZIP)",
524
+ data=zip_bytes,
525
+ file_name="ai_lightbox_beauty_outputs.zip",
526
+ mime="application/zip",
527
+ key=f"{key_prefix}_zip_dl"
528
+ )
529
+
530
+ # Debug
531
  with st.expander("Debug"):
532
  st.json({
533
  "job_id": res.get("job_id"),
 
551
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
552
  if st.button("Ping /health"):
553
  try:
554
+ r = requests.get(f"{API_BASE}/health",
555
+ headers=HEADERS if HF_TOKEN else None,
556
+ timeout=10)
557
  st.write(r.status_code)
558
+ try:
559
+ st.json(r.json())
560
+ except Exception:
561
+ st.code((r.text or "")[:1000])
562
  except Exception as e:
563
  st.error(f"Health error: {e}")
564
  if st.button("Clear cache"):