renderfy commited on
Commit
644dfcf
·
verified ·
1 Parent(s): 55f1076

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +156 -180
streamlit_app.py CHANGED
@@ -1,12 +1,13 @@
1
- # streamlit_app.py — Fit Studio AI (Fashion & Apparel) v0.5 UI
2
  # 9:16 dikey kadraj — Still Image (VTO) → (optional) Runway Video
3
  # Demo: aynı klasörde input.jpg, tryon.jpg, fitstudio_video.mp4 varsa otomatik önizleme
 
4
 
5
  import os, io, base64, zipfile, requests, streamlit as st
6
  from pathlib import Path
7
  from PIL import Image
8
 
9
- # ================= Env & API (robust) =================
10
  def _env(k):
11
  return (os.getenv(k) or "").strip().strip("'\"")
12
 
@@ -24,28 +25,25 @@ if not API_BASE:
24
 
25
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
26
 
27
- # Tüm görsel/video kadrajları için tek tip 9:16 boyut (yan yana eşit)
28
- PREVIEW_H = int(os.getenv("PREVIEW_H", "640")) # 640 px yükseklik
29
- PREVIEW_W = int(round(PREVIEW_H * 9 / 16)) # 360 px genişlik
30
  PREVIEW_BG = (255, 255, 255)
31
 
32
- # ================= Session State (ilk yük) =================
33
- defaults = {
34
  "results": None,
35
  "job_counter": 0,
36
  "duration": "6",
37
- }
38
- for k, v in defaults.items():
39
- if k not in st.session_state:
40
- st.session_state[k] = v
41
 
42
- # ================= Yardımcılar =================
43
  def _needs_auth(url: str) -> bool:
44
  return (url.startswith(API_BASE) or url.startswith("outputs/"))
45
 
46
  @st.cache_data(ttl=300, show_spinner=False)
47
  def fetch_bytes(url_or_path: str | None):
48
- """URL, outputs/ yolu veya bulunduğumuz klasörden dosyayı okur."""
49
  if not url_or_path:
50
  return None
51
  try:
@@ -77,7 +75,6 @@ def infer_ext(data: bytes) -> str:
77
 
78
  @st.cache_data(ttl=300, show_spinner=False)
79
  def vertical_preview_9x16(img_bytes: bytes, out_w: int = PREVIEW_W, out_h: int = PREVIEW_H, bg_rgb=PREVIEW_BG) -> bytes:
80
- """Görüntüyü 9:16 canvas’a (out_w×out_h) pad’ler, crop yapmaz."""
81
  im = Image.open(io.BytesIO(img_bytes)).convert("RGB")
82
  w, h = im.size
83
  scale = min(out_w / w, out_h / h)
