renderfy commited on
Commit
55f1076
·
verified ·
1 Parent(s): 2322a12

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +207 -220
streamlit_app.py CHANGED
@@ -1,39 +1,22 @@
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
- # Notlar:
5
- # - Tüm kutular (Input / Output / Video) eş boyutta kare kutular olarak gösterilir.
6
- # - Cinsiyet ve yaş grubu seçimleri prompt’a akıllıca enjekte edilir.
7
 
8
- import os, io, zipfile, base64, requests, streamlit as st
9
  from pathlib import Path
10
  from PIL import Image
11
- import streamlit.components.v1 as components
12
 
13
  # ================= Env & API (robust) =================
14
- def _env(k): return (os.getenv(k) or "").strip().strip("'\"")
 
15
 
16
  API_BASE = (
17
  _env("FITSTUDIO_API")
18
  or _env("AI_LIGHTBOX_API")
19
  or _env("LUXFIT_API")
20
- or _env("AI_BEAUTYBOX_API")
21
- or _env("AI_BEAUTY_API")
22
- ).rstrip("/") if (
23
- _env("FITSTUDIO_API")
24
- or _env("AI_LIGHTBOX_API")
25
- or _env("LUXFIT_API")
26
- or _env("AI_BEAUTYBOX_API")
27
- or _env("AI_BEAUTY_API")
28
- ) else ""
29
-
30
- HF_TOKEN = (
31
- _env("FITSTUDIO_TOKEN")
32
- or _env("AI_LIGHTBOX_TOKEN")
33
- or _env("LUXFIT_TOKEN")
34
- or _env("AI_BEAUTYBOX_TOKEN")
35
- or _env("AI_BEAUTY_TOKEN")
36
- )
37
 
38
  if not API_BASE:
39
  st.error("API_BASE bulunamadı. Settings > Variables: FITSTUDIO_API (veya AI_LIGHTBOX_API / LUXFIT_API) tanımlayın.")
@@ -41,19 +24,20 @@ if not API_BASE:
41
 
42
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
43
 
44
- # Kare kutu hedef boyutu (px)
45
- def _clampi(v, lo, hi):
46
- try: v = int(v)
47
- except: v = 360
48
- return max(lo, min(hi, v))
49
-
50
- TILE = _clampi(os.getenv("TILE_SIZE", "360"), 240, 720) # tüm kutular için tek boyut
51
- PREVIEW_SIZE = TILE # görüntü önizleme üretiminde de aynı değer
52
 
53
  # ================= Session State (ilk yük) =================
54
- defaults = {"results": None, "job_counter": 0, "duration": "6"}
 
 
 
 
55
  for k, v in defaults.items():
56
- if k not in st.session_state: st.session_state[k] = v
 
57
 
58
  # ================= Yardımcılar =================
59
  def _needs_auth(url: str) -> bool:
@@ -61,10 +45,9 @@ def _needs_auth(url: str) -> bool:
61
 
62
  @st.cache_data(ttl=300, show_spinner=False)
63
  def fetch_bytes(url_or_path: str | None):
64
- """
65
- URL, outputs/ yolu veya mevcut klasörden dosya okur.
66
- """
67
- if not url_or_path: return None
68
  try:
69
  p = Path(url_or_path)
70
  if p.is_file():
@@ -80,8 +63,7 @@ def fetch_bytes(url_or_path: str | None):
80
  @st.cache_data(ttl=300, show_spinner=False)
81
  def image_size_from_bytes(b: bytes):
82
  try:
83
- im = Image.open(io.BytesIO(b))
84
- return im.size
85
  except Exception:
86
  return None
87
 
@@ -94,70 +76,80 @@ def infer_ext(data: bytes) -> str:
94
  return ".jpg"
95
 
96
  @st.cache_data(ttl=300, show_spinner=False)
97
- def square_preview_bytes(img_bytes: bytes, size: int = PREVIEW_SIZE, bg_rgb=(255,255,255)) -> bytes:
98
- im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")
 
