renderfy commited on
Commit
d82dcdf
·
verified ·
1 Parent(s): 852b891

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +122 -155
streamlit_app.py CHANGED
@@ -1,14 +1,14 @@
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
5
  from PIL import Image
 
6
 
7
  # ================= Env & API (robust) =================
8
  def _env(k):
9
  return (os.getenv(k) or "").strip().strip("'\"")
10
 
11
- # Desteklenen env anahtarları (öncelik sırası)
12
  API_BASE = (
13
  _env("AI_BEAUTYBOX_API") or
14
  _env("AI_LIGHTBOX_API") or
@@ -29,12 +29,19 @@ 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
 
33
  # ================= Session State (ilk yükte) =================
34
  defaults = {
35
  "results": None,
36
  "job_counter": 0,
37
  "duration": "6",
 
 
38
  }
39
  for k, v in defaults.items():
40
  if k not in st.session_state:
@@ -51,7 +58,6 @@ HAIR = {"hair_color"}
51
  REGIONS = ["auto","lips","eyes","face","nails","hand","hair","brow","body","lifestyle"]
52
 
53
  def tryon_label(category: str, region_override: str, sr: bool) -> str:
54
- # Region override önce gelir
55
  r = (region_override or "auto").lower()
56
  if r != "auto":
57
  base = {
@@ -60,7 +66,6 @@ def tryon_label(category: str, region_override: str, sr: bool) -> str:
60
  "lifestyle":"Lifestyle"
61
  }.get(r, "Try-on")
62
  return f"{base} (SR)" if sr else base
63
-
64
  c = (category or "auto").lower()
65
  if c in LIPS: base = "On-Lips"
66
  elif c in EYES: base = "On-Eyes"
@@ -91,12 +96,8 @@ def post_chain(data: dict, files_payload):
91
  return r.json()
92
 
93
  def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=False):
94
- payload = {
95
- "image_url": image_url,
96
- "duration": duration,
97
- "resolution": resolution,
98
- "prompt_optimizer": "true" if prompt_optimizer else "false",
99
- }
100
  r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload,
101
  headers=HEADERS if HF_TOKEN else None, timeout=600)
102
  r.raise_for_status()
@@ -104,8 +105,12 @@ def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer
104
 
105
  @st.cache_data(ttl=300, show_spinner=False)
106
  def fetch_bytes(url_or_path: str):
107
- if not url_or_path:
108
- return None
 
 
 
 
109
  try:
110
  url = f"{API_BASE}/{url_or_path}" if url_or_path.startswith("outputs/") else url_or_path
111
  headers = (HEADERS if (_needs_auth(url) and HF_TOKEN) else None)
@@ -118,8 +123,7 @@ def fetch_bytes(url_or_path: str):
118
  @st.cache_data(ttl=300, show_spinner=False)
119
  def image_size_from_bytes(b: bytes):
120
  try:
121
- im = Image.open(io.BytesIO(b))
122
- return im.size
123
  except Exception:
124
  return None
125
 
@@ -127,15 +131,14 @@ def infer_ext(data: bytes) -> str:
127
  try:
128
  im = Image.open(io.BytesIO(data))
129
  fmt = (im.format or "JPEG").lower()
130
- return "." + {"jpeg": "jpg", "jpg": "jpg", "png": "png", "webp": "webp"}.get(fmt, "jpg")
131
  except Exception:
132
  return ".jpg"
133
 
134
  @st.cache_data(ttl=300, show_spinner=False)
135
  def square_preview_bytes(img_bytes: bytes, size: int = PREVIEW_SIZE, bg_rgb=(255, 255, 255)) -> bytes:
136
  im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")
137
- w, h = im.size
138
- scale = min(size / w, size / h)
139
  new_w, new_h = max(1, int(w * scale)), max(1, int(h * scale))
140
  im_resized = im.resize((new_w, new_h), Image.LANCZOS)