@@ -88,75 +85,11 @@ def vertical_preview_9x16(img_bytes: bytes, out_w: int = PREVIEW_W, out_h: int =
88
  canvas.paste(im_resized, off)
89
  buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
90
 
91
- def first_present(d: dict, keys: list, default=None):
92
- for k in keys:
93
- v = d.get(k)
94
- if v:
95
- return v
96
- return default
97
-
98
- def show_img_tile_9x16(col, title, src: str | None, filename_stub="image", key_prefix=""):
99
- col.markdown(f"**{title}**")
100
- if not src:
101
- placeholder = Image.new("RGB", (PREVIEW_W, PREVIEW_H), PREVIEW_BG)
102
- col.image(placeholder, width=PREVIEW_W)
103
- col.caption("—"); return None, None
104
-
105
- b = fetch_bytes(src)
106
- if not b:
107
- col.warning("Görsel yüklenemedi"); return None, None
108
-
109
- pv = vertical_preview_9x16(b, PREVIEW_W, PREVIEW_H, PREVIEW_BG)
110
- col.image(pv, width=PREVIEW_W)
111
- sz = image_size_from_bytes(b)
112
- if sz: col.caption(f"Gerçek: {sz[0]}×{sz[1]} px")
113
-
114
- ext = infer_ext(b)
115
- mime = "image/jpeg" if ext in [".jpg",".jpeg"] else ("image/png" if ext==".png" else "image/webp")
116
- col.download_button(
117
- "İndir (tam kalite)", data=b, file_name=f"{filename_stub}{ext}", mime=mime,
118
- key=f"{key_prefix}_{filename_stub}_dl"
119
- )
120
- return src, b
121
-
122
- def _video_tag_from_bytes(vb: bytes, width: int = PREVIEW_W, height: int = PREVIEW_H, autoplay=True, loop=True, muted=True):
123
- """HTML5 video tag (inline, loop)."""
124
- b64 = base64.b64encode(vb).decode("utf-8")
125
- auto = "autoplay" if autoplay else ""
126
- loo = "loop" if loop else ""
127
- mut = "muted" if muted else ""
128
- return f"""
129
- <video width="{width}" height="{height}" {auto} {loo} {mut} playsinline style="border-radius:8px; display:block;">
130
- <source src="data:video/mp4;base64,{b64}" type="video/mp4" />
131
- Tarayıcı video etiketini desteklemiyor.
132
- </video>
133
- """
134
-
135
- def show_video_tile_9x16(col, title, src_or_bytes: str | bytes | None, key_prefix=""):
136
- col.markdown(f"**{title}**")
137
- if not src_or_bytes:
138
- col.caption("—"); return None
139
- try:
140
- if isinstance(src_or_bytes, (bytes, bytearray)):
141
- vb = bytes(src_or_bytes)
142
- else:
143
- vb = fetch_bytes(src_or_bytes)
144
- if not vb:
145
- col.info("Video yüklenemedi."); return None
146
- html = _video_tag_from_bytes(vb, PREVIEW_W, PREVIEW_H, autoplay=True, loop=True, muted=True)
147
- col.markdown(html, unsafe_allow_html=True)
148
- col.download_button("Video indir (MP4)", data=vb, file_name="fitstudio_video.mp4", mime="video/mp4",
149
- key=f"{key_prefix}_video_dl")
150
- return vb
151
- except Exception:
152
- col.info("Video gösterilemedi."); return None
153
 
154
- def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
155
- buf = io.BytesIO()
156
- with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
157
- for fname, b in named_bytes:
158
- if b: z.writestr(fname, b)
159
- buf.seek(0); return buf.read()
160
 
161
  def backend_ok() -> bool:
162
  try:
@@ -171,7 +104,7 @@ def post_image_edit(data: dict, files_payload):
171
  r.raise_for_status(); return r.json()
172
 
173
  def post_chain(data: dict, files_payload):
174
- payload = {**data, "to_video": "false"} # video ayrı akış
175
  r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
176
  headers=HEADERS if HF_TOKEN else None, timeout=600)
177
  r.raise_for_status(); return r.json()
@@ -181,15 +114,68 @@ def post_video(data: dict):
181
  headers=HEADERS if HF_TOKEN else None, timeout=600)
182
  r.raise_for_status(); return r.json()
183
 
184
- # ================= Sayfa =================
 
 
 
 
 
 
 
185
  st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  st.title("🧵 Fit Studio AI")
187
  st.caption("Fashion & Apparel — Dikey 9:16 Still Image (VTO) → (opsiyonel) Runway Video")
188
 
189
- # ================= Üst Kontrol Şeridi =================
190
  ok = backend_ok()
191
- c0, c1, c2, c3, c4, c5 = st.columns([1.0, 1.8, 1.8, 1.2, 1.2, 1.2])
192
-
193
  with c0:
194
  category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
195
  with c1:
@@ -205,73 +191,70 @@ with c5:
205
 
206
  prompt_box = st.text_area("Özel prompt (boş bırak → varsayılan)", value="", height=90, key="custom_prompt")
207
 
208
- bA, bB = st.columns([1.0, 1.0])
209
- with bA:
210
  st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
211
- with bB:
212
  run_image = st.button("Çalıştır (Sadece Foto)", type="primary", use_container_width=True, key="run_img_btn")
213
  run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
214
 
215
- # ================= Demo Preview (aynı klasörden) =================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  st.markdown("---")
217
  st.subheader("Demo preview (çalıştırmadan önce)")
218
- st.caption("Aynı klasörden **input.jpg**, **tryon.jpg** ve **fitstudio_video.mp4** okunur. Tüm kadrajlar 9:16 ve aynı boyutta.")
219
 
220
- dp1, dp2, dp3 = st.columns([1,1,1])
221
  in_b = fetch_bytes("input.jpg")
