renderfy commited on
Commit
8a30a74
·
verified ·
1 Parent(s): c35deb3

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +354 -209
streamlit_app.py CHANGED
@@ -1,39 +1,87 @@
1
- # streamlit_app.py — Fit Studio AI (Fashion & Apparel) v0.8 UI
2
- # Hedef: Gerçek Input + Look + Video KARTLARI aynı satırda, aynı 9:16 kadrajda, HTML kullanmadan.
3
- # Demo: klasörde input.jpg, tryon.jpg, fitstudio_video.mp4 varsa çalıştırmadan önce gösterilir.
 
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):
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
- HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
21
 
22
  if not API_BASE:
23
- st.error("API_BASE yok. Variables: FITSTUDIO_API (veya AI_LIGHTBOX_API/LUXFIT_API) ekleyin.")
24
  st.stop()
25
 
26
- # Tüm görsel/video kartları 9:16
27
- ASPECT_W, ASPECT_H = 9, 16
28
- PREVIEW_H = int(os.getenv("PREVIEW_H", "640")) # 640 px yükseklik
29
- PREVIEW_W = int(round(PREVIEW_H * ASPECT_W / ASPECT_H)) # 360 px genişlik
30
- BG = (255, 255, 255)
 
31
 
32
- # ================= State =================
33
  st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
34
- st.session_state.setdefault("results", None)
35
- st.session_state.setdefault("job_counter", 0)
36
- st.session_state.setdefault("duration", "6")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  # ================= Helpers =================
39
  def _needs_auth(url: str) -> bool:
@@ -41,6 +89,9 @@ def _needs_auth(url: str) -> bool:
41
 
42
  @st.cache_data(ttl=300, show_spinner=False)
43
  def fetch_bytes(url_or_path: str | None):
 
 
 
44
  if not url_or_path:
45
  return None
46
  try:
@@ -56,266 +107,360 @@ def fetch_bytes(url_or_path: str | None):
56
  return None
57
 
58
  @st.cache_data(ttl=300, show_spinner=False)
59
- def pad_to_9x16(img_bytes: bytes, out_w=PREVIEW_W, out_h=PREVIEW_H, bg=BG) -> bytes:
60
- im = Image.open(io.BytesIO(img_bytes)).convert("RGB")
61
- w, h = im.size
62
- scale = min(out_w / w, out_h / h)
63
- new_w, new_h = max(1, int(w*scale)), max(1, int(h*scale))
64
- im_r = im.resize((new_w, new_h), Image.LANCZOS)
65
- canvas = Image.new("RGB", (out_w, out_h), bg)
66
- off = ((out_w - new_w)//2, (out_h - new_h)//2)
67
- canvas.paste(im_r, off)
68
- buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
69
-
70
- def infer_ext(b: bytes) -> str:
71
  try:
72
- fmt = (Image.open(io.BytesIO(b)).format or "JPEG").lower()
 
 
 
 
 
 
 
 
73
  return "." + {"jpeg":"jpg","jpg":"jpg","png":"png","webp":"webp"}.get(fmt, "jpg")
74
  except Exception:
75
  return ".jpg"
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  def backend_ok() -> bool:
78
  try:
79
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
80
- r.raise_for_status(); return True
 
81
  except Exception:
82
  return False
83
 
84
  def post_image_edit(data: dict, files_payload):
85
  r = requests.post(f"{API_BASE}/v1/image/edit", data=data, files=files_payload or None,
86
  headers=HEADERS if HF_TOKEN else None, timeout=600)
87
- r.raise_for_status(); return r.json()
 
88
 
89
  def post_chain(data: dict, files_payload):
90
- payload = {**data, "to_video": "false"} # video ayrı
91
  r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
92
  headers=HEADERS if HF_TOKEN else None, timeout=600)
93
- r.raise_for_status(); return r.json()
94
-
95
- def post_video(data: dict):
96
- r = requests.post(f"{API_BASE}/v1/video/from-image", data=data,
 
 
 
 
 
 
 
 
 
 
 
97
  headers=HEADERS if HF_TOKEN else None, timeout=600)
98
- r.raise_for_status(); return r.json()
99
-
100
- def make_zip(named: list[tuple[str, bytes]]) -> bytes:
101
- buf = io.BytesIO()
102
- with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
103
- for name, b in named:
104
- if b: z.writestr(name, b)
105
- buf.seek(0); return buf.read()
106
 
