renderfy commited on
Commit
5fff162
·
verified ·
1 Parent(s): 4cf040e

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +296 -272
streamlit_app.py CHANGED
@@ -1,21 +1,18 @@
1
- # streamlit_app.py — AI LightBox · Jewelry (uniform cards + native backend outputs + loop video)
 
2
  import os, io, base64, zipfile, requests, streamlit as st
3
  from PIL import Image
4
 
5
  # ================= Env & API =================
6
- API_BASE = (os.getenv("AI_LIGHTBOX_API", "http://127.0.0.1:7860") or "http://127.0.0.1:7860").strip().strip("'\"").rstrip("/")
7
  HF_TOKEN = (os.getenv("AI_LIGHTBOX_TOKEN", "") or "").strip().strip("'\"")
8
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
9
 
10
  # UI vars
11
- ASPECT_OPTIONS = ["9:16", "3:4", "1:1"]
12
- DEFAULT_ASPECT = os.getenv("PREVIEW_ASPECT", "9:16")
13
- DEFAULT_LONG = int(os.getenv("PREVIEW_LONG_EDGE", "1920"))
14
- DEFAULT_RATIO = float(os.getenv("PREVIEW_RATIO", "0.50"))
15
-
16
  VIDEO_RES_OPTIONS = ["512P", "768P"]
17
  VIDEO_DUR_OPTIONS = ["6", "10"]
18
- VIDEO_MAX_PX_DEFAULT = int(os.getenv("VIDEO_MAX_PX", "720"))
19
 
20
  # ================= Session =================
21
  if "results" not in st.session_state:
@@ -25,14 +22,14 @@ if "job_counter" not in st.session_state:
25
  if "video_ui_max_px" not in st.session_state:
26
  st.session_state["video_ui_max_px"] = VIDEO_MAX_PX_DEFAULT
27
 
28
- # ================= Backend helpers =================
29
  def _needs_auth(url: str) -> bool:
30
  return url.startswith(API_BASE) or url.startswith("outputs/") or url.startswith("/outputs/")
31
 
32
  def _abs_backend_url(url_or_path: str) -> str:
33
  if not url_or_path:
34
  return ""
35
- if url_or_path.startswith("http://") or url_or_path.startswith("https://"):
36
  return url_or_path
37
  if url_or_path.startswith("/outputs/"):
38
  return f"{API_BASE}{url_or_path}"
@@ -68,12 +65,8 @@ def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer
68
 
69
  @st.cache_data(ttl=300, show_spinner=False)
70
  def fetch_bytes(url_or_path: str):
71
- if not url_or_path:
72
- return None
73
  try:
74
- if os.path.exists(url_or_path): # local file support
75
- with open(url_or_path, "rb") as f:
76
- return f.read()
77
  url = _abs_backend_url(url_or_path)
78
  headers = HEADERS if _needs_auth(url) else None
79
  r = requests.get(url, headers=headers, timeout=180)
@@ -82,29 +75,23 @@ def fetch_bytes(url_or_path: str):
82
  except Exception:
83
  return None
84
 
 
 
 
 
85
  @st.cache_data(ttl=300, show_spinner=False)
86
  def image_size_from_bytes(b: bytes):
87
  try:
88
- im = Image.open(io.BytesIO(b))
89
- return im.size
90
  except Exception:
91
  return None
92
 
93
- def image_bytes_to_data_url(b: bytes, mime="image/png") -> str:
94
- enc = base64.b64encode(b).decode("ascii")
95
- return f"data:{mime};base64,{enc}"
96
-
97
- def video_bytes_to_data_url(b: bytes) -> str:
98
- enc = base64.b64encode(b).decode("ascii")
99
- return f"data:video/mp4;base64,{enc}"
100
-
101
  def infer_ext(data: bytes) -> str:
102
  try:
103
  im = Image.open(io.BytesIO(data))
104
  fmt = (im.format or "JPEG").lower()
105
- return "." + {"jpeg":"jpg","jpg":"jpg","png":"png","webp":"webp"}.get(fmt, "jpg")
106
  except Exception:
107
- # mp4 sihirli sayfa kontrolü
108
  if data[:4] == b"\x00\x00\x00\x18" or data[4:8] == b"ftyp":
109
  return ".mp4"
110
  return ".bin"
@@ -113,133 +100,112 @@ def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
113
  buf = io.BytesIO()
114
  with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
115
  for fname, b in named_bytes:
116
- if b:
117
- z.writestr(fname, b)
118
- buf.seek(0)
119
- return buf.read()
120
 
121
- # ================= Uniform Card CSS =================
122
  def _parse_aspect_str(s: str):
123
  try:
124
  a, b = s.split(":"); return max(1, int(a)), max(1, int(b))
125
  except Exception:
126
  return (9, 16)
127
 
128
- def inject_card_css(aspect: str):
129
  aw, ah = _parse_aspect_str(aspect)
130
  st.markdown(f"""
131
  <style>
132
- .lb-card-title {{ font-weight:700; font-size:1.10rem; margin:0 0 8px 0; }}
 
 
133
  .lb-frame {{
134
  position: relative; width: 100%;
135
- aspect-ratio: {aw} / {ah}; /* tüm kartlar aynı oran */
136
  border-radius: 12px; overflow: hidden; background: #fff;
137
  display:flex; align-items:center; justify-content:center;
138
- box-shadow: 0 0 0 1px rgba(0,0,0,0.06) inset;
139
- }}
140
- .lb-frame img, .lb-frame video {{
141
- width:100%; height:100%; object-fit: contain; /* FIT */
142
- display:block; background:#fff;
143
  }}
144
- .lb-placeholder {{
145
- width:100%; height:100%;
146
- display:flex; align-items:center; justify-content:center;
147
- color:#9aa0a6; font-size:0.9rem;
148
  }}
 
 
149
  </style>
150
  """, unsafe_allow_html=True)
151
 
152
- # ================= Card render helpers =================
153
- def show_image_card(col, title, url: str | None = None, bytes_data: bytes | None = None,
154
- filename_stub="image", key_prefix=""):
155
  col.markdown(f'<div class="lb-card-title">{title}</div>', unsafe_allow_html=True)