222
- if in_b:
223
- pv = vertical_preview_9x16(in_b, PREVIEW_W, PREVIEW_H, PREVIEW_BG)
224
- dp1.markdown("**Input**"); dp1.image(pv, width=PREVIEW_W)
225
- sz = image_size_from_bytes(in_b);
226
- if sz: dp1.caption(f"Girdi: {sz[0]}×{sz[1]} px")
227
- dp1.download_button("İndir (tam kalite)", data=in_b, file_name=f"input{infer_ext(in_b)}",
228
- mime="image/jpeg", key="demo_input_dl")
229
- else:
230
- show_img_tile_9x16(dp1, "Input", None)
231
-
232
- try_b = fetch_bytes("tryon.jpg")
233
- if try_b:
234
- pv2 = vertical_preview_9x16(try_b, PREVIEW_W, PREVIEW_H, PREVIEW_BG)
235
- dp2.markdown("**Sample Output**"); dp2.image(pv2, width=PREVIEW_W)
236
- sz2 = image_size_from_bytes(try_b);
237
- if sz2: dp2.caption(f"Örnek: {sz2[0]}×{sz2[1]} px")
238
- dp2.download_button("İndir (tam kalite)", data=try_b, file_name=f"tryon{infer_ext(try_b)}",
239
- mime="image/jpeg", key="demo_try_dl")
240
  else:
241
- show_img_tile_9x16(dp2, "Sample Output", None)
242
 
243
- demo_vb = fetch_bytes("fitstudio_video.mp4")
244
- if demo_vb:
245
- show_video_tile_9x16(dp3, "Sample Video", demo_vb, key_prefix="demo")
246
- else:
247
- show_video_tile_9x16(dp3, "Sample Video", None)
248
-
249
- # ================= Girdi Önizleme =================
250
  st.markdown("---")
251
- g1, g2, g3 = st.columns([1,1,1])
252
- white_bg = PREVIEW_BG
253
-
254
  input_preview = None
255
  if file_list:
256
- try:
257
- input_preview = file_list[0].getvalue()
258
- except Exception:
259
- input_preview = None
260
  elif image_url:
261
  input_preview = fetch_bytes(image_url)
262
 
263
  if input_preview:
264
- g1.markdown("**Input**")
265
- g1.image(vertical_preview_9x16(input_preview, PREVIEW_W, PREVIEW_H, white_bg), width=PREVIEW_W)
266
- try:
267
- sz = Image.open(io.BytesIO(input_preview)).size
268
- g1.caption(f"{sz[0]}×{sz[1]} px")
269
- except Exception:
270
- g1.caption("—")
271
- else:
272
- show_img_tile_9x16(g1, "Input", None)
273
 
274
- # ================ Çalıştır ================
275
  def _build_files_payload():
276
  files_payload = []
277
  if file_list:
@@ -310,13 +293,10 @@ if run_image or run_chain:
310
  out = post_chain(form, files_payload) # to_video=false
311
  image_json = out.get("image_step") or out
312
 
313
- # ---- Çıktılar ----
314
  imgs_norm = (image_json.get("result") or {}).get("images_9_16") or []
315
- # Fallback: raw images
316
  if not imgs_norm:
317
  imgs_raw = (image_json.get("result") or {}).get("images") or []
318
  imgs_norm = [{"url": i.get("url")} for i in imgs_raw if isinstance(i, dict) and i.get("url")]
319
-
320
  urls = [i.get("url") for i in imgs_norm if isinstance(i, dict) and i.get("url")]
321
 
322
  st.session_state["job_counter"] += 1
@@ -326,51 +306,51 @@ if run_image or run_chain:
326
  "image_urls": urls,
327
  "video_url": None,
328
  }
329
-
330
  except requests.HTTPError as e:
331
  st.error(f"HTTP {e.response.status_code}")
332
  except Exception as e:
333
  st.error(f"Hata: {e}")
334
 
335
- # ================ Sonuçlar (yan yana 9:16) ================
336
  res = st.session_state.get("results") or {}
337
  if res:
338
- key_prefix = f"job{st.session_state['job_counter']}"
339
- img_urls = res.get("image_urls") or []
340
  video_url = res.get("video_url")
341
 
342
- # 1. satır: 3 görsel + 1 video (hepsi 9:16, aynı boyut)
343
- c1, c2, c3, c4 = st.columns([1,1,1,1])
344
  named = []
345
 
