renderfy commited on
Commit
0026423
·
verified ·
1 Parent(s): 349595d

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +167 -237
streamlit_app.py CHANGED
@@ -1,15 +1,13 @@
1
  # streamlit_app.py — Fit Studio AI (Fashion & Apparel) v0.5 UI
2
- # Still Image (VTO) → (optional) Runway Video
3
- # Demo preview: same folder "input.jpg", "tryon.jpg", "fitstudio_video.mp4"
4
- # NOT: Tüm kutular 9:16 kadrajla aynı boyutta, video loop + autoplay + muted.
5
 
6
  import os, io, zipfile, base64, requests, streamlit as st
7
  from pathlib import Path
8
  from PIL import Image
9
 
10
- # ================= Env & API (robust) =================
11
- def _env(k):
12
- return (os.getenv(k) or "").strip().strip("'\"")
13
 
14
  API_BASE = (
15
  _env("FITSTUDIO_API")
@@ -19,7 +17,7 @@ API_BASE = (
19
  _env("FITSTUDIO_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")
20
  ) else ""
21
 
22
- HF_TOKEN = (_env("FITSTUDIO_TOKEN") or _env("AI_LIGHTBOX_TOKEN") or _env("LUXFIT_TOKEN"))
23
 
24
  if not API_BASE:
25
  st.error("API_BASE bulunamadı. Settings > Variables: FITSTUDIO_API (veya AI_LIGHTBOX_API / LUXFIT_API) tanımlayın.")
@@ -27,71 +25,24 @@ if not API_BASE:
27
 
28
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
29
 
30
- # --- 9:16 kadraj önizleme boyutları (tüm kutular aynı) ---
31
- PREVIEW_H = int(os.getenv("PREVIEW_H", "640")) # uzun kenar (yükseklik)
32
- PREVIEW_W = int(round(PREVIEW_H * 9 / 16)) # 9:16 genişlik
33
- BG_RGB = (255, 255, 255)
34
-
35
- # ================= Global UI config =================
36
- st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
37
-
38
- # Basit CSS (tile yüzeyi, başlık boşluğu, video responsive 9:16)
39
- st.markdown(
40
- f"""
41
- <style>
42
- .fit-tile {{
43
- background: #fafafa;
44
- border: 1px solid #eee;
45
- border-radius: 10px;
46
- padding: 10px;
47
- }}
48
- .fit-title {{
49
- font-weight: 600; margin-bottom: 6px;
50
- }}
51
- .fit-frame {{
52
- width: 100%;
53
- max-width: {PREVIEW_W}px;
54
- aspect-ratio: 9 / 16;
55
- display: block;
56
- margin: 0 auto;
57
- background: #fff;
58
- border-radius: 8px;
59
- overflow: hidden;
60
- border: 1px solid #f0f0f0;
61
- }}
62
- .fit-caption {{
63
- text-align: center;
64
- color: #666;
65
- font-size: 12px;
66
- margin-top: 6px;
67
- }}
68
- .stDownloadButton > button {{
69
- width: 100%;
70
- }}
71
- </style>
72
- """,
73
- unsafe_allow_html=True
74
- )
75
 
76
  # ================= Session State =================
77
- defaults = {
78
- "results": None,
79
- "job_counter": 0,
80
- "duration": "6",
81
- }
82
  for k, v in defaults.items():
83
  if k not in st.session_state:
84
  st.session_state[k] = v
85
 
86
  # ================= Helpers =================
87
  def _needs_auth(url: str) -> bool:
88
- return (url.startswith(API_BASE) or url.startswith("outputs/"))
89
 
90
  @st.cache_data(ttl=300, show_spinner=False)
91
  def fetch_bytes(url_or_path: str | None):
92
- """
93
- URL, outputs/ yolu veya mevcut klasörden dosya okur.
94
- """
95
  if not url_or_path:
96
  return None
97
  try:
@@ -107,10 +58,22 @@ def fetch_bytes(url_or_path: str | None):
107
  return None
108
 
109
  @st.cache_data(ttl=300, show_spinner=False)
110
- def img_size(b: bytes):
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  try:
112
- im = Image.open(io.BytesIO(b))
113
- return im.size
114
  except Exception:
115
  return None
116
 
@@ -122,191 +85,149 @@ def infer_ext(data: bytes) -> str:
122
  except Exception:
123
  return ".jpg"
124
 
125
- @st.cache_data(ttl=300, show_spinner=False)
126
- def to_9_16_preview(img_bytes: bytes, w=PREVIEW_W, h=PREVIEW_H, bg_rgb=BG_RGB) -> bytes:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  """
128
- Görseli 9:16 önizleme tuvaline sığdır (letterbox/pillarbox), PNG döndür.
129
- """
130
- im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")
131
- iw, ih = im.size
132
- scale = min(w / iw, h / ih)
133
- nw, nh = max(1, int(iw * scale)), max(1, int(ih * scale))
134
- im_resized = im.resize((nw, nh), Image.LANCZOS)
135
- canvas = Image.new("RGBA", (w, h), (*bg_rgb, 255))
136
- off = ((w - nw)//2, (h - nh)//2)
137
- canvas.paste(im_resized, off, im_resized)
138
- buf = io.BytesIO()
139
- canvas.save(buf, format="PNG")
140
- return buf.getvalue()
141
-
142
- def b64(data: bytes) -> str:
143
- return base64.b64encode(data).decode("ascii")
144
-
145
- def show_image_tile(col, title: str, source: str | bytes | None, filename_stub="image", key=""):
146
- with col:
147
- st.markdown('<div class="fit-tile">', unsafe_allow_html=True)
148
- st.markdown(f'<div class="fit-title">{title}</div>', unsafe_allow_html=True)
149
-
150
- if source is None:
151
- # boş kutu
152
- ph = Image.new("RGBA", (PREVIEW_W, PREVIEW_H), (255,255,255,255))
153
- buf = io.BytesIO(); ph.save(buf, "PNG")
154
- st.image(buf.getvalue(), use_container_width=False, width=PREVIEW_W)
155
- st.markdown('<div class="fit-caption">—</div>', unsafe_allow_html=True)
156
- st.markdown('</div>', unsafe_allow_html=True)
157
- return None
158
-
159
- b = source if isinstance(source, (bytes, bytearray)) else fetch_bytes(source)
160
- if not b:
161
- ph = Image.new("RGBA", (PREVIEW_W, PREVIEW_H), (255,255,255,255))
162
- buf = io.BytesIO(); ph.save(buf, "PNG")
163
- st.image(buf.getvalue(), use_container_width=False, width=PREVIEW_W)
164
- st.markdown('<div class="fit-caption">Yüklenemedi</div>', unsafe_allow_html=True)
165
- st.markdown('</div>', unsafe_allow_html=True)
166
- return None
167
-
168
- pv = to_9_16_preview(b)
169
- st.image(pv, use_container_width=False, width=PREVIEW_W)
170
-
171
- sz = img_size(b)
172
- if sz:
173
- st.markdown(f'<div class="fit-caption">{sz[0]}×{sz[1]} px</div>', unsafe_allow_html=True)
174
-
175
- ext = infer_ext(b)
176
- mime = "image/jpeg" if ext in [".jpg",".jpeg"] else ("image/png" if ext==".png" else "image/webp")
177
- st.download_button("İndir (tam kalite)", data=b, file_name=f"{filename_stub}{ext}", mime=mime, key=f"{key}_img_dl")
178
- st.markdown('</div>', unsafe_allow_html=True)
179
- return b
180
-
181
- def show_video_tile(col, title: str, source: str | bytes | None, key=""):
182
- with col:
183
- st.markdown('<div class="fit-tile">', unsafe_allow_html=True)
184
- st.markdown(f'<div class="fit-title">{title}</div>', unsafe_allow_html=True)
185
-
186
- if source is None:
187
- # boş 9:16 kutu
188
- st.markdown(f'<div class="fit-frame"></div>', unsafe_allow_html=True)
189
- st.markdown('<div class="fit-caption">—</div>', unsafe_allow_html=True)
190
- st.markdown('</div>', unsafe_allow_html=True)
191
- return None
192
-
193
- # Video bytes (tercihen bytes ile loop için data URI)
194
- vb = source if isinstance(source, (bytes, bytearray)) else fetch_bytes(source)
195
- if not vb:
196
- st.markdown(f'<div class="fit-frame"></div>', unsafe_allow_html=True)
197
- st.markdown('<div class="fit-caption">Video yüklenemedi</div>', unsafe_allow_html=True)
198
- st.markdown('</div>', unsafe_allow_html=True)
199
- return None
200
-
201
- data_uri = f"data:video/mp4;base64,{b64(vb)}"
202
- html = f"""
203
- <video class="fit-frame" src="{data_uri}" controls autoplay loop muted playsinline></video>
204
- """
205
- st.markdown(html, unsafe_allow_html=True)
206
- st.download_button("Video indir (MP4)", data=vb, file_name="fitstudio_video.mp4", mime="video/mp4", key=f"{key}_vid_dl")
207
- st.markdown('</div>', unsafe_allow_html=True)
208
- return vb
209
 
210
  def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
211
  buf = io.BytesIO()
212
  with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
213
  for fname, b in named_bytes:
214
- if b:
215
- z.writestr(fname, b)
216
- buf.seek(0)
217
- return buf.read()
218
 
219
  def backend_ok() -> bool:
220
  try:
221
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
222
- r.raise_for_status()
223
- return True
224
  except Exception:
225
  return False
226
 
227
  def post_image_edit(data: dict, files_payload):
228
  r = requests.post(f"{API_BASE}/v1/image/edit", data=data, files=files_payload or None,
229
  headers=HEADERS if HF_TOKEN else None, timeout=600)
230
- r.raise_for_status()
231
- return r.json()
232
 
233
  def post_chain(data: dict, files_payload):
234
- payload = {**data, "to_video": "false"} # video UI'da ayrı
235
  r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
236
  headers=HEADERS if HF_TOKEN else None, timeout=600)
237
- r.raise_for_status()
238
- return r.json()
239
 
240
- def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=True,
241
- gender="auto", age_group="auto", frame_aspect="9:16"):
242
  payload = {
243
- "image_url": image_url,
244
- "duration": duration,
245
- "resolution": resolution,
246
  "prompt_optimizer": "true" if prompt_optimizer else "false",
247
- "gender": gender,
248
- "age_group": age_group,
249
- "frame_aspect": frame_aspect,
250
  }
