renderfy commited on
Commit
78d342c
·
verified ·
1 Parent(s): 89ea25f

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +108 -166
streamlit_app.py CHANGED
@@ -1,13 +1,15 @@
1
- # streamlit_app.py — Fit Studio AI (Fashion & Apparel) v0.7 UI
2
  # 9:16 dikey kadraj — Still Image (VTO) → (optional) Runway Video
3
- # DÜZELTME: “Input + Look + Video” tek satır sabit 3 sütun grid; boşluk yok, hepsi aynı boy.
4
- # Demo: aynı klasörde input.jpg, tryon.jpg, fitstudio_video.mp4 varsa ilk bölümde ö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
 
@@ -18,33 +20,30 @@ API_BASE = (
18
  ).rstrip("/") if (_env("FITSTUDIO_API") or _env("AI_LIGHTBOX_API") or _env("LUXFIT_API")) else ""
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
- # Tek tip 9:16 kart ölçüsü (yan yana eşit)
28
- PREVIEW_H = int(os.getenv("PREVIEW_H", "640")) # px
29
- PREVIEW_W = int(round(PREVIEW_H * 9 / 16)) # 360 px
 
 
30
  PREVIEW_BG = (255, 255, 255)
31
 
32
- # ============== State =====================
33
  st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
34
- for k, v in {
35
- "results": None,
36
- "job_counter": 0,
37
- "duration": "6",
38
- "input_preview_bytes": None,
39
- }.items():
40
  st.session_state.setdefault(k, v)
41
 
42
- # ============== Helpers ===================
43
  def _needs_auth(url: str) -> bool:
44
  return (url.startswith(API_BASE) or url.startswith("outputs/"))
45
 
46
  @st.cache_data(ttl=300, show_spinner=False)
47
  def fetch_bytes(url_or_path: str | None):
 
48
  if not url_or_path:
49
  return None
50
  try:
@@ -76,6 +75,7 @@ def infer_ext(data: bytes) -> str:
76
 
77
  @st.cache_data(ttl=300, show_spinner=False)
78
  def vertical_preview_9x16(img_bytes: bytes, out_w: int = PREVIEW_W, out_h: int = PREVIEW_H, bg_rgb=PREVIEW_BG) -> 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,11 +86,12 @@ 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 b64_img_png(png_bytes: bytes) -> str:
90
- return "data:image/png;base64," + base64.b64encode(png_bytes).decode("utf-8")
91
-
92
- def b64_video_mp4(mp4_bytes: bytes) -> str:
93
- return "data:video/mp4;base64," + base64.b64encode(mp4_bytes).decode("utf-8")
 
94
 
95
  def backend_ok() -> bool:
96
  try:
@@ -105,7 +106,7 @@ def post_image_edit(data: dict, files_payload):
105
  r.raise_for_status(); return r.json()
106
 
107
  def post_chain(data: dict, files_payload):
108
- payload = {**data, "to_video": "false"} # video ayrı
109
  r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
110
  headers=HEADERS if HF_TOKEN else None, timeout=600)
111
  r.raise_for_status(); return r.json()
@@ -115,57 +116,40 @@ def post_video(data: dict):
115
  headers=HEADERS if HF_TOKEN else None, timeout=600)
116
  r.raise_for_status(); return r.json()
117
 
118
- def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
119
- buf = io.BytesIO()
120
- with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
121
- for fname, b in named_bytes:
122
- if b: z.writestr(fname, b)
123
- buf.seek(0); return buf.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
- # ============== CSS (sabit 3 sütun grid) ==============
126
- st.markdown(f"""
127
- <style>
128
- :root {{
129
- --frameW: {PREVIEW_W}px;
130
- --frameH: {PREVIEW_H}px;
131
- }}
132
- .media-row {{
133
- display: grid;
134
- grid-template-columns: repeat(3, var(--frameW)); /* HER ZAMAN 3 sütun */
135
- gap: 28px;
136
- justify-content: center; /* ortala, sağda boşluk yok */
137
- align-items: start;
138
- margin: 8px 0 18px 0;
139
- }}
140
- .media-card {{
141
- width: var(--frameW);
142
- margin: 0;
143
- }}
144
- .media-title {{ font-weight: 600; margin-bottom: 8px; }}
145
- .media-frame {{
146
- width: var(--frameW); height: var(--frameH);
147
- background: #fff; border: 1px solid #e9e9ec; border-radius: 12px;
148
- overflow: hidden; display:flex; align-items:center; justify-content:center;
149
- box-shadow: 0 1px 2px rgba(0,0,0,.05);
150
- }}
151
- .media-frame img, .media-frame video {{
152
- width: 100%; height: 100%; object-fit: contain; display:block;
153
- }}
154
- .media-actions {{ margin-top: 6px; font-size: 12px; color: #666; text-align:center; }}
155
- .media-actions a {{
156
- text-decoration: none; color: #444; border:1px solid #ddd; padding:4px 8px;
157
- border-radius: 6px; display:inline-block;
158
- }}
159
- .ph {{ color:#9aa; font-size: 13px; }}
160
- </style>
161
- """, unsafe_allow_html=True)
162
-
163
- # ============== UI: Header & Controls =================
164
  st.title("🧵 Fit Studio AI")