346
- for idx, col in enumerate([c1, c2, c3]):
347
- if idx < len(img_urls):
348
- u = img_urls[idx]
349
- title = "Look" if idx == 0 else f"Look #{idx+1}"
350
- _, b = show_img_tile_9x16(col, title, u, filename_stub=f"look_{idx+1}", key_prefix=f"{key_prefix}_{idx}")
351
- if b: named.append((f"look_{idx+1}{infer_ext(b)}", b))
352
- else:
353
- show_img_tile_9x16(col, f"Look #{idx+1}", None)
354
 
 
355
  vbytes = None
356
  if video_url:
357
- vbytes = show_video_tile_9x16(c4, "Video", video_url, key_prefix=key_prefix)
358
- else:
359
- c4.markdown("**Video**")
360
- c4.caption("Henüz üretilmedi.")
361
 
362
- # 2. satır: Video üretim kontrolleri
363
  st.markdown("---")
364
- colv1, colv2, colv3 = st.columns([1.1, 1.0, 1.2])
365
- with colv1:
 
 
 
 
 
 
 
366
  gen_video = st.checkbox("Bu sonuçtan video üret (loop)", value=False, key="video_toggle")
367
- with colv2:
368
  st.selectbox("Video süresi", ["6","10"], index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")
369
- with colv3:
370
  go_video = st.button("Video Oluştur", use_container_width=True, key="video_btn")
371
 
372
  if gen_video and go_video:
373
- src_for_video = img_urls[0] if img_urls else None
374
  if not src_for_video:
375
  st.warning("Video için kaynak görsel yok.")
376
  else:
@@ -387,22 +367,22 @@ if res:
387
  vurl = (v.get("result") or {}).get("video", {}).get("url")
388
  if vurl:
389
  st.session_state["results"]["video_url"] = vurl
390
- video_url = vurl
391
- vbytes = show_video_tile_9x16(c4, "Video", video_url, key_prefix=key_prefix)
 
 
392
  else:
393
  st.info("Video URL gelmedi.")
394
 
395
- # ZIP indir
396
  if named or vbytes:
397
- if vbytes:
398
- named.append(("video.mp4", vbytes))
399
- zip_bytes = make_zip(named)
400
  st.download_button(
401
  "Tümünü indir (ZIP)",
402
- data=zip_bytes,
403
  file_name="fitstudio_outputs.zip",
404
  mime="application/zip",
405
- key=f"{key_prefix}_zip_dl"
406
  )
407
 
408
  # Debug
@@ -412,27 +392,23 @@ if res:
412
  "schema_version": res.get("schema_version"),
413
  "api_base": API_BASE,
414
  "has_auth_header": bool(HF_TOKEN),
415
- "image_urls": img_urls,
416
  "video_url": res.get("video_url"),
417
- "preview_size": f"{PREVIEW_W}×{PREVIEW_H}",
418
  })
419
 
420
- # ================= Sidebar (teşhis) =================
421
  with st.sidebar:
422
  st.caption(f"Resolved API_BASE: {API_BASE}")
423
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
424
-
425
  if st.button("Ping /health"):
426
  try:
427
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
428
  st.write(r.status_code)
429
- try:
430
- st.json(r.json())
431
- except Exception:
432
- st.code((r.text or "")[:1000])
433
  except Exception as e:
434
  st.error(f"Health error: {e}")
435
-
436
  if st.button("Clear cache"):
437
  st.cache_data.clear(); st.success("Cache cleared.")
438
  if st.button("Clear results"):
 
1
+ # streamlit_app.py — Fit Studio AI (Fashion & Apparel) v0.6 UI
2
  # 9:16 dikey kadraj — Still Image (VTO) → (optional) Runway Video
3
  # Demo: aynı klasörde input.jpg, tryon.jpg, fitstudio_video.mp4 varsa otomatik önizleme
4
+ # Bu sürümde tüm “Input / Look / Video” kutuları aynı boyutta, yan yana ve merkezde.
5
 
6
  import os, io, base64, zipfile, requests, streamlit as st
7
  from pathlib import Path
8
  from PIL import Image
9
 
10
+ # ============== Env & API =================
11
  def _env(k):
12
  return (os.getenv(k) or "").strip().strip("'\"")
13
 
 
25
 
26
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
27
 
28
+ # Tek tip 9:16 kart ölçüsü
29
+ PREVIEW_H = int(os.getenv("PREVIEW_H", "640")) # piksel
30
+ PREVIEW_W = int(round(PREVIEW_H * 9 / 16)) # 360
31
  PREVIEW_BG = (255, 255, 255)