251
  r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload,
252
  headers=HEADERS if HF_TOKEN else None, timeout=600)
253
- r.raise_for_status()
254
- return r.json()
255
 
256
- # ================= Header =================
 
257
  st.title("🧵 Fit Studio AI")
258
- st.caption("Fashion & Apparel — 9:16 stills (VTO) → (optional) runway video")
259
 
 
260
  ok = backend_ok()
261
- c0, c1, c2, c3, c4, c5 = st.columns([1.1, 1.7, 1.7, 1.2, 1.8, 1.8])
262
 
263
  with c0:
264
  category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
265
- with c1:
 
266
  gender = st.selectbox("Cinsiyet", ["auto","female","male","unisex"], index=0, key="gender")
267
- with c2:
 
268
  age_group = st.selectbox("Yaş grubu", ["auto","teen","young_adult","adult","mature"], index=0, key="age_group")
 
 
 
 
 
269
  with c3:
270
- num_images = st.selectbox("Çıktı adedi", [1,2,3,4], index=0, key="num_images")
271
- with c4:
272
- file_list = st.file_uploader("Garment image (upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
273
- with c5:
274
  image_url = st.text_input("veya Garment URL", key="image_url")
275
 
276
- row_btn1, row_btn2 = st.columns([1,1])
277
- with row_btn1:
278
- st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
279
- with row_btn2:
 
 
 
280
  run_image = st.button("Çalıştır (Sadece Foto)", type="primary", use_container_width=True, key="run_img_btn")
281
  run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
282
 
283
- # ================= Demo Preview: 3 sütun (hepsi 9:16) =================
284
  st.markdown("---")
285
- st.subheader("Demo preview")
 
286
 
287
- dp_in, dp_out, dp_vid = st.columns(3)
288
- demo_in_b = fetch_bytes("input.jpg")
289
- demo_try_b = fetch_bytes("tryon.jpg")
290
- demo_v_b = fetch_bytes("fitstudio_video.mp4")
291
 
292
- show_image_tile(dp_in, "Input", demo_in_b, filename_stub="input", key="demo_in")
293
- show_image_tile(dp_out, "Sample Output", demo_try_b, filename_stub="tryon", key="demo_out")
294
- show_video_tile(dp_vid, "Sample Video", demo_v_b, key="demo_vid") # loop + autoplay
295
 
296
- # ================= Girdi Önizleme (yan yana 9:16) =================
297
- st.markdown("---")
298
- g1, g2, g3, g4 = st.columns(4)
299
 
 
 
 
 
 
 
300
  input_preview = None
301
  if file_list:
302
- try:
303
- input_preview = file_list[0].getvalue()
304
- except Exception:
305
- input_preview = None
306
  elif image_url:
307
  input_preview = fetch_bytes(image_url)
308
 
309
- show_image_tile(g1, "Input", input_preview, filename_stub="input_current", key="cur_in")
310
 
311
  # ================ Çalıştır ================
312
  def _build_files_payload():
@@ -324,8 +245,7 @@ def _common_form_data():
324
  "frame_aspect": "9:16",
325
  "num_images": str(st.session_state["num_images"]),
326
  }