156
 
157
- native_bytes = None
158
  if not url and not bytes_data:
159
- col.markdown('<div class="lb-frame"><div class="lb-placeholder">Beklemede</div></div>', unsafe_allow_html=True)
160
- col.caption("—")
161
- return None, None
162
 
 
163
  if bytes_data is not None:
164
  src = image_bytes_to_data_url(bytes_data, "image/png")
165
  col.markdown(f'<div class="lb-frame"><img src="{src}"/></div>', unsafe_allow_html=True)
166
- native_bytes = bytes_data
167
  else:
168
  abs_url = _abs_backend_url(url)
169
  col.markdown(f'<div class="lb-frame"><img src="{abs_url}"/></div>', unsafe_allow_html=True)
170
- native_bytes = fetch_bytes(abs_url)
171
 
172
- if not native_bytes:
173
- col.caption("—")
174
- return (url or "data"), None
175
 
176
- sz = image_size_from_bytes(native_bytes)
177
- if sz:
178
- col.caption(f"Gerçek çözünürlük: {sz[0]}×{sz[1]} px")
179
 
180
- ext = infer_ext(native_bytes)
181
  mime = "image/jpeg" if ext.lower() in [".jpg",".jpeg"] else ("image/png" if ext.lower()==".png" else "image/webp")
182
- col.download_button("İndir (tam kalite)", data=native_bytes,
183
  file_name=f"{filename_stub}{ext if ext!='.bin' else '.jpg'}",
184
  mime=mime, key=f"{key_prefix}_{filename_stub}_dl")
185
- return (url or "data"), native_bytes
186
-
187
- def show_video_card(col, title, url: str | None = None, bytes_data: bytes | None = None,
188
- key_prefix=""):
189
- col.markdown(f'<div class="lb-card-title">{title}</div>', unsafe_allow_html=True)
190
-
191
- native_bytes = None
192
- if not url and not bytes_data:
193
- col.markdown('<div class="lb-frame"><div class="lb-placeholder">Beklemede</div></div>', unsafe_allow_html=True)
194
- col.caption("—")
195
- return None, None
196
 
197
- if bytes_data is not None:
198
- src = video_bytes_to_data_url(bytes_data)
199
- col.markdown(
200
- f'''
201
- <div class="lb-frame">
202
- <video loop autoplay muted playsinline controls>
203
- <source src="{src}" type="video/mp4">
204
- </video>
205
- </div>
206
- ''',
207
- unsafe_allow_html=True
208
- )
209
- native_bytes = bytes_data
210
  else:
211
- abs_url = _abs_backend_url(url)
212
- col.markdown(
213
- f'''
214
- <div class="lb-frame">
215
- <video loop autoplay muted playsinline controls>
216
- <source src="{abs_url}" type="video/mp4">
217
- </video>
218
- </div>
219
- ''',
220
- unsafe_allow_html=True
221
- )
222
- native_bytes = fetch_bytes(abs_url)
223
-
224
- if not native_bytes:
225
- col.caption("—")
226
- return (url or "data"), None
227
-
228
- col.caption("MP4 loop")
229
- col.download_button("Videoyu indir (MP4)", data=native_bytes,
230
- file_name="video.mp4", mime="video/mp4",
231
- key=f"{key_prefix}_video_dl")
232
- return (url or "data"), native_bytes
233
-
234
- # ================= Page =================
 
 
235
  st.set_page_config(page_title="AI LightBox · Jewelry", layout="wide", page_icon="💎")
236
  st.title("💎 AI LightBox · Jewelry")
237
- st.caption("Akış: Input Packshot (backend, BG temiz) → Padded (backend, oran/ölçek) Model üzerinde (backend) (opsiyonel) Video")
238
 
239
  ok = backend_ok()
240
 
241
- # Üst kontrol şeridi
242
- col1, col2, col3, col4, col5, col6, col7 = st.columns([1.6, 1.6, 1.0, 1.1, 1.0, 1.0, 1.1])
243
  with col1:
244
  file_list = st.file_uploader("Takı görseli (yükle)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
245
  with col2:
@@ -247,228 +213,286 @@ with col2:
247
  with col3:
248
  jewel_type = st.selectbox("Tür", ["auto","earring","ring","necklace","bracelet","pendant","brooch","set","watch"], index=0, key="jewel_type")
249
  with col4:
250
- preview_aspect = st.selectbox("Kadraj Oranı", ASPECT_OPTIONS, index=ASPECT_OPTIONS.index(DEFAULT_ASPECT) if DEFAULT_ASPECT in ASPECT_OPTIONS else 0, key="canvas_aspect")
 
251
  with col5:
252
- preview_long_edge = st.number_input("Uzun Kenar (px)", min_value=256, max_value=8192, value=DEFAULT_LONG, step=64, key="canvas_size")
253
  with col6:
254
- preview_ratio = st.number_input("Preview ölçeği (0.10–0.99)", min_value=0.10, max_value=0.99, value=DEFAULT_RATIO, step=0.01, key="preview_ratio")
255
  with col7:
256
- run = st.button("Çalıştır", type="primary", use_container_width=True, key="run_btn")
257
 
258
- colA, colB, colC, colD, colE = st.columns([1.0, 1.0, 1.1, 0.9, 1.0])
259
  with colA:
260
  st.info(f"Bağlantı: {'Online' if ok else 'Offline'}", icon="🔌")
261
  with colB:
262
  identity_lock = st.checkbox("Identity lock", True, key="identity_lock")
263
  with colC:
264
- upscale = st.checkbox("Super Resolution", False, key="upscale")
265
  with colD:
266
- upscale_stage = st.selectbox("SR aşaması", ["both","final","packshot"], index=0, help="Varsayılan: both", key="upscale_stage")
267
- with colE:
268
- upscale_factor = st.selectbox("SR katsayı", ["2","4"], index=0, key="upscale_factor")
269
-
270
- colV1, colV2, colV3 = st.columns([1.0, 1.0, 1.0])
271
- with colV1:
272
- to_video = st.checkbox("Video üret (opsiyonel)", False, key="to_video")
273
- with colV2:
274
  duration = st.selectbox("Süre (sn)", VIDEO_DUR_OPTIONS, index=0, key="duration_sel")
275
- with colV3:
276
  resolution = st.selectbox("Video çözünürlük", VIDEO_RES_OPTIONS, index=1, key="resolution_sel")
 
 
277
 
278
- # Kart CSS (tüm kartlar aynı oran)
279
- inject_card_css(preview_aspect)
 
 
 
 
 
 
280
 
281
- # ============== Kart Alanları (her zaman sabit) ==============
282
- # Satır 1: Input, Packshot (backend), Padded (backend), Model üzerinde (backend)
283
- c_in, c_pack, c_pad, c_model = st.columns(4)
284
- # Satır 2: Video (loop)
285
- c_video = st.container()
286
 
287
- # ==== Pre-run örnek doldurma (mevcut klasörde dosyalar varsa) ====
288
- input_sample = fetch_bytes("input.jpg") if os.path.exists("input.jpg") else None
289
- packshot_sample = fetch_bytes("packshot.jpg") if os.path.exists("packshot.jpg") else None
290
- padded_sample = fetch_bytes("padded.jpg") if os.path.exists("padded.jpg") else None
291
- model_sample = fetch_bytes("model.jpg") if os.path.exists("model.jpg") else None
292
- video_sample = fetch_bytes("tryon.mp4") if os.path.exists("tryon.mp4") else None
293
 
294
- # ==== Input kartı: upload/URL seçildiğinde anında dolmalı; yoksa sample; yoksa boş ====
295
  input_bytes = None
296
  if file_list:
297
- try:
298
- input_bytes = file_list[0].getvalue()
299
- except Exception:
300
- input_bytes = None
301
  elif image_url:
302
  input_bytes = fetch_bytes(image_url)
303
- elif input_sample:
304
- input_bytes = input_sample
305
 
306
- show_image_card(c_in, "Input", bytes_data=input_bytes, filename_stub="input", key_prefix="in")
 
 
307
 
308
- # ==== Diğer kartlar: run sonrası backend'ten; run yapılmadan varsa sample göster ====
309
- # Placeholderları sabit tutmak için her zaman bir kart çiziyoruz.
310
- res = st.session_state.get("results") or {}
 
311
 
312
- # Packshot (backend)
313
- pack_url = res.get("packshot_native_url")
314
- if not pack_url and packshot_sample is not None:
315
- _, _ = show_image_card(c_pack, "Packshot (backend)", bytes_data=packshot_sample, filename_stub="packshot_native", key_prefix="pk")
316
- else:
317
- _, _ = show_image_card(c_pack, "Packshot (backend)", url=pack_url, filename_stub="packshot_native", key_prefix="pk")
318
-
319
- # Padded (backend)
320
- pad_url = res.get("padded_native_url")
321
- if not pad_url and padded_sample is not None:
322
- _, _ = show_image_card(c_pad, "Padded (backend)", bytes_data=padded_sample, filename_stub="padded_native", key_prefix="pd")
323
- else:
324
- _, _ = show_image_card(c_pad, "Padded (backend)", url=pad_url, filename_stub="padded_native", key_prefix="pd")
325
-
326
- # Model üzerinde (backend)
327
- model_url = res.get("placed_native_url")
328
- if not model_url and model_sample is not None:
329
- _, _ = show_image_card(c_model, "Model üzerinde (backend)", bytes_data=model_sample, filename_stub="model_native", key_prefix="md")
 
 
 
 
 
 
 
 
 
330
  else:
331
- _, _ = show_image_card(c_model, "Model üzerinde (backend)", url=model_url, filename_stub="model_native", key_prefix="md")
332
-
333
- # Video (loop) — ayrı sabit kart, her zaman aynı oranda
334
- with c_video:
335
- st.markdown(f"### Video (loop)")
336
- # Kartı aynı CSS ile sar
337
- if res.get("video_url"):
338
- show_video_card(st, "", url=res.get("video_url"), key_prefix=f"job{st.session_state['job_counter']}")
339
- elif video_sample is not None:
340
- show_video_card(st, "", bytes_data=video_sample, key_prefix="sample_video")
341
- else:
342
- st.markdown('<div class="lb-frame"><div class="lb-placeholder">Beklemede</div></div>', unsafe_allow_html=True)
343
- st.caption("—")
344
 
345
- # ================ Çalıştır ================
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 görsel girişi yapın (dosya yükle veya URL).")
351
  else:
352
  try:
 
 
 
 
 
 
353
  data = {
354
- "category": jewel_type,
355
  "edit_prompt": "",
356
  "num_images": "1",
357
- "image_urls": (image_url or ""),
358
- "mannequin_image_url": "",
359
  "identity_lock": "true" if identity_lock else "false",
360
- # PAD/Preview kontrolleri (backend native PAD için)
361
  "preview_aspect": preview_aspect,
362
  "preview_long_edge": str(int(preview_long_edge)),
363
  "preview_ratio": str(float(preview_ratio)),
 
364
  # SR
365
  "upscale": "true" if upscale else "false",
366
- "upscale_factor": st.session_state.get("upscale_factor", "2"),
367
- "upscale_stage": st.session_state.get("upscale_stage", "both"),
368
- # Video zincirde opsiyonel
369
  "to_video": "true" if to_video else "false",
370
  "video_prompt": "",
371
- "duration": st.session_state.get("duration_sel", "6"),
372
- "resolution": st.session_state.get("resolution_sel", "768P"),
373
  "prompt_optimizer": "false",
374
  }
375
- files_payload = []
376
- if file_list:
377
- for f in file_list:
378
- files_payload.append(("files", (f.name, f.getvalue(), f.type or "image/jpeg")))
379
 
380
  with st.spinner("Zincir çalışıyor…"):
381
- out = post_chain(data, files_payload)
382
 
383
- # ---- ÇIKTILAR (NATIVE ODAKLI) ----
384
- pack_native = (out["packshot_step"]["result"]["images_native"] or [{}])[0].get("url")
385
- padded_native = (out["packshot_step"]["result"]["padded_native"] or [{}])[0].get("url")
386
- placed_native = (out["image_step"]["result"]["images_native"] or [{}])[0].get("url") if out.get("image_step") else None
387
- vurl = (out.get("video_step") or {}).get("result", {}).get("video", {}).get("url")
388
 
389
- # UI video ipuçları
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  ui_hints = (out.get("video_step") or {}).get("ui_hints") or {}
391
- if isinstance(ui_hints, dict) and ui_hints.get("suggested_video_max_px"):
392
- st.session_state["video_ui_max_px"] = int(ui_hints["suggested_video_max_px"])
393
 
394
- st.session_state["job_counter"] += 1
395
  st.session_state["results"] = {
396
  "job_id": out.get("job_id"),
397
  "schema_version": out.get("schema_version"),
398
  "packshot_native_url": pack_native,
399
- "padded_native_url": padded_native,
400
  "placed_native_url": placed_native,
401
  "video_url": vurl,
402
  "api_base": API_BASE,
403
  "has_auth_header": bool(HEADERS),
 
404
  }
405
 
406
- # Sayfayı yeniden çiz (kartlar otomatik güncellenecek)
407
- st.experimental_rerun()
408
-
409
  except requests.HTTPError as e:
410
  st.error(f"HTTP {e.response.status_code}")
411
  except Exception as e:
412
  st.error(f"Hata: {e}")
413
 
414
- # ================ Opsiyonel: Sonradan video üret ================
415
- st.markdown("---")
416
- st.subheader("Bu sonuçtan video üret (opsiyonel)")
417
- res2 = st.session_state.get("results") or {}
418
- final_src_for_video = res2.get("placed_native_url") or res2.get("padded_native_url") or res2.get("packshot_native_url")
419
-
420
- colvv1, colvv2, colvv3, colvv4 = st.columns([1.0, 1.0, 1.0, 1.0])
421
- with colvv1:
422
- post_video_toggle = st.checkbox("Videoya çevir", value=False, key="video_toggle_manual")
423
- with colvv2:
424
- duration2 = st.selectbox("Süre (sn)", VIDEO_DUR_OPTIONS, index=0, key="duration_sel2")
425
- with colvv3:
426
- resolution2 = st.selectbox("Çözünürlük", VIDEO_RES_OPTIONS, index=1, key="resolution_sel2")
427
- with colvv4:
428
- make_video_btn = st.button("Üret", disabled=not post_video_toggle or not final_src_for_video)
429
-
430
- if make_video_btn:
431
- try:
432
- with st.spinner("Video üretiliyor…"):
433
- v = post_video(final_src_for_video, duration=duration2, resolution=resolution2, prompt_optimizer=False, preview_aspect=preview_aspect)
434
- vurl2 = (v.get("result") or {}).get("video", {}).get("url")
435
- if vurl2:
436
- st.session_state["results"]["video_url"] = vurl2
437
- st.success("Video hazır.")
438
- st.experimental_rerun()
439
- else:
440
- st.info("Video URL gelmedi.")
441
- except requests.HTTPError as e:
442
- st.error(f"HTTP {e.response.status_code}")
443
- except Exception as e:
444
- st.error(f"Hata: {e}")
445
-
446
- # ================ Hepsini ZIP ================
447
- res3 = st.session_state.get("results") or {}
448
- named = []
449
- pb = fetch_bytes(res3.get("packshot_native_url")) if res3.get("packshot_native_url") else None
450
- if pb: named.append((f"packshot{infer_ext(pb)}", pb))
451
- pdb = fetch_bytes(res3.get("padded_native_url")) if res3.get("padded_native_url") else None
452
- if pdb: named.append((f"padded{infer_ext(pdb)}", pdb))
453
- mb = fetch_bytes(res3.get("placed_native_url")) if res3.get("placed_native_url") else None
454
- if mb: named.append((f"model{infer_ext(mb)}", mb))
455
- vb = fetch_bytes(res3.get("video_url")) if res3.get("video_url") else None
456
- if vb: named.append(("video.mp4", vb))
457
- if named:
458
- zip_bytes = make_zip(named)
459
- st.download_button("Hepsini ZIP indir", data=zip_bytes,
460
- file_name="ai_lightbox_outputs.zip", mime="application/zip",
461
- key=f"zip_{st.session_state['job_counter']}")
462
-
463
- # ================ Debug ================
464
- with st.expander("Debug / Kaynak Alanlar"):
465
- st.json({
466
- "job_id": res3.get("job_id"),
467
- "schema_version": res3.get("schema_version"),
468
- "api_base": res3.get("api_base"),
469
- "has_auth_header": res3.get("has_auth_header"),
470
- "packshot_native_url": res3.get("packshot_native_url"),
471
- "padded_native_url": res3.get("padded_native_url"),
472
- "placed_native_url": res3.get("placed_native_url"),
473
- "video_url": res3.get("video_url"),
474
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # streamlit_app.py — AI LightBox · Jewelry
2
+ # Pre-run sabit 4 kart (Input/Packshot/Padded/Video loop) + Run sonrası native odaklı kartlar
3
  import os, io, base64, zipfile, requests, streamlit as st
4
  from PIL import Image
5
 
6
  # ================= Env & API =================
7
+ API_BASE = (os.getenv("AI_LIGHTBOX_API", "http://127.0.0.1:8000") or "http://127.0.0.1:8000").strip().strip("'\"").rstrip("/")
8
  HF_TOKEN = (os.getenv("AI_LIGHTBOX_TOKEN", "") or "").strip().strip("'\"")
9
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
10
 
11
  # UI vars
12
+ VIDEO_MAX_PX_DEFAULT = int(os.getenv("VIDEO_MAX_PX", "720"))
 
 
 
 
13
  VIDEO_RES_OPTIONS = ["512P", "768P"]
14
  VIDEO_DUR_OPTIONS = ["6", "10"]
15
+ ASPECT_OPTIONS = ["9:16", "3:4", "1:1"] # preview kadraj oranları
16
 
17
  # ================= Session =================
18
  if "results" not in st.session_state:
 
22
  if "video_ui_max_px" not in st.session_state:
23
  st.session_state["video_ui_max_px"] = VIDEO_MAX_PX_DEFAULT
24
 
25
+ # ================= HTTP/Backend helpers =================
26
  def _needs_auth(url: str) -> bool:
27
  return url.startswith(API_BASE) or url.startswith("outputs/") or url.startswith("/outputs/")
28
 
29
  def _abs_backend_url(url_or_path: str) -> str:
30
  if not url_or_path:
31
  return ""
32
+ if url_or_path.startswith(("http://","https://")):
33
  return url_or_path
34
  if url_or_path.startswith("/outputs/"):
35
  return f"{API_BASE}{url_or_path}"
 
65
 
66
  @st.cache_data(ttl=300, show_spinner=False)
67
  def fetch_bytes(url_or_path: str):
68
+ if not url_or_path: return None
 
69
  try:
 
 
 
70
  url = _abs_backend_url(url_or_path)
71
  headers = HEADERS if _needs_auth(url) else None
72
  r = requests.get(url, headers=headers, timeout=180)
 
75
  except Exception:
76
  return None
77
 
78
+ def image_bytes_to_data_url(b: bytes, mime="image/png") -> str:
79
+ enc = base64.b64encode(b).decode("ascii")
80
+ return f"data:{mime};base64,{enc}"
81
+
82
  @st.cache_data(ttl=300, show_spinner=False)
83
  def image_size_from_bytes(b: bytes):
84
  try:
85
+ im = Image.open(io.BytesIO(b)); return im.size
 
86
  except Exception:
87
  return None
88
 
 
 
 
 
 
 
 
 
89
  def infer_ext(data: bytes) -> str:
90
  try:
91
  im = Image.open(io.BytesIO(data))
92
  fmt = (im.format or "JPEG").lower()
93
+ return "." + {"jpeg":"jpg","jpg":"jpg","png":"png","webp":"webp"}.get(fmt,"jpg")
94
  except Exception:
 
95
  if data[:4] == b"\x00\x00\x00\x18" or data[4:8] == b"ftyp":
96
  return ".mp4"
97
  return ".bin"
 
100
  buf = io.BytesIO()
101
  with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
102
  for fname, b in named_bytes:
103
+ if b: z.writestr(fname, b)
104
+ buf.seek(0); return buf.read()
 
 
105
 
106
+ # ================= Preview helpers (uniform cards) =================
107
  def _parse_aspect_str(s: str):
108
  try:
109
  a, b = s.split(":"); return max(1, int(a)), max(1, int(b))
110
  except Exception:
111
  return (9, 16)
112
 
113
+ def inject_preview_css(aspect: str, fit_mode="contain"):
114
  aw, ah = _parse_aspect_str(aspect)
115
  st.markdown(f"""
116
  <style>
117
+ .lb-card-title {{
118
+ font-weight:700; font-size:1.15rem; margin:0 0 8px 0;
119
+ }}
120
  .lb-frame {{
121
  position: relative; width: 100%;
122
+ aspect-ratio: {aw} / {ah};
123
  border-radius: 12px; overflow: hidden; background: #fff;
124
  display:flex; align-items:center; justify-content:center;
125
+ box-shadow: 0 0 0 1px rgba(255,255,255,0.06) inset;
 
 
 
 
126
  }}
127
+ .lb-frame img {{
128
+ width:100%; height:100%; object-fit:{fit_mode}; /* contain=fit, cover=fill */
129
+ display:block;
 
130
  }}
131
+ .lb-video {{ display:flex; justify-content:center; }}
132
+ .lb-video video {{ border-radius:8px; max-width: 540px; width:100%; height:auto; }}
133
  </style>
134
  """, unsafe_allow_html=True)
135
 
136
+ def show_tile_card(col, title, url: str | None = None, bytes_data: bytes | None = None,
137
+ filename_stub="image", key_prefix=""):
 
138
  col.markdown(f'<div class="lb-card-title">{title}</div>', unsafe_allow_html=True)
139
 
 
140
  if not url and not bytes_data:
141
+ col.markdown('<div class="lb-frame"></div>', unsafe_allow_html=True)
142
+ col.caption("—"); return None, None
 
143
 
144
+ # Render (HTML <img>) + native download
145
  if bytes_data is not None:
146
  src = image_bytes_to_data_url(bytes_data, "image/png")
147
  col.markdown(f'<div class="lb-frame"><img src="{src}"/></div>', unsafe_allow_html=True)
148
+ native = bytes_data
149
  else:
150
  abs_url = _abs_backend_url(url)
151
  col.markdown(f'<div class="lb-frame"><img src="{abs_url}"/></div>', unsafe_allow_html=True)
152
+ native = fetch_bytes(abs_url)
153
 
154
+ if not native:
155
+ col.caption("—"); return url or "data", None
 
156
 
157
+ sz = image_size_from_bytes(native)
158
+ if sz: col.caption(f"Gerçek çözünürlük: {sz[0]}×{sz[1]} px")
 
159
 
160
+ ext = infer_ext(native)
161
  mime = "image/jpeg" if ext.lower() in [".jpg",".jpeg"] else ("image/png" if ext.lower()==".png" else "image/webp")
162
+ col.download_button("İndir (tam kalite)", data=native,
163
  file_name=f"{filename_stub}{ext if ext!='.bin' else '.jpg'}",
164
  mime=mime, key=f"{key_prefix}_{filename_stub}_dl")
165
+ return (url or "data"), native
 
 
 
 
 
 
 
 
 
 
166
 
167
+ def make_preview_frame(img_bytes: bytes, aspect: str, long_edge: int, ratio: float, bg_rgb=(255,255,255)) -> bytes:
168
+ """Sadece UI için: seçilen aspect’te tuval + içerik oranı."""
169
+ im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")
170
+ aw, ah = _parse_aspect_str(aspect)
171
+ # hedef tuval boyutu (uzun kenar sabit)
172
+ if aw >= ah:
173
+ W = max(256, min(8192, int(long_edge))); H = max(1, int(W * ah / aw))
 
 
 
 
 
 
174
  else:
175
+ H = max(256, min(8192, int(long_edge))); W = max(1, int(H * aw / ah))
176
+ ratio = max(0.30, min(0.99, float(ratio)))
177
+ target = int(min(W, H) * ratio if aw == ah else (H * ratio if aw < ah else W * ratio))
178
+ w, h = im.size
179
+ if w >= h:
180
+ new_w, new_h = target, max(1, int(h * (target / max(1, w))))
181
+ else:
182
+ new_h, new_w = target, max(1, int(w * (target / max(1, h))))
183
+ im_resized = im.resize((new_w, new_h), Image.LANCZOS)
184
+ canvas = Image.new("RGBA", (W, H), (*bg_rgb, 255))
185
+ off = ((W - new_w)//2, (H - new_h)//2)
186
+ canvas.paste(im_resized, off, im_resized)
187
+ out = io.BytesIO(); canvas.convert("RGB").save(out, format="JPEG", quality=95, subsampling=1)
188
+ return out.getvalue()
189
+
190
+ # ================= Yerel DEMO dosyaları =================
191
+ def load_local_demo():
192
+ demo = {}
193
+ for fname in ["input.jpg", "packshot.jpg", "model.jpg", "tryon.mp4"]:
194
+ if os.path.exists(fname):
195
+ with open(fname, "rb") as f: demo[fname] = f.read()
196
+ return demo
197
+
198
+ demo_files = load_local_demo()
199
+
200
+ # ================= Sayfa =================
201
  st.set_page_config(page_title="AI LightBox · Jewelry", layout="wide", page_icon="💎")
202
  st.title("💎 AI LightBox · Jewelry")
203
+ st.caption("Pre-run: sabit preview kartları. Run sonrası: Packshot/Model çıktıları native indirilir. Padded sadece ara adımdır.")
204
 
205
  ok = backend_ok()
206
 
207
+ # -------- Üst Kontrol Şeridi (Üretim Parametreleri) --------
208
+ col1, col2, col3, col4, col5, col6, col7 = st.columns([1.6, 1.6, 1.0, 1.2, 0.9, 1.1, 0.9])
209
  with col1:
210
  file_list = st.file_uploader("Takı görseli (yükle)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
211
  with col2:
 
213
  with col3:
214
  jewel_type = st.selectbox("Tür", ["auto","earring","ring","necklace","bracelet","pendant","brooch","set","watch"], index=0, key="jewel_type")
215
  with col4:
216
+ preview_ratio = st.number_input("Preview ölçeği (0.10–0.99)", min_value=0.10, max_value=0.99, value=0.50, step=0.01,
217
+ help="Sadece önizleme kadrajında büyüklüğü etkiler.", key="preview_ratio")
218
  with col5:
219
+ upscale = st.checkbox("Super Resolution", False, key="upscale")
220
  with col6:
221
+ upscale_stage = st.selectbox("SR aşaması", ["both","final","packshot"], index=0, help="Varsayılan: both", key="upscale_stage")
222
  with col7:
223
+ upscale_factor = st.selectbox("SR katsayı", ["2","4"], index=0, key="upscale_factor")
224
 
225
+ colA, colB, colC, colD, colE, colF = st.columns([1.0, 1.0, 1.1, 0.9, 0.9, 1.2])
226
  with colA:
227
  st.info(f"Bağlantı: {'Online' if ok else 'Offline'}", icon="🔌")
228
  with colB:
229
  identity_lock = st.checkbox("Identity lock", True, key="identity_lock")
230
  with colC:
231
+ to_video = st.checkbox("Zincirde video üret", False, key="to_video")
232
  with colD:
 
 
 
 
 
 
 
 
233
  duration = st.selectbox("Süre (sn)", VIDEO_DUR_OPTIONS, index=0, key="duration_sel")
234
+ with colE:
235
  resolution = st.selectbox("Video çözünürlük", VIDEO_RES_OPTIONS, index=1, key="resolution_sel")
236
+ with colF:
237
+ run = st.button("Çalıştır", type="primary", use_container_width=True, key="run_btn")
238
 
239
+ # Tuval/kadraj + fit/fill
240
+ colT1, colT2, colT3 = st.columns([1.0, 1.0, 1.0])
241
+ with colT1:
242
+ preview_aspect = st.selectbox("Preview Kadraj Oranı", ASPECT_OPTIONS, index=0, key="preview_aspect")
243
+ with colT2:
244
+ preview_long_edge = st.number_input("Preview uzun kenar (px)", min_value=256, max_value=8192, value=1920, step=64, key="preview_long_edge")
245
+ with colT3:
246
+ fit_mode = st.selectbox("Önizleme yerleşimi", ["fit","fill"], index=0, help="Sadece önizlemeyi etkiler.", key="fit_mode")
247
 
248
+ # Inject CSS (uniform kartlar)
249
+ inject_preview_css(preview_aspect, fit_mode="contain" if fit_mode=="fit" else "cover")
 
 
 
250
 
251
+ # ================= PRE-RUN PREVIEWS (her zaman sabit) =================
252
+ st.subheader("Önizlemeler")
253
+ col_in, col_pack, col_pad, col_vid = st.columns(4)
 
 
 
254
 
255
+ # Input bytes (upload/URL öncelikli; yoksa demo)
256
  input_bytes = None
257
  if file_list:
258
+ try: input_bytes = file_list[0].getvalue()
259
+ except Exception: input_bytes = None
 
 
260
  elif image_url:
261
  input_bytes = fetch_bytes(image_url)
262
+ elif demo_files.get("input.jpg"):
263
+ input_bytes = demo_files["input.jpg"]
264
 
265
+ show_tile_card(col_in, "Input preview",
266
+ bytes_data=input_bytes if input_bytes else demo_files.get("input.jpg"),
267
+ filename_stub="input", key_prefix="pre")
268
 
269
+ # Packshot preview (demo dosyası varsa; yoksa input görüntü)
270
+ pack_bytes_demo = demo_files.get("packshot.jpg") or input_bytes
271
+ show_tile_card(col_pack, "Packshot preview",
272
+ bytes_data=pack_bytes_demo, filename_stub="packshot", key_prefix="pre")
273
 
274
+ # Padded preview (packshot.jpg veya input.jpg → seçilen kadraja, UI-only)
275
+ padded_bytes = None
276
+ if pack_bytes_demo:
277
+ try:
278
+ padded_bytes = make_preview_frame(pack_bytes_demo, aspect=preview_aspect,
279
+ long_edge=int(preview_long_edge),
280
+ ratio=float(preview_ratio))
281
+ except Exception:
282
+ padded_bytes = None
283
+ show_tile_card(col_pad, "Padded preview (UI only)",
284
+ bytes_data=padded_bytes or pack_bytes_demo, filename_stub="padded", key_prefix="pre")
285
+
286
+ # Video preview (loop; yerel tryon.mp4 varsa)
287
+ vid_demo = demo_files.get("tryon.mp4")
288
+ col_vid.markdown('<div class="lb-card-title">Video preview (loop)</div>', unsafe_allow_html=True)
289
+ if vid_demo:
290
+ data_url = "data:video/mp4;base64," + base64.b64encode(vid_demo).decode("ascii")
291
+ col_vid.markdown(
292
+ f"""
293
+ <div class="lb-frame" style="display:flex;align-items:center;justify-content:center;">
294
+ <video autoplay loop muted playsinline style="width:100%;height:100%;object-fit:cover;">
295
+ <source src="{data_url}" type="video/mp4">
296
+ </video>
297
+ </div>
298
+ """, unsafe_allow_html=True
299
+ )
300
+ col_vid.download_button("Videoyu indir (MP4)", data=vid_demo, file_name="tryon.mp4", mime="video/mp4", key="pre_video_dl")
301
  else:
302
+ col_vid.markdown('<div class="lb-frame"></div>', unsafe_allow_html=True)
303
+ col_vid.caption("—")
 
 
 
 
 
 
 
 
 
 
 
304
 
305
+ # ================= ÇALIŞTIR =================
306
  if run:
307
  if not ok:
308
  st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
309
+ elif not (file_list or image_url or demo_files.get("input.jpg")):
310
  st.error("En az bir görsel girişi yapın (dosya yükle veya URL).")
311
  else:
312
  try:
313
+ img_urls = image_url or ""
314
+ files_payload = []
315
+ if file_list:
316
+ for f in file_list:
317
+ files_payload.append(("files", (f.name, f.getvalue(), f.type or "image/jpeg")))
318
+
319
  data = {
320
+ "category": st.session_state.get("jewel_type","auto"),
321
  "edit_prompt": "",
322
  "num_images": "1",
323
+ "image_urls": img_urls,
324
+ "mannequin_image_url": "", # opsiyonel
325
  "identity_lock": "true" if identity_lock else "false",
326
+ # Preview controls (backend pad/preview için)
327
  "preview_aspect": preview_aspect,
328
  "preview_long_edge": str(int(preview_long_edge)),
329
  "preview_ratio": str(float(preview_ratio)),
330
+ "padding_ratio": str(float(preview_ratio)), # backward-compat
331
  # SR
332
  "upscale": "true" if upscale else "false",
333
+ "upscale_factor": st.session_state.get("upscale_factor","2"),
334
+ "upscale_stage": st.session_state.get("upscale_stage","both"),
335
+ # Video (opsiyonel zincir)
336
  "to_video": "true" if to_video else "false",
337
  "video_prompt": "",
338
+ "duration": st.session_state.get("duration_sel", VIDEO_DUR_OPTIONS[0]),
339
+ "resolution": st.session_state.get("resolution_sel", VIDEO_RES_OPTIONS[1]),
340
  "prompt_optimizer": "false",
341
  }
 
 
 
 
342
 
343
  with st.spinner("Zincir çalışıyor…"):
344
+ out = post_chain(data, files_payload if files_payload else None)
345
 
346
+ st.session_state["job_counter"] += 1
 
 
 
 
347
 
348
+ # ---- ÇIKTILAR (NATIVE ODAKLI) ----
349
+ # Packshot NATIVE (arka planı temiz PNG)
350
+ pack_native = None
351
+ try:
352
+ pack_native = (out["packshot_step"]["result"]["images_native"] or [{}])[0].get("url")
353
+ except Exception: pass
354
+
355
+ # Padded BACKEND (ara adım olarak packshot native'den UI'da üretiyoruz; download mümkün)
356
+ # 0.5 oranlı 9:16 görünüm — native dosyayı etkilemez
357
+ pad_backend_bytes = None
358
+ if pack_native:
359
+ nb = fetch_bytes(pack_native)
360
+ if nb:
361
+ try:
362
+ pad_backend_bytes = make_preview_frame(nb, aspect=preview_aspect,
363
+ long_edge=int(preview_long_edge),
364
+ ratio=float(preview_ratio))
365
+ except Exception:
366
+ pad_backend_bytes = None
367
+
368
+ # Model NATIVE
369
+ placed_native = None
370
+ try:
371
+ placed_native = (out["image_step"]["result"]["images_native"] or [{}])[0].get("url")
372
+ except Exception: pass
373
+
374
+ # Video (zincirde üretildiyse)
375
+ vurl = None
376
+ try:
377
+ vurl = (out.get("video_step") or {}).get("result", {}).get("video", {}).get("url")
378
+ except Exception:
379
+ vurl = None
380
+
381
+ # UI ipucu
382
  ui_hints = (out.get("video_step") or {}).get("ui_hints") or {}
383
+ if isinstance(ui_hints, dict):
384
+ st.session_state["video_ui_max_px"] = int(ui_hints.get("suggested_video_max_px", VIDEO_MAX_PX_DEFAULT))
385
 
 
386
  st.session_state["results"] = {
387
  "job_id": out.get("job_id"),
388
  "schema_version": out.get("schema_version"),
389
  "packshot_native_url": pack_native,
390
+ "padded_backend_bytes": pad_backend_bytes, # ara adım UI-only
391
  "placed_native_url": placed_native,
392
  "video_url": vurl,
393
  "api_base": API_BASE,
394
  "has_auth_header": bool(HEADERS),
395
+ "preview_aspect": preview_aspect,
396
  }
397
 
 
 
 
398
  except requests.HTTPError as e:
399
  st.error(f"HTTP {e.response.status_code}")
400
  except Exception as e:
401
  st.error(f"Hata: {e}")
402
 
403
+ # ================= SONUÇLAR =================
404
+ res = st.session_state.get("results")
405
+ vbytes = None
406
+ if res:
407
+ st.subheader("Üretilen Sonuçlar")
408
+ c_pack, c_pad, c_model, c_vid = st.columns(4)
409
+
410
+ # Packshot (backend) — NATIVE
411
+ p_url, p_bytes = show_tile_card(
412
+ c_pack, "Packshot (backend)",
413
+ url=res.get("packshot_native_url"),
414
+ filename_stub="packshot_native",
415
+ key_prefix=f"job{st.session_state['job_counter']}"
416
+ )
417
+
418
+ # Padded (backend) — ara adım (UI’da packshot native’den seçilen oranla kadraj)
419
+ pad_bytes = res.get("padded_backend_bytes")
420
+ _, _ = show_tile_card(
421
+ c_pad, "Padded (backend) • ara adım",
422
+ bytes_data=pad_bytes if pad_bytes else (p_bytes or b""),
423
+ filename_stub="padded_backend",
424
+ key_prefix=f"job{st.session_state['job_counter']}"
425
+ )
426
+
427
+ # Model üzerinde (backend) — NATIVE
428
+ m_url, m_bytes = show_tile_card(
429
+ c_model, "Model üzerinde (backend)",
430
+ url=res.get("placed_native_url"),
431
+ filename_stub="model_native",
432
+ key_prefix=f"job{st.session_state['job_counter']}"
433
+ )
434
+
435
+ # Video: varsa göster; yoksa kullanıcı isterse üretecek (loop)
436
+ vurl = res.get("video_url")
437
+ c_vid.markdown('<div class="lb-card-title">Video</div>', unsafe_allow_html=True)
438
+ if vurl:
439
+ abs_v = _abs_backend_url(vurl)
440
+ c_vid.markdown(
441
+ f"""
442
+ <div class="lb-frame" style="display:flex;align-items:center;justify-content:center;">
443
+ <video autoplay loop muted controls playsinline style="width:100%;height:100%;object-fit:cover;">
444
+ <source src="{abs_v}" type="video/mp4">
445
+ </video>
446
+ </div>
447
+ """, unsafe_allow_html=True
448
+ )
449
+ vb = fetch_bytes(abs_v)
450
+ if vb:
451
+ vbytes = vb
452
+ c_vid.download_button("Videoyu indir (MP4)", data=vb, file_name="ai_lightbox_video.mp4",
453
+ mime="video/mp4", key=f"job{st.session_state['job_counter']}_video_dl")
454
+ else:
455
+ c_vid.markdown('<div class="lb-frame"></div>', unsafe_allow_html=True)
456
+ if st.checkbox("Bu sonuçtan video üret", value=False, key="video_toggle"):
457
+ # Öncelik: model native → değilse packshot native
458
+ src_for_video = res.get("placed_native_url") or res.get("packshot_native_url")
459
+ if not src_for_video:
460
+ st.warning("Video için kullanılacak native görsel bulunamadı.")
461
+ else:
462
+ with st.spinner("Video üretiliyor…"):
463
+ v = post_video(
464
+ src_for_video,
465
+ duration=st.session_state.get("duration_sel", VIDEO_DUR_OPTIONS[0]),
466
+ resolution=st.session_state.get("resolution_sel", VIDEO_RES_OPTIONS[1]),
467
+ preview_aspect=st.session_state.get("preview_aspect","9:16"),
468
+ prompt_optimizer=False,
469
+ )
470
+ vurl2 = (v.get("result") or {}).get("video", {}).get("url")
471
+ ui_hints = v.get("ui_hints") or {}
472
+ if vurl2:
473
+ st.session_state["results"]["video_url"] = vurl2
474
+ if isinstance(ui_hints, dict):
475
+ st.session_state["video_ui_max_px"] = int(ui_hints.get("suggested_video_max_px", VIDEO_MAX_PX_DEFAULT))
476
+ st.success("Video hazır. Yukarıdaki Video kartında döngü halinde oynatılıyor.")
477
+ else:
478
+ st.info("Video URL gelmedi.")
479
+
480
+ # Native ZIP (packshot_native + model_native + video)
481
+ named = []
482
+ if p_bytes: named.append((f"packshot_native{infer_ext(p_bytes)}", p_bytes))
483
+ if m_bytes: named.append((f"model_native{infer_ext(m_bytes)}", m_bytes))
484
+ if vbytes: named.append(("video.mp4", vbytes))
485
+ if named:
486
+ zip_bytes = make_zip(named)
487
+ st.download_button("Hepsini ZIP indir", data=zip_bytes,
488
+ file_name="ai_lightbox_outputs.zip", mime="application/zip",
489
+ key=f"job{st.session_state['job_counter']}_zip_dl")
490
+
491
+ # ================= Sidebar =================
492
+ with st.sidebar:
493
+ st.caption(f"API_BASE: {API_BASE}")
494
+ if HF_TOKEN: st.caption("Auth: Bearer (aktif)")
495
+ st.slider("Video önizleme genişliği (px)", 360, 1080, st.session_state["video_ui_max_px"],
496
+ step=10, key="video_ui_max_px")
497
+ if st.button("Önbelleği temizle"): st.cache_data.clear(); st.success("Önbellek temizlendi.")
498
+ if st.button("Son çıktıları temizle"): st.session_state["results"] = None; st.success("Çıktılar temizlendi.")