renderfy commited on
Commit
c35deb3
·
verified ·
1 Parent(s): 3f170b0

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +147 -187
streamlit_app.py CHANGED
@@ -1,8 +1,8 @@
1
  # streamlit_app.py — Fit Studio AI (Fashion & Apparel) v0.8 UI
2
- # Amaç: HTML/CSS KULLANMADAN sabit 3 sütun (Input Look Video) yan yana ve aynı boy görünüm.
3
- # Demo: klasörde input.jpg, tryon.jpg, fitstudio_video.mp4 varsa "Demo preview" ve "Studio" satırında gösterilir.
4
 
5
- import os, io, zipfile, requests, streamlit as st
6
  from pathlib import Path
7
  from PIL import Image
8
 
@@ -20,23 +20,20 @@ HF_TOKEN = (_env("FITSTUDIO_TOKEN") or _env("AI_LIGHTBOX_TOKEN") or _env("LUXFIT
20
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
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.")
24
  st.stop()
25
 
26
- # Tüm görsel/video kadrajı hedef oranı (9:16). Görseller bu orana pad’lenir.
27
- TARGET_ASPECT = (9, 16)
28
- PREVIEW_BG = (255, 255, 255)
 
 
29
 
30
  # ================= State =================
31
  st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
32
- defaults = {
33
- "results": None,
34
- "job_counter": 0,
35
- "duration": "6",
36
- "input_preview_bytes": None,
37
- }
38
- for k, v in defaults.items():
39
- st.session_state.setdefault(k, v)
40
 
41
  # ================= Helpers =================
42
  def _needs_auth(url: str) -> bool:
@@ -59,45 +56,24 @@ def fetch_bytes(url_or_path: str | None):
59
  return None
60
 
61
  @st.cache_data(ttl=300, show_spinner=False)
62
- def image_size_from_bytes(b: bytes):
63
- try:
64
- im = Image.open(io.BytesIO(b)); return im.size
65
- except Exception:
66
- return None
 
 
 
 
 
67
 
68
- def infer_ext(data: bytes) -> str:
69
  try:
70
- im = Image.open(io.BytesIO(data))
71
- fmt = (im.format or "JPEG").lower()
72
  return "." + {"jpeg":"jpg","jpg":"jpg","png":"png","webp":"webp"}.get(fmt, "jpg")
73
  except Exception:
74
  return ".jpg"
75
 
76
- @st.cache_data(ttl=300, show_spinner=False)
77
- def pad_to_aspect(img_bytes: bytes, aspect=(9,16), bg_rgb=PREVIEW_BG) -> bytes:
78
- """
79
- Görseli crop yapmadan istenen orana pad’ler. Çıktı PNG (şeffaflık yok, düz arka plan).
80
- Genişlik/yükseklik UI’da sütun genişliğine göre otomatik ölçeklenecek.
81
- """
82
- im = Image.open(io.BytesIO(img_bytes)).convert("RGB")
83
- w, h = im.size
84
- target_w, target_h = aspect
85
- # hedef oran
86
- target_ratio = target_w / target_h
87
- img_ratio = w / h
88
- # hedef canvas’ı hesapla (en kısa kenar bazlı)
89
- if img_ratio > target_ratio:
90
- # görüntü daha geniş → hedef yükseklik = w / target_ratio
91
- new_h = int(round(w / target_ratio))
92
- new_w = w
93
- else:
94
- new_w = int(round(h * target_ratio))
95
- new_h = h
96
- canvas = Image.new("RGB", (new_w, new_h), bg_rgb)
97
- off = ((new_w - w)//2, (new_h - h)//2)
98
- canvas.paste(im, off)
99
- buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
100
-
101
  def backend_ok() -> bool:
102
  try:
103
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
@@ -121,217 +97,203 @@ def post_video(data: dict):
121
  headers=HEADERS if HF_TOKEN else None, timeout=600)
122
  r.raise_for_status(); return r.json()
123
 
124
- def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
125
  buf = io.BytesIO()
126
  with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
127
- for fname, b in named_bytes:
128
- if b: z.writestr(fname, b)
129
  buf.seek(0); return buf.read()
130
 
131
- # ================= Header & Controls =================
132
  st.title("🧵 Fit Studio AI")
133
- st.caption("HTML/CSS olmadan sabit 3 sütun: Input Look Video (9:16 görünüm)")
134
 
135
  ok = backend_ok()
136
- c0, c1, c2, c3, c4, c5 = st.columns([1.0, 1.4, 1.4, 1.0, 1.8, 1.6])
137
  with c0:
138
- category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
139
  with c1:
140
- gender = st.selectbox("Cinsiyet", ["auto","female","male","unisex"], index=0, key="gender")
141
  with c2:
142
- age_group = st.selectbox("Yaş", ["auto","teen","young_adult","adult","mature"], index=0, key="age_group")
143
  with c3:
144
- num_images = st.selectbox("Adet", [1,2,3,4], index=0, key="num_images")
145
  with c4:
146
- file_list = st.file_uploader("Upload", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
147
  with c5:
148
- image_url = st.text_input("veya URL", key="image_url")
149
 
150
- prompt_box = st.text_area("Özel prompt (opsiyonel)", value="", height=80, key="custom_prompt")
151
 
152
  cA, cB = st.columns([1,1])
153
  with cA:
154
  st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
155
  with cB:
156
- run_image = st.button("Çalıştır (Foto)", type="primary", use_container_width=True, key="run_img_btn")
157
- run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
158
 
159
- # ================= Demo preview (3 sabit sütun) =================
160
  st.markdown("---")
161
  st.subheader("Demo preview (çalıştırmadan önce)")
162
- st.caption("Klasörde **input.jpg**, **tryon.jpg**, **fitstudio_video.mp4** varsa yan yana gösterilir.")
163
 
164
- d1, d2, d3 = st.columns(3)
165
- demo_in = fetch_bytes("input.jpg")
166
- demo_out = fetch_bytes("tryon.jpg")
167
- demo_vid = fetch_bytes("fitstudio_video.mp4")
168
 
169
- def _placeholder_img(col, title):
170
  col.markdown(f"**{title}**")
171
- ph = Image.new("RGB", (900, 1600), PREVIEW_BG) # 9:16 boş görsel
172
- col.image(ph, use_container_width=True)
173
- col.caption("—")
174
-
175
- if demo_in:
176
- d1.markdown("**Input**")
177
- d1.image(pad_to_aspect(demo_in, TARGET_ASPECT), use_container_width=True)
178
- sz = image_size_from_bytes(demo_in)
179
- if sz: d1.caption(f"{sz[0]}×{sz[1]} px")
180
- else:
181
- _placeholder_img(d1, "Input")
182
-
183
- if demo_out:
184
- d2.markdown("**Sample Output**")
185
- d2.image(pad_to_aspect(demo_out, TARGET_ASPECT), use_container_width=True)
186
- sz2 = image_size_from_bytes(demo_out)
187
- if sz2: d2.caption(f"{sz2[0]}×{sz2[1]} px")
188
- else:
189
- _placeholder_img(d2, "Sample Output")
190
-
191
- if demo_vid:
192
- d3.markdown("**Sample Video**")
193
- # Streamlit st.video loop desteklemez; fakat sütun genişliği ile aynı görünür.
194
- d3.video(demo_vid, format="video/mp4", start_time=0)
195
- else:
196
- _placeholder_img(d3, "Sample Video")
197
 
198
- # ================= Input preview (state) =================
199
- if file_list:
200
- try:
201
- st.session_state["input_preview_bytes"] = file_list[0].getvalue()
202
- except Exception:
203
- st.session_state["input_preview_bytes"] = None
 
 
 
 
 
 
 
 
 
 
 
 
204
  elif image_url:
205
- st.session_state["input_preview_bytes"] = fetch_bytes(image_url)
206
 
207
  # ================= Run =================
208
- def _build_files_payload():
209
- files_payload = []
210
- if file_list:
211
- for f in file_list:
212
- files_payload.append(("files", (f.name, f.getvalue(), f.type or "image/jpeg")))
213
- return files_payload
214
-
215
- def _common_form_data():
216
  data = {
217
- "category": st.session_state["category"],
218
- "gender": st.session_state["gender"],
219
- "age_group": st.session_state["age_group"],
220
  "frame_aspect": "9:16",
221
- "num_images": str(st.session_state["num_images"]),
222
  }
223
- if st.session_state.get("custom_prompt","").strip():
224
- data["prompt"] = st.session_state["custom_prompt"].strip()
225
- if image_url:
226
- data["image_urls"] = image_url.strip()
227
  return data
228
 
229
- if run_image or run_chain:
230
  if not ok:
231
- st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
232
- elif not (file_list or image_url):
233
- st.error("En az bir görsel girin (upload veya URL).")
234
  else:
235
  try:
236
- files_payload = _build_files_payload()
237
- form = _common_form_data()
238
  with st.spinner("Çalışıyor…"):
239
- if run_image:
240
- out = post_image_edit(form, files_payload)
241
- image_json = out
242
  else:
243
- out = post_chain(form, files_payload) # to_video=false
244
  image_json = out.get("image_step") or out
245
 
246
- # URLs
247
  imgs_norm = (image_json.get("result") or {}).get("images_9_16") or []
248
  if not imgs_norm:
249
- imgs_raw = (image_json.get("result") or {}).get("images") or []
250
- imgs_norm = [{"url": i.get("url")} for i in imgs_raw if isinstance(i, dict) and i.get("url")]
251
  urls = [i.get("url") for i in imgs_norm if isinstance(i, dict) and i.get("url")]
252
 
253
  st.session_state["job_counter"] += 1
254
- st.session_state["results"] = {
255
- "job_id": (image_json.get("job_id") or out.get("job_id")),
256
- "schema_version": (image_json.get("schema_version") or out.get("schema_version")),
257
- "image_urls": urls,
258
- "video_url": None,
259
- }
260
  except requests.HTTPError as e:
261
  st.error(f"HTTP {e.response.status_code}")
262
  except Exception as e:
263
  st.error(f"Hata: {e}")
264
 
265
- # ================= STUDIO — TEK SATIR: Input Look • Video =================
266
  st.markdown("---")
267
- st.subheader("Studio (yan yana 3 sabit sütun)")
268
 
269
- c1, c2, c3 = st.columns(3)
270
 
271
- # 1) Input
272
- inp_b = st.session_state.get("input_preview_bytes")
273
- if inp_b:
274
- c1.markdown("**Input**")
275
- c1.image(pad_to_aspect(inp_b, TARGET_ASPECT), use_container_width=True)
276
  else:
277
- _placeholder_img(c1, "Input")
278
-
279
- # 2) Look (ilk sonuç / yoksa demo_out / yoksa boş)
280
- res = st.session_state.get("results") or {}
281
- urls = (res.get("image_urls") or [])
282
- look_url = urls[0] if urls else None
283
- look_b = fetch_bytes(look_url) if look_url else (demo_out if demo_out else None)
284
 
 
 
 
 
285
  if look_b:
286
- c2.markdown("**Look**")
287
- c2.image(pad_to_aspect(look_b, TARGET_ASPECT), use_container_width=True)
288
  else:
289
- _placeholder_img(c2, "Look")
290
-
291
- # 3) Video (üretilmiş URL / yoksa demo)
292
- vurl = res.get("video_url")
293
- v_b = fetch_bytes(vurl) if vurl else (demo_vid if demo_vid else None)
294
- if v_b:
295
- c3.markdown("**Video**")
296
- c3.video(v_b, format="video/mp4", start_time=0)
 
 
297
  else:
298
- _placeholder_img(c3, "Video")
299
-
300
- # ================= Video üretim kontrolleri =================
301
- v1, v2, v3 = st.columns([1.1, 1.0, 1.2])
302
- with v1:
303
- gen_video = st.checkbox("Bu sonuçtan video üret", value=False, key="video_toggle")
304
- with v2:
305
- st.selectbox("Video süresi", ["6","10"], index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")
306
- with v3:
307
- go_video = st.button("Video Oluştur", use_container_width=True, key="video_btn")
 
 
308
 
309
  if gen_video and go_video:
310
- src_for_video = urls[0] if urls else None
311
- if not src_for_video:
312
  st.warning("Video için kaynak görsel yok.")
313
  else:
314
  with st.spinner("Video üretiliyor…"):
315
  v = post_video({
316
- "image_url": src_for_video,
317
- "duration": st.session_state.get("duration_select","6"),
318
  "resolution": "768P",
319
  "prompt_optimizer": "true",
320
- "gender": st.session_state["gender"],
321
- "age_group": st.session_state["age_group"],
322
  "frame_aspect": "9:16",
323
  })
324
  vurl = (v.get("result") or {}).get("video", {}).get("url")
325
  if vurl:
326
  st.session_state["results"]["video_url"] = vurl
327
- st.success("Video hazır — Studio satırındaki Video kutusunda görüntülenir.")
328
  else:
329
  st.info("Video URL gelmedi.")
330
 
331
  # ================= ZIP indir =================
332
  named = []
333
- if look_b: named.append((f"look_1{infer_ext(look_b)}", look_b))
334
- if v_b: named.append(("video.mp4", v_b))
335
  if named:
336
  st.download_button(
337
  "Tümünü indir (ZIP)",
@@ -343,19 +305,17 @@ if named:
343
 
344
  # ================= Sidebar =================
345
  with st.sidebar:
346
- st.caption(f"Resolved API_BASE: {API_BASE}")
347
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
348
  if st.button("Ping /health"):
349
  try:
350
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
351
  st.write(r.status_code)
352
  try: st.json(r.json())
353
- except Exception: st.code((r.text or "")[:1000])
354
  except Exception as e:
355
  st.error(f"Health error: {e}")
356
  if st.button("Clear cache"):
357
  st.cache_data.clear(); st.success("Cache cleared.")
358
  if st.button("Clear results"):
359
- st.session_state["results"] = None
360
- st.session_state["input_preview_bytes"] = None
361
- st.success("Results cleared.")
 
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
 
 
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:
 
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)
 
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)",
 
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.")