327
- # Opsiyonel özel prompt alanı (kullanıcı isterse açsın)
328
- if "custom_prompt" in st.session_state and st.session_state["custom_prompt"].strip():
329
  data["prompt"] = st.session_state["custom_prompt"].strip()
330
  if image_url:
331
  data["image_urls"] = image_url.strip()
@@ -340,7 +260,6 @@ if run_image or run_chain:
340
  try:
341
  files_payload = _build_files_payload()
342
  form = _common_form_data()
343
-
344
  with st.spinner("Çalışıyor…"):
345
  if run_image:
346
  out = post_image_edit(form, files_payload)
@@ -349,9 +268,19 @@ if run_image or run_chain:
349
  out = post_chain(form, files_payload) # to_video=false
350
  image_json = out.get("image_step") or out
351
 
352
- # 9:16 normalize edilmiş seti kullan
353
- imgs_norm = (image_json.get("result") or {}).get("images_9_16") or []
354
- urls = [i.get("url") for i in imgs_norm if isinstance(i, dict) and i.get("url")]
 
 
 
 
 
 
 
 
 
 
355
 
356
  st.session_state["job_counter"] += 1
357
  st.session_state["results"] = {
@@ -366,34 +295,36 @@ if run_image or run_chain:
366
  except Exception as e:
367
  st.error(f"Hata: {e}")
368
 
369
- # ================ Sonuçları Satırda Yan Yana (hepsi 9:16) ================
370
  res = st.session_state.get("results") or {}
371
  if res:
372
  key_prefix = f"job{st.session_state['job_counter']}"
373
  img_urls = res.get("image_urls") or []
374
  video_url = res.get("video_url")
375
 
376
- # 1. Satır: 3 görsel + 1 video sütunu (aynı ebat, 9:16)
377
- c1, c2, c3, c4 = st.columns(4)
378
  named = []
379
 
 
380
  for idx, col in enumerate([c1, c2, c3]):
381
  if idx < len(img_urls):
382
  u = img_urls[idx]
383
  title = "Look" if idx == 0 else f"Look #{idx+1}"
384
- b = show_image_tile(col, title, u, filename_stub=f"look_{idx+1}", key=f"{key_prefix}_{idx}")
385
- if b:
386
- named.append((f"look_{idx+1}{infer_ext(b)}", b))
387
  else:
388
- show_image_tile(col, f"Look #{idx+1}", None, key=f"{key_prefix}_{idx}_empty")
389
 
 
390
  vbytes = None
391
  if video_url:
392
- vbytes = show_video_tile(c4, "Video", video_url, key=key_prefix)
393
  else:
394
- show_video_tile(c4, "Video", None, key=f"{key_prefix}_novid")
 
395
 
396
- # 2. Satır: Video üretim kontrolleri
397
  st.markdown("---")
398
  colv1, colv2, colv3 = st.columns([1.1, 1.0, 1.2])
399
  with colv1:
@@ -414,15 +345,14 @@ if res:
414
  duration=st.session_state.get("duration_select","6"),
415
  resolution="768P",
416
  prompt_optimizer=True,
417
- gender=st.session_state["gender"],
418
- age_group=st.session_state["age_group"],
419
- frame_aspect="9:16",
420
  )