99
  w, h = im.size
100
- scale = min(size / w, size / h)
101
  new_w, new_h = max(1, int(w*scale)), max(1, int(h*scale))
102
  im_resized = im.resize((new_w, new_h), Image.LANCZOS)
103
- canvas = Image.new("RGBA", (size, size), (*bg_rgb, 255))
104
- off = ((size - new_w)//2, (size - new_h)//2)
105
- canvas.paste(im_resized, off, im_resized)
106
  buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
107
 
108
- def show_img_tile(col, title, src: str | bytes | None, filename_stub="image", key_prefix=""):
109
- col.subheader(title)
 
 
 
 
 
 
 
110
  if not src:
111
- placeholder = Image.new("RGBA", (PREVIEW_SIZE, PREVIEW_SIZE), (0,0,0,0))
112
- col.image(placeholder, width=TILE)
113
  col.caption("—"); return None, None
114
- b = None
115
- try:
116
- b = src if isinstance(src, (bytes, bytearray)) else fetch_bytes(src)
117
- except Exception:
118
- b = None
119
  if not b:
120
  col.warning("Görsel yüklenemedi"); return None, None
121
- pv = square_preview_bytes(b, size=PREVIEW_SIZE, bg_rgb=(255,255,255))
122
- col.image(pv, width=TILE)
 
123
  sz = image_size_from_bytes(b)
124
  if sz: col.caption(f"Gerçek: {sz[0]}×{sz[1]} px")
 
125
  ext = infer_ext(b)
126
  mime = "image/jpeg" if ext in [".jpg",".jpeg"] else ("image/png" if ext==".png" else "image/webp")
127
- col.download_button("İndir (tam kalite)", data=b, file_name=f"{filename_stub}{ext}", mime=mime,
128
- key=f"{key_prefix}_{filename_stub}_dl")
 
 
129
  return src, b
130
 
131
- def _b64(vb: bytes) -> str:
132
- return base64.b64encode(vb).decode("utf-8")
133
-
134
- def show_video_tile(col, title, src: str | bytes | None, key_prefix=""):
 
 
 
 
 
 
 
135
  """
136
- Kare video kutusu: WIDTH=TILE, HEIGHT=TILE. İçeride <video> object-fit: cover.
137
- """
138
- col.subheader(title)
139
- if not src:
140
- # boş kare placeholder
141
- ph_html = f"""
142
- <div style="width:{TILE}px;height:{TILE}px;border-radius:12px;background:#111;display:flex;align-items:center;justify-content:center;color:#bbb;font:500 14px system-ui;">
143
- —
144
- </div>"""
145
- components.html(ph_html, height=TILE+4)
146
- return None
147
- vb = src if isinstance(src, (bytes, bytearray)) else fetch_bytes(src)
148
- if not vb:
149
- components.html(f"<div style='width:{TILE}px;height:{TILE}px;background:#111;border-radius:12px'></div>", height=TILE+4)
150
- col.caption("Video yüklenemedi."); return None
151
- data_url = f"data:video/mp4;base64,{_b64(vb)}"
152
- html = f"""
153
- <div style="width:{TILE}px;height:{TILE}px;border-radius:12px;overflow:hidden;background:#000;">
154
- <video src="{data_url}" controls playsinline style="width:100%;height:100%;object-fit:cover;display:block;"></video>
155
- </div>
156
- """
157
- components.html(html, height=TILE+48)
158
- col.download_button("Video indir (MP4)", data=vb, file_name="fitstudio_video.mp4", mime="video/mp4",
159
- key=f"{key_prefix}_video_dl")
160
- return vb
161
 
162
  def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
163
  buf = io.BytesIO()
@@ -179,129 +171,105 @@ def post_image_edit(data: dict, files_payload):
179
  r.raise_for_status(); return r.json()
180
 
181
  def post_chain(data: dict, files_payload):
182
- payload = {**data, "to_video": "false"} # video UI'da ayrı akış
183
  r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
184
  headers=HEADERS if HF_TOKEN else None, timeout=600)