107
- # ================= UI — Controls =================
108
  st.title("🧵 Fit Studio AI")
109
- st.caption("9:16 Still Image (VTO) → (opsiyonel) Runway Video — HTML’siz sabit 3 kart düzeni")
110
 
111
  ok = backend_ok()
112
- c0, c1, c2, c3, c4, c5 = st.columns([1.0, 1.4, 1.4, 1.1, 1.8, 1.6])
 
113
  with c0:
114
- category = st.selectbox("Kategori", ["general","underwear"], index=0)
115
  with c1:
116
- gender = st.selectbox("Cinsiyet", ["auto","female","male","unisex"], index=0)
117
  with c2:
118
- age_group = st.selectbox("Yaş Grubu", ["auto","teen","young_adult","adult","mature"], index=0)
119
  with c3:
120
- num_images = st.selectbox("Çıktı adedi", [1,2,3,4], index=0)
121
  with c4:
122
- files = st.file_uploader("Garment upload", type=["jpg","jpeg","png","webp"], accept_multiple_files=True)
123
  with c5:
124
- image_url = st.text_input("veya Garment URL")
125
-
126
- prompt = st.text_area("Özel prompt (boş → varsayılan)", value="", height=90)
127
 
128
- cA, cB = st.columns([1,1])
129
- with cA:
130
  st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
131
- with cB:
132
- run_img = st.button("Çalıştır (Foto)", type="primary", use_container_width=True)
133
- run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True)
134
 
135
- # ================= Demo Preview (Streamlit-only) =================
136
  st.markdown("---")
137
- st.subheader("Demo preview (çalıştırmadan önce)")
138
- dcol1, dcol2, dcol3 = st.columns(3)
139
-
140
- demo_input_b = fetch_bytes("input.jpg")
141
- demo_try_b = fetch_bytes("tryon.jpg")
142
- demo_vid_b = fetch_bytes("fitstudio_video.mp4")
143
-
144
- def show_image_card(col, title, b: bytes | None, fname="image.jpg"):
145
- col.markdown(f"**{title}**")
146
- if b:
147
- pv = pad_to_9x16(b)
148
- col.image(pv, use_container_width=True)
149
- col.download_button("İndir", data=b, file_name=fname, mime="image/jpeg")
150
- else:
151
- col.image(Image.new("RGB", (PREVIEW_W, PREVIEW_H), BG), use_container_width=True)
152
- col.caption("—")
153
-
154
- def show_video_card(col, title, vb: bytes | None):
155
- col.markdown(f"**{title}**")
156
- if vb:
157
- col.video(vb, format="video/mp4", start_time=0)
158
- col.download_button("Video indir", data=vb, file_name="fitstudio_video.mp4", mime="video/mp4")
159
- else:
160
- col.image(Image.new("RGB", (PREVIEW_W, PREVIEW_H), BG), use_container_width=True)
161
- col.caption("—")
162
-
163
- show_image_card(dcol1, "Sample Input", demo_input_b, f"input{infer_ext(demo_input_b or b'')}")
164
- show_image_card(dcol2, "Sample Output", demo_try_b, f"tryon{infer_ext(demo_try_b or b'')}")
165
- show_video_card(dcol3, "Sample Video", demo_vid_b)
166
-
167
- # ================= Input preview (for Studio row) =================
168
- runtime_input_b = None
169
- if files:
170
- try: runtime_input_b = files[0].getvalue()
171
- except Exception: runtime_input_b = None
172
  elif image_url:
173
- runtime_input_b = fetch_bytes(image_url)
174
 
175
- # ================= Run =================
176
- def build_files_payload():
177
- payload = []
178
- if files:
179
- for f in files:
180
- payload.append(("files", (f.name, f.getvalue(), f.type or "image/jpeg")))
181
- return payload
182
 
183
- def common_form():
 
 
 
 
 
 
 
 
184
  data = {
185
- "category": category,
186
- "gender": gender,
187
- "age_group": age_group,
188
  "frame_aspect": "9:16",
189
- "num_images": str(num_images),
190
  }
191
- if prompt.strip(): data["prompt"] = prompt.strip()
192
- if image_url.strip(): data["image_urls"] = image_url.strip()
 
 
 
193
  return data
194
 
195
- if run_img or run_chain:
196
  if not ok:
197
- st.error("Backend erişilemiyor.")
198
- elif not (files or image_url):
199
- st.error("En az bir garment görseli girin.")
200
  else:
201
  try:
202
- fp = build_files_payload()
203
- form = common_form()
 
204
  with st.spinner("Çalışıyor…"):
205
- if run_img:
206
- image_json = post_image_edit(form, fp)
 
207
  else:
208
- out = post_chain(form, fp) # to_video=false
209
  image_json = out.get("image_step") or out
210
 
 
211
  imgs_norm = (image_json.get("result") or {}).get("images_9_16") or []
212
- if not imgs_norm:
213
- raw = (image_json.get("result") or {}).get("images") or []
214
- imgs_norm = [{"url": i.get("url")} for i in raw if isinstance(i, dict) and i.get("url")]
215
  urls = [i.get("url") for i in imgs_norm if isinstance(i, dict) and i.get("url")]
216
 
217
  st.session_state["job_counter"] += 1
218
- st.session_state["results"] = {"image_urls": urls, "video_url": None}
 
 
 
 
 
 
219
  except requests.HTTPError as e:
220
  st.error(f"HTTP {e.response.status_code}")
221
  except Exception as e:
222
  st.error(f"Hata: {e}")
223
 
224
- # ================= STUDIO — HTML YOK: 3 sabit kolon =================
225
- st.markdown("---")
226
- st.subheader("Studio (Gerçek çıktılar)")
227
-
228
- col_in, col_look, col_vid = st.columns(3)
229
-
230
- # 1) Input kartı (her durumda dolu/placeholder) — 9:16’e pad’lenmiş
231
- col_in.markdown("**Input**")
232
- if runtime_input_b:
233
- col_in.image(pad_to_9x16(runtime_input_b), use_container_width=True)
234
- col_in.download_button("İndir", data=runtime_input_b, file_name=f"input_runtime{infer_ext(runtime_input_b)}", mime="image/jpeg")
235
- else:
236
- col_in.image(Image.new("RGB", (PREVIEW_W, PREVIEW_H), BG), use_container_width=True)
237
- col_in.caption("—")
238
-
239
- # 2) Look kartı
240
- urls = (st.session_state.get("results") or {}).get("image_urls") or []
241
- look_b = fetch_bytes(urls[0]) if urls else None
242
- col_look.markdown("**Look**")
243
- if look_b:
244
- col_look.image(pad_to_9x16(look_b), use_container_width=True)
245
- col_look.download_button("İndir", data=look_b, file_name=f"look_1{infer_ext(look_b)}", mime="image/jpeg")
246
- else:
247
- col_look.image(Image.new("RGB", (PREVIEW_W, PREVIEW_H), BG), use_container_width=True)
248
- col_look.caption("—")
249
-
250
- # 3) Video kartı
251
- vurl = (st.session_state.get("results") or {}).get("video_url")
252
- vbytes = fetch_bytes(vurl) if vurl else None
253
- col_vid.markdown("**Video**")
254
- if vbytes:
255
- col_vid.video(vbytes, format="video/mp4", start_time=0)
256
- col_vid.download_button("Video indir", data=vbytes, file_name="fitstudio_video.mp4", mime="video/mp4")
257
- else:
258
- col_vid.video(demo_vid_b, format="video/mp4", start_time=0) if demo_vid_b else col_vid.image(Image.new("RGB",(PREVIEW_W,PREVIEW_H),BG), use_container_width=True)
259
- if not demo_vid_b: col_vid.caption("—")
260
-
261
- # ================= Video üretimi (opsiyonel) =================
262
- st.markdown("")
263
- cV1, cV2, cV3 = st.columns([1.1, 1.0, 1.2])
264
- with cV1:
265
- gen_video = st.checkbox("Bu sonuçtan video üret", value=False)
266
- with cV2:
267
- dur = st.selectbox("Video süresi", ["6","10"], index=(0 if st.session_state["duration"]=="6" else 1))
268
- with cV3:
269
- go_video = st.button("Video Oluştur", use_container_width=True)
270
-
271
- if gen_video and go_video:
272
- src = urls[0] if urls else None
273
- if not src:
274
- st.warning("Video için kaynak görsel yok.")
275
  else:
276
- with st.spinner("Video üretiliyor…"):
277
- v = post_video({
278
- "image_url": src,
279
- "duration": dur,
280
- "resolution": "768P",
281
- "prompt_optimizer": "true",
282
- "gender": gender,
283
- "age_group": age_group,
284
- "frame_aspect": "9:16",
285
- })
286
- vurl = (v.get("result") or {}).get("video", {}).get("url")
287
- if vurl:
288
- st.session_state["results"]["video_url"] = vurl
289
- st.success("Video hazır — yukarıdaki Video kartında oynatılabilir.")
 
 
290
  else:
291
- st.info("Video URL gelmedi.")
292
-
293
- # ================= ZIP indir =================
294
- named = []
295
- if look_b: named.append((f"look_1{infer_ext(look_b)}", look_b))
296
- if vbytes: named.append(("video.mp4", vbytes))
297
- if named:
298
- st.download_button(
299
- "Tümünü indir (ZIP)",
300
- data=make_zip(named),
301
- file_name="fitstudio_outputs.zip",
302
- mime="application/zip",
303
- key=f"zip_{st.session_state['job_counter']}"
304
- )
305
-
306
- # ================= Sidebar =================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  with st.sidebar:
308
  st.caption(f"API_BASE: {API_BASE}")
309
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
 
 
310
  if st.button("Ping /health"):
311
  try:
312
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
313
  st.write(r.status_code)
314
- try: st.json(r.json())
315
- except Exception: st.code((r.text or "")[:800])
 
 
316
  except Exception as e:
317
  st.error(f"Health error: {e}")
 
318
  if st.button("Clear cache"):
319
  st.cache_data.clear(); st.success("Cache cleared.")
320
  if st.button("Clear results"):
321
- st.session_state["results"] = None; st.success("Results cleared.")
 
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")
16
  or _env("AI_LIGHTBOX_API")
17
  or _env("LUXFIT_API")
18
+ ).rstrip("/") if (
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.")
26
  st.stop()
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:
 
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
  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
+
117
+ def infer_ext(data: bytes) -> str:
118
+ try:
119
+ im = Image.open(io.BytesIO(data))
120
+ fmt = (im.format or "JPEG").lower()
121
  return "." + {"jpeg":"jpg","jpg":"jpg","png":"png","webp":"webp"}.get(fmt, "jpg")
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 (çalıştırmadan önce örnek) — tüm kutular 9:16")
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():
313
+ files_payload = []
314
+ if file_list:
315
+ for f in file_list:
316
+ files_payload.append(("files", (f.name, f.getvalue(), f.type or "image/jpeg")))
317
+ return files_payload
318
+
319
+ def _common_form_data():
320
  data = {
321
+ "category": st.session_state["category"],
322
+ "gender": st.session_state["gender"],
323
+ "age_group": st.session_state["age_group"],
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()
332
  return data
333
 
334
+ if run_image or run_chain:
335
  if not ok:
336
+ st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
337
+ elif not (file_list or image_url):
338
+ st.error("En az bir garment görseli girin (upload veya URL).")
339
  else:
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)
347
+ image_json = out
348
  else:
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"] = {
358
+ "job_id": (image_json.get("job_id") or out.get("job_id")),
359
+ "schema_version": (image_json.get("schema_version") or out.get("schema_version")),
360
+ "image_urls": urls,
361
+ "video_url": None,
362
+ }
363
+
364
  except requests.HTTPError as e:
365
  st.error(f"HTTP {e.response.status_code}")
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:
400
+ gen_video = st.checkbox("Bu sonuçtan video üret", value=False, key="video_toggle")
401
+ with colv2:
402
+ st.selectbox("Video süresi", ["6","10"], index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")
403
+ with colv3:
404
+ go_video = st.button("Video Oluştur", use_container_width=True, key="video_btn")
405
+
406
+ if gen_video and go_video:
407
+ src_for_video = img_urls[0] if img_urls else None
408
+ if not src_for_video:
409
+ st.warning("Video için kaynak görsel yok.")
410
  else:
411
+ with st.spinner("Video üretiliyor…"):
412
+ v = post_video(
413
+ src_for_video,
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
+
429
+ # ZIP indir
430
+ if named or vbytes:
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
+
463
  if st.button("Clear cache"):
464
  st.cache_data.clear(); st.success("Cache cleared.")
465
  if st.button("Clear results"):
466
+ st.session_state["results"] = None; st.success("Results cleared.")