32
 
33
+ # ============== State =====================
34
+ for k, v in {
35
  "results": None,
36
  "job_counter": 0,
37
  "duration": "6",
38
+ }.items():
39
+ st.session_state.setdefault(k, v)
 
 
40
 
41
+ # ============== Helpers ===================
42
  def _needs_auth(url: str) -> bool:
43
  return (url.startswith(API_BASE) or url.startswith("outputs/"))
44
 
45
  @st.cache_data(ttl=300, show_spinner=False)
46
  def fetch_bytes(url_or_path: str | None):
 
47
  if not url_or_path:
48
  return None
49
  try:
 
75
 
76
  @st.cache_data(ttl=300, show_spinner=False)
77
  def vertical_preview_9x16(img_bytes: bytes, out_w: int = PREVIEW_W, out_h: int = PREVIEW_H, bg_rgb=PREVIEW_BG) -> bytes:
 
78
  im = Image.open(io.BytesIO(img_bytes)).convert("RGB")
79
  w, h = im.size
80
  scale = min(out_w / w, out_h / h)
 
85
  canvas.paste(im_resized, off)
86
  buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
87
 
88
+ def b64_img_png(png_bytes: bytes) -> str:
89
+ return "data:image/png;base64," + base64.b64encode(png_bytes).decode("utf-8")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
+ def b64_video_mp4(mp4_bytes: bytes) -> str:
92
+ return "data:video/mp4;base64," + base64.b64encode(mp4_bytes).decode("utf-8")
 
 
 
 
93
 
94
  def backend_ok() -> bool:
95
  try:
 
104
  r.raise_for_status(); return r.json()
105
 
106
  def post_chain(data: dict, files_payload):
107
+ payload = {**data, "to_video": "false"}
108
  r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
109
  headers=HEADERS if HF_TOKEN else None, timeout=600)
110
  r.raise_for_status(); return r.json()
 
114
  headers=HEADERS if HF_TOKEN else None, timeout=600)
115
  r.raise_for_status(); return r.json()
116
 
117
+ def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
118
+ buf = io.BytesIO()
119
+ with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
120
+ for fname, b in named_bytes:
121
+ if b: z.writestr(fname, b)
122
+ buf.seek(0); return buf.read()
123
+
124
+ # ============== Common CSS (kartlar sabit, merkezde, yan yana) ==============
125
  st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
126
+ st.markdown(f"""
127
+ <style>
128
+ :root {{
129
+ --frameW: {PREVIEW_W}px;
130
+ --frameH: {PREVIEW_H}px;
131
+ }}
132
+ .media-grid {{
133
+ display: grid;
134
+ grid-template-columns: repeat(auto-fit, minmax(var(--frameW), 1fr));
135
+ gap: 28px;
136
+ justify-content: center;
137
+ align-items: start;
138
+ }}
139
+ .media-card {{
140
+ width: var(--frameW);
141
+ margin: 0 auto;
142
+ }}
143
+ .media-title {{
144
+ font-weight: 600;
145
+ margin-bottom: 8px;
146
+ }}
147
+ .media-frame {{
148
+ width: var(--frameW);
149
+ height: var(--frameH);
150
+ background: #fff;
151
+ border: 1px solid #e9e9ec;
152
+ border-radius: 12px;
153
+ overflow: hidden;
154
+ display:flex;
155
+ align-items:center;
156
+ justify-content:center;
157
+ box-shadow: 0 1px 2px rgba(0,0,0,.05);
158
+ }}
159
+ .media-frame img, .media-frame video {{
160
+ width: 100%; height: 100%; object-fit: contain;
161
+ display:block;
162
+ }}
163
+ .media-actions {{
164
+ margin-top: 6px; font-size: 12px; color: #666; text-align:center;
165
+ }}
166
+ .media-actions a {{
167
+ text-decoration: none; color: #444; border:1px solid #ddd; padding:4px 8px;
168
+ border-radius: 6px; display:inline-block;
169
+ }}
170
+ </style>
171
+ """, unsafe_allow_html=True)
172
+
173
+ # ============== Header & Controls =================
174
  st.title("🧵 Fit Studio AI")
175
  st.caption("Fashion & Apparel — Dikey 9:16 Still Image (VTO) → (opsiyonel) Runway Video")
176
 
 
177
  ok = backend_ok()
