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

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +162 -148
streamlit_app.py CHANGED
@@ -1,21 +1,20 @@
1
  # streamlit_app.py — Fit Studio AI (Fashion & Apparel) v0.8 UI
2
- # 9:16 dikey kadraj — Still Image (VTO) → (optional) Runway Video
3
- # NOT: HTML/CSS yok. Yalnızca Streamlit bileşenleri. Tüm kutular aynı satırda ve aynı boyutta.
4
- # Demo: aynı klasörde input.jpg, tryon.jpg, fitstudio_video.mp4 varsa otomatik önizleme.
5
 
6
- import os, io, base64, zipfile, requests, streamlit as st
7
  from pathlib import Path
8
  from PIL import Image
9
 
10
- # ============== Env & API =================
11
  def _env(k):
12
  return (os.getenv(k) or "").strip().strip("'\"")
13
 
14
  API_BASE = (
15
- _env("FITSTUDIO_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")
16
- )
17
- if API_BASE:
18
- API_BASE = API_BASE.rstrip("/")
19
 
20
  HF_TOKEN = (_env("FITSTUDIO_TOKEN") or _env("AI_LIGHTBOX_TOKEN") or _env("LUXFIT_TOKEN"))
21
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
@@ -24,24 +23,27 @@ if not API_BASE:
24
  st.error("API_BASE bulunamadı. Settings > Variables: FITSTUDIO_API (veya AI_LIGHTBOX_API / LUXFIT_API) tanımlayın.")
25
  st.stop()
26
 
27
- # Sabit 9:16 önizleme hedefi (tüm kutular aynı)
28
- PREVIEW_H = int(os.getenv("PREVIEW_H", "640")) # 640 px
29
- PREVIEW_W = int(round(PREVIEW_H * 9 / 16)) # 360 px
30
  PREVIEW_BG = (255, 255, 255)
31
 
 
32
  st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
33
-
34
- # ============== State =====================
35
- for k, v in {"results": None, "job_counter": 0, "duration": "6"}.items():
 
 
 
 
36
  st.session_state.setdefault(k, v)
37
 
38
- # ============== Helpers ===================
39
  def _needs_auth(url: str) -> bool:
40
  return (url.startswith(API_BASE) or url.startswith("outputs/"))
41
 
42
  @st.cache_data(ttl=300, show_spinner=False)
43
  def fetch_bytes(url_or_path: str | None):
44
- """URL, outputs/ veya mevcut klasörden oku."""
45
  if not url_or_path:
46
  return None
47
  try:
@@ -72,16 +74,28 @@ def infer_ext(data: bytes) -> str:
72
  return ".jpg"
73
 
74
  @st.cache_data(ttl=300, show_spinner=False)
75
- def pad_to_9x16(img_bytes: bytes, out_w: int = PREVIEW_W, out_h: int = PREVIEW_H, bg_rgb=PREVIEW_BG) -> bytes:
76
- """Görüntüyü 9:16 canvas’a (out_w×out_h) pad’ler, crop yok."""
 
 
 
77
  im = Image.open(io.BytesIO(img_bytes)).convert("RGB")
78
  w, h = im.size