421
  vurl = (v.get("result") or {}).get("video", {}).get("url")
422
  if vurl:
423
  st.session_state["results"]["video_url"] = vurl
424
  video_url = vurl
425
- vbytes = show_video_tile(c4, "Video", video_url, key=key_prefix)
426
  else:
427
  st.info("Video URL gelmedi.")
428
 
@@ -431,32 +361,32 @@ if res:
431
  if vbytes:
432
  named.append(("video.mp4", vbytes))
433
  zip_bytes = make_zip(named)
434
- st.download_button(
435
- "Tümünü indir (ZIP)",
436
- data=zip_bytes,
437
- file_name="fitstudio_outputs.zip",
438
- mime="application/zip",
439
- key=f"{key_prefix}_zip_dl"
440
- )
441
-
442
- # ================= Optional: Özel prompt alanı (isteğe bağlı) =================
443
- with st.expander("Özel prompt (isteğe bağlı)"):
444
- st.text_area("Prompt", key="custom_prompt", height=120, help="Boş bırakabilirsiniz; backend cinsiyet/yaş ve 9:16 kompozisyonu kendisi ayarlıyor.")
445
-
446
- # ================= Sidebar (teşhis) =================
 
 
 
447
  with st.sidebar:
448
- st.caption(f"API_BASE: {API_BASE}")
449
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
450
- st.caption(f"Preview frame: {PREVIEW_W}×{PREVIEW_H} (9:16)")
451
 