178
+ c0, c1, c2, c3, c4, c5 = st.columns([1.0, 1.5, 1.5, 1.2, 1.6, 1.6])
 
179
  with c0:
180
  category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
181
  with c1:
 
191
 
192
  prompt_box = st.text_area("Özel prompt (boş bırak → varsayılan)", value="", height=90, key="custom_prompt")
193
 
194
+ cA, cB = st.columns([1,1])
195
+ with cA:
196
  st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
197
+ with cB:
198
  run_image = st.button("Çalıştır (Sadece Foto)", type="primary", use_container_width=True, key="run_img_btn")
199
  run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
200
 
201
+ # ============== Demo Preview (aynı satırda, eşit kadraj) =================
202
+ def card_html_img(title: str, raw_bytes: bytes, fname: str):
203
+ pv = vertical_preview_9x16(raw_bytes, PREVIEW_W, PREVIEW_H, PREVIEW_BG)
204
+ data_url = b64_img_png(pv)
205
+ dl_url = "data:image/jpeg;base64," + base64.b64encode(raw_bytes).decode("utf-8")
206
+ return f"""
207
+ <div class="media-card">
208
+ <div class="media-title">{title}</div>
209
+ <div class="media-frame"><img src="{data_url}" alt="{title}"/></div>
210
+ <div class="media-actions"><a download="{fname}" href="{dl_url}">İndir</a></div>
211
+ </div>
212
+ """
213
+
214
+ def card_html_video(title: str, video_bytes: bytes, fname: str="fitstudio_video.mp4"):
215
+ vurl = b64_video_mp4(video_bytes)
216
+ return f"""
217
+ <div class="media-card">
218
+ <div class="media-title">{title}</div>
219
+ <div class="media-frame">
220
+ <video src="{vurl}" autoplay loop muted playsinline></video>
221
+ </div>
222
+ <div class="media-actions"><a download="{fname}" href="{vurl}">Video indir</a></div>
223
+ </div>
224
+ """
225
+
226
  st.markdown("---")
227
  st.subheader("Demo preview (çalıştırmadan önce)")
228
+ st.caption("Aynı klasörden **input.jpg**, **tryon.jpg** ve **fitstudio_video.mp4** okunur. Tüm kartlar 9:16 ve aynı boyutta.")
229
 
230
+ demo_cards = []
231
  in_b = fetch_bytes("input.jpg")