185
  r.raise_for_status(); return r.json()
186
 
187
- def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=True):
188
- payload = {"image_url": image_url, "duration": duration, "resolution": resolution,
189
- "prompt_optimizer": "true" if prompt_optimizer else "false"}
190
- r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload,
191
  headers=HEADERS if HF_TOKEN else None, timeout=600)
192
  r.raise_for_status(); return r.json()
193
 
194
- # ================= Prompt bileşimi (cinsiyet + yaş) =================
195
- def _map_gender_txt(tr):
196
- if tr == "Kadın": return "female"
197
- if tr == "Erkek": return "male"
198
- return "auto"
199
-
200
- def _map_age_txt(tr):
201
- # Güvenli aralıklar (18+)
202
- if tr.startswith("Genç"): return "young adult (18–25)"
203
- if tr.startswith("Yetişkin"): return "adult (26–45)"
204
- if tr.startswith("Olgun"): return "mature adult (46+)"
205
- return "auto"
206
-
207
- def compose_prompt(base_user_prompt: str, category: str, gender_tr: str, age_tr: str) -> str:
208
- gender = _map_gender_txt(gender_tr)
209
- age = _map_age_txt(age_tr)
210
- parts = []
211
- if base_user_prompt and len(base_user_prompt.strip()) > 10:
212
- parts.append(base_user_prompt.strip())
213
- else:
214
- # kısa, sağlam bir temel prompt
215
- base = ("Commercial fashion catalog still; full-body, face visible, feet not cropped; "
216
- "photorealistic; preserve garment identity (design, texture, color, stitching, logos) exactly; "
217
- "no added text/logos; modern Istanbul street style; neutral daylight.")
218
- if category == "underwear":
219
- base += " Wear ONLY the uploaded underwear garment; neutral, non-sexual catalog pose."
220
- parts.append(base)
221
- model_rules = []
222
- if gender != "auto":
223
- model_rules.append(f"Model gender presentation: {gender}.")
224
- if age != "auto":
225
- model_rules.append(f"Age group cues: {age}. Avoid depicting minors.")
226
- if model_rules:
227
- parts.append(" ".join(model_rules))
228
- # tutarlılık
229
- parts.append("Hands visible; 3/4 or frontal pose; avoid cropping; single person; no artefacts.")
230
- return " ".join(parts)
231
-
232
  # ================= Sayfa =================
233
  st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
234
  st.title("🧵 Fit Studio AI")
235
- st.caption("Fashion & Apparel — Still Image (VTO) → (optional) Runway Video")
236
 
237
  # ================= Üst Kontrol Şeridi =================
238
  ok = backend_ok()
239
- c0, c_gender, c_age, c1, c2, c3 = st.columns([1.0, 1.0, 1.2, 1.6, 1.4, 1.6])
240
 
241
  with c0:
242
  category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
243
-
244
- with c_gender:
245
- gender_tr = st.radio("Cinsiyet", ["Otomatik","Kadın","Erkek"], index=0, horizontal=True, key="gender_tr",
246
- help="Modelin cinsiyet sunumunu seçin.")
247
-
248
- with c_age:
249
- age_tr = st.selectbox("Yaş grubu",
250
- ["Otomatik","Genç (18–25)","Yetişkin (26–45)","Olgun (46+)"],
251
- index=0, key="age_tr",
252
- help="Güvenli aralık: 18 yaş ve üzeri.")
253
-
254
  with c1:
255
- file_list = st.file_uploader("Garment image (upload)", type=["jpg","jpeg","png","webp"],
256
- accept_multiple_files=True, key="file_upl")
257
  with c2:
258
- image_url = st.text_input("veya Garment URL", key="image_url")
259
  with c3:
260
- custom_prompt = st.text_area("Özel prompt (opsiyonel)", value="", height=80, key="custom_prompt")
261
-
262
- bA, bB, bC = st.columns([1.0, 1.0, 1.0])
263
- with bA:
264
- st.info(f"Backend: {'Online' if ok else 'Offline'} • Tile: {TILE}px", icon="🔌")
265
- with bB:
266
  num_images = st.selectbox("Çıktı adedi", [1,2,3,4], index=0, key="num_images")
267
- with bC:
268
- colbtn1, colbtn2 = st.columns(2)
269
- with colbtn1:
270
- run_image = st.button("Çalıştır (Foto)", type="primary", use_container_width=True, key="run_img_btn")
271
- with colbtn2:
272
- run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
273
-
274
- # Güvenlik: underwear + 18 altı seçenek yok; ek bir uyarı da koyalım
275
- if category == "underwear" and age_tr in {"Otomatik"}:
276
- st.caption("ℹ️ Underwear için yaş grubu otomatik bırakıldı; sistem yetişkin güvenli aralıkta yorumlar.")
277
-
278
- # ================= Demo Preview (mevcut klasörden) =================
279
- st.markdown("---")
280
- st.subheader("Demo preview (çalıştırmadan önce örnek)")
281
- st.caption("Aynı klasörden **input.jpg**, **tryon.jpg**, **fitstudio_video.mp4** okunur. Tüm kutular eş boyutta kare.")
282
 
283
- dp_in, dp_out, dp_vid = st.columns(3)
284
- in_bytes = fetch_bytes("input.jpg")
285
- show_img_tile(dp_in, "Input", in_bytes, filename_stub="input", key_prefix="demo")
286
 
287
- try_bytes = fetch_bytes("tryon.jpg")
288
- show_img_tile(dp_out, "Sample Output", try_bytes, filename_stub="tryon", key_prefix="demo")
 
 
 
 
289
 
290
- demo_vbytes = fetch_bytes("fitstudio_video.mp4")
291
- show_video_tile(dp_vid, "Sample Video", demo_vbytes, key_prefix="demo")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
  # ================= Girdi Önizleme =================
294
  st.markdown("---")
295
- g1, g2, g3, g4 = st.columns(4)
 
296
 
297
  input_preview = None
298
  if file_list:
299
- try: input_preview = file_list[0].getvalue()
300
- except Exception: input_preview = None
 
 
301
  elif image_url:
302
  input_preview = fetch_bytes(image_url)
303
 
304
- show_img_tile(g1, "Input (preview)", input_preview, filename_stub="input_preview", key_prefix="preview")
 
 
 
 
 
 
 
 
 
305
 
306
  # ================ Çalıştır ================
307
  def _build_files_payload():
@@ -312,17 +280,17 @@ def _build_files_payload():
312
  return files_payload
313
 
314
  def _common_form_data():
315
- # Prompt’u cinsiyet+yaş ile kompoze et
316
- prompt_final = compose_prompt(st.session_state.get("custom_prompt",""),
317
- st.session_state["category"],
318
- st.session_state["gender_tr"],
319
- st.session_state["age_tr"])
320
  data = {
321
  "category": st.session_state["category"],
 
 
 
322
  "num_images": str(st.session_state["num_images"]),
323
- "prompt": prompt_final,
324
  }
325
- if image_url: data["image_urls"] = image_url.strip()
 
 
 
326
  return data
327
 
328
  if run_image or run_chain:
@@ -342,9 +310,15 @@ if run_image or run_chain:
342
  out = post_chain(form, files_payload) # to_video=false
343
  image_json = out.get("image_step") or out
344
 
345
- # ---- Çıktıları çıkar ----
346
- imgs = (image_json.get("result") or {}).get("images") or []
347
- urls = [i.get("url") for i in imgs if isinstance(i, dict) and i.get("url")]
 
 
 
 
 
 
348
  st.session_state["job_counter"] += 1
