renderfy commited on
Commit
89ea25f
·
verified ·
1 Parent(s): 644dfcf

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +121 -147
streamlit_app.py CHANGED
@@ -1,7 +1,7 @@
1
- # streamlit_app.py — Fit Studio AI (Fashion & Apparel) v0.6 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
- # Bu sürümde tüm “Input / Look / Video” kutuları aynı boyutta, yan yana ve merkezde.
5
 
6
  import os, io, base64, zipfile, requests, streamlit as st
7
  from pathlib import Path
@@ -18,23 +18,24 @@ 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
 
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
- HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
27
-
28
- # Tek tip 9:16 kart ölçüsü
29
- PREVIEW_H = int(os.getenv("PREVIEW_H", "640")) # piksel
30
- PREVIEW_W = int(round(PREVIEW_H * 9 / 16)) # 360
31
  PREVIEW_BG = (255, 255, 255)
32
 
33
  # ============== State =====================
 
34
  for k, v in {
35
  "results": None,
36
  "job_counter": 0,
37
  "duration": "6",
 
38
  }.items():
39
  st.session_state.setdefault(k, v)
40
 
@@ -104,7 +105,7 @@ def post_image_edit(data: dict, files_payload):
104
  r.raise_for_status(); return r.json()
105
 
106
  def post_chain(data: dict, files_payload):
107
- payload = {**data, "to_video": "false"}
108
  r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
109
  headers=HEADERS if HF_TOKEN else None, timeout=600)
110
  r.raise_for_status(); return r.json()
@@ -121,61 +122,50 @@ def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
121
  if b: z.writestr(fname, b)
122
  buf.seek(0); return buf.read()
123
 
124
- # ============== Common CSS (kartlar sabit, merkezde, yan yana) ==============
125
- st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
126
  st.markdown(f"""
127
  <style>
128
  :root {{
129
  --frameW: {PREVIEW_W}px;
130
  --frameH: {PREVIEW_H}px;
131
  }}
132
- .media-grid {{
133
  display: grid;
134
- grid-template-columns: repeat(auto-fit, minmax(var(--frameW), 1fr));
135
  gap: 28px;
136
- justify-content: center;
137
  align-items: start;
 
138
  }}
139
  .media-card {{
140
  width: var(--frameW);
141
- margin: 0 auto;
142
- }}
143
- .media-title {{
144
- font-weight: 600;
145
- margin-bottom: 8px;
146
  }}
 
147
  .media-frame {{
148
- width: var(--frameW);
149
- height: var(--frameH);
150
- background: #fff;
151
- border: 1px solid #e9e9ec;
152
- border-radius: 12px;
153
- overflow: hidden;
154
- display:flex;
155
- align-items:center;
156
- justify-content:center;
157
  box-shadow: 0 1px 2px rgba(0,0,0,.05);
158
  }}
159
  .media-frame img, .media-frame video {{
160
- width: 100%; height: 100%; object-fit: contain;
161
- display:block;
162
- }}
163
- .media-actions {{
164
- margin-top: 6px; font-size: 12px; color: #666; text-align:center;
165
  }}
 
166
  .media-actions a {{
167
  text-decoration: none; color: #444; border:1px solid #ddd; padding:4px 8px;
168
  border-radius: 6px; display:inline-block;
169
  }}
 
170
  </style>
171
  """, unsafe_allow_html=True)
172
 
173
- # ============== Header & Controls =================
174
  st.title("🧵 Fit Studio AI")
175
  st.caption("Fashion & Apparel — Dikey 9:16 Still Image (VTO) → (opsiyonel) Runway Video")
176
 
177
  ok = backend_ok()
178
- c0, c1, c2, c3, c4, c5 = st.columns([1.0, 1.5, 1.5, 1.2, 1.6, 1.6])
179
  with c0:
180
  category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
181
  with c1:
@@ -198,8 +188,8 @@ with cB:
198
  run_image = st.button("Çalıştır (Sadece Foto)", type="primary", use_container_width=True, key="run_img_btn")
199
  run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
200
 
201
- # ============== Demo Preview (aynı satırda, eşit kadraj) =================
202
- def card_html_img(title: str, raw_bytes: bytes, fname: str):
203
  pv = vertical_preview_9x16(raw_bytes, PREVIEW_W, PREVIEW_H, PREVIEW_BG)
204
  data_url = b64_img_png(pv)
205
  dl_url = "data:image/jpeg;base64," + base64.b64encode(raw_bytes).decode("utf-8")
@@ -211,7 +201,7 @@ def card_html_img(title: str, raw_bytes: bytes, fname: str):
211
  </div>
212
  """
213
 
214
- def card_html_video(title: str, video_bytes: bytes, fname: str="fitstudio_video.mp4"):
215
  vurl = b64_video_mp4(video_bytes)
216
  return f"""
217
  <div class="media-card">
@@ -223,36 +213,37 @@ def card_html_video(title: str, video_bytes: bytes, fname: str="fitstudio_video.
223
  </div>
224
  """
225
 
 
 
 
 
 
 
 
 
 
226
  st.markdown("---")
227
  st.subheader("Demo preview (çalıştırmadan önce)")
228
- st.caption("Aynı klasörden **input.jpg**, **tryon.jpg** ve **fitstudio_video.mp4** okunur. Tüm kartlar 9:16 ve aynı boyutta.")
229
-
230
- demo_cards = []
231
- in_b = fetch_bytes("input.jpg")
232
- if in_b: demo_cards.append(card_html_img("Input", in_b, f"input{infer_ext(in_b)}"))
233
- tr_b = fetch_bytes("tryon.jpg")
234
- if tr_b: demo_cards.append(card_html_img("Sample Output", tr_b, f"tryon{infer_ext(tr_b)}"))
235
- dv_b = fetch_bytes("fitstudio_video.mp4")
236
- if dv_b: demo_cards.append(card_html_video("Sample Video", dv_b))
237
-
238
- if demo_cards:
239
- st.markdown('<div class="media-grid">' + "\n".join(demo_cards) + "</div>", unsafe_allow_html=True)
240
- else:
241
- st.info("Demo içerik bulunamadı (input.jpg / tryon.jpg / fitstudio_video.mp4).")
242
-
243
- # ============== Input preview (aynı kart stilinde) =================
244
- st.markdown("---")
245
- in_cards = []
246
- input_preview = None
247
  if file_list:
248
- try: input_preview = file_list[0].getvalue()
249
- except Exception: input_preview = None
 
 
250
  elif image_url:
251
- input_preview = fetch_bytes(image_url)
252
-
253
- if input_preview:
254
- in_cards.append(card_html_img("Input", input_preview, f"input_runtime{infer_ext(input_preview)}"))
255
- st.markdown('<div class="media-grid">' + "\n".join(in_cards) + "</div>", unsafe_allow_html=True)
256
 
257
  # ============== Run =================
258
  def _build_files_payload():
@@ -311,91 +302,74 @@ if run_image or run_chain:
311
  except Exception as e:
312
  st.error(f"Hata: {e}")
313
 
314
- # ============== Results grid (Look1/2/3 + Video) =================
 
 
 
 
 
 
 
 
 
315
  res = st.session_state.get("results") or {}
316
- if res:
317
- urls = res.get("image_urls") or []
318
- video_url = res.get("video_url")
319
-
320
- cards = []
321
- named = []
322
-
323
- for idx in range(min(3, len(urls))):
324
- u = urls[idx]
325
- b = fetch_bytes(u)
326
- if not b: continue
327
- cards.append(card_html_img("Look" if idx==0 else f"Look #{idx+1}", b, f"look_{idx+1}{infer_ext(b)}"))
328
- named.append((f"look_{idx+1}{infer_ext(b)}", b))
329
-
330
- # Video hücresi (küçük kadraj, otomatik loop)
331
- vbytes = None
332
- if video_url:
333
- vbytes = fetch_bytes(video_url)
334
- if vbytes: cards.append(card_html_video("Video", vbytes))
335
-
336
- st.markdown("---")
337
- st.subheader("Sonuçlar")
338
- if cards:
339
- st.markdown('<div class="media-grid">' + "\n".join(cards) + "</div>", unsafe_allow_html=True)
 
 
340
  else:
341
- st.info("Gösterilecek sonuç bulunamadı.")
342
-
343
- # Video üretim kontrolleri
344
- ctl1, ctl2, ctl3 = st.columns([1.1, 1.0, 1.2])
345
- with ctl1:
346
- gen_video = st.checkbox("Bu sonuçtan video üret (loop)", value=False, key="video_toggle")
347
- with ctl2:
348
- st.selectbox("Video süresi", ["6","10"], index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")
349
- with ctl3:
350
- go_video = st.button("Video Oluştur", use_container_width=True, key="video_btn")
351
-
352
- if gen_video and go_video:
353
- src_for_video = urls[0] if urls else None
354
- if not src_for_video:
355
- st.warning("Video için kaynak görsel yok.")
356
  else:
357
- with st.spinner("Video üretiliyor…"):
358
- v = post_video({
359
- "image_url": src_for_video,
360
- "duration": st.session_state.get("duration_select","6"),
361
- "resolution": "768P",
362
- "prompt_optimizer": "true",
363
- "gender": st.session_state["gender"],
364
- "age_group": st.session_state["age_group"],
365
- "frame_aspect": "9:16",
366
- })
367
- vurl = (v.get("result") or {}).get("video", {}).get("url")
368
- if vurl:
369
- st.session_state["results"]["video_url"] = vurl
370
- vbytes = fetch_bytes(vurl)
371
- # tekrar kartları çiz
372
- cards.append(card_html_video("Video", vbytes if vbytes else b""))
373
- st.markdown('<div class="media-grid">' + "\n".join(cards) + "</div>", unsafe_allow_html=True)
374
- else:
375
- st.info("Video URL gelmedi.")
376
-
377
- # ZIP indir (Streamlit butonu)
378
- if named or vbytes:
379
- if vbytes: named.append(("video.mp4", vbytes))
380
- st.download_button(
381
- "Tümünü indir (ZIP)",
382
- data=make_zip(named),
383
- file_name="fitstudio_outputs.zip",
384
- mime="application/zip",
385
- key=f"zip_{st.session_state['job_counter']}"
386
- )
387
-
388
- # Debug
389
- with st.expander("Debug"):
390
- st.json({
391
- "job_id": res.get("job_id"),
392
- "schema_version": res.get("schema_version"),
393
- "api_base": API_BASE,
394
- "has_auth_header": bool(HF_TOKEN),
395
- "image_urls": urls,
396
- "video_url": res.get("video_url"),
397
- "frame": f"{PREVIEW_W}×{PREVIEW_H} (9:16)"
398
- })
399
 
400
  # ============== Sidebar =================
401
  with st.sidebar:
@@ -412,4 +386,4 @@ with st.sidebar:
412
  if st.button("Clear cache"):
413
  st.cache_data.clear(); st.success("Cache cleared.")
414
  if st.button("Clear results"):
415
- st.session_state["results"] = None; st.success("Results cleared.")
 
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
 
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
 
 
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()
 
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:
 
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")
 
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">
 
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():
 
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:
343
+ with st.spinner("Video üretiliyor…"):
344
+ v = post_video({
345
+ "image_url": src_for_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)",
368
+ data=make_zip(named),
369
+ file_name="fitstudio_outputs.zip",
370
+ mime="application/zip",
371
+ key=f"zip_{st.session_state['job_counter']}"
372
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
  # ============== Sidebar =================
375
  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.")