165
  st.caption("Fashion & Apparel — Dikey 9:16 Still Image (VTO) → (opsiyonel) Runway Video")
166
 
167
  ok = backend_ok()
168
- c0, c1, c2, c3, c4, c5 = st.columns([1.0, 1.5, 1.5, 1.2, 1.8, 1.6])
169
  with c0:
170
  category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
171
  with c1:
@@ -181,71 +165,36 @@ with c5:
181
 
182
  prompt_box = st.text_area("Özel prompt (boş bırak → varsayılan)", value="", height=90, key="custom_prompt")
183
 
184
- cA, cB = st.columns([1,1])
185
- with cA:
186
  st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
187
- with cB:
188
  run_image = st.button("Çalıştır (Sadece Foto)", type="primary", use_container_width=True, key="run_img_btn")
189
  run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
190
 
191
- # ============== Demo Preview (çalıştırmadan önce) =================
192
- def card_img(title: str, raw_bytes: bytes, fname: str):
193
- pv = vertical_preview_9x16(raw_bytes, PREVIEW_W, PREVIEW_H, PREVIEW_BG)
194
- data_url = b64_img_png(pv)
195
- dl_url = "data:image/jpeg;base64," + base64.b64encode(raw_bytes).decode("utf-8")
196
- return f"""
197
- <div class="media-card">
198
- <div class="media-title">{title}</div>
199
- <div class="media-frame"><img src="{data_url}" alt="{title}"/></div>
200
- <div class="media-actions"><a download="{fname}" href="{dl_url}">İndir</a></div>
201
- </div>
202
- """
203
-
204
- def card_video(title: str, video_bytes: bytes, fname: str="fitstudio_video.mp4"):
205
- vurl = b64_video_mp4(video_bytes)
206
- return f"""
207
- <div class="media-card">
208
- <div class="media-title">{title}</div>
209
- <div class="media-frame">
210
- <video src="{vurl}" autoplay loop muted playsinline></video>
211
- </div>
212
- <div class="media-actions"><a download="{fname}" href="{vurl}">Video indir</a></div>
213
- </div>
214
- """
215
-
216
- def card_placeholder(title: str, text: str="Hazır değil"):
217
- return f"""
218
- <div class="media-card">
219
- <div class="media-title">{title}</div>
220
- <div class="media-frame"><div class="ph">{text}</div></div>
221
- <div class="media-actions">&nbsp;</div>
222
- </div>
223
- """
224
-
225
  st.markdown("---")
226
  st.subheader("Demo preview (çalıştırmadan önce)")
227
- st.caption("Aynı klasörden **input.jpg**, **tryon.jpg**, **fitstudio_video.mp4** okunur. Tüm kartlar 9:16 ve aynı boyutta.")
228
 
229
- demo_left = fetch_bytes("input.jpg")
230
- demo_mid = fetch_bytes("tryon.jpg")
231
- demo_right = fetch_bytes("fitstudio_video.mp4")
 
232
 
233
- demo_row = []
234
- demo_row.append(card_img("Input", demo_left, f"input{infer_ext(demo_left)}") if demo_left else card_placeholder("Input"))
235
- demo_row.append(card_img("Sample Output", demo_mid, f"tryon{infer_ext(demo_mid)}") if demo_mid else card_placeholder("Sample Output"))
236
- demo_row.append(card_video("Sample Video", demo_right) if demo_right else card_placeholder("Sample Video"))
237
- st.markdown('<div class="media-row">' + "\n".join(demo_row) + "</div>", unsafe_allow_html=True)
238
 
239
- # ============== Input preview (state’e yaz) =================
 
240
  if file_list:
241
- try:
242
- st.session_state["input_preview_bytes"] = file_list[0].getvalue()
243
- except Exception:
244
- st.session_state["input_preview_bytes"] = None
245
  elif image_url:
246
- st.session_state["input_preview_bytes"] = fetch_bytes(image_url)
247
 
248
- # ============== Run =================
249
  def _build_files_payload():
250
  files_payload = []
251
  if file_list:
@@ -255,18 +204,19 @@ def _build_files_payload():
255
 
256
  def _common_form_data():
257
  data = {
258
- "category": st.session_state["category"],
259
- "gender": st.session_state["gender"],
260
- "age_group": st.session_state["age_group"],
261
  "frame_aspect": "9:16",
262
- "num_images": str(st.session_state["num_images"]),
263
  }
264
- if st.session_state.get("custom_prompt","").strip():
265
- data["prompt"] = st.session_state["custom_prompt"].strip()
266
  if image_url:
267
  data["image_urls"] = image_url.strip()