452
  if st.button("Ping /health"):
453
  try:
454
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
455
  st.write(r.status_code)
456
- try:
457
- st.json(r.json())
458
- except Exception:
459
- st.code((r.text or "")[:1000])
460
  except Exception as e:
461
  st.error(f"Health error: {e}")
462
 
 
1
  # streamlit_app.py — Fit Studio AI (Fashion & Apparel) v0.5 UI
2
+ # 9:16 uniform tiles, gender & age controls, looping sample/result videos
3
+ # Demo: put these next to this file → input.jpg, tryon.jpg, fitstudio_video.mp4
 
4
 
5
  import os, io, zipfile, base64, requests, streamlit as st
6
  from pathlib import Path
7
  from PIL import Image
8
 
9
+ # ================= Env & API =================
10
+ def _env(k): return (os.getenv(k) or "").strip().strip("'\"")
 
11
 
12
  API_BASE = (
13
  _env("FITSTUDIO_API")
 
17
  _env("FITSTUDIO_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")
18
  ) else ""
19
 
20
+ HF_TOKEN = _env("FITSTUDIO_TOKEN") or _env("AI_LIGHTBOX_TOKEN") or _env("LUXFIT_TOKEN")
21
 
22
  if not API_BASE:
23
  st.error("API_BASE bulunamadı. Settings > Variables: FITSTUDIO_API (veya AI_LIGHTBOX_API / LUXFIT_API) tanımlayın.")
 
25
 
26
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
27
 
28
+ # === Tile geometry (strict 9:16) ===
29
+ TILE_H = int(os.getenv("TILE_H", "640")) # all tiles same height
30
+ TILE_W = max(180, int(round(TILE_H * 9 / 16))) # width derived from 9:16
31
+ PREVIEW_BG = (255, 255, 255)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  # ================= Session State =================
34
+ defaults = {"results": None, "job_counter": 0, "duration": "6"}
 
 
 
 
35
  for k, v in defaults.items():
36
  if k not in st.session_state:
37
  st.session_state[k] = v
38
 
39
  # ================= Helpers =================
40
  def _needs_auth(url: str) -> bool:
41
+ return url.startswith(API_BASE) or url.startswith("outputs/")
42
 
43
  @st.cache_data(ttl=300, show_spinner=False)
44
  def fetch_bytes(url_or_path: str | None):
45
+ """Reads local file (same folder) or fetches URL."""
 
 
46
  if not url_or_path:
47
  return None
48
  try:
 
58
  return None
59
 
60
  @st.cache_data(ttl=300, show_spinner=False)