349
  st.session_state["results"] = {
350
  "job_id": (image_json.get("job_id") or out.get("job_id")),
@@ -352,42 +326,46 @@ if run_image or run_chain:
352
  "image_urls": urls,
353
  "video_url": None,
354
  }
 
355
  except requests.HTTPError as e:
356
  st.error(f"HTTP {e.response.status_code}")
357
  except Exception as e:
358
  st.error(f"Hata: {e}")
359
 
360
- # ================ Sonuçları Satırda Yan Yana (eş boyut) ================
361
  res = st.session_state.get("results") or {}
362
  if res:
363
  key_prefix = f"job{st.session_state['job_counter']}"
364
  img_urls = res.get("image_urls") or []
365
  video_url = res.get("video_url")
366
 
367
- # 1. Satır: 3 kare görsel + 1 kare video (varsa)
368
- c1, c2, c3, c4 = st.columns(4)
369
  named = []
370
- tiles = [c1, c2, c3]
371
- for idx in range(min(3, len(img_urls))):
372
- u = img_urls[idx]
373
- title = "Look" if idx == 0 else f"Look #{idx+1}"
374
- _, b = show_img_tile(tiles[idx], title, u, filename_stub=f"look_{idx+1}", key_prefix=f"{key_prefix}_{idx}")
375
- if b: named.append((f"look_{idx+1}{infer_ext(b)}", b))
 
 
 
376
 
377
  vbytes = None
378
  if video_url:
379
- vbytes = show_video_tile(c4, "Video", video_url, key_prefix=key_prefix)
380
  else:
381
- show_video_tile(c4, "Video", None, key_prefix=key_prefix)
 
382
 
383
- # 2. Satır: Video üretim kontrolleri
384
  st.markdown("---")
385
  colv1, colv2, colv3 = st.columns([1.1, 1.0, 1.2])
386
  with colv1:
387
- gen_video = st.checkbox("Bu sonuçtan video üret", value=False, key="video_toggle")
388
  with colv2:
389
- st.selectbox("Video süresi", ["6","10"],
390
- index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")
391
  with colv3:
392
  go_video = st.button("Video Oluştur", use_container_width=True, key="video_btn")
393
 
@@ -397,25 +375,35 @@ if res:
397
  st.warning("Video için kaynak görsel yok.")
398
  else:
399
  with st.spinner("Video üretiliyor…"):
400
- v = post_video(src_for_video,
401
- duration=st.session_state.get("duration_select","6"),
402
- resolution="768P",
403
- prompt_optimizer=True)
 
 
 
 
 
404
  vurl = (v.get("result") or {}).get("video", {}).get("url")
405
  if vurl:
406
  st.session_state["results"]["video_url"] = vurl
407
  video_url = vurl
408
- vbytes = show_video_tile(c4, "Video", video_url, key_prefix=key_prefix)
409
  else:
410
  st.info("Video URL gelmedi.")
411
 
412
  # ZIP indir
413
  if named or vbytes:
414
- if vbytes: named.append(("video.mp4", vbytes))
 
415
  zip_bytes = make_zip(named)
416
- st.download_button("Tümünü indir (ZIP)", data=zip_bytes,
417
- file_name="fitstudio_outputs.zip", mime="application/zip",
418
- key=f"{key_prefix}_zip_dl")
 
 
 
 
419
 
420
  # Debug
421
  with st.expander("Debug"):
@@ -426,23 +414,22 @@ if res:
426
  "has_auth_header": bool(HF_TOKEN),
427
  "image_urls": img_urls,
428
  "video_url": res.get("video_url"),
429
- "tile_px": TILE,
430
- "gender": gender_tr,
431
- "age_group": age_tr,
432
  })
433
 
434
  # ================= Sidebar (teşhis) =================
435
  with st.sidebar:
436
  st.caption(f"Resolved API_BASE: {API_BASE}")