79
- scale = min(out_w / w, out_h / h)
80
- new_w, new_h = max(1, int(w*scale)), max(1, int(h*scale))
81
- im_resized = im.resize((new_w, new_h), Image.LANCZOS)
82
- canvas = Image.new("RGB", (out_w, out_h), bg_rgb)
83
- off = ((out_w - new_w)//2, (out_h - new_h)//2)
84
- canvas.paste(im_resized, off)
 
 
 
 
 
 
 
 
 
85
  buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
86
 
87
  def backend_ok() -> bool:
@@ -114,88 +128,83 @@ def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
114
  if b: z.writestr(fname, b)
115
  buf.seek(0); return buf.read()
116
 
117
- # ============== Header & Controls =================
118
  st.title("🧵 Fit Studio AI")
119
- st.caption("Fashion & Apparel — Dikey 9:16 Still Image (VTO) → (opsiyonel) Runway Video")
120
 
121
  ok = backend_ok()
122
- c0, c1, c2, c3, c4, c5 = st.columns([1.0, 1.4, 1.4, 1.0, 1.6, 1.6])
123
  with c0:
124
  category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
125
  with c1:
126
  gender = st.selectbox("Cinsiyet", ["auto","female","male","unisex"], index=0, key="gender")
127
  with c2:
128
- age_group = st.selectbox("Yaş Grubu", ["auto","teen","young_adult","adult","mature"], index=0, key="age_group")
129
  with c3:
130
- num_images = st.selectbox("Çıktı adedi", [1,2,3,4], index=0, key="num_images")
131
  with c4:
132
- file_list = st.file_uploader("Garment upload", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
133
  with c5:
134
- image_url = st.text_input("veya Garment URL", key="image_url")
135
 
136
- prompt_box = st.text_area("Özel prompt (boş bırak → varsayılan)", value="", height=90, key="custom_prompt")
137
 
138
- info_col, run_col = st.columns([1,1])
139
- with info_col:
140
  st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
141
- with run_col:
142
- run_image = st.button("Çalıştır (Sadece Foto)", type="primary", use_container_width=True, key="run_img_btn")
143
  run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
144
 
145
- # ============== Demo Preview =================
146
- st.divider()
147
  st.subheader("Demo preview (çalıştırmadan önce)")
148
- st.caption("Aynı klasörden **input.jpg**, **tryon.jpg**, **fitstudio_video.mp4** okunur. Tüm kutular 9:16 ve aynı boyutta.")
149
 
150
- dcol1, dcol2, dcol3 = st.columns(3, gap="large")
151
  demo_in = fetch_bytes("input.jpg")
152
  demo_out = fetch_bytes("tryon.jpg")
153
  demo_vid = fetch_bytes("fitstudio_video.mp4")
154
 
 
 
 
 
 
 
155
  if demo_in:
156
- dcol1.markdown("**Input**")
157
- dcol1.image(pad_to_9x16(demo_in), use_container_width=True)
158
  sz = image_size_from_bytes(demo_in)
159
- if sz: dcol1.caption(f"Girdi: {sz[0]}×{sz[1]} px")
160
  else:
161
- dcol1.markdown("**Input**"); dcol1.image(Image.new("RGB",(PREVIEW_W,PREVIEW_H),PREVIEW_BG), use_container_width=True)
162
 
163
  if demo_out:
164
- dcol2.markdown("**Sample Output**")
165
- dcol2.image(pad_to_9x16(demo_out), use_container_width=True)
 
 
166
  else:
167
- dcol2.markdown("**Sample Output**"); dcol2.image(Image.new("RGB",(PREVIEW_W,PREVIEW_H),PREVIEW_BG), use_container_width=True)
168
 
169
  if demo_vid:
170
- dcol3.markdown("**Sample Video**")
171
- # Streamlit loop desteği yok; sadece aynı genişlikte gösteriyoruz.
172
- dcol3.video(demo_vid, format="video/mp4", start_time=0)
173
  else:
174
- dcol3.markdown("**Sample Video**"); dcol3.image(Image.new("RGB",(PREVIEW_W,PREVIEW_H),PREVIEW_BG), use_container_width=True)
175
 
176
- # ============== Girdi Önizleme =================
177
- st.divider()
178
- icol1, icol2, icol3 = st.columns(3, gap="large")
179
-
180
- input_preview = None
181
  if file_list:
182
- try: input_preview = file_list[0].getvalue()
183
- except Exception: input_preview = None
184
- elif image_url:
185
- input_preview = fetch_bytes(image_url)
186
-
187
- icol1.markdown("**Input**")
188
- if input_preview:
189
- icol1.image(pad_to_9x16(input_preview), use_container_width=True)
190
  try:
191
- sz = Image.open(io.BytesIO(input_preview)).size
192
- icol1.caption(f"{sz[0]}×{sz[1]} px")
193
  except Exception:
194
- pass
195
- else:
196
- icol1.image(Image.new("RGB",(PREVIEW_W,PREVIEW_H),PREVIEW_BG), use_container_width=True)
197
 
198
- # ============== Çalıştır =================
199
  def _build_files_payload():
200
  files_payload = []
201
  if file_list:
@@ -221,7 +230,7 @@ if run_image or run_chain:
221
  if not ok:
222
  st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
223
  elif not (file_list or image_url):
224
- st.error("En az bir garment görseli girin (upload veya URL).")
225
  else:
226
  try:
227
  files_payload = _build_files_payload()
@@ -234,6 +243,7 @@ if run_image or run_chain:
234
  out = post_chain(form, files_payload) # to_video=false
235
  image_json = out.get("image_step") or out
236
 
 
237
  imgs_norm = (image_json.get("result") or {}).get("images_9_16") or []
238
  if not imgs_norm:
239
  imgs_raw = (image_json.get("result") or {}).get("images") or []
@@ -252,84 +262,86 @@ if run_image or run_chain:
252
  except Exception as e:
253
  st.error(f"Hata: {e}")
254
 
255
- # ============== Sonuçlar — Tek satır, 3 sütun (Input/Look/Video değil; Look1/2/Video) ==============
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  res = st.session_state.get("results") or {}
257
- if res:
258
- st.divider()
259
- st.subheader("Sonuçlar")
260
-
261
- img_urls = res.get("image_urls") or []
262
- video_url = res.get("video_url")
263
-
264
- r1, r2, r3 = st.columns(3, gap="large")
265
-
266
- # Look 1
267
- r1.markdown("**Look**")
268
- if img_urls:
269
- b = fetch_bytes(img_urls[0])
270
- if b:
271
- r1.image(pad_to_9x16(b), use_container_width=True)
272
- dl_ext = infer_ext(b)
273
- r1.download_button("İndir (Look)", b, file_name=f"look_1{dl_ext}",
274
- mime="image/jpeg" if dl_ext in [".jpg",".jpeg"] else "image/png")
275
- else:
276
- r1.image(Image.new("RGB",(PREVIEW_W,PREVIEW_H),PREVIEW_BG), use_container_width=True)
277
-
278
- # Look 2 (varsa)
279
- r2.markdown("**Look #2**")
280
- if len(img_urls) >= 2:
281
- b2 = fetch_bytes(img_urls[1])
282
- if b2:
283
- r2.image(pad_to_9x16(b2), use_container_width=True)
284
- dl_ext2 = infer_ext(b2)
285
- r2.download_button("İndir (Look #2)", b2, file_name=f"look_2{dl_ext2}",
286
- mime="image/jpeg" if dl_ext2 in [".jpg",".jpeg"] else "image/png")
287
- else:
288
- r2.image(Image.new("RGB",(PREVIEW_W,PREVIEW_H),PREVIEW_BG), use_container_width=True)
289
-
290
- # Video
291
- r3.markdown("**Video**")
292
- if video_url:
293
- vb = fetch_bytes(video_url)
294
- if vb:
295
- r3.video(vb, format="video/mp4", start_time=0)
296
- r3.download_button("Video indir (MP4)", data=vb, file_name="fitstudio_video.mp4", mime="video/mp4")
297
  else:
298
- r3.image(Image.new("RGB",(PREVIEW_W,(PREVIEW_H)),PREVIEW_BG), use_container_width=True)
299
-
300
- # Video üretim kontrolleri
301
- st.divider()
302
- v1, v2, v3 = st.columns(3)
303
- with v1:
304
- gen_video = st.checkbox("Bu sonuçtan video üret", value=False, key="video_toggle")
305
- with v2:
306
- st.selectbox("Video süresi", ["6","10"], index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")
307
- with v3:
308
- go_video = st.button("Video Oluştur", use_container_width=True, key="video_btn")
309
-
310
- if gen_video and go_video:
311
- src_for_video = img_urls[0] if img_urls else None
312
- if not src_for_video:
313
- st.warning("Video için kaynak görsel yok.")
314
  else:
315
- with st.spinner("Video üretiliyor…"):
316
- v = post_video({
317
- "image_url": src_for_video,
318
- "duration": st.session_state.get("duration_select","6"),
319
- "resolution": "768P",
320
- "prompt_optimizer": "true",
321
- "gender": st.session_state["gender"],
322
- "age_group": st.session_state["age_group"],
323
- "frame_aspect": "9:16",
324
- })
325
- vurl = (v.get("result") or {}).get("video", {}).get("url")
326
- if vurl:
327
- st.session_state["results"]["video_url"] = vurl
328
- st.success("Video hazır. Yukarıdaki Video kutusunda görüntülenir.")
329
- else:
330
- st.info("Video URL gelmedi.")
331
-
332
- # ============== Sidebar =================
333
  with st.sidebar:
334
  st.caption(f"Resolved API_BASE: {API_BASE}")
335
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
@@ -344,4 +356,6 @@ with st.sidebar:
344
  if st.button("Clear cache"):
345
  st.cache_data.clear(); st.success("Cache cleared.")
346
  if st.button("Clear results"):
347
- st.session_state["results"] = None; st.success("Results cleared.")
 
 
 
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
 
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 {}
 
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:
43
  return (url.startswith(API_BASE) or url.startswith("outputs/"))
44
 
45
  @st.cache_data(ttl=300, show_spinner=False)
46
  def fetch_bytes(url_or_path: str | None):
 
47
  if not url_or_path:
48
  return None
49
  try:
 
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:
 
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:
 
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()
 
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 []
 
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)",
338
+ data=make_zip(named),
339
+ file_name="fitstudio_outputs.zip",
340
+ mime="application/zip",
341
+ key=f"zip_{st.session_state['job_counter']}"
342
+ )
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"))
 
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.")