61
+ def image_to_9_16_canvas(img_bytes: bytes, w: int = TILE_W, h: int = TILE_H, bg_rgb=PREVIEW_BG) -> bytes:
62
+ """Letterbox/pillarbox to exact 9:16 tile size, no crop."""
63
+ im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")
64
+ iw, ih = im.size
65
+ scale = min(w / iw, h / ih)
66
+ nw, nh = max(1, int(iw*scale)), max(1, int(ih*scale))
67
+ im_resized = im.resize((nw, nh), Image.LANCZOS)
68
+ canvas = Image.new("RGBA", (w, h), (*bg_rgb, 255))
69
+ off = ((w - nw)//2, (h - nh)//2)
70
+ canvas.paste(im_resized, off, im_resized)
71
+ out = io.BytesIO(); canvas.save(out, format="PNG"); return out.getvalue()
72
+
73
+ @st.cache_data(ttl=300, show_spinner=False)
74
+ def image_size_from_bytes(b: bytes):
75
  try:
76
+ im = Image.open(io.BytesIO(b)); return im.size
 
77
  except Exception:
78
  return None
79
 
 
85
  except Exception:
86
  return ".jpg"
87
 
88
+ def show_image_tile(col, title: str, src: str|bytes|None, filename_stub="image", key_prefix=""):
89
+ col.markdown(f"#### {title}")
90
+ if not src:
91
+ placeholder = Image.new("RGBA", (TILE_W, TILE_H), (240,240,240,255))
92
+ buf = io.BytesIO(); placeholder.save(buf, "PNG")
93
+ col.image(buf.getvalue(), width=TILE_W)
94
+ col.caption("—"); return None, None
95
+ b = src if isinstance(src, (bytes, bytearray)) else fetch_bytes(src)
96
+ if not b:
97
+ col.warning("Görsel yüklenemedi"); return None, None
98
+ tile = image_to_9_16_canvas(b, w=TILE_W, h=TILE_H, bg_rgb=PREVIEW_BG)
99
+ col.image(tile, width=TILE_W)
100
+ sz = image_size_from_bytes(b)
101
+ if sz: col.caption(f"Kaynak: {sz[0]}×{sz[1]} px")
102
+ ext = infer_ext(b)
103
+ mime = "image/jpeg" if ext in [".jpg",".jpeg"] else ("image/png" if ext==".png" else "image/webp")
104
+ col.download_button("İndir (tam kalite)", data=b, file_name=f"{filename_stub}{ext}", mime=mime,
105
+ key=f"{key_prefix}_{filename_stub}_dl")
106
+ return src, b
107
+
108
+ def _video_html(src: str, w: int = TILE_W, h: int = TILE_H) -> str:
109
+ return f"""
110
+ <video src="{src}" width="{w}" height="{h}" autoplay muted loop playsinline controls
111
+ style="display:block; background:#fff; object-fit:contain; border-radius:12px;
112
+ box-shadow:0 1px 6px rgba(0,0,0,.08);"></video>
113
  """
114
+
115
+ def show_video_tile(col, title: str, src_or_bytes: str|bytes|None, key_prefix=""):
116
+ col.markdown(f"#### {title}")
117
+ if not src_or_bytes:
118
+ col.caption("—"); return None
119
+ if isinstance(src_or_bytes, (bytes, bytearray)):
120
+ b64 = base64.b64encode(src_or_bytes).decode("ascii")
121
+ html = _video_html(f"data:video/mp4;base64,{b64}", w=TILE_W, h=TILE_H)
122
+ col.markdown(html, unsafe_allow_html=True)
123
+ vb = bytes(src_or_bytes)
124
+ else:
125
+ # remote/local path → try to fetch for download button, but loop via URL
126
+ vb = fetch_bytes(src_or_bytes)
127
+ html = _video_html(src_or_bytes, w=TILE_W, h=TILE_H)
128
+ col.markdown(html, unsafe_allow_html=True)
129
+ if vb:
130
+ col.download_button("Video indir (MP4)", data=vb, file_name="fitstudio_video.mp4", mime="video/mp4",
131
+ key=f"{key_prefix}_video_dl")
132
+ return vb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
135
  buf = io.BytesIO()
136
  with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
137
  for fname, b in named_bytes:
138
+ if b: z.writestr(fname, b)
139
+ buf.seek(0); return buf.read()
 
 
140
 
141
  def backend_ok() -> bool:
142
  try:
143
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
144
+ r.raise_for_status(); return True
 
145
  except Exception:
146
  return False
147
 
148
  def post_image_edit(data: dict, files_payload):
149
  r = requests.post(f"{API_BASE}/v1/image/edit", data=data, files=files_payload or None,
150
  headers=HEADERS if HF_TOKEN else None, timeout=600)
151
+ r.raise_for_status(); return r.json()
 
152
 
153
  def post_chain(data: dict, files_payload):
154
+ payload = {**data, "to_video": "false"} # video ayrı
155
  r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
156
  headers=HEADERS if HF_TOKEN else None, timeout=600)
157
+ r.raise_for_status(); return r.json()
 
158
 
159
+ def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=True, gender="auto", age_group="auto"):
 