232
+ if in_b: demo_cards.append(card_html_img("Input", in_b, f"input{infer_ext(in_b)}"))
233
+ tr_b = fetch_bytes("tryon.jpg")
234
+ if tr_b: demo_cards.append(card_html_img("Sample Output", tr_b, f"tryon{infer_ext(tr_b)}"))
235
+ dv_b = fetch_bytes("fitstudio_video.mp4")
236
+ if dv_b: demo_cards.append(card_html_video("Sample Video", dv_b))
237
+
238
+ if demo_cards:
239
+ st.markdown('<div class="media-grid">' + "\n".join(demo_cards) + "</div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
240
  else:
241
+ st.info("Demo içerik bulunamadı (input.jpg / tryon.jpg / fitstudio_video.mp4).")
242
 
243
+ # ============== Input preview (aynı kart stilinde) =================
 
 
 
 
 
 
244
  st.markdown("---")
245
+ in_cards = []
 
 
246
  input_preview = None
247
  if file_list:
248
+ try: input_preview = file_list[0].getvalue()
249
+ except Exception: input_preview = None
 
 
250
  elif image_url:
251
  input_preview = fetch_bytes(image_url)
252
 
253
  if input_preview:
254
+ in_cards.append(card_html_img("Input", input_preview, f"input_runtime{infer_ext(input_preview)}"))
255
+ st.markdown('<div class="media-grid">' + "\n".join(in_cards) + "</div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
256
 
257
+ # ============== Run =================
258
  def _build_files_payload():
259
  files_payload = []
260
  if file_list:
 
293
  out = post_chain(form, files_payload) # to_video=false
294
  image_json = out.get("image_step") or out
295
 
 
296
  imgs_norm = (image_json.get("result") or {}).get("images_9_16") or []
 
297
  if not imgs_norm:
298
  imgs_raw = (image_json.get("result") or {}).get("images") or []
299
  imgs_norm = [{"url": i.get("url")} for i in imgs_raw if isinstance(i, dict) and i.get("url")]
 
300
  urls = [i.get("url") for i in imgs_norm if isinstance(i, dict) and i.get("url")]
301
 
302
  st.session_state["job_counter"] += 1
 
306
  "image_urls": urls,
307
  "video_url": None,
308
  }
 
309
  except requests.HTTPError as e:
310
  st.error(f"HTTP {e.response.status_code}")
311
  except Exception as e:
312
  st.error(f"Hata: {e}")
313
 
314
+ # ============== Results grid (Look1/2/3 + Video) =================
315
  res = st.session_state.get("results") or {}
316
  if res:
317
+ urls = res.get("image_urls") or []
 
318
  video_url = res.get("video_url")
319
 
320
+ cards = []
 
321
  named = []
322
 
323
+ for idx in range(min(3, len(urls))):
324
+ u = urls[idx]
325
+ b = fetch_bytes(u)
326
+ if not b: continue
327
+ cards.append(card_html_img("Look" if idx==0 else f"Look #{idx+1}", b, f"look_{idx+1}{infer_ext(b)}"))
328
+ named.append((f"look_{idx+1}{infer_ext(b)}", b))
 
 
329
 
330
+ # Video hücresi (küçük kadraj, otomatik loop)
331
  vbytes = None
332
  if video_url:
333
+ vbytes = fetch_bytes(video_url)
334
+ if vbytes: cards.append(card_html_video("Video", vbytes))
 
 
335
 
 
336
  st.markdown("---")
337
+ st.subheader("Sonuçlar")
338
+ if cards:
339
+ st.markdown('<div class="media-grid">' + "\n".join(cards) + "</div>", unsafe_allow_html=True)
340
+ else:
341
+ st.info("Gösterilecek sonuç bulunamadı.")
342
+
343
+ # Video üretim kontrolleri
344
+ ctl1, ctl2, ctl3 = st.columns([1.1, 1.0, 1.2])
345
+ with ctl1:
346
  gen_video = st.checkbox("Bu sonuçtan video üret (loop)", value=False, key="video_toggle")
347
+ with ctl2:
348
  st.selectbox("Video süresi", ["6","10"], index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")
349
+ with ctl3:
350
  go_video = st.button("Video Oluştur", use_container_width=True, key="video_btn")
351
 
352
  if gen_video and go_video:
353
+ src_for_video = urls[0] if urls else None
354
  if not src_for_video:
355
  st.warning("Video için kaynak görsel yok.")
356
  else:
 
367
  vurl = (v.get("result") or {}).get("video", {}).get("url")
368
  if vurl:
369
  st.session_state["results"]["video_url"] = vurl
370
+ vbytes = fetch_bytes(vurl)
371
+ # tekrar kartları çiz
372
+ cards.append(card_html_video("Video", vbytes if vbytes else b""))
373
+ st.markdown('<div class="media-grid">' + "\n".join(cards) + "</div>", unsafe_allow_html=True)
374
  else:
375
  st.info("Video URL gelmedi.")
376
 
377
+ # ZIP indir (Streamlit butonu)
378
  if named or vbytes:
379
+ if vbytes: named.append(("video.mp4", vbytes))
 
 
380
  st.download_button(
381
  "Tümünü indir (ZIP)",
382
+ data=make_zip(named),
383
  file_name="fitstudio_outputs.zip",
384
  mime="application/zip",
385
+ key=f"zip_{st.session_state['job_counter']}"
386
  )
387
 
388
  # Debug
 
392
  "schema_version": res.get("schema_version"),
393
  "api_base": API_BASE,
394
  "has_auth_header": bool(HF_TOKEN),
395
+ "image_urls": urls,
396
  "video_url": res.get("video_url"),
397
+ "frame": f"{PREVIEW_W}×{PREVIEW_H} (9:16)"
398
  })
399
 
400
+ # ============== Sidebar =================
401
  with st.sidebar:
402
  st.caption(f"Resolved API_BASE: {API_BASE}")
403
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
 
404
  if st.button("Ping /health"):
405
  try:
406
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
407
  st.write(r.status_code)
408
+ try: st.json(r.json())
409
+ except Exception: st.code((r.text or "")[:1000])
 
 
410
  except Exception as e:
411
  st.error(f"Health error: {e}")
 
412
  if st.button("Clear cache"):
413
  st.cache_data.clear(); st.success("Cache cleared.")
414
  if st.button("Clear results"):