renderfy commited on
Commit
f088758
·
verified ·
1 Parent(s): bf733bd

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +286 -309
streamlit_app.py CHANGED
@@ -1,169 +1,59 @@
1
- # streamlit_app.py — AI LightBox · Jewelry (Frontend · v0.6.1)
2
- # Not: Previews kartlarının yapısı korunmuştur. Dark turuncu tema + modern Jewelry UI.
 
3
  import os, io, base64, zipfile, requests, streamlit as st
4
  from PIL import Image
5
 
6
- # ======================= PAGE CONFIG (ilk satırlarda olmalı) =======================
7
- st.set_page_config(page_title="AI LightBox · Jewelry", layout="wide", page_icon="💎")
8
-
9
- # ======================= THEME (dark + jewelry orange) =======================
10
- def apply_theme():
11
- THEME = {
12
- "base": "dark",
13
- "primaryColor": "#ff7a1a",
14
- "backgroundColor": "#0e0f12",
15
- "secondaryBackgroundColor": "#14161b",
16
- "textColor": "#e7eaee",
17
- "mutedColor": "#9aa1ab",
18
- "cardOutline": "rgba(255,255,255,0.06)",
19
- "accent2": "#ffd6b0",
20
- "accent3": "#2a2f3a",
21
- }
22
- try:
23
- st._config.set_option("theme.base", THEME["base"])
24
- st._config.set_option("theme.primaryColor", THEME["primaryColor"])
25
- st._config.set_option("theme.backgroundColor", THEME["backgroundColor"])
26
- st._config.set_option("theme.secondaryBackgroundColor", THEME["secondaryBackgroundColor"])
27
- st._config.set_option("theme.textColor", THEME["textColor"])
28
- except Exception:
29
- pass
30
-
31
- st.markdown(f"""
32
- <style>
33
- :root {{
34
- --bg: {THEME["backgroundColor"]};
35
- --sb: {THEME["secondaryBackgroundColor"]};
36
- --text: {THEME["textColor"]};
37
- --muted: {THEME["mutedColor"]};
38
- --primary: {THEME["primaryColor"]};
39
- --outline: {THEME["cardOutline"]};
40
- --accent2: {THEME["accent2"]};
41
- --accent3: {THEME["accent3"]};
42
- }}
43
-
44
- /* Header beyaz bant FIX */
45
- header[data-testid="stHeader"] {{ background: transparent !important; box-shadow:none !important; border:0 !important; }}
46
- header[data-testid="stHeader"] * {{ background: transparent !important; box-shadow:none !important; }}
47
- .block-container {{ padding-top: .6rem !important; }}
48
-
49
- /* Background */
50
- .stApp {{
51
- background:
52
- radial-gradient(1200px 800px at 0% 0%, rgba(255,122,26,0.06), transparent 50%),
53
- radial-gradient(1000px 700px at 100% 100%, rgba(255,122,26,0.04), transparent 50%),
54
- var(--bg) !important;
55
- }}
56
-
57
- /* Hero */
58
- .hero-wrap {{ position: relative; z-index: 5; }}
59
- .hero {{
60
- padding: 16px 18px; border-radius: 16px;
61
- background: linear-gradient(180deg, rgba(255,122,26,0.12) 0%, rgba(255,122,26,0.06) 100%);
62
- box-shadow: 0 0 0 1px rgba(255,122,26,0.18) inset, 0 6px 30px rgba(0,0,0,0.35);
63
- }}
64
- .hero h1 {{ margin:0; font-size:1.4rem; color: var(--accent2); }}
65
- .hero p {{ margin:4px 0 0 0; color:var(--text); opacity:.9; }}
66
-
67
- /* Panels */
68
- .panel {{
69
- background: linear-gradient(180deg, rgba(20,22,27,0.9) 0%, rgba(20,22,27,0.75) 100%);
70
- border-radius: 16px; padding: 14px 14px 8px 14px; margin-top: 12px;
71
- box-shadow: 0 0 0 1px var(--outline) inset, 0 10px 30px rgba(0,0,0,0.35);
72
- }}
73
- .panel h3 {{
74
- margin: 0 0 10px 0; font-size: 1.05rem; font-weight: 700; color: var(--primary);
75
- display: inline-flex; align-items: center; gap: 8px;
76
- }}
77
-
78
- /* Chips */
79
- .chipbar {{ display:flex; flex-wrap:wrap; gap:8px; margin:6px 0 2px 0; }}
80
- .chip {{
81
- display:inline-flex; align-items:center; gap:8px; padding:6px 10px; border-radius:999px;
82
- background: rgba(255,122,26,0.10); color: var(--accent2); border: 1px solid rgba(255,122,26,0.25);
83
- font-size:.85rem; font-weight:600; letter-spacing:.2px;
84
- }}
85
- .chip .dot {{ width:8px; height:8px; border-radius:50%; background:#2ee06b; box-shadow:0 0 0 2px rgba(46,224,107,0.15); }}
86
- .chip.muted {{ background: rgba(154,161,171,0.10); color: var(--text); border-color: rgba(154,161,171,0.25); }}
87
- .chip.badge {{ background: rgba(42,47,58,0.9); color: var(--text); border-color: rgba(255,255,255,0.08); }}
88
-
89
- /* Labels & captions: turuncu */
90
- label, [data-testid="stWidgetLabel"] p, .lb-card-title, h2, h3, .stCaption, .stMarkdown > p {{ color: var(--primary) !important; }}
91
-
92
- /* Buttons */
93
- .stButton > button {{
94
- background: var(--primary) !important; color: #101215 !important; font-weight: 800;
95
- border: none; border-radius: 12px; padding: .55rem .9rem;
96
- box-shadow: 0 0 0 1px #00000033 inset, 0 8px 18px rgba(255,122,26,0.25);
97
- transition: transform .08s ease;
98
- }}
99
- .stButton > button:hover {{ filter: brightness(1.06); transform: translateY(-1px); }}
100
- .stDownloadButton > button {{
101
- background: transparent !important; color: var(--primary) !important;
102
- border: 1px solid var(--primary); border-radius: 12px; font-weight:700;
103
- }}
104
- .stDownloadButton > button:hover {{ filter: brightness(1.1); }}
105
-
106
- /* Inputs */
107
- .stTextInput, .stNumberInput, .stSelectbox, .stFileUploader, .stCheckbox, .stSlider, .stRadio {{
108
- background: var(--accent3) !important; border-radius: 12px !important;
109
- box-shadow: 0 0 0 1px var(--outline) inset !important; padding: 6px 8px !important;
110
- }}
111
- [data-testid="stFileUploaderDropzone"] {{
112
- background: rgba(42,47,58,0.6); border: 1px dashed rgba(255,255,255,0.15); border-radius: 12px;
113
- }}
114
-
115
- /* Alerts */
116
- .stAlert, .stAlert p, .stAlert span {{
117
- color: var(--accent2) !important; background: rgba(255,122,26,0.08) !important;
118
- border-radius: 12px; box-shadow: 0 0 0 1px rgba(255,122,26,0.25) inset;
119
- }}
120
-
121
- /* ---- Previews cards (STRUCTURE UNCHANGED) ---- */
122
- .lb-card-title {{ font-weight:700; font-size:1.15rem; margin:0 0 8px 0; color: var(--primary); }}
123
- .lb-frame {{
124
- border-radius: 12px; overflow: hidden; background: var(--sb);
125
- display:flex; align-items:center; justify-content:center;
126
- box-shadow: 0 0 0 1px var(--outline) inset;
127
- }}
128
- </style>
129
- """, unsafe_allow_html=True)
130
-
131
- apply_theme()
132
-
133
- # ======================= Env & API =======================
134
  API_BASE = (os.getenv("AI_LIGHTBOX_API", "http://127.0.0.1:8000") or "http://127.0.0.1:8000").strip().strip("'\"").rstrip("/")
