renderfy commited on
Commit
6a64dc9
·
verified ·
1 Parent(s): 78d342c

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +164 -148
streamlit_app.py CHANGED
@@ -1,49 +1,47 @@
1
  # streamlit_app.py — Fit Studio AI (Fashion & Apparel) v0.8 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
- # DÜZELTME: HTML/CSS YOK. Sadece Streamlit bileşenleriyle
5
- # “Input | Look | Video” HER ZAMAN aynı satırda, aynı genişlikte.
6
 
7
- import os, io, zipfile, requests, streamlit as st
8
  from pathlib import Path
9
  from PIL import Image
10
- import base64
11
 
12
- # ================= Env & API (robust) =================
13
  def _env(k):
14
  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
- ).rstrip("/") if (_env("FITSTUDIO_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")) 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
- # Tek tip 9:16 kadraj (tüm görüntü/video buraya padlenir)
31
- PREVIEW_H = int(os.getenv("PREVIEW_H", "640")) # 640 px
32
- PREVIEW_W = int(round(PREVIEW_H * 9 / 16)) # 360 px
33
  PREVIEW_BG = (255, 255, 255)
34
 
35
- # ================= Session State =================
36
  st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
 
 
37
  for k, v in {"results": None, "job_counter": 0, "duration": "6"}.items():
38
  st.session_state.setdefault(k, v)
39
 
40
- # ================= Helpers =================
41
  def _needs_auth(url: str) -> bool:
42
  return (url.startswith(API_BASE) or url.startswith("outputs/"))
43
 
44
  @st.cache_data(ttl=300, show_spinner=False)
45
  def fetch_bytes(url_or_path: str | None):
46
- """URL, outputs/ yolu veya bulunduğumuz klasörden dosyayı okur."""
47
  if not url_or_path:
48
  return None
49
  try:
@@ -74,8 +72,8 @@ def infer_ext(data: bytes) -> str:
74
  return ".jpg"
75
 
76
  @st.cache_data(ttl=300, show_spinner=False)
77
- def vertical_preview_9x16(img_bytes: bytes, out_w: int = PREVIEW_W, out_h: int = PREVIEW_H, bg_rgb=PREVIEW_BG) -> bytes:
78
- """Görüntüyü 9:16 canvasa pad’ler (crop yok), çıktı PNG bytes."""
79
  im = Image.open(io.BytesIO(img_bytes)).convert("RGB")
80
  w, h = im.size
81
  scale = min(out_w / w, out_h / h)
@@ -86,13 +84,6 @@ def vertical_preview_9x16(img_bytes: bytes, out_w: int = PREVIEW_W, out_h: int =
86
  canvas.paste(im_resized, off)
87
  buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
88
 
89
- def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
90
- buf = io.BytesIO()
91
- with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
92
- for fname, b in named_bytes:
93
- if b: z.writestr(fname, b)
94
- buf.seek(0); return buf.read()
95
-
96
  def backend_ok() -> bool:
97
  try:
98
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
@@ -106,7 +97,7 @@ def post_image_edit(data: dict, files_payload):
106
  r.raise_for_status(); return r.json()
107
 
108
  def post_chain(data: dict, files_payload):
109
- payload = {**data, "to_video": "false"} # video ayrı akış
110
  r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
111
  headers=HEADERS if HF_TOKEN else None, timeout=600)
112
  r.raise_for_status(); return r.json()
@@ -116,40 +107,19 @@ def post_video(data: dict):
116
  headers=HEADERS if HF_TOKEN else None, timeout=600)
117
  r.raise_for_status(); return r.json()
118
 
119
- # Ortak “kart” renderer’ları (SADECE STREAMLIT)
120
- def render_image_card(col, title: str, img_bytes: bytes | None, dl_name: str | None = None):
121
- col.markdown(f"**{title}**")
122
- if img_bytes is None:
123
- # Boş kart (tam genişlikte sabit kadraj)
124
- col.image(Image.new("RGB",(PREVIEW_W, PREVIEW_H), PREVIEW_BG), use_container_width=True)
125
- col.caption("—")
126
- return None
127
- pv = vertical_preview_9x16(img_bytes, PREVIEW_W, PREVIEW_H, PREVIEW_BG)
128
- col.image(pv, use_container_width=True)
129
- sz = image_size_from_bytes(img_bytes)
130
- if sz: col.caption(f"Gerçek: {sz[0]}×{sz[1]} px")
131
- if dl_name:
132
- ext = infer_ext(img_bytes)
133
- mime = "image/jpeg" if ext in [".jpg",".jpeg"] else ("image/png" if ext==".png" else "image/webp")
134
- col.download_button("İndir (tam kalite)", data=img_bytes, file_name=f"{dl_name}{ext}", mime=mime)
135
-
136
- def render_video_card(col, title: str, video_bytes: bytes | None):
137
- col.markdown(f"**{title}**")
138
- if not video_bytes:
139
- col.image(Image.new("RGB",(PREVIEW_W, PREVIEW_H), PREVIEW_BG), use_container_width=True)
140
- col.caption("—")
141
- return None
142
- # st.video: HTML kullanmadan, sütun genişliğini doldurur (9:16 video ise hizalama tam olur)
143
- col.video(video_bytes, format="video/mp4", start_time=0)
144
- col.download_button("Video indir (MP4)", data=video_bytes, file_name="fitstudio_video.mp4", mime="video/mp4")
145
- return video_bytes
146
 