160
  payload = {
161
+ "image_url": image_url, "duration": duration, "resolution": resolution,
 
 
162
  "prompt_optimizer": "true" if prompt_optimizer else "false",
163
+ "gender": gender, "age_group": age_group, "frame_aspect": "9:16"
 
 
164
  }
165
  r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload,
166
  headers=HEADERS if HF_TOKEN else None, timeout=600)
167
+ r.raise_for_status(); return r.json()
 
168
 
169
+ # ================= Page =================
170
+ st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
171
  st.title("🧵 Fit Studio AI")
172
+ st.caption("Fashion & Apparel — 9:16 stills (VTO) → optional runway video (looping)")
173
 
174
+ # ================= Controls =================
175
  ok = backend_ok()
176
+ c0, c_gender, c_age, c2, c3, c4 = st.columns([1.0, 1.0, 1.2, 1.8, 1.4, 1.6])
177
 
178
  with c0:
179
  category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
180
+
181
+ with c_gender:
182
  gender = st.selectbox("Cinsiyet", ["auto","female","male","unisex"], index=0, key="gender")
183
+
184
+ with c_age:
185
  age_group = st.selectbox("Yaş grubu", ["auto","teen","young_adult","adult","mature"], index=0, key="age_group")
186
+
187
+ with c2:
188
+ file_list = st.file_uploader("Garment image (upload)", type=["jpg","jpeg","png","webp"],
189
+ accept_multiple_files=True, key="file_upl")
190
+
191
  with c3:
 
 
 
 
192
  image_url = st.text_input("veya Garment URL", key="image_url")
193
 
194
+ with c4:
195
+ num_images = st.selectbox("Çıktı adedi", [1,2,3,4], index=0, key="num_images")
196
+ custom_prompt = st.text_area("Özel prompt (boş → varsayılan)", value="", height=90, key="custom_prompt")
197
+
198
+ bA, bB = st.columns([1.0, 1.0])
199
+ with bA: st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
200
+ with bB:
201
  run_image = st.button("Çalıştır (Sadece Foto)", type="primary", use_container_width=True, key="run_img_btn")
202
  run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
203
 
204
+ # ================= Demo preview (uniform 9:16 tiles) =================
205
  st.markdown("---")
206
+ st.subheader("Demo preview (çalıştırmadan önce örnek)")
207
+ st.caption("Aynı klasörden okunur: input.jpg, tryon.jpg, fitstudio_video.mp4")
208
 
209
+ dp_in, dp_sp, dp_v = st.columns([1,1,1])
 
 
 
210
 
211
+ in_bytes = fetch_bytes("input.jpg")
212
+ show_image_tile(dp_in, "Input", in_bytes, filename_stub="input", key_prefix="demo_in")
 
213
 
214
+ try_bytes = fetch_bytes("tryon.jpg")
215
+ show_image_tile(dp_sp, "Sample Output", try_bytes, filename_stub="tryon", key_prefix="demo_out")
 
216
 
217
+ demo_vbytes = fetch_bytes("fitstudio_video.mp4")
218
+ show_video_tile(dp_v, "Sample Video (loop)", demo_vbytes, key_prefix="demo_vid")
219
+
220
+ # ================= Girdi önizleme (uniform 9:16) =================
221
+ st.markdown("---")
222
+ g1, _, _, _ = st.columns([1,1,1,1]) # sadece 1 tile gösteriyoruz
223
  input_preview = None
224
  if file_list:
225
+ try: input_preview = file_list[0].getvalue()
226
+ except Exception: input_preview = None
 
 
227
  elif image_url:
228
  input_preview = fetch_bytes(image_url)
229
 
230
+ show_image_tile(g1, "Input (preview)", input_preview, filename_stub="preview", key_prefix="preview")
231
 
232
  # ================ Çalıştır ================
233
  def _build_files_payload():
 
245
  "frame_aspect": "9:16",
246
  "num_images": str(st.session_state["num_images"]),
247
  }
248
+ if st.session_state.get("custom_prompt","").strip():
 
249
  data["prompt"] = st.session_state["custom_prompt"].strip()
250
  if image_url:
251
  data["image_urls"] = image_url.strip()
 
260
  try:
261
  files_payload = _build_files_payload()
262
  form = _common_form_data()
 
263
  with st.spinner("Çalışıyor…"):
264
  if run_image:
265
  out = post_image_edit(form, files_payload)
 