135
  HF_TOKEN = (os.getenv("AI_LIGHTBOX_TOKEN", "") or "").strip().strip("'\"")
136
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
137
 
 
138
  VIDEO_MAX_PX_DEFAULT = int(os.getenv("VIDEO_MAX_PX", "720"))
139
  VIDEO_RES_OPTIONS = ["512P", "768P"]
140
  VIDEO_DUR_OPTIONS = ["6", "10"]
141
- ASPECT_OPTIONS = ["9:16", "3:4", "1:1"]
142
 
143
- # ======================= Session =======================
144
- defaults = {"results": None, "job_counter": 0, "video_ui_max_px": VIDEO_MAX_PX_DEFAULT, "health": {}}
145
- for k, v in defaults.items():
146
- if k not in st.session_state: st.session_state[k] = v
 
 
 
147
 
148
- # ======================= HTTP helpers =======================
149
  def _needs_auth(url: str) -> bool:
150
  return url.startswith(API_BASE) or url.startswith("outputs/") or url.startswith("/outputs/")
151
 
152
  def _abs_backend_url(url_or_path: str) -> str:
153
- if not url_or_path: return ""
154
- if url_or_path.startswith(("http://","https://")): return url_or_path
155
- if url_or_path.startswith("/outputs/"): return f"{API_BASE}{url_or_path}"
156
- if url_or_path.startswith("outputs/"): return f"{API_BASE}/{url_or_path}"
 
 
 
 
157
  return f"{API_BASE}/{url_or_path.lstrip('/')}"
158
 
159
  def backend_ok() -> bool:
160
  try:
161
  r = requests.get(f"{API_BASE}/health", headers=HEADERS, timeout=10)
162
  r.raise_for_status()
163
- st.session_state["health"] = r.json()
164
  return True
165
  except Exception:
166
- st.session_state["health"] = {}
167
  return False
168
 
169
  def post_chain(data: dict, files_payload):
@@ -196,7 +86,6 @@ def fetch_bytes(url_or_path: str):
196
  except Exception:
197
  return None
198
 
199
- # ======================= Image utils =======================
200
  def _sniff_mime_from_bytes(b: bytes) -> str:
201
  try:
202
  fmt = (Image.open(io.BytesIO(b)).format or "JPEG").lower()
@@ -205,7 +94,8 @@ def _sniff_mime_from_bytes(b: bytes) -> str:
205
  return "image/jpeg"
206
 
207
  def image_bytes_to_data_url(b: bytes, mime: str | None = None) -> str:
208
- if not mime: mime = _sniff_mime_from_bytes(b)
 
209
  enc = base64.b64encode(b).decode("ascii")
210
  return f"data:{mime};base64,{enc}"
211
 
@@ -222,51 +112,87 @@ def infer_ext(data: bytes) -> str:
222
  fmt = (im.format or "JPEG").lower()
223
  return "." + {"jpeg":"jpg","jpg":"jpg","png":"png","webp":"webp"}.get(fmt,"jpg")
224
  except Exception:
225
- if data[:4] == b"\x00\x00\x00\x18" or data[4:8] == b"ftyp": return ".mp4"
 
226
  return ".bin"
227
 
228
- def rgba_to_rgb_on_bg(img_rgba: Image.Image, bg_rgb=(23,26,31)) -> Image.Image:
229
- if img_rgba.mode != "RGBA": return img_rgba.convert("RGB")
230
- bg = Image.new("RGBA", img_rgba.size, (*bg_rgb, 255))
231
- bg.paste(img_rgba, (0,0), img_rgba)
232
- return bg.convert("RGB")
233
-
234
- @st.cache_data(ttl=300, show_spinner=False)
235
- def make_small_jpeg_from_native(native: bytes, target_long_edge: int = 1280, bg_rgb=(23,26,31)) -> bytes:
236
- im = Image.open(io.BytesIO(native))
237
- if im.mode != "RGBA" and "transparency" not in im.info: im = im.convert("RGB")
238
- else: im = im.convert("RGBA")
239
- w, h = im.size
240
- if w >= h:
241
- new_w = min(target_long_edge, w); new_h = max(1, int(h * (new_w / max(1, w))))
242
- else:
243
- new_h = min(target_long_edge, h); new_w = max(1, int(w * (new_h / max(1, h))))
244
- im2 = im.resize((new_w, new_h), Image.LANCZOS)
245
- im2_rgb = rgba_to_rgb_on_bg(im2, bg_rgb=bg_rgb)
246
- out = io.BytesIO(); im2_rgb.save(out, format="JPEG", quality=88, subsampling=1)
247
- return out.getvalue()
248
 
 
249
  def _parse_aspect_str(s: str):
250
  try:
251
  a, b = s.split(":"); return max(1, int(a)), max(1, int(b))
252
  except Exception:
253
  return (9, 16)
254
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  def inject_preview_css(aspect: str, fit_mode="contain"):
256
  aw, ah = _parse_aspect_str(aspect)
257
  st.markdown(f"""
258
  <style>
259
- .lb-card-title {{ font-weight:700; font-size:1.15rem; margin:0 0 8px 0; color: var(--primary); }}
260
- .lb-frame {{
261
- position: relative; width: 100%;
262
- aspect-ratio: {aw} / {ah};
263
- border-radius: 12px; overflow: hidden; background: var(--sb);
264
- display:flex; align-items:center; justify-content:center;
265
- box-shadow: 0 0 0 1px var(--outline) inset;
266
- }}
267
- .lb-frame img {{ width:100%; height:100%; object-fit:{fit_mode}; display:block; }}
268
- .lb-video {{ display:flex; justify-content:center; }}
269
- .lb-video video {{ border-radius:8px; max-width: 540px; width:100%; height:auto; }}
270
  </style>
271
  """, unsafe_allow_html=True)
272
 
@@ -279,21 +205,25 @@ def show_tile_card_display_download(
279
  filename_stub: str = "image",
280
  key_prefix: str = "",
281
  ):
 
282
  col.markdown(f'<div class="lb-card-title">{title}</div>', unsafe_allow_html=True)
283
 
 
284
  if not display_url and not display_bytes:
285
  col.markdown('<div class="lb-frame"></div>', unsafe_allow_html=True)