147
- # ================= Header & Controls =================
148
  st.title("🧵 Fit Studio AI")
149
  st.caption("Fashion & Apparel — Dikey 9:16 Still Image (VTO) → (opsiyonel) Runway Video")
150
 
151
  ok = backend_ok()
152
- c0, c1, c2, c3, c4, c5 = st.columns([1.0, 1.6, 1.6, 1.1, 1.4, 1.4])
153
  with c0:
154
  category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
155
  with c1:
@@ -165,36 +135,67 @@ with c5:
165
 
166
  prompt_box = st.text_area("Özel prompt (boş bırak → varsayılan)", value="", height=90, key="custom_prompt")
167
 
168
- bA, bB = st.columns([1.0, 1.0])
169
- with bA:
170
  st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
171
- with bB:
172
  run_image = st.button("Çalıştır (Sadece Foto)", type="primary", use_container_width=True, key="run_img_btn")
173
  run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
174
 
175
- # ================= Demo Preview =================
176
- st.markdown("---")
177
  st.subheader("Demo preview (çalıştırmadan önce)")
178
- st.caption("Aynı klasörde **input.jpg**, **tryon.jpg**, **fitstudio_video.mp4** okunur. Tüm kadrajlar 9:16 ve eşit genişlikte.")
179
-
180
- demo_c1, demo_c2, demo_c3 = st.columns(3)
181
- render_image_card(demo_c1, "Input", fetch_bytes("input.jpg"), "input")
182
- render_image_card(demo_c2, "Sample Output", fetch_bytes("tryon.jpg"), "tryon")
183
- render_video_card(demo_c3, "Sample Video", fetch_bytes("fitstudio_video.mp4"))
184
-
185
- # ================= Girdi Önizleme (Input | Look | Video aynı satır) =================
186
- st.markdown("---")
187
- st.subheader("Studio")
188
-
189
- # Input bytes
190
- input_bytes = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  if file_list:
192
- try: input_bytes = file_list[0].getvalue()
193
- except Exception: input_bytes = None
194
  elif image_url:
195
- input_bytes = fetch_bytes(image_url)
196
 
197
- # Çalıştırma yardımcıları
 
 
 
 
 
 
 
 
 
 
 
198
  def _build_files_payload():
199
  files_payload = []
200
  if file_list:
@@ -204,19 +205,18 @@ def _build_files_payload():
204
 
205
  def _common_form_data():
206
  data = {
207
- "category": category,
208
- "gender": gender,
209
- "age_group": age_group,
210
  "frame_aspect": "9:16",
211
- "num_images": str(num_images),
212
  }
213
- if prompt_box.strip():
214
- data["prompt"] = prompt_box.strip()
215
  if image_url:
216
  data["image_urls"] = image_url.strip()
217
  return data
218
 
219
- # Run
220
  if run_image or run_chain:
221
  if not ok:
222
  st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
@@ -242,6 +242,8 @@ if run_image or run_chain:
242
 
243
  st.session_state["job_counter"] += 1
244
  st.session_state["results"] = {
 
 
245
  "image_urls": urls,
246
  "video_url": None,
247
  }
@@ -250,70 +252,84 @@ if run_image or run_chain:
250
  except Exception as e:
251
  st.error(f"Hata: {e}")
252
 
253
- # === Aynı SATIR: Input | Look | Video ===
254
- c1, c2, c3 = st.columns(3)
255
-
256
- # Input (her durumda sol kolon dolu — boşsa placeholder 9:16)
257
- render_image_card(c1, "Input", input_bytes, "input_runtime")
258
-
259
- # Look (ilk çıktı veya boş kart)
260
  res = st.session_state.get("results") or {}