268
  out = post_chain(form, files_payload) # to_video=false
269
  image_json = out.get("image_step") or out
270
 
271
+ # Prefer normalized images_9_16
272
+ res_block = image_json.get("result") or {}
273
+ imgs_norm = res_block.get("images_9_16") or []
274
+ imgs_raw = res_block.get("images") or []
275
+ urls = []
276
+ if imgs_norm:
277
+ for it in imgs_norm:
278
+ u = it.get("url");
279
+ if u: urls.append(u)
280
+ elif imgs_raw:
281
+ for it in imgs_raw:
282
+ u = it.get("url")
283
+ if u: urls.append(u)
284
 
285
  st.session_state["job_counter"] += 1
286
  st.session_state["results"] = {
 
295
  except Exception as e:
296
  st.error(f"Hata: {e}")
297
 
298
+ # ================ Sonuçları Yan Yana (aynı kadraj) ================
299
  res = st.session_state.get("results") or {}
300
  if res:
301
  key_prefix = f"job{st.session_state['job_counter']}"
302
  img_urls = res.get("image_urls") or []
303
  video_url = res.get("video_url")
304
 
305
+ st.markdown("### Sonuçlar")
306
+ c1, c2, c3, c4 = st.columns([1,1,1,1])
307
  named = []
308
 
309
+ # İlk 3 görsel
310
  for idx, col in enumerate([c1, c2, c3]):
311
  if idx < len(img_urls):
312
  u = img_urls[idx]
313
  title = "Look" if idx == 0 else f"Look #{idx+1}"
314
+ _, b = show_image_tile(col, title, u, filename_stub=f"look_{idx+1}", key_prefix=f"{key_prefix}_{idx}")
315
+ if b: named.append((f"look_{idx+1}{infer_ext(b)}", b))
 
316
  else:
317
+ show_image_tile(col, f"Look #{idx+1}", None, key_prefix=f"{key_prefix}_{idx}")
318
 
319
+ # Video sütunu (loop)
320
  vbytes = None
321
  if video_url:
322
+ vbytes = show_video_tile(c4, "Video (loop)", video_url, key_prefix=key_prefix)
323
  else:
324
+ c4.markdown("#### Video (loop)")
325
+ c4.caption("Henüz üretilmedi.")
326
 
327
+ # Video üretim kontrolleri
328
  st.markdown("---")
329
  colv1, colv2, colv3 = st.columns([1.1, 1.0, 1.2])
330
  with colv1:
 
345
  duration=st.session_state.get("duration_select","6"),
346
  resolution="768P",
347
  prompt_optimizer=True,
348
+ gender=st.session_state.get("gender","auto"),
349
+ age_group=st.session_state.get("age_group","auto")
 
350
  )
351
  vurl = (v.get("result") or {}).get("video", {}).get("url")
352
  if vurl:
353
  st.session_state["results"]["video_url"] = vurl
354
  video_url = vurl
355
+ vbytes = show_video_tile(c4, "Video (loop)", video_url, key_prefix=key_prefix)
356
  else:
357
  st.info("Video URL gelmedi.")
358
 
 
361
  if vbytes:
362
  named.append(("video.mp4", vbytes))
363
  zip_bytes = make_zip(named)
364
+ st.download_button("Tümünü indir (ZIP)", data=zip_bytes, file_name="fitstudio_outputs.zip",
365
+ mime="application/zip", key=f"{key_prefix}_zip_dl")
366
+
367
+ # Debug
368
+ with st.expander("Debug"):
369
+ st.json({
370
+ "job_id": res.get("job_id"),
371
+ "schema_version": res.get("schema_version"),
372
+ "api_base": API_BASE,
373
+ "has_auth_header": bool(HF_TOKEN),
374
+ "image_urls": img_urls,
375
+ "video_url": res.get("video_url"),
376
+ "tile": f"{TILE_W}x{TILE_H} (9:16)"
377
+ })
378
+
379
+ # ================= Sidebar =================
380
  with st.sidebar:
381
+ st.caption(f"Resolved API_BASE: {API_BASE}")
382
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
 
383
 
384
  if st.button("Ping /health"):
385
  try:
386
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
387
  st.write(r.status_code)
388
+ try: st.json(r.json())
389
+ except Exception: st.code((r.text or "")[:1000])
 
 
390
  except Exception as e:
391
  st.error(f"Health error: {e}")
392