286
  col.caption("—"); return None, None
287
 
288
  if display_bytes is not None:
289
- src = image_bytes_to_data_url(display_bytes)
290
  col.markdown(f'<div class="lb-frame"><img src="{src}"/></div>', unsafe_allow_html=True)
291
  else:
292
  abs_url = _abs_backend_url(display_url)
293
  col.markdown(f'<div class="lb-frame"><img src="{abs_url}"/></div>', unsafe_allow_html=True)
294
 
 
295
  native = download_bytes or (fetch_bytes(download_url) if download_url else None)
296
- if not native: native = display_bytes
 
297
 
298
  if native:
299
  sz = image_size_from_bytes(native)
@@ -308,104 +238,139 @@ def show_tile_card_display_download(
308
 
309
  return display_url or "data", native
310
 
311
- # ======================= Hero =======================
312
- st.markdown("""
313
- <div class="hero-wrap">
314
- <div class="hero">
315
- <h1>💎 AI LightBox · Jewelry Studio</h1>
316
- <p>Packshot, mannequin placement ve kısa video — tek akış.</p>
317
- </div>
318
- </div>
319
- """, unsafe_allow_html=True)
320
-
321
- # ======================= Status chips =======================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  ok = backend_ok()
323
- version = st.session_state["health"].get("version", "—")
324
- st.markdown(f"""
325
- <div class="chipbar">
326
- <div class="chip"><span class="dot"></span>{'Online' if ok else 'Offline'}</div>
327
- <div class="chip badge">Backend v{version}</div>
328
- </div>
329
- """, unsafe_allow_html=True)
330
-
331
- # ======================= Panels: Inputs · Studio Controls · Video =======================
332
- st.markdown('<div class="panel"> <h3>🧰 Inputs</h3>', unsafe_allow_html=True)
333
- col1, col2, col3 = st.columns([1.6, 1.6, 1.0])
334
  with col1:
335
  file_list = st.file_uploader("Product Image (Upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
336
  with col2:
337
- image_url = st.text_input("Or URL", key="image_url")
338
  with col3:
339
  jewel_type = st.selectbox("Type", ["auto","earring","ring","necklace","bracelet","pendant","brooch","set","watch"], index=0, key="jewel_type")
340
- st.markdown('</div>', unsafe_allow_html=True)
341
-
342
- st.markdown('<div class="panel"> <h3>🪄 Studio Controls</h3>', unsafe_allow_html=True)
343
- colS1, colS2, colS3, colS4 = st.columns([1.1, 1.1, 1.1, 1.1])
344
- with colS1:
345
  preview_ratio = st.number_input("Preview Scale (0.10–0.99)", min_value=0.10, max_value=0.99, value=0.50, step=0.01,
346
  help="Sadece önizleme kadrajında büyüklüğü etkiler.", key="preview_ratio")
347
- with colS2:
348
- preview_aspect = st.selectbox("Preview Frame Ratio", ASPECT_OPTIONS, index=0, key="preview_aspect")
349
- with colS3:
350
- preview_long_edge = st.number_input("Preview Long Edge (px)", min_value=256, max_value=8192, value=1920, step=64, key="preview_long_edge")
351
- with colS4:
352
- fit_mode = st.selectbox("Preview Layout", ["fit","fill"], index=0, help="Sadece önizlemeyi etkiler.", key="fit_mode")
353
-
354
- colS5, colS6, colS7, colS8 = st.columns([1.0, 1.0, 1.0, 1.0])
355
- with colS5:
356
  upscale = st.checkbox("Super Res", False, key="upscale")
357
- with colS6:
358
  upscale_stage = st.selectbox("SR Step", ["both","final","packshot"], index=0, help="Varsayılan: both", key="upscale_stage")
359
- with colS7:
360
  upscale_factor = st.selectbox("SR Factor", ["2","4"], index=0, key="upscale_factor")
361
- with colS8:
362
- identity_lock = st.checkbox("Identity lock", True, key="identity_lock")
363
- st.markdown('</div>', unsafe_allow_html=True)
364
 
365
- st.markdown('<div class="panel"> <h3>🎬 Video</h3>', unsafe_allow_html=True)
366
- colV1, colV2, colV3, colV4 = st.columns([1.0, 1.0, 1.0, 1.0])
367
- with colV1:
 
 
 
368
  to_video = st.checkbox("One Click Generate Video", False, key="to_video")
369
- with colV2:
370
  duration = st.selectbox("Duration ", VIDEO_DUR_OPTIONS, index=0, key="duration_sel")
371
- with colV3:
372
  resolution = st.selectbox("Video Res", VIDEO_RES_OPTIONS, index=1, key="resolution_sel")
373
- with colV4:
374
  run = st.button("Run 💎", type="primary", use_container_width=True, key="run_btn")
375
- st.markdown('</div>', unsafe_allow_html=True)
376
 
377
- # Inject CSS for previews (unchanged structure)
378
- inject_preview_css(st.session_state.get("preview_aspect","9:16"),
379
- fit_mode="contain" if st.session_state.get("fit_mode","fit")=="fit" else "cover")
 
 
 
 
 
380
 
381
- # ======================= PREVIEWS (UNCHANGED STRUCTURE) =======================
 
 
 
382
  st.subheader("Previews")
383
  col_in, col_pack, col_pad, col_vid = st.columns(4)
384
 
385
- # Input bytes (upload/URL/demos)
386
  input_bytes = None
387
  if file_list:
388
  try: input_bytes = file_list[0].getvalue()
389
  except Exception: input_bytes = None
390
  elif image_url:
391
  input_bytes = fetch_bytes(image_url)
392
- else:
393
- if os.path.exists("input.jpg"):
394
- with open("input.jpg", "rb") as f: input_bytes = f.read()
395
 
396
  show_tile_card_display_download(
397
  col_in, "Sample Input preview",
398
- display_bytes=input_bytes,
399
- download_bytes=input_bytes,
400
  filename_stub="input", key_prefix="pre"
401
  )
402
 
403
- pack_bytes_demo = None
404
- if os.path.exists("packshot.jpg"):
405
- with open("packshot.jpg", "rb") as f: pack_bytes_demo = f.read()
406
- if not pack_bytes_demo:
407
- pack_bytes_demo = input_bytes
408
-
409
  show_tile_card_display_download(
410
  col_pack, "Sample Packshot (BG Removed)",
411
  display_bytes=pack_bytes_demo,
@@ -413,41 +378,25 @@ show_tile_card_display_download(
413
  filename_stub="packshot", key_prefix="pre"
414
  )
415
 
416
- # Padded (UI-only)
417
  padded_bytes = None
418
  if pack_bytes_demo:
419
  try:
420
- im = Image.open(io.BytesIO(pack_bytes_demo)).convert("RGBA")
421
- aw, ah = (lambda s: (int(s.split(":")[0]), int(s.split(":")[1])))(st.session_state.get("preview_aspect","9:16"))
422
- if aw >= ah:
423
- W = int(st.session_state.get("preview_long_edge", 1920)); H = max(1, int(W * ah / aw))
424
- else:
425
- H = int(st.session_state.get("preview_long_edge", 1920)); W = max(1, int(H * aw / ah))
426
- ratio_val = max(0.10, min(0.99, float(st.session_state.get("preview_ratio", 0.5))))
427
- w, h = im.size
428
- target = int(min(W, H) * ratio_val if aw == ah else (H * ratio_val if aw < ah else W * ratio_val))
429
- if w >= h: new_w, new_h = target, max(1, int(h * (target / max(1, w))))
430
- else: new_h, new_w = target, max(1, int(w * (target / max(1, h))))
431
- im_resized = im.resize((new_w, new_h), Image.LANCZOS)
432
- canvas = Image.new("RGBA", (W, H), (255,255,255,255))
433
- off = ((W - new_w)//2, (H - new_h)//2); canvas.paste(im_resized, off, im_resized)
434
- out = io.BytesIO(); canvas.convert("RGB").save(out, format="JPEG", quality=90, subsampling=1)
435
- padded_bytes = out.getvalue()
436
  except Exception:
437
  padded_bytes = None
438
-
439
  show_tile_card_display_download(
440
- col_pad, "Sample Padded Preview",
441
  display_bytes=padded_bytes or pack_bytes_demo,
442
  download_bytes=padded_bytes or pack_bytes_demo,
443
  filename_stub="padded_ui", key_prefix="pre"
444
  )
445
 
446
- # Video demo
447
- vid_demo = None
448
- if os.path.exists("tryon.mp4"):
449
- with open("tryon.mp4", "rb") as f: vid_demo = f.read()
450
- col_vid.markdown('<div class="lb-card-title">AI Generated Video Preview</div>', unsafe_allow_html=True)
451
  if vid_demo:
452
  data_url = "data:video/mp4;base64," + base64.b64encode(vid_demo).decode("ascii")
453
  col_vid.markdown(
@@ -464,11 +413,11 @@ else:
464
  col_vid.markdown('<div class="lb-frame"></div>', unsafe_allow_html=True)
465
  col_vid.caption("—")
466
 
467
- # ======================= RUN =======================
468
  if run:
469
  if not ok:
470
- st.error("Bağlantı sorunu. API_BASE / TOKEN kontrol edin.")
471
- elif not (file_list or image_url or input_bytes):
472
  st.error("En az bir görsel girişi yapın (dosya yükle veya URL).")
473
  else:
474
  try:
@@ -484,43 +433,68 @@ if run:
484
  "num_images": "1",
485
  "image_urls": img_urls,
486
  "mannequin_image_url": "",
487
- "identity_lock": "true" if st.session_state.get("identity_lock", True) else "false",
488
- "preview_aspect": st.session_state.get("preview_aspect","9:16"),
489
- "preview_long_edge": str(int(st.session_state.get("preview_long_edge", 1920))),
490
- "preview_ratio": str(float(st.session_state.get("preview_ratio", 0.5))),
491
- "padding_ratio": str(float(st.session_state.get("preview_ratio", 0.5))),
492
- "upscale": "true" if st.session_state.get("upscale", False) else "false",
 
 
493
  "upscale_factor": st.session_state.get("upscale_factor","2"),
494
  "upscale_stage": st.session_state.get("upscale_stage","both"),
495
- "to_video": "true" if st.session_state.get("to_video", False) else "false",
 
496
  "video_prompt": "",
497
  "duration": st.session_state.get("duration_sel", VIDEO_DUR_OPTIONS[0]),
498
  "resolution": st.session_state.get("resolution_sel", VIDEO_RES_OPTIONS[1]),
499
  "prompt_optimizer": "false",
500
  }
501
 
502
- with st.spinner("AI chain çalışıyor…"):
503
  out = post_chain(data, files_payload if files_payload else None)
504
 
505
  st.session_state["job_counter"] += 1
506
 
507
- pack_native_url = (out.get("packshot_step", {}).get("result", {}).get("images_native") or [{}])[0].get("url")
508
- padded_native_url = (out.get("packshot_step", {}).get("result", {}).get("padded_native") or [{}])[0].get("url")
509
- placed_native_url = (out.get("image_step", {}).get("result", {}).get("images_native") or [{}])[0].get("url")
510
- vurl = (out.get("video_step") or {}).get("result", {}).get("video", {}).get("url")
511
-
512
- pack_display_jpeg = None
513
- if pack_native_url:
514
- pn = fetch_bytes(pack_native_url)
515
- if pn: pack_display_jpeg = make_small_jpeg_from_native(pn, 1280, bg_rgb=(23,26,31))
516
- padded_display_jpeg = None
517
- if padded_native_url:
518
- nb = fetch_bytes(padded_native_url)
519
- if nb: padded_display_jpeg = make_small_jpeg_from_native(nb, 1280, bg_rgb=(23,26,31))
520
- placed_display_jpeg = None
521
- if placed_native_url:
522
- mn = fetch_bytes(placed_native_url)
523
- if mn: placed_display_jpeg = make_small_jpeg_from_native(mn, 1280, bg_rgb=(23,26,31))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
 
525
  st.session_state["results"] = {
526
  "job_id": out.get("job_id"),
@@ -534,7 +508,7 @@ if run:
534
  "video_url": vurl,
535
  "api_base": API_BASE,
536
  "has_auth_header": bool(HEADERS),
537
- "preview_aspect": st.session_state.get("preview_aspect","9:16"),
538
  }
539
 
540
  except requests.HTTPError as e:
@@ -542,13 +516,14 @@ if run:
542
  except Exception as e:
543
  st.error(f"Hata: {e}")
544
 
545
- # ======================= RESULTS =======================
546
  res = st.session_state.get("results")
547
  vbytes = None
548
  if res:
549
- st.markdown('<div class="panel"><h3>🧾 Product Results</h3>', unsafe_allow_html=True)
550
  c_pack, c_pad, c_model, c_vid = st.columns(4)
551
 
 
552
  show_tile_card_display_download(
553
  c_pack, "Packshot (Removed BG)",
554
  display_bytes=res.get("packshot_display_jpeg"),
@@ -556,6 +531,8 @@ if res:
556
  filename_stub="packshot_native",
557
  key_prefix=f"job{st.session_state['job_counter']}"
558
  )
 
 
559
  show_tile_card_display_download(
560
  c_pad, "Padded",
561
  display_bytes=res.get("padded_display_jpeg"),
@@ -563,6 +540,8 @@ if res:
563
  filename_stub="padded_backend",
564
  key_prefix=f"job{st.session_state['job_counter']}"
565
  )
 
 
566
  show_tile_card_display_download(
567
  c_model, "TryON Model",
568
  display_bytes=res.get("placed_display_jpeg"),
@@ -571,6 +550,7 @@ if res:
571
  key_prefix=f"job{st.session_state['job_counter']}"
572
  )
573
 
 
574
  vurl = res.get("video_url")
575
  c_vid.markdown('<div class="lb-card-title">AI Generated Video</div>', unsafe_allow_html=True)
576
  if vurl:
@@ -605,14 +585,16 @@ if res:
605
  prompt_optimizer=False,
606
  )
607
  vurl2 = (v.get("result") or {}).get("video", {}).get("url")
 
608
  if vurl2:
609
  st.session_state["results"]["video_url"] = vurl2
610
- st.success("Video hazır.")
 
 
611
  else:
612
  st.info("Video URL gelmedi.")
613
- st.markdown('</div>', unsafe_allow_html=True)
614
 
615
- # Bundle ZIP
616
  named = []
617
  pn = fetch_bytes(res.get("packshot_native_url")) if res.get("packshot_native_url") else None
618
  mn = fetch_bytes(res.get("placed_native_url")) if res.get("placed_native_url") else None
@@ -620,18 +602,13 @@ if res:
620
  if mn: named.append((f"model_native{infer_ext(mn)}", mn))
621
  if vbytes: named.append(("video.mp4", vbytes))
622
  if named:
623
- buf = io.BytesIO()
624
- with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
625
- for fname, b in named:
626
- if b: z.writestr(fname, b)
627
- buf.seek(0)
628
- st.download_button("Download All (ZIP)", data=buf.read(),
629
  file_name="ai_lightbox_outputs.zip", mime="application/zip",
630
  key=f"job{st.session_state['job_counter']}_zip_dl")
631
 
632
- # ======================= Sidebar =======================
633
  with st.sidebar:
634
- st.markdown("### ⚙️ Studio Settings")
635
  st.caption(f"API_BASE: {API_BASE}")
636
  if HF_TOKEN: st.caption("Auth: Bearer (aktif)")
637
  st.slider("Video önizleme genişliği (px)", 360, 1080, st.session_state["video_ui_max_px"],
 
1
+ # streamlit_app.py — AI LightBox · Jewelry (dark-orange UI)
2
+ # Pre-run sabit 4 kart (Input/Packshot/Padded/Video loop) + Run sonrası native odaklı kartlar
3
+
4
  import os, io, base64, zipfile, requests, streamlit as st
5
  from PIL import Image
6
 
7
+ # ===== Theme (programmatic dark orange) =====
8
+ try:
9
+ st._config.set_option("theme.base", "dark")
10
+ st._config.set_option("theme.primaryColor", "#ff7a1a") # dark orange
11
+ st._config.set_option("theme.backgroundColor", "#0e0f12") # app bg
12
+ st._config.set_option("theme.secondaryBackgroundColor", "#161a1f")
13
+ st._config.set_option("theme.textColor", "#e7e9ed")
14
+ except Exception:
15
+ pass
16
+
17
+ # ================= Env & API =================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  API_BASE = (os.getenv("AI_LIGHTBOX_API", "http://127.0.0.1:8000") or "http://127.0.0.1:8000").strip().strip("'\"").rstrip("/")
19
  HF_TOKEN = (os.getenv("AI_LIGHTBOX_TOKEN", "") or "").strip().strip("'\"")
20
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
21
 
22
+ # UI vars
23
  VIDEO_MAX_PX_DEFAULT = int(os.getenv("VIDEO_MAX_PX", "720"))
24
  VIDEO_RES_OPTIONS = ["512P", "768P"]
25
  VIDEO_DUR_OPTIONS = ["6", "10"]
26
+ ASPECT_OPTIONS = ["9:16", "3:4", "1:1"] # preview kadraj oranları
27
 
28
+ # ================= Session =================
29
+ if "results" not in st.session_state:
30
+ st.session_state["results"] = None
31
+ if "job_counter" not in st.session_state:
32
+ st.session_state["job_counter"] = 0
33
+ if "video_ui_max_px" not in st.session_state:
34
+ st.session_state["video_ui_max_px"] = VIDEO_MAX_PX_DEFAULT
35
 
36
+ # ================= HTTP/Backend helpers =================
37
  def _needs_auth(url: str) -> bool:
38
  return url.startswith(API_BASE) or url.startswith("outputs/") or url.startswith("/outputs/")
39
 
40
  def _abs_backend_url(url_or_path: str) -> str:
41
+ if not url_or_path:
42
+ return ""
43
+ if url_or_path.startswith(("http://","https://")):
44
+ return url_or_path
45
+ if url_or_path.startswith("/outputs/"):
46
+ return f"{API_BASE}{url_or_path}"
47
+ if url_or_path.startswith("outputs/"):
48
+ return f"{API_BASE}/{url_or_path}"
49
  return f"{API_BASE}/{url_or_path.lstrip('/')}"
50
 
51
  def backend_ok() -> bool:
52
  try:
53
  r = requests.get(f"{API_BASE}/health", headers=HEADERS, timeout=10)
54
  r.raise_for_status()
 
55
  return True
56
  except Exception:
 
57
  return False
58
 
59
  def post_chain(data: dict, files_payload):
 
86
  except Exception:
87
  return None
88
 
 
89
  def _sniff_mime_from_bytes(b: bytes) -> str:
90
  try:
91
  fmt = (Image.open(io.BytesIO(b)).format or "JPEG").lower()
 
94
  return "image/jpeg"
95
 
96
  def image_bytes_to_data_url(b: bytes, mime: str | None = None) -> str:
97
+ if not mime:
98
+ mime = _sniff_mime_from_bytes(b)
99
  enc = base64.b64encode(b).decode("ascii")
100
  return f"data:{mime};base64,{enc}"
101
 
 
112
  fmt = (im.format or "JPEG").lower()
113
  return "." + {"jpeg":"jpg","jpg":"jpg","png":"png","webp":"webp"}.get(fmt,"jpg")
114
  except Exception:
115
+ if data[:4] == b"\x00\x00\x00\x18" or data[4:8] == b"ftyp":
116
+ return ".mp4"
117
  return ".bin"
118
 
119
+ def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
120
+ buf = io.BytesIO()
121
+ with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
122
+ for fname, b in named_bytes:
123
+ if b: z.writestr(fname, b)
124
+ buf.seek(0); return buf.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ # ================= Preview helpers (uniform cards) =================
127
  def _parse_aspect_str(s: str):
128
  try:
129
  a, b = s.split(":"); return max(1, int(a)), max(1, int(b))
130
  except Exception:
131
  return (9, 16)
132
 
133
+ def inject_base_css():
134
+ st.markdown("""
135
+ <style>
136
+ /* App-wide */
137
+ .stApp { background: #0e0f12; }
138
+ header[data-testid="stHeader"] { background: transparent; }
139
+ .block-container { padding-top: 1.25rem; }
140
+
141
+ /* Title + caption */
142
+ h1, .stMarkdown h1 { font-size: 1.85rem; line-height: 1.3; margin-bottom: 0.15rem; }
143
+ .stMarkdown p { margin-top: 0.15rem; }
144
+
145
+ /* Labels to dark-orange */
146
+ label, .stTextInput label, .stSelectbox label, .stNumberInput label, .stFileUploader label,
147
+ .stCheckbox label, .stSlider label, .stRadio label {
148
+ color: #ff8a33 !important;
149
+ font-weight: 600 !important;
150
+ }
151
+
152
+ /* Primary buttons (also Run 💎) */
153
+ .stButton > button[kind="primary"]{
154
+ background: #ff7a1a !important; border: 0 !important; color: #111 !important;
155
+ }
156
+ .stButton > button:hover { filter: brightness(1.05); }
157
+
158
+ /* Info/Warning boxes tweak */
159
+ .stAlert { border-radius: 10px; }
160
+
161
+ /* Inputs container subtle borders */
162
+ .stSelectbox, .stTextInput, .stNumberInput, .stFileUploader, .stCheckbox, .stSlider, .stRadio {
163
+ --border-color: rgba(255,255,255,0.08);
164
+ }
165
+
166
+ /* Card titles */
167
+ .lb-card-title {
168
+ font-weight: 700; font-size: 1.05rem; margin: 2px 0 8px 0; color: #eaecef;
169
+ }
170
+
171
+ /* Frames: FIX white band — make them dark and subtle outline */
172
+ .lb-frame {
173
+ position: relative; width: 100%;
174
+ border-radius: 12px; overflow: hidden;
175
+ background: #111419; /* dark, not white */
176
+ display:flex; align-items:center; justify-content:center;
177
+ box-shadow: 0 0 0 1px rgba(255,122,26,0.15) inset; /* orange-tinted inner border */
178
+ }
179
+ .lb-frame img { width:100%; height:100%; display:block; }
180
+
181
+ /* Video styling */
182
+ .lb-video { display:flex; justify-content:center; }
183
+ .lb-video video { border-radius:8px; max-width: 540px; width:100%; height:auto; }
184
+
185
+ /* Orange accents for captions */
186
+ .stCaption, .st-emotion-cache-1r6slb0 { color: #ffb07a !important; }
187
+ </style>
188
+ """, unsafe_allow_html=True)
189
+
190
  def inject_preview_css(aspect: str, fit_mode="contain"):
191
  aw, ah = _parse_aspect_str(aspect)
192
  st.markdown(f"""
193
  <style>
194
+ .lb-frame {{ aspect-ratio: {aw} / {ah}; }}
195
+ .lb-frame img {{ object-fit:{fit_mode}; }}
 
 
 
 
 
 
 
 
 
196
  </style>
197
  """, unsafe_allow_html=True)
198
 
 
205
  filename_stub: str = "image",
206
  key_prefix: str = "",
207
  ):
208
+ """Gösterimde HAFİF JPEG/PNG (display_bytes) tercih edilir; indirmede NATIVE."""
209
  col.markdown(f'<div class="lb-card-title">{title}</div>', unsafe_allow_html=True)
210
 
211
+ # ---- Display ----
212
  if not display_url and not display_bytes:
213
  col.markdown('<div class="lb-frame"></div>', unsafe_allow_html=True)
214
  col.caption("—"); return None, None
215
 
216
  if display_bytes is not None:
217
+ src = image_bytes_to_data_url(display_bytes) # mime otomatik
218
  col.markdown(f'<div class="lb-frame"><img src="{src}"/></div>', unsafe_allow_html=True)
219
  else:
220
  abs_url = _abs_backend_url(display_url)
221
  col.markdown(f'<div class="lb-frame"><img src="{abs_url}"/></div>', unsafe_allow_html=True)
222
 
223
+ # ---- Native (caption + download) ----
224
  native = download_bytes or (fetch_bytes(download_url) if download_url else None)
225
+ if not native:
226
+ native = display_bytes # en azından çözünürlük gösterebilmek için
227
 
228
  if native:
229
  sz = image_size_from_bytes(native)
 
238
 
239
  return display_url or "data", native
240
 
241
+ def rgba_to_rgb_on_bg(img_rgba: Image.Image, bg_rgb=(255,255,255)) -> Image.Image:
242
+ """RGBA görseli seçilen arka plana kompoze ederek RGB'ye çevir (siyah sorununu çözer)."""
243
+ if img_rgba.mode != "RGBA":
244
+ return img_rgba.convert("RGB")
245
+ bg = Image.new("RGBA", img_rgba.size, (*bg_rgb, 255))
246
+ bg.paste(img_rgba, (0,0), img_rgba)
247
+ return bg.convert("RGB")
248
+
249
+ @st.cache_data(ttl=300, show_spinner=False)
250
+ def make_small_jpeg_from_native(native: bytes, target_long_edge: int = 1280, bg_rgb=(255,255,255)) -> bytes:
251
+ """Şeffaf PNG beyaz zemine kompoze → hafif JPEG (piksel piksel yüklenmeyi azaltır)."""
252
+ im = Image.open(io.BytesIO(native))
253
+ if im.mode != "RGBA" and "transparency" not in im.info:
254
+ im = im.convert("RGB")
255
+ else:
256
+ im = im.convert("RGBA")
257
+ w, h = im.size
258
+ if w >= h:
259
+ new_w = min(target_long_edge, w)
260
+ new_h = max(1, int(h * (new_w / max(1, w))))
261
+ else:
262
+ new_h = min(target_long_edge, h)
263
+ new_w = max(1, int(w * (new_h / max(1, h))))
264
+ im2 = im.resize((new_w, new_h), Image.LANCZOS)
265
+ im2_rgb = rgba_to_rgb_on_bg(im2, bg_rgb=bg_rgb)
266
+ out = io.BytesIO(); im2_rgb.save(out, format="JPEG", quality=88, subsampling=1)
267
+ return out.getvalue()
268
+
269
+ def make_preview_frame(img_bytes: bytes, aspect: str, long_edge: int, ratio: float, bg_rgb=(255,255,255)) -> bytes:
270
+ """UI-only: seçilen aspect’te tuval + içerik oranı (JPEG)."""
271
+ im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")
272
+ aw, ah = _parse_aspect_str(aspect)
273
+ if aw >= ah:
274
+ W = max(256, min(8192, int(long_edge))); H = max(1, int(W * ah / aw))
275
+ else:
276
+ H = max(256, min(8192, int(long_edge))); W = max(1, int(H * aw / ah))
277
+ ratio = max(0.10, min(0.99, float(ratio)))
278
+ target = int(min(W, H) * ratio if aw == ah else (H * ratio if aw < ah else W * ratio))
279
+ w, h = im.size
280
+ if w >= h: new_w, new_h = target, max(1, int(h * (target / max(1, w))))
281
+ else: new_h, new_w = target, max(1, int(w * (target / max(1, h))))
282
+ im_resized = im.resize((new_w, new_h), Image.LANCZOS)
283
+ canvas = Image.new("RGBA", (W, H), (*bg_rgb, 255))
284
+ off = ((W - new_w)//2, (H - new_h)//2); canvas.paste(im_resized, off, im_resized)
285
+ out = io.BytesIO(); canvas.convert("RGB").save(out, format="JPEG", quality=90, subsampling=1)
286
+ return out.getvalue()
287
+
288
+ # ================= Yerel DEMO dosyaları =================
289
+ def load_local_demo():
290
+ demo = {}
291
+ for fname in ["input.jpg", "packshot.jpg", "model.jpg", "tryon.mp4"]:
292
+ if os.path.exists(fname):
293
+ with open(fname, "rb") as f: demo[fname] = f.read()
294
+ return demo
295
+
296
+ demo_files = load_local_demo()
297
+
298
+ # ================= Sayfa =================
299
+ st.set_page_config(page_title="AI LightBox · Jewelry", layout="wide", page_icon="💎")
300
+ inject_base_css()
301
+
302
+ st.title("💎 AI LightBox · Jewelry Studio")
303
+ st.caption("Upload · Try-on · Download")
304
+
305
  ok = backend_ok()
306
+
307
+ # -------- Üst Kontrol Şeridi --------
308
+ col1, col2, col3, col4, col5, col6, col7 = st.columns([1.6, 1.6, 1.0, 1.2, 0.9, 1.1, 0.9])
 
 
 
 
 
 
 
 
309
  with col1:
310
  file_list = st.file_uploader("Product Image (Upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
311
  with col2:
312
+ image_url = st.text_input("Or URL", key="image_url", placeholder="https://... (optional)")
313
  with col3:
314
  jewel_type = st.selectbox("Type", ["auto","earring","ring","necklace","bracelet","pendant","brooch","set","watch"], index=0, key="jewel_type")
315
+ with col4:
 
 
 
 
316
  preview_ratio = st.number_input("Preview Scale (0.10–0.99)", min_value=0.10, max_value=0.99, value=0.50, step=0.01,
317
  help="Sadece önizleme kadrajında büyüklüğü etkiler.", key="preview_ratio")
318
+ with col5:
 
 
 
 
 
 
 
 
319
  upscale = st.checkbox("Super Res", False, key="upscale")
320
+ with col6:
321
  upscale_stage = st.selectbox("SR Step", ["both","final","packshot"], index=0, help="Varsayılan: both", key="upscale_stage")
322
+ with col7:
323
  upscale_factor = st.selectbox("SR Factor", ["2","4"], index=0, key="upscale_factor")
 
 
 
324
 
325
+ colA, colB, colC, colD, colE, colF = st.columns([1.0, 1.0, 1.1, 0.9, 0.9, 1.2])
326
+ with colA:
327
+ st.info(f"Connection: {'Online' if ok else 'Offline'}", icon="🔌")
328
+ with colB:
329
+ identity_lock = st.checkbox("Identity lock", True, key="identity_lock")
330
+ with colC:
331
  to_video = st.checkbox("One Click Generate Video", False, key="to_video")
332
+ with colD:
333
  duration = st.selectbox("Duration ", VIDEO_DUR_OPTIONS, index=0, key="duration_sel")
334
+ with colE:
335
  resolution = st.selectbox("Video Res", VIDEO_RES_OPTIONS, index=1, key="resolution_sel")
336
+ with colF:
337
  run = st.button("Run 💎", type="primary", use_container_width=True, key="run_btn")
 
338
 
339
+ # Tuval/kadraj + fit/fill
340
+ colT1, colT2, colT3 = st.columns([1.0, 1.0, 1.0])
341
+ with colT1:
342
+ preview_aspect = st.selectbox("Preview Frame Ratio", ASPECT_OPTIONS, index=0, key="preview_aspect")
343
+ with colT2:
344
+ preview_long_edge = st.number_input("Preview Long Edge (px)", min_value=256, max_value=8192, value=1920, step=64, key="preview_long_edge")
345
+ with colT3:
346
+ fit_mode = st.selectbox("Preview Layout", ["fit","fill"], index=0, help="Sadece önizlemeyi etkiler.", key="fit_mode")
347
 
348
+ # Inject CSS (uniform kartlar)
349
+ inject_preview_css(preview_aspect, fit_mode="contain" if fit_mode=="fit" else "cover")
350
+
351
+ # ================= PRE-RUN PREVIEWS =================
352
  st.subheader("Previews")
353
  col_in, col_pack, col_pad, col_vid = st.columns(4)
354
 
355
+ # Input bytes (upload/URL öncelikli; yoksa demo)
356
  input_bytes = None
357
  if file_list:
358
  try: input_bytes = file_list[0].getvalue()
359
  except Exception: input_bytes = None
360
  elif image_url:
361
  input_bytes = fetch_bytes(image_url)
362
+ elif demo_files.get("input.jpg"):
363
+ input_bytes = demo_files["input.jpg"]
 
364
 
365
  show_tile_card_display_download(
366
  col_in, "Sample Input preview",
367
+ display_bytes=input_bytes if input_bytes else demo_files.get("input.jpg"),
368
+ download_bytes=input_bytes if input_bytes else demo_files.get("input.jpg"),
369
  filename_stub="input", key_prefix="pre"
370
  )
371
 
372
+ # Packshot preview (demo dosyası varsa; yoksa input görüntü)
373
+ pack_bytes_demo = demo_files.get("packshot.jpg") or input_bytes
 
 
 
 
374
  show_tile_card_display_download(
375
  col_pack, "Sample Packshot (BG Removed)",
376
  display_bytes=pack_bytes_demo,
 
378
  filename_stub="packshot", key_prefix="pre"
379
  )
380
 
381
+ # Padded preview (packshot.jpg veya input.jpg → seçilen kadraja, UI-only)
382
  padded_bytes = None
383
  if pack_bytes_demo:
384
  try:
385
+ padded_bytes = make_preview_frame(pack_bytes_demo, aspect=preview_aspect,
386
+ long_edge=int(preview_long_edge),
387
+ ratio=float(preview_ratio))
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  except Exception:
389
  padded_bytes = None
 
390
  show_tile_card_display_download(
391
+ col_pad, "Sample Padded Preview ",
392
  display_bytes=padded_bytes or pack_bytes_demo,
393
  download_bytes=padded_bytes or pack_bytes_demo,
394
  filename_stub="padded_ui", key_prefix="pre"
395
  )
396
 
397
+ # Video preview (loop; yerel tryon.mp4 varsa)
398
+ vid_demo = demo_files.get("tryon.mp4")
399
+ col_vid.markdown('<div class="lb-card-title">AI Generated Video Preview </div>', unsafe_allow_html=True)
 
 
400
  if vid_demo:
401
  data_url = "data:video/mp4;base64," + base64.b64encode(vid_demo).decode("ascii")
402
  col_vid.markdown(
 
413
  col_vid.markdown('<div class="lb-frame"></div>', unsafe_allow_html=True)
414
  col_vid.caption("—")
415
 
416
+ # ================= ÇALIŞTIR =================
417
  if run:
418
  if not ok:
419
+ st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
420
+ elif not (file_list or image_url or demo_files.get("input.jpg")):
421
  st.error("En az bir görsel girişi yapın (dosya yükle veya URL).")
422
  else:
423
  try:
 
433
  "num_images": "1",
434
  "image_urls": img_urls,
435
  "mannequin_image_url": "",
436
+ "identity_lock": "true" if identity_lock else "false",
437
+ # Preview controls (backend pad/preview için)
438
+ "preview_aspect": preview_aspect,
439
+ "preview_long_edge": str(int(preview_long_edge)),
440
+ "preview_ratio": str(float(preview_ratio)),
441
+ "padding_ratio": str(float(preview_ratio)), # backward-compat
442
+ # SR
443
+ "upscale": "true" if upscale else "false",
444
  "upscale_factor": st.session_state.get("upscale_factor","2"),
445
  "upscale_stage": st.session_state.get("upscale_stage","both"),
446
+ # Video (opsiyonel zincir)
447
+ "to_video": "true" if to_video else "false",
448
  "video_prompt": "",
449
  "duration": st.session_state.get("duration_sel", VIDEO_DUR_OPTIONS[0]),
450
  "resolution": st.session_state.get("resolution_sel", VIDEO_RES_OPTIONS[1]),
451
  "prompt_optimizer": "false",
452
  }
453
 
454
+ with st.spinner("AI Chain Running…"):
455
  out = post_chain(data, files_payload if files_payload else None)
456
 
457
  st.session_state["job_counter"] += 1
458
 
459
+ # ---- ÇIKTILAR ----
460
+ # Packshot NATIVE DISPLAY küçük JPEG (şeffaflık beyaza kompoze)
461
+ pack_native_url = None; pack_display_jpeg = None
462
+ try:
463
+ pack_native_url = (out["packshot_step"]["result"]["images_native"] or [{}])[0].get("url")
464
+ if pack_native_url:
465
+ pn = fetch_bytes(pack_native_url)
466
+ if pn: pack_display_jpeg = make_small_jpeg_from_native(pn, 1280, bg_rgb=(255,255,255))
467
+ except Exception: pass
468
+
469
+ # Padded NATIVE → DISPLAY küçük JPEG (pad etkisi görünür)
470
+ padded_native_url = None; padded_display_jpeg = None
471
+ try:
472
+ padded_native_url = (out["packshot_step"]["result"]["padded_native"] or [{}])[0].get("url")
473
+ if padded_native_url:
474
+ nb = fetch_bytes(padded_native_url)
475
+ if nb: padded_display_jpeg = make_small_jpeg_from_native(nb, 1280, bg_rgb=(255,255,255))
476
+ except Exception: pass
477
+
478
+ # Model NATIVE → DISPLAY küçük JPEG
479
+ placed_native_url = None; placed_display_jpeg = None
480
+ try:
481
+ placed_native_url = (out["image_step"]["result"]["images_native"] or [{}])[0].get("url")
482
+ if placed_native_url:
483
+ mn = fetch_bytes(placed_native_url)
484
+ if mn: placed_display_jpeg = make_small_jpeg_from_native(mn, 1280, bg_rgb=(255,255,255))
485
+ except Exception: pass
486
+
487
+ # Video (zincirde üretildiyse)
488
+ vurl = None
489
+ try:
490
+ vurl = (out.get("video_step") or {}).get("result", {}).get("video", {}).get("url")
491
+ except Exception:
492
+ vurl = None
493
+
494
+ # UI ipucu
495
+ ui_hints = (out.get("video_step") or {}).get("ui_hints") or {}
496
+ if isinstance(ui_hints, dict):
497
+ st.session_state["video_ui_max_px"] = int(ui_hints.get("suggested_video_max_px", VIDEO_MAX_PX_DEFAULT))
498
 
499
  st.session_state["results"] = {
500
  "job_id": out.get("job_id"),
 
508
  "video_url": vurl,
509
  "api_base": API_BASE,
510
  "has_auth_header": bool(HEADERS),
511
+ "preview_aspect": preview_aspect,
512
  }
513
 
514
  except requests.HTTPError as e:
 
516
  except Exception as e:
517
  st.error(f"Hata: {e}")
518
 
519
+ # ================= SONUÇLAR =================
520
  res = st.session_state.get("results")
521
  vbytes = None
522
  if res:
523
+ st.subheader("Product Results")
524
  c_pack, c_pad, c_model, c_vid = st.columns(4)
525
 
526
+ # Packshot (backend) — DISPLAY = küçük JPEG, DOWNLOAD = NATIVE
527
  show_tile_card_display_download(
528
  c_pack, "Packshot (Removed BG)",
529
  display_bytes=res.get("packshot_display_jpeg"),
 
531
  filename_stub="packshot_native",
532
  key_prefix=f"job{st.session_state['job_counter']}"
533
  )
534
+
535
+ # Padded (backend) — DISPLAY = padded küçük JPEG, DOWNLOAD = padded NATIVE
536
  show_tile_card_display_download(
537
  c_pad, "Padded",
538
  display_bytes=res.get("padded_display_jpeg"),
 
540
  filename_stub="padded_backend",
541
  key_prefix=f"job{st.session_state['job_counter']}"
542
  )
543
+
544
+ # Model üzerinde (backend) — DISPLAY = küçük JPEG (native’den), DOWNLOAD = NATIVE
545
  show_tile_card_display_download(
546
  c_model, "TryON Model",
547
  display_bytes=res.get("placed_display_jpeg"),
 
550
  key_prefix=f"job{st.session_state['job_counter']}"
551
  )
552
 
553
+ # Video
554
  vurl = res.get("video_url")
555
  c_vid.markdown('<div class="lb-card-title">AI Generated Video</div>', unsafe_allow_html=True)
556
  if vurl:
 
585
  prompt_optimizer=False,
586
  )
587
  vurl2 = (v.get("result") or {}).get("video", {}).get("url")
588
+ ui_hints = v.get("ui_hints") or {}
589
  if vurl2:
590
  st.session_state["results"]["video_url"] = vurl2
591
+ if isinstance(ui_hints, dict):
592
+ st.session_state["video_ui_max_px"] = int(ui_hints.get("suggested_video_max_px", VIDEO_MAX_PX_DEFAULT))
593
+ st.success("Video is Ready!.")
594
  else:
595
  st.info("Video URL gelmedi.")
 
596
 
597
+ # Native ZIP
598
  named = []
599
  pn = fetch_bytes(res.get("packshot_native_url")) if res.get("packshot_native_url") else None
600
  mn = fetch_bytes(res.get("placed_native_url")) if res.get("placed_native_url") else None
 
602
  if mn: named.append((f"model_native{infer_ext(mn)}", mn))
603
  if vbytes: named.append(("video.mp4", vbytes))
604
  if named:
605
+ zip_bytes = make_zip(named)
606
+ st.download_button("Download All (ZIP)", data=zip_bytes,
 
 
 
 
607
  file_name="ai_lightbox_outputs.zip", mime="application/zip",
608
  key=f"job{st.session_state['job_counter']}_zip_dl")
609
 
610
+ # ================= Sidebar =================
611
  with st.sidebar:
 
612
  st.caption(f"API_BASE: {API_BASE}")
613
  if HF_TOKEN: st.caption("Auth: Bearer (aktif)")
614
  st.slider("Video önizleme genişliği (px)", 360, 1080, st.session_state["video_ui_max_px"],