437
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
438
- st.caption(f"Tile size: {TILE}px (env: TILE_SIZE)")
439
 
440
  if st.button("Ping /health"):
441
  try:
442
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
443
  st.write(r.status_code)
444
- try: st.json(r.json())
445
- except Exception: st.code((r.text or "")[:1000])
 
 
446
  except Exception as e:
447
  st.error(f"Health error: {e}")
448
 
 
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
 
13
  API_BASE = (
14
  _env("FITSTUDIO_API")
15
  or _env("AI_LIGHTBOX_API")
16
  or _env("LUXFIT_API")
17
+ ).rstrip("/") if (_env("FITSTUDIO_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")) else ""
18
+
19
+ HF_TOKEN = (_env("FITSTUDIO_TOKEN") or _env("AI_LIGHTBOX_TOKEN") or _env("LUXFIT_TOKEN"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  if not API_BASE:
22
  st.error("API_BASE bulunamadı. Settings > Variables: FITSTUDIO_API (veya AI_LIGHTBOX_API / LUXFIT_API) tanımlayın.")
 
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:
 
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:
52
  p = Path(url_or_path)
53
  if p.is_file():
 
63
  @st.cache_data(ttl=300, show_spinner=False)
64
  def image_size_from_bytes(b: bytes):
65
  try:
66
+ im = Image.open(io.BytesIO(b)); return im.size
 
67
  except Exception:
68
  return None
69
 
 
76
  return ".jpg"
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)
84
  new_w, new_h = max(1, int(w*scale)), max(1, int(h*scale))
85
  im_resized = im.resize((new_w, new_h), Image.LANCZOS)
86
+ canvas = Image.new("RGB", (out_w, out_h), bg_rgb)
87
+ off = ((out_w - new_w)//2, (out_h - new_h)//2)
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()
 
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()
178
 
179
+ def post_video(data: dict):
180
+ r = requests.post(f"{API_BASE}/v1/video/from-image", data=data,
 
 
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:
196
+ gender = st.selectbox("Cinsiyet", ["auto","female","male","unisex"], index=0, key="gender")
 
197
  with c2:
198
+ age_group = st.selectbox("Yaş Grubu", ["auto","teen","young_adult","adult","mature"], index=0, key="age_group")
199
  with c3:
 
 
 
 
 
 
200
  num_images = st.selectbox("Çıktı adedi", [1,2,3,4], index=0, key="num_images")
201
+ with c4:
202
+ file_list = st.file_uploader("Garment upload", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
203
+ with c5:
204
+ image_url = st.text_input("veya Garment URL", key="image_url")
 
 
 
 
 
 
 
 
 
 
 
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():
 
280
  return files_payload
281
 
282
  def _common_form_data():
 
 
 
 
 
283
  data = {
284
  "category": st.session_state["category"],
285
+ "gender": st.session_state["gender"],
286
+ "age_group": st.session_state["age_group"],
287
+ "frame_aspect": "9:16",
288
  "num_images": str(st.session_state["num_images"]),
 
289
  }
290
+ if st.session_state.get("custom_prompt","").strip():
291
+ data["prompt"] = st.session_state["custom_prompt"].strip()
292
+ if image_url:
293
+ data["image_urls"] = image_url.strip()
294
  return data
295
 
296
  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
323
  st.session_state["results"] = {
324
  "job_id": (image_json.get("job_id") or out.get("job_id")),
 
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
 
 
375
  st.warning("Video için kaynak görsel yok.")
376
  else:
377
  with st.spinner("Video üretiliyor…"):
378
+ v = post_video({
379
+ "image_url": src_for_video,
380
+ "duration": st.session_state.get("duration_select","6"),
381
+ "resolution": "768P",
382
+ "prompt_optimizer": "true",
383
+ "gender": st.session_state["gender"],
384
+ "age_group": st.session_state["age_group"],
385
+ "frame_aspect": "9:16",
386
+ })
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
409
  with st.expander("Debug"):
 
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