141
  canvas = Image.new("RGBA", (size, size), (*bg_rgb, 255))
@@ -144,57 +147,40 @@ def square_preview_bytes(img_bytes: bytes, size: int = PREVIEW_SIZE, bg_rgb=(255
144
  buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
145
 
146
  def clamp(v: float, lo: float, hi: float) -> float:
147
- try:
148
- v = float(v)
149
- except Exception:
150
- v = (lo + hi) / 2.0
151
  return max(lo, min(hi, v))
152
 
153
  def first_present(d: dict, keys: list, default=None):
154
  for k in keys:
155
  v = d.get(k)
156
- if v:
157
- 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)
165
- col.caption("—")
166
- return None, None
167
-
168
  b = fetch_bytes(url)
169
  if not b:
170
- col.warning("Görsel yüklenemedi")
171
- return None, None
172
-
173
  pv = square_preview_bytes(b, size=PREVIEW_SIZE, bg_rgb=bg_rgb)
174
  col.image(pv, use_container_width=True)
175
  sz = image_size_from_bytes(b)
176
- if sz:
177
- col.caption(f"Gerçek çözünürlük: {sz[0]}×{sz[1]} px")
178
-
179
  ext = infer_ext(b)
180
- mime = "image/jpeg" if ext.lower() in [".jpg", ".jpeg"] else ("image/png" if ext.lower()==".png" else "image/webp")
181
- col.download_button(
182
- "İndir (tam kalite)",
183
- data=b,
184
- file_name=f"{filename_stub}{ext}",
185
- mime=mime,
186
- key=f"{key_prefix}_{filename_stub}_dl"
187
- )
188
  return url, b
189
 
190
  def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
191
  buf = io.BytesIO()
192
  with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
193
  for fname, b in named_bytes:
194
- if b:
195
- z.writestr(fname, b)
196
- buf.seek(0)
197
- return buf.read()
198
 
199
  # ================= Sayfa =================
200
  st.set_page_config(page_title="AI LightBox · Beauty", layout="wide", page_icon="💄")
@@ -204,37 +190,20 @@ st.caption("Packshot → Padding (canvas) → Try-on (lips/eyes/face/nails/hair/
204
  # ================= Üst Kontrol Şeridi =================
205
  ok = backend_ok()
206
  col1, col2, col3, col4, col5, col6, col7 = st.columns([1.8, 1.6, 1.4, 1.3, 0.9, 1.1, 0.9])
207
-
208
  with col1:
209
  file_list = st.file_uploader("Product image (upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
210
  with col2:
211
  image_url = st.text_input("or Product URL", key="image_url")
212
  with col3:
213
- category = st.selectbox(
214
- "Category",
215
- [
216
- # Lips
217
- "auto","lipstick","lipgloss","lipliner",
218
- # Eyes
219
- "eyeshadow","eyeliner","mascara","brow","false_lashes",
220
- # Face/complexion
221
- "foundation","concealer","blush","bronzer","contour","highlighter","body_makeup",
222
- # Nails
223
- "nail_polish",
224
- # Lifestyle
225
- "skincare","perfume","tools","set",
226
- # Hair/Body care
227
- "hair_color","haircare","shampoo","conditioner","body_lotion","body_care",
228
- ],
229
- index=0, key="category"
230
- )
231
  with col4:
232
- padding_ratio_val = st.number_input(
233
- "Content scale (0.30–0.95)",
234
- min_value=0.30, max_value=0.95, value=0.50, step=0.01,
235
- help="Kare tuvalde ürünün uzun kenarı = tuval × bu oran.",
236
- key="padding_ratio"
237
- )
238
  with col5:
239
  upscale = st.checkbox("Super Resolution", False, key="upscale")
240
  with col6:
@@ -242,7 +211,6 @@ with col6:
242
  with col7:
243
  upscale_factor = st.selectbox("SR factor", ["2","4"], index=0, key="upscale_factor")
244
 
245
- # == Tuval & Try-on kontrolleri ==
246
  t1, t2, t3, t4 = st.columns([1.15, 1.15, 1.2, 1.5])
247
  with t1:
248
  canvas_policy = st.selectbox("Canvas", ["fixed_1200","match_long_edge","keep_input"], index=0, key="canvas_policy")
@@ -261,61 +229,95 @@ with b2:
261
  with b3:
262
  identity_lock = st.checkbox("Identity lock", True, key="identity_lock")
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  cA, cB = st.columns([1.0, 1.0])
265
  with cA:
266
  st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
267
  with cB:
268
- run = st.button("Run", type="primary", use_container_width=True, key="run_btn")
269
 
270
- # ================= Girdi Önizleme =================
271
- c_in, c_pack, c_pad, c_try = st.columns(4)
272
  white_bg = (255, 255, 255)
273
 
274
  input_preview_bytes = None
275
  if file_list:
276
- try:
277
- input_preview_bytes = file_list[0].getvalue()
278
- except Exception:
279
- input_preview_bytes = None
280
- elif image_url:
281
- input_preview_bytes = fetch_bytes(image_url)
282
 
283
  if input_preview_bytes:
284
- c_in.subheader("Input")
285
- c_in.image(square_preview_bytes(input_preview_bytes, size=PREVIEW_SIZE, bg_rgb=white_bg), use_container_width=True)
286
  try:
287
  sz = Image.open(io.BytesIO(input_preview_bytes)).size
288
- c_in.caption(f"Input resolution: {sz[0]}×{sz[1]} px")
289
  except Exception:
290
- c_in.caption("—")
291
  else:
292
- show_tile(c_in, "Input", None)
293
 
294
  # ================ Koş & Sonuçları Kaydet ================
295
  if run:
296
  if not ok:
297
  st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
298
- elif not (file_list or image_url):
299
  st.error("En az bir product görseli girin (upload veya URL).")
300
  else:
301
  try:
302
  padding_ratio = clamp(padding_ratio_val, 0.30, 0.95)
303
  data = {
304
- "category": category,
305
  "edit_prompt": "",
306
  "num_images": "1",
307
- "image_urls": (image_url or ""),
308
  "mannequin_image_url": (model_ref_url or ""),
309
  "padding_ratio": str(padding_ratio),
310
  "identity_lock": "true" if identity_lock else "false",
311
- # Canvas
312
- "canvas_policy": canvas_policy,
313
  "canvas_size": str(int(canvas_size or 1200)),
314
- # Shade / Region
315
  "shade_hex": (shade_hex or "").strip() or None,
316
  "shade_swatch_url": (shade_swatch_url or "").strip() or None,
317
- "apply_region": apply_region,
318
- # SR
319
  "upscale": "true" if upscale else "false",
320
  "upscale_factor": st.session_state["upscale_factor"],
321
  "upscale_stage": st.session_state["upscale_stage"],
@@ -332,20 +334,16 @@ if run:
332
  packshot_url = None
333
  try:
334
  imgs = out["packshot_step"]["result"]["images"]
335
- if imgs:
336
- packshot_url = imgs[0].get("url")
337
- except Exception:
338
- 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:
346
- padded_url = pads[0]
347
- except Exception:
348
- pass
349
 
350
  packshot_sr_list = first_present(out.get("packshot_step", {}), [
351
  "upscaled_packshot_urls", "upscaled_packshot_files"
@@ -355,10 +353,8 @@ if run:
355
  placed_url = None
356
  try:
357
  imgs = out["image_step"]["result"]["images"]
358
- if imgs:
359
- placed_url = imgs[0].get("url")
360
- except Exception:
361
- pass
362
 
363
  final_sr_list = first_present(out.get("image_step", {}), [
364
  "upscaled_final_urls", "upscaled_final_files"
@@ -381,9 +377,8 @@ if run:
381
  "video_url": None,
382
  "api_base": API_BASE,
383
  "has_auth_header": bool(HEADERS),
384
- # UI state snapshot (etiket/isimlendirme için)
385
- "category": category,
386
- "apply_region": apply_region,
387
  }
388
 
389
  except requests.HTTPError as e:
@@ -398,22 +393,15 @@ if res:
398
  key_prefix = f"job{st.session_state['job_counter']}"
399
 
400
  p_title = "Packshot (SR)" if res.get("packshot_sr_url") else "Packshot"
401
- p_url, p_bytes = show_tile(
402
- c_pack, p_title, res.get("packshot_sr_url") or res.get("packshot_url"),
403
- bg_rgb=white_bg, filename_stub="packshot", key_prefix=key_prefix
404
- )
405
-
406
- pad_url, pad_bytes = show_tile(
407
- c_pad, "Padded", res.get("padded_url"),
408
- bg_rgb=white_bg, filename_stub="padded", key_prefix=key_prefix
409
- )
410
 
411
  shown_final_url = res.get("placed_sr_url") or res.get("placed_url")
412
  m_title = tryon_label(res.get("category","auto"), res.get("apply_region","auto"), bool(res.get("placed_sr_url")))
413
- m_url, m_bytes = show_tile(
414
- c_try, m_title, shown_final_url,
415
- bg_rgb=white_bg, filename_stub="tryon", key_prefix=key_prefix
416
- )
417
 
418
  # ---- Video (final görselden) ----
419
  st.markdown("---")
@@ -431,17 +419,11 @@ if res:
431
  st.warning("Video için final görsel yok.")
432
  else:
433
  with st.spinner("Generating video…"):
434
- v = post_video(
435
- src_for_video,
436
- duration=st.session_state.get("duration_select","6"),
437
- resolution="768P",
438
- prompt_optimizer=False
439
- )
440
  vurl = (v.get("result") or {}).get("video", {}).get("url")
441
- if vurl:
442
- st.session_state["results"]["video_url"] = vurl
443
- else:
444
- st.info("Video URL gelmedi.")
445
 
446
  vurl = st.session_state["results"].get("video_url")
447
  if vurl:
@@ -450,13 +432,8 @@ if res:
450
  vb = fetch_bytes(vurl)
451
  if vb:
452
  vbytes = vb
453
- st.download_button(
454
- "Download video (MP4)",
455
- data=vb,
456
- file_name="ai_lightbox_video.mp4",
457
- mime="video/mp4",
458
- key=f"{key_prefix}_video_dl"
459
- )
460
  else:
461
  st.info("Video URL var ama içerik indirilemedi.")
462
 
@@ -468,15 +445,9 @@ if res:
468
  if vbytes: named.append(("video.mp4", vbytes))
469
  if named:
470
  zip_bytes = make_zip(named)
471
- st.download_button(
472
- "Download all (ZIP)",
473
- data=zip_bytes,
474
- file_name="ai_lightbox_beauty_outputs.zip",
475
- mime="application/zip",
476
- key=f"{key_prefix}_zip_dl"
477
- )
478
-
479
- # Debug
480
  with st.expander("Debug"):
481
  st.json({
482
  "job_id": res.get("job_id"),
@@ -500,14 +471,10 @@ with st.sidebar:
500
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
501
  if st.button("Ping /health"):
502
  try:
503
- r = requests.get(f"{API_BASE}/health",
504
- headers=HEADERS if HF_TOKEN else None,
505
- timeout=10)
506
  st.write(r.status_code)
507
- try:
508
- st.json(r.json())
509
- except Exception:
510
- st.code((r.text or "")[:1000])
511
  except Exception as e:
512
  st.error(f"Health error: {e}")
513
  if st.button("Clear cache"):
 
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
5
  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
 
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
  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
  "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"
 
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()
 
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)
 
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
  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
  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
  # ================= Ü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
  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
  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
  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
  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
  "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
  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
  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
  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
  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
  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"):