261
- img_urls = res.get("image_urls") or []
262
- look_bytes = fetch_bytes(img_urls[0]) if img_urls else None
263
- render_image_card(c2, "Look", look_bytes, "look_1")
264
-
265
- # Video (üretilmiş varsa göster, yoksa boş kart)
266
- video_url = res.get("video_url")
267
- video_bytes = fetch_bytes(video_url) if video_url else None
268
- render_video_card(c3, "Video", video_bytes)
269
-
270
- # === Video üretimi ===
271
- st.markdown("")
272
- vc1, vc2, vc3 = st.columns([1.1, 1.0, 1.2])
273
- with vc1:
274
- gen_video = st.checkbox("Bu sonuçtan video üret", value=False, key="video_toggle")
275
- with vc2:
276
- st.selectbox("Video süresi", ["6","10"], index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")
277
- with vc3:
278
- go_video = st.button("Video Oluştur", use_container_width=True, key="video_btn")
279
-
280
- if gen_video and go_video:
281
- src_for_video = img_urls[0] if img_urls else None
282
- if not src_for_video:
283
- st.warning("Video için kaynak görsel yok.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  else:
285
- with st.spinner("Video üretiliyor…"):
286
- v = post_video({
287
- "image_url": src_for_video,
288
- "duration": st.session_state.get("duration_select","6"),
289
- "resolution": "768P",
290
- "prompt_optimizer": "true",
291
- "gender": gender,
292
- "age_group": age_group,
293
- "frame_aspect": "9:16",
294
- })
295
- vurl = (v.get("result") or {}).get("video", {}).get("url")
296
- if vurl:
297
- st.session_state["results"]["video_url"] = vurl
298
- # Güncel satırı yeniden çizmek için sayfayı tekrar çalıştır
299
- st.experimental_rerun()
 
300
  else:
301
- st.info("Video URL gelmedi.")
302
-
303
- # === ZIP indir ===
304
- named = []
305
- if look_bytes: named.append((f"look_1{infer_ext(look_bytes)}", look_bytes))
306
- if video_bytes: named.append(("video.mp4", video_bytes))
307
- if named:
308
- st.download_button(
309
- "Tümünü indir (ZIP)",
310
- data=make_zip(named),
311
- file_name="fitstudio_outputs.zip",
312
- mime="application/zip",
313
- key=f"zip_{st.session_state['job_counter']}"
314
- )
315
-
316
- # ================= Sidebar =================
 
 
317
  with st.sidebar:
318
  st.caption(f"Resolved API_BASE: {API_BASE}")
319
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
@@ -328,4 +344,4 @@ with st.sidebar:
328
  if st.button("Clear cache"):
329
  st.cache_data.clear(); st.success("Cache cleared.")
330
  if st.button("Clear results"):
331
- st.session_state["results"] = None; st.success("Results cleared.")
 
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 {}
22
 
23
  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
  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)
 
84
  canvas.paste(im_resized, off)
85
  buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
86
 
 
 
 
 
 
 
 
87
  def backend_ok() -> bool:
88
  try:
89
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
 
97
  r.raise_for_status(); return r.json()
98
 
99
  def post_chain(data: dict, files_payload):
100
+ payload = {**data, "to_video": "false"} # video ayrı
101
  r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
102
  headers=HEADERS if HF_TOKEN else None, timeout=600)
103
  r.raise_for_status(); return r.json()
 
107
  headers=HEADERS if HF_TOKEN else None, timeout=600)
108
  r.raise_for_status(); return r.json()
109
 
110
+ def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
111
+ buf = io.BytesIO()
112
+ with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
113
+ for fname, b in named_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:
 
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:
 
205
 
206
  def _common_form_data():
207
  data = {
208
+ "category": st.session_state["category"],
209
+ "gender": st.session_state["gender"],
210
+ "age_group": st.session_state["age_group"],
211
  "frame_aspect": "9:16",
212
+ "num_images": str(st.session_state["num_images"]),
213
  }
214
+ if st.session_state.get("custom_prompt","").strip():
215
+ data["prompt"] = st.session_state["custom_prompt"].strip()
216
  if image_url:
217
  data["image_urls"] = image_url.strip()
218
  return data
219
 
 
220
  if run_image or run_chain:
221
  if not ok:
222
  st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
 
242
 
243
  st.session_state["job_counter"] += 1
244
  st.session_state["results"] = {
245
+ "job_id": (image_json.get("job_id") or out.get("job_id")),
246
+ "schema_version": (image_json.get("schema_version") or out.get("schema_version")),
247
  "image_urls": urls,
248
  "video_url": None,
249
  }
 
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
  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.")