268
  return data
269
 
 
270
  if run_image or run_chain:
271
  if not ok:
272
  st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
@@ -292,8 +242,6 @@ if run_image or run_chain:
292
 
293
  st.session_state["job_counter"] += 1
294
  st.session_state["results"] = {
295
- "job_id": (image_json.get("job_id") or out.get("job_id")),
296
- "schema_version": (image_json.get("schema_version") or out.get("schema_version")),
297
  "image_urls": urls,
298
  "video_url": None,
299
  }
@@ -302,41 +250,35 @@ if run_image or run_chain:
302
  except Exception as e:
303
  st.error(f"Hata: {e}")
304
 
305
- # ============== STUDIO — Tek satırda Input + Look + Video =================
306
- st.markdown("---")
307
- st.subheader("Studio")
308
 
309
- # 1) Input kartı (her zaman dolu veya placeholder)
310
- row_cards = []
311
- in_bytes = st.session_state.get("input_preview_bytes")
312
- row_cards.append(card_img("Input", in_bytes, f"input_runtime{infer_ext(in_bytes)}") if in_bytes else card_placeholder("Input", "Görsel seç"))
313
 
314
- # 2) Look (ilk çıktı veya placeholder ya da tryon.jpg fallback)
315
  res = st.session_state.get("results") or {}
316
- urls = (res.get("image_urls") or [])
317
- look_url = urls[0] if urls else None
318
- look_bytes = fetch_bytes(look_url) if look_url else (demo_mid if demo_mid else None)
319
- row_cards.append(card_img("Look", look_bytes, f"look_1{infer_ext(look_bytes)}") if look_bytes else card_placeholder("Look"))
320
-
321
- # 3) Video (üretilmiş → URL / yoksa örnek / yoksa placeholder)
322
- vurl = res.get("video_url")
323
- vbytes = fetch_bytes(vurl) if vurl else (demo_right if demo_right else None)
324
- row_cards.append(card_video("Video", vbytes) if vbytes else card_placeholder("Video"))
325
-
326
- # Satırı çiz — 3 sütun sabit → sağda/ solda boşluk oluşmaz
327
- st.markdown('<div class="media-row">' + "\n".join(row_cards) + "</div>", unsafe_allow_html=True)
328
-
329
- # ============== Video üretim kontrolleri (aynı sayfada) =================
330
- ctl1, ctl2, ctl3 = st.columns([1.1, 1.0, 1.2])
331
- with ctl1:
332
- gen_video = st.checkbox("Bu sonuçtan video üret (loop)", value=False, key="video_toggle")
333
- with ctl2:
334
  st.selectbox("Video süresi", ["6","10"], index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")
335
- with ctl3:
336
  go_video = st.button("Video Oluştur", use_container_width=True, key="video_btn")
337
 
338
  if gen_video and go_video:
339
- src_for_video = urls[0] if urls else None
340
  if not src_for_video:
341
  st.warning("Video için kaynak görsel yok.")
342
  else:
@@ -346,22 +288,22 @@ if gen_video and go_video:
346
  "duration": st.session_state.get("duration_select","6"),
347
  "resolution": "768P",
348
  "prompt_optimizer": "true",
349
- "gender": st.session_state["gender"],
350
- "age_group": st.session_state["age_group"],
351
  "frame_aspect": "9:16",
352
  })
353
  vurl = (v.get("result") or {}).get("video", {}).get("url")
354
  if vurl:
355
  st.session_state["results"]["video_url"] = vurl
356
- # satırı yeniden çizmek yerine bilgi ver
357
- st.success("Video hazır — üstteki Video kartında otomatik oynar.")
358
  else:
359
  st.info("Video URL gelmedi.")
360
 
361
- # ============== ZIP indir (Look + Video varsa) =================
362
  named = []
363
  if look_bytes: named.append((f"look_1{infer_ext(look_bytes)}", look_bytes))
364
- if vbytes: named.append(("video.mp4", vbytes))
365
  if named:
366
  st.download_button(
367
  "Tümünü indir (ZIP)",
@@ -371,7 +313,7 @@ if named:
371
  key=f"zip_{st.session_state['job_counter']}"
372
  )
373
 
374
- # ============== Sidebar =================
375
  with st.sidebar:
376
  st.caption(f"Resolved API_BASE: {API_BASE}")
377
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
@@ -386,4 +328,4 @@ with st.sidebar:
386
  if st.button("Clear cache"):
387
  st.cache_data.clear(); st.success("Cache cleared.")
388
  if st.button("Clear results"):
389
- st.session_state["results"] = None; st.session_state["input_preview_bytes"] = 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
+ # 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
 
 
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:
 
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
  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:
 
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
  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
 
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
 
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
 
243
  st.session_state["job_counter"] += 1
244
  st.session_state["results"] = {
 
 
245
  "image_urls": urls,
246
  "video_url": None,
247
  }
 
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:
 
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)",
 
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
  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.")