renderfy commited on
Commit
2322a12
·
verified ·
1 Parent(s): 93b5651

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +176 -163
streamlit_app.py CHANGED
@@ -1,14 +1,17 @@
1
- # streamlit_app.py — Fit Studio AI (Fashion & Apparel) v0.4 UI
2
  # Still Image (VTO) → (optional) Runway Video
3
- # Demo preview: same folder "input.jpg" (girdi), "tryon.jpg" (örnek çıktı), "fitstudio_video.mp4" (örnek video)
 
 
 
4
 
5
- import os, io, zipfile, requests, streamlit as st
6
  from pathlib import Path
7
  from PIL import Image
 
8
 
9
  # ================= Env & API (robust) =================
10
- def _env(k):
11
- return (os.getenv(k) or "").strip().strip("'\"")
12
 
13
  API_BASE = (
14
  _env("FITSTUDIO_API")
@@ -37,17 +40,20 @@ if not API_BASE:
37
  st.stop()
38
 
39
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
40
- PREVIEW_SIZE = int(os.getenv("PREVIEW_SIZE", "512"))
 
 
 
 
 
 
 
 
41
 
42
  # ================= Session State (ilk yük) =================
43
- defaults = {
44
- "results": None,
45
- "job_counter": 0,
46
- "duration": "6",
47
- }
48
  for k, v in defaults.items():
49
- if k not in st.session_state:
50
- st.session_state[k] = v
51
 
52
  # ================= Yardımcılar =================
53
  def _needs_auth(url: str) -> bool:
@@ -58,8 +64,7 @@ def fetch_bytes(url_or_path: str | None):
58
  """
59
  URL, outputs/ yolu veya mevcut klasörden dosya okur.
60
  """
61
- if not url_or_path:
62
- return None
63
  try:
64
  p = Path(url_or_path)
65
  if p.is_file():
@@ -100,101 +105,129 @@ def square_preview_bytes(img_bytes: bytes, size: int = PREVIEW_SIZE, bg_rgb=(255
100
  canvas.paste(im_resized, off, im_resized)
101
  buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
102
 
103
- def first_present(d: dict, keys: list, default=None):
104
- for k in keys:
105
- v = d.get(k)
106
- if v:
107
- return v
108
- return default
109
-
110
- def show_tile(col, title, url: str | None, bg_rgb=(255,255,255), filename_stub="image", key_prefix=""):
111
  col.subheader(title)
112
- if not url:
113
  placeholder = Image.new("RGBA", (PREVIEW_SIZE, PREVIEW_SIZE), (0,0,0,0))
114
- col.image(placeholder, use_container_width=True)
115
- col.caption("—")
116
- return None, None
117
-
118
- b = fetch_bytes(url)
 
 
119
  if not b:
120
- col.warning("Görsel yüklenemedi")
121
- return None, None
122
-
123
- pv = square_preview_bytes(b, size=PREVIEW_SIZE, bg_rgb=bg_rgb)
124
- col.image(pv, use_container_width=True)
125
  sz = image_size_from_bytes(b)
126
- if sz:
127
- col.caption(f"Gerçek çözünürlük: {sz[0]}×{sz[1]} px")
128
-
129
  ext = infer_ext(b)
130
  mime = "image/jpeg" if ext in [".jpg",".jpeg"] else ("image/png" if ext==".png" else "image/webp")
131
- col.download_button(
132
- "İndir (tam kalite)",
133
- data=b,
134
- file_name=f"{filename_stub}{ext}",
135
- mime=mime,
136
- key=f"{key_prefix}_{filename_stub}_dl"
137
- )
138
- return url, b
139
-
140
- def show_video_tile(col, title, video_source: str | bytes | None, key_prefix=""):
 
141
  col.subheader(title)
142
- if not video_source:
143
- col.caption("—")
144
- return None
145
- try:
146
- if isinstance(video_source, (bytes, bytearray)):
147
- col.video(video_source, format="video/mp4")
148
- vb = video_source
149
- else:
150
- vb = fetch_bytes(video_source) if isinstance(video_source, str) else None
151
- if vb:
152
- col.video(vb, format="video/mp4")
153
- if vb:
154
- col.download_button("Video indir (MP4)", data=vb, file_name="fitstudio_video.mp4", mime="video/mp4",
155
- key=f"{key_prefix}_video_dl")
156
- return vb
157
- except Exception:
158
- col.info("Video yüklenemedi.")
159
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
162
  buf = io.BytesIO()
163
  with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
164
  for fname, b in named_bytes:
165
- if b:
166
- z.writestr(fname, b)
167
- buf.seek(0)
168
- return buf.read()
169
 
170
  def backend_ok() -> bool:
171
  try:
172
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
173
- r.raise_for_status()
174
- return True
175
  except Exception:
176
  return False
177
 
178
  def post_image_edit(data: dict, files_payload):
179
  r = requests.post(f"{API_BASE}/v1/image/edit", data=data, files=files_payload or None,
180
  headers=HEADERS if HF_TOKEN else None, timeout=600)
181
- r.raise_for_status()
182
- return r.json()
183
 
184
  def post_chain(data: dict, files_payload):
185
  payload = {**data, "to_video": "false"} # video UI'da ayrı akış
186
  r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
187
  headers=HEADERS if HF_TOKEN else None, timeout=600)
188
- r.raise_for_status()
189
- return r.json()
190
 
191
  def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=True):
192
  payload = {"image_url": image_url, "duration": duration, "resolution": resolution,
193
  "prompt_optimizer": "true" if prompt_optimizer else "false"}
194
  r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload,
195
  headers=HEADERS if HF_TOKEN else None, timeout=600)
196
- r.raise_for_status()
197
- return r.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
  # ================= Sayfa =================
200
  st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
@@ -203,84 +236,72 @@ st.caption("Fashion & Apparel — Still Image (VTO) → (optional) Runway Video"
203
 
204
  # ================= Üst Kontrol Şeridi =================
205
  ok = backend_ok()
206
- c0, c1, c2, c3, c4 = st.columns([1.0, 1.8, 1.8, 1.4, 1.6])
207
 
208
  with c0:
209
  category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
 
 
 
 
 
 
 
 
 
 
 
210
  with c1:
211
- file_list = st.file_uploader("Garment image (upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
 
212
  with c2:
213
  image_url = st.text_input("veya Garment URL", key="image_url")
214
  with c3:
215
- num_images = st.selectbox("Çıktı adedi", [1,2,3,4], index=0, key="num_images")
216
- with c4:
217
- custom_prompt = st.text_area("Özel prompt (boş bırak → varsayılan)", value="", height=80, key="custom_prompt")
218
 
219
- bA, bB = st.columns([1.0, 1.0])
220
  with bA:
221
- st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
222
  with bB:
223
- run_image = st.button("Çalıştır (Sadece Foto)", type="primary", use_container_width=True, key="run_img_btn")
224
- run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
 
 
 
 
 
 
 
 
 
225
 
226
  # ================= Demo Preview (mevcut klasörden) =================
227
  st.markdown("---")
228
  st.subheader("Demo preview (çalıştırmadan önce örnek)")
 
229
 
230
- dp_in, dp_sp, dp_v = st.columns([1,1,1])
231
  in_bytes = fetch_bytes("input.jpg")
232
- if in_bytes:
233
- dp_in.subheader("Input")
234
- dp_in.image(square_preview_bytes(in_bytes, size=PREVIEW_SIZE, bg_rgb=(255,255,255)), use_container_width=True)
235
- sz = image_size_from_bytes(in_bytes)
236
- if sz: dp_in.caption(f"Input: {sz[0]}×{sz[1]} px")
237
- dp_in.download_button("İndir (tam kalite)", data=in_bytes, file_name=f"input{infer_ext(in_bytes)}",
238
- mime="image/jpeg", key="demo_input_dl")
239
- else:
240
- show_tile(dp_in, "Input", None)
241
 
242
  try_bytes = fetch_bytes("tryon.jpg")
243
- if try_bytes:
244
- dp_sp.subheader("Sample Output")
245
- dp_sp.image(square_preview_bytes(try_bytes, size=PREVIEW_SIZE, bg_rgb=(255,255,255)), use_container_width=True)
246
- sz2 = image_size_from_bytes(try_bytes)
247
- if sz2: dp_sp.caption(f"Output: {sz2[0]}×{sz2[1]} px")
248
- dp_sp.download_button("İndir (tam kalite)", data=try_bytes, file_name=f"tryon{infer_ext(try_bytes)}",
249
- mime="image/jpeg", key="demo_try_dl")
250
- else:
251
- show_tile(dp_sp, "Sample Output", None)
252
 
253
  demo_vbytes = fetch_bytes("fitstudio_video.mp4")
254
- if demo_vbytes:
255
- show_video_tile(dp_v, "Sample Video", demo_vbytes, key_prefix="demo")
256
- else:
257
- dp_v.subheader("Sample Video")
258
- dp_v.caption("—")
259
 
260
  # ================= Girdi Önizleme =================
261
  st.markdown("---")
262
  g1, g2, g3, g4 = st.columns(4)
263
- white_bg = (255,255,255)
264
 
265
  input_preview = None
266
  if file_list:
267
- try:
268
- input_preview = file_list[0].getvalue()
269
- except Exception:
270
- input_preview = None
271
  elif image_url:
272
  input_preview = fetch_bytes(image_url)
273
 
274
- if input_preview:
275
- g1.subheader("Input")
276
- g1.image(square_preview_bytes(input_preview, size=PREVIEW_SIZE, bg_rgb=white_bg), use_container_width=True)
277
- try:
278
- sz = Image.open(io.BytesIO(input_preview)).size
279
- g1.caption(f"{sz[0]}×{sz[1]} px")
280
- except Exception:
281
- g1.caption("—")
282
- else:
283
- show_tile(g1, "Input", None)
284
 
285
  # ================ Çalıştır ================
286
  def _build_files_payload():
@@ -291,14 +312,17 @@ def _build_files_payload():
291
  return files_payload
292
 
293
  def _common_form_data():
 
 
 
 
 
294
  data = {
295
  "category": st.session_state["category"],
296
  "num_images": str(st.session_state["num_images"]),
 
297
  }
298
- if st.session_state.get("custom_prompt","").strip():
299
- data["prompt"] = st.session_state["custom_prompt"].strip()
300
- if image_url:
301
- data["image_urls"] = image_url.strip()
302
  return data
303
 
304
  if run_image or run_chain:
@@ -310,7 +334,6 @@ if run_image or run_chain:
310
  try:
311
  files_payload = _build_files_payload()
312
  form = _common_form_data()
313
-
314
  with st.spinner("Çalışıyor…"):
315
  if run_image:
316
  out = post_image_edit(form, files_payload)
@@ -319,6 +342,7 @@ if run_image or run_chain:
319
  out = post_chain(form, files_payload) # to_video=false
320
  image_json = out.get("image_step") or out
321
 
 
322
  imgs = (image_json.get("result") or {}).get("images") or []
323
  urls = [i.get("url") for i in imgs if isinstance(i, dict) and i.get("url")]
324
  st.session_state["job_counter"] += 1
@@ -328,47 +352,42 @@ if run_image or run_chain:
328
  "image_urls": urls,
329
  "video_url": None,
330
  }
331
-
332
  except requests.HTTPError as e:
333
  st.error(f"HTTP {e.response.status_code}")
334
  except Exception as e:
335
  st.error(f"Hata: {e}")
336
 
337
- # ================ Sonuçları Satırda Yan Yana Göster ================
338
  res = st.session_state.get("results") or {}
339
  if res:
340
  key_prefix = f"job{st.session_state['job_counter']}"
341
  img_urls = res.get("image_urls") or []
342
  video_url = res.get("video_url")
343
 
344
- # 1. Satır: 3 görsel + 1 video sütunu (video varsa göster)
345
- c1, c2, c3, c4 = st.columns([1,1,1,1])
346
  named = []
347
-
348
- # İlk 3 görsel
349
  tiles = [c1, c2, c3]
350
  for idx in range(min(3, len(img_urls))):
351
  u = img_urls[idx]
352
  title = "Look" if idx == 0 else f"Look #{idx+1}"
353
- _, b = show_tile(tiles[idx], title, u, bg_rgb=white_bg, filename_stub=f"look_{idx+1}", key_prefix=f"{key_prefix}_{idx}")
354
- if b:
355
- named.append((f"look_{idx+1}{infer_ext(b)}", b))
356
 
357
- # Video sütunu (küçük kadraj: sütun genişliği kadar)
358
  vbytes = None
359
  if video_url:
360
  vbytes = show_video_tile(c4, "Video", video_url, key_prefix=key_prefix)
361
  else:
362
- c4.subheader("Video")
363
- c4.caption("Henüz üretilmedi.")
364
 
365
- # 2. Satır: Video üretim kontrolleri (isteğe bağlı)
366
  st.markdown("---")
367
  colv1, colv2, colv3 = st.columns([1.1, 1.0, 1.2])
368
  with colv1:
369
  gen_video = st.checkbox("Bu sonuçtan video üret", value=False, key="video_toggle")
370
  with colv2:
371
- st.selectbox("Video süresi", ["6","10"], index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")
 
372
  with colv3:
373
  go_video = st.button("Video Oluştur", use_container_width=True, key="video_btn")
374
 
@@ -378,33 +397,25 @@ if res:
378
  st.warning("Video için kaynak görsel yok.")
379
  else:
380
  with st.spinner("Video üretiliyor…"):
381
- v = post_video(
382
- src_for_video,
383
- duration=st.session_state.get("duration_select","6"),
384
- resolution="768P",
385
- prompt_optimizer=True
386
- )
387
  vurl = (v.get("result") or {}).get("video", {}).get("url")
388
  if vurl:
389
  st.session_state["results"]["video_url"] = vurl
390
  video_url = vurl
391
- # Hemen üst satırdaki sütuna küçük kadrajda göster
392
  vbytes = show_video_tile(c4, "Video", video_url, key_prefix=key_prefix)
393
  else:
394
  st.info("Video URL gelmedi.")
395
 
396
  # ZIP indir
397
  if named or vbytes:
398
- if vbytes:
399
- named.append(("video.mp4", vbytes))
400
  zip_bytes = make_zip(named)
401
- st.download_button(
402
- "Tümünü indir (ZIP)",
403
- data=zip_bytes,
404
- file_name="fitstudio_outputs.zip",
405
- mime="application/zip",
406
- key=f"{key_prefix}_zip_dl"
407
- )
408
 
409
  # Debug
410
  with st.expander("Debug"):
@@ -415,21 +426,23 @@ if res:
415
  "has_auth_header": bool(HF_TOKEN),
416
  "image_urls": img_urls,
417
  "video_url": res.get("video_url"),
 
 
 
418
  })
419
 
420
  # ================= Sidebar (teşhis) =================
421
  with st.sidebar:
422
  st.caption(f"Resolved API_BASE: {API_BASE}")
423
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
 
424
 
425
  if st.button("Ping /health"):
426
  try:
427
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
428
  st.write(r.status_code)
429
- try:
430
- st.json(r.json())
431
- except Exception:
432
- st.code((r.text or "")[:1000])
433
  except Exception as e:
434
  st.error(f"Health error: {e}")
435
 
 
1
+ # streamlit_app.py — Fit Studio AI (Fashion & Apparel) v0.5 UI
2
  # Still Image (VTO) → (optional) Runway Video
3
+ # Demo preview: same folder "input.jpg", "tryon.jpg", "fitstudio_video.mp4"
4
+ # Notlar:
5
+ # - Tüm kutular (Input / Output / Video) eş boyutta kare kutular olarak gösterilir.
6
+ # - Cinsiyet ve yaş grubu seçimleri prompt’a akıllıca enjekte edilir.
7
 
8
+ import os, io, zipfile, base64, requests, streamlit as st
9
  from pathlib import Path
10
  from PIL import Image
11
+ import streamlit.components.v1 as components
12
 
13
  # ================= Env & API (robust) =================
14
+ def _env(k): return (os.getenv(k) or "").strip().strip("'\"")
 
15
 
16
  API_BASE = (
17
  _env("FITSTUDIO_API")
 
40
  st.stop()
41
 
42
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
43
+
44
+ # Kare kutu hedef boyutu (px)
45
+ def _clampi(v, lo, hi):
46
+ try: v = int(v)
47
+ except: v = 360
48
+ return max(lo, min(hi, v))
49
+
50
+ TILE = _clampi(os.getenv("TILE_SIZE", "360"), 240, 720) # tüm kutular için tek boyut
51
+ PREVIEW_SIZE = TILE # görüntü önizleme üretiminde de aynı değer
52
 
53
  # ================= Session State (ilk yük) =================
54
+ defaults = {"results": None, "job_counter": 0, "duration": "6"}
 
 
 
 
55
  for k, v in defaults.items():
56
+ if k not in st.session_state: st.session_state[k] = v
 
57
 
58
  # ================= Yardımcılar =================
59
  def _needs_auth(url: str) -> bool:
 
64
  """
65
  URL, outputs/ yolu veya mevcut klasörden dosya okur.
66
  """
67
+ if not url_or_path: return None
 
68
  try:
69
  p = Path(url_or_path)
70
  if p.is_file():
 
105
  canvas.paste(im_resized, off, im_resized)
106
  buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
107
 
108
+ def show_img_tile(col, title, src: str | bytes | None, filename_stub="image", key_prefix=""):
 
 
 
 
 
 
 
109
  col.subheader(title)
110
+ if not src:
111
  placeholder = Image.new("RGBA", (PREVIEW_SIZE, PREVIEW_SIZE), (0,0,0,0))
112
+ col.image(placeholder, width=TILE)
113
+ col.caption("—"); return None, None
114
+ b = None
115
+ try:
116
+ b = src if isinstance(src, (bytes, bytearray)) else fetch_bytes(src)
117
+ except Exception:
118
+ b = None
119
  if not b:
120
+ col.warning("Görsel yüklenemedi"); return None, None
121
+ pv = square_preview_bytes(b, size=PREVIEW_SIZE, bg_rgb=(255,255,255))
122
+ col.image(pv, width=TILE)
 
 
123
  sz = image_size_from_bytes(b)
124
+ if sz: col.caption(f"Gerçek: {sz[0]}×{sz[1]} px")
 
 
125
  ext = infer_ext(b)
126
  mime = "image/jpeg" if ext in [".jpg",".jpeg"] else ("image/png" if ext==".png" else "image/webp")
127
+ col.download_button("İndir (tam kalite)", data=b, file_name=f"{filename_stub}{ext}", mime=mime,
128
+ key=f"{key_prefix}_{filename_stub}_dl")
129
+ return src, b
130
+
131
+ def _b64(vb: bytes) -> str:
132
+ return base64.b64encode(vb).decode("utf-8")
133
+
134
+ def show_video_tile(col, title, src: str | bytes | None, key_prefix=""):
135
+ """
136
+ Kare video kutusu: WIDTH=TILE, HEIGHT=TILE. İçeride <video> object-fit: cover.
137
+ """
138
  col.subheader(title)
139
+ if not src:
140
+ # boş kare placeholder
141
+ ph_html = f"""
142
+ <div style="width:{TILE}px;height:{TILE}px;border-radius:12px;background:#111;display:flex;align-items:center;justify-content:center;color:#bbb;font:500 14px system-ui;">
143
+ —
144
+ </div>"""
145
+ components.html(ph_html, height=TILE+4)
 
 
 
 
 
 
 
 
 
 
146
  return None
147
+ vb = src if isinstance(src, (bytes, bytearray)) else fetch_bytes(src)
148
+ if not vb:
149
+ components.html(f"<div style='width:{TILE}px;height:{TILE}px;background:#111;border-radius:12px'></div>", height=TILE+4)
150
+ col.caption("Video yüklenemedi."); return None
151
+ data_url = f"data:video/mp4;base64,{_b64(vb)}"
152
+ html = f"""
153
+ <div style="width:{TILE}px;height:{TILE}px;border-radius:12px;overflow:hidden;background:#000;">
154
+ <video src="{data_url}" controls playsinline style="width:100%;height:100%;object-fit:cover;display:block;"></video>
155
+ </div>
156
+ """
157
+ components.html(html, height=TILE+48)
158
+ col.download_button("Video indir (MP4)", data=vb, file_name="fitstudio_video.mp4", mime="video/mp4",
159
+ key=f"{key_prefix}_video_dl")
160
+ return vb
161
 
162
  def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
163
  buf = io.BytesIO()
164
  with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
165
  for fname, b in named_bytes:
166
+ if b: z.writestr(fname, b)
167
+ buf.seek(0); return buf.read()
 
 
168
 
169
  def backend_ok() -> bool:
170
  try:
171
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
172
+ r.raise_for_status(); return True
 
173
  except Exception:
174
  return False
175
 
176
  def post_image_edit(data: dict, files_payload):
177
  r = requests.post(f"{API_BASE}/v1/image/edit", data=data, files=files_payload or None,
178
  headers=HEADERS if HF_TOKEN else None, timeout=600)
179
+ r.raise_for_status(); return r.json()
 
180
 
181
  def post_chain(data: dict, files_payload):
182
  payload = {**data, "to_video": "false"} # video UI'da ayrı akış
183
  r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
184
  headers=HEADERS if HF_TOKEN else None, timeout=600)
185
+ r.raise_for_status(); return r.json()
 
186
 
187
  def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=True):
188
  payload = {"image_url": image_url, "duration": duration, "resolution": resolution,
189
  "prompt_optimizer": "true" if prompt_optimizer else "false"}
190
  r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload,
191
  headers=HEADERS if HF_TOKEN else None, timeout=600)
192
+ r.raise_for_status(); return r.json()
193
+
194
+ # ================= Prompt bileşimi (cinsiyet + yaş) =================
195
+ def _map_gender_txt(tr):
196
+ if tr == "Kadın": return "female"
197
+ if tr == "Erkek": return "male"
198
+ return "auto"
199
+
200
+ def _map_age_txt(tr):
201
+ # Güvenli aralıklar (18+)
202
+ if tr.startswith("Genç"): return "young adult (18–25)"
203
+ if tr.startswith("Yetişkin"): return "adult (26–45)"
204
+ if tr.startswith("Olgun"): return "mature adult (46+)"
205
+ return "auto"
206
+
207
+ def compose_prompt(base_user_prompt: str, category: str, gender_tr: str, age_tr: str) -> str:
208
+ gender = _map_gender_txt(gender_tr)
209
+ age = _map_age_txt(age_tr)
210
+ parts = []
211
+ if base_user_prompt and len(base_user_prompt.strip()) > 10:
212
+ parts.append(base_user_prompt.strip())
213
+ else:
214
+ # kısa, sağlam bir temel prompt
215
+ base = ("Commercial fashion catalog still; full-body, face visible, feet not cropped; "
216
+ "photorealistic; preserve garment identity (design, texture, color, stitching, logos) exactly; "
217
+ "no added text/logos; modern Istanbul street style; neutral daylight.")
218
+ if category == "underwear":
219
+ base += " Wear ONLY the uploaded underwear garment; neutral, non-sexual catalog pose."
220
+ parts.append(base)
221
+ model_rules = []
222
+ if gender != "auto":
223
+ model_rules.append(f"Model gender presentation: {gender}.")
224
+ if age != "auto":
225
+ model_rules.append(f"Age group cues: {age}. Avoid depicting minors.")
226
+ if model_rules:
227
+ parts.append(" ".join(model_rules))
228
+ # tutarlılık
229
+ parts.append("Hands visible; 3/4 or frontal pose; avoid cropping; single person; no artefacts.")
230
+ return " ".join(parts)
231
 
232
  # ================= Sayfa =================
233
  st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
 
236
 
237
  # ================= Üst Kontrol Şeridi =================
238
  ok = backend_ok()
239
+ c0, c_gender, c_age, c1, c2, c3 = st.columns([1.0, 1.0, 1.2, 1.6, 1.4, 1.6])
240
 
241
  with c0:
242
  category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
243
+
244
+ with c_gender:
245
+ gender_tr = st.radio("Cinsiyet", ["Otomatik","Kadın","Erkek"], index=0, horizontal=True, key="gender_tr",
246
+ help="Modelin cinsiyet sunumunu seçin.")
247
+
248
+ with c_age:
249
+ age_tr = st.selectbox("Yaş grubu",
250
+ ["Otomatik","Genç (18–25)","Yetişkin (26–45)","Olgun (46+)"],
251
+ index=0, key="age_tr",
252
+ help="Güvenli aralık: 18 yaş ve üzeri.")
253
+
254
  with c1:
255
+ file_list = st.file_uploader("Garment image (upload)", type=["jpg","jpeg","png","webp"],
256
+ accept_multiple_files=True, key="file_upl")
257
  with c2:
258
  image_url = st.text_input("veya Garment URL", key="image_url")
259
  with c3:
260
+ custom_prompt = st.text_area("Özel prompt (opsiyonel)", value="", height=80, key="custom_prompt")
 
 
261
 
262
+ bA, bB, bC = st.columns([1.0, 1.0, 1.0])
263
  with bA:
264
+ st.info(f"Backend: {'Online' if ok else 'Offline'} • Tile: {TILE}px", icon="🔌")
265
  with bB:
266
+ num_images = st.selectbox("Çıktı adedi", [1,2,3,4], index=0, key="num_images")
267
+ with bC:
268
+ colbtn1, colbtn2 = st.columns(2)
269
+ with colbtn1:
270
+ run_image = st.button("Çalıştır (Foto)", type="primary", use_container_width=True, key="run_img_btn")
271
+ with colbtn2:
272
+ run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
273
+
274
+ # Güvenlik: underwear + 18 altı seçenek yok; ek bir uyarı da koyalım
275
+ if category == "underwear" and age_tr in {"Otomatik"}:
276
+ st.caption("ℹ️ Underwear için yaş grubu otomatik bırakıldı; sistem yetişkin güvenli aralıkta yorumlar.")
277
 
278
  # ================= Demo Preview (mevcut klasörden) =================
279
  st.markdown("---")
280
  st.subheader("Demo preview (çalıştırmadan önce örnek)")
281
+ st.caption("Aynı klasörden **input.jpg**, **tryon.jpg**, **fitstudio_video.mp4** okunur. Tüm kutular eş boyutta kare.")
282
 
283
+ dp_in, dp_out, dp_vid = st.columns(3)
284
  in_bytes = fetch_bytes("input.jpg")
285
+ show_img_tile(dp_in, "Input", in_bytes, filename_stub="input", key_prefix="demo")
 
 
 
 
 
 
 
 
286
 
287
  try_bytes = fetch_bytes("tryon.jpg")
288
+ show_img_tile(dp_out, "Sample Output", try_bytes, filename_stub="tryon", key_prefix="demo")
 
 
 
 
 
 
 
 
289
 
290
  demo_vbytes = fetch_bytes("fitstudio_video.mp4")
291
+ show_video_tile(dp_vid, "Sample Video", demo_vbytes, key_prefix="demo")
 
 
 
 
292
 
293
  # ================= Girdi Önizleme =================
294
  st.markdown("---")
295
  g1, g2, g3, g4 = st.columns(4)
 
296
 
297
  input_preview = None
298
  if file_list:
299
+ try: input_preview = file_list[0].getvalue()
300
+ except Exception: input_preview = None
 
 
301
  elif image_url:
302
  input_preview = fetch_bytes(image_url)
303
 
304
+ show_img_tile(g1, "Input (preview)", input_preview, filename_stub="input_preview", key_prefix="preview")
 
 
 
 
 
 
 
 
 
305
 
306
  # ================ Çalıştır ================
307
  def _build_files_payload():
 
312
  return files_payload
313
 
314
  def _common_form_data():
315
+ # Prompt’u cinsiyet+yaş ile kompoze et
316
+ prompt_final = compose_prompt(st.session_state.get("custom_prompt",""),
317
+ st.session_state["category"],
318
+ st.session_state["gender_tr"],
319
+ st.session_state["age_tr"])
320
  data = {
321
  "category": st.session_state["category"],
322
  "num_images": str(st.session_state["num_images"]),
323
+ "prompt": prompt_final,
324
  }
325
+ if image_url: data["image_urls"] = image_url.strip()
 
 
 
326
  return data
327
 
328
  if run_image or run_chain:
 
334
  try:
335
  files_payload = _build_files_payload()
336
  form = _common_form_data()
 
337
  with st.spinner("Çalışıyor…"):
338
  if run_image:
339
  out = post_image_edit(form, files_payload)
 
342
  out = post_chain(form, files_payload) # to_video=false
343
  image_json = out.get("image_step") or out
344
 
345
+ # ---- Çıktıları çıkar ----
346
  imgs = (image_json.get("result") or {}).get("images") or []
347
  urls = [i.get("url") for i in imgs if isinstance(i, dict) and i.get("url")]
348
  st.session_state["job_counter"] += 1
 
352
  "image_urls": urls,
353
  "video_url": None,
354
  }
 
355
  except requests.HTTPError as e:
356
  st.error(f"HTTP {e.response.status_code}")
357
  except Exception as e:
358
  st.error(f"Hata: {e}")
359
 
360
+ # ================ Sonuçları Satırda Yan Yana (eş boyut) ================
361
  res = st.session_state.get("results") or {}
362
  if res:
363
  key_prefix = f"job{st.session_state['job_counter']}"
364
  img_urls = res.get("image_urls") or []
365
  video_url = res.get("video_url")
366
 
367
+ # 1. Satır: 3 kare görsel + 1 kare video (varsa)
368
+ c1, c2, c3, c4 = st.columns(4)
369
  named = []
 
 
370
  tiles = [c1, c2, c3]
371
  for idx in range(min(3, len(img_urls))):
372
  u = img_urls[idx]
373
  title = "Look" if idx == 0 else f"Look #{idx+1}"
374
+ _, b = show_img_tile(tiles[idx], title, u, filename_stub=f"look_{idx+1}", key_prefix=f"{key_prefix}_{idx}")
375
+ if b: named.append((f"look_{idx+1}{infer_ext(b)}", b))
 
376
 
 
377
  vbytes = None
378
  if video_url:
379
  vbytes = show_video_tile(c4, "Video", video_url, key_prefix=key_prefix)
380
  else:
381
+ show_video_tile(c4, "Video", None, key_prefix=key_prefix)
 
382
 
383
+ # 2. Satır: Video üretim kontrolleri
384
  st.markdown("---")
385
  colv1, colv2, colv3 = st.columns([1.1, 1.0, 1.2])
386
  with colv1:
387
  gen_video = st.checkbox("Bu sonuçtan video üret", value=False, key="video_toggle")
388
  with colv2:
389
+ st.selectbox("Video süresi", ["6","10"],
390
+ index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")
391
  with colv3:
392
  go_video = st.button("Video Oluştur", use_container_width=True, key="video_btn")
393
 
 
397
  st.warning("Video için kaynak görsel yok.")
398
  else:
399
  with st.spinner("Video üretiliyor…"):
400
+ v = post_video(src_for_video,
401
+ duration=st.session_state.get("duration_select","6"),
402
+ resolution="768P",
403
+ prompt_optimizer=True)
 
 
404
  vurl = (v.get("result") or {}).get("video", {}).get("url")
405
  if vurl:
406
  st.session_state["results"]["video_url"] = vurl
407
  video_url = vurl
 
408
  vbytes = show_video_tile(c4, "Video", video_url, key_prefix=key_prefix)
409
  else:
410
  st.info("Video URL gelmedi.")
411
 
412
  # ZIP indir
413
  if named or vbytes:
414
+ if vbytes: named.append(("video.mp4", vbytes))
 
415
  zip_bytes = make_zip(named)
416
+ st.download_button("Tümünü indir (ZIP)", data=zip_bytes,
417
+ file_name="fitstudio_outputs.zip", mime="application/zip",
418
+ key=f"{key_prefix}_zip_dl")
 
 
 
 
419
 
420
  # Debug
421
  with st.expander("Debug"):
 
426
  "has_auth_header": bool(HF_TOKEN),
427
  "image_urls": img_urls,
428
  "video_url": res.get("video_url"),
429
+ "tile_px": TILE,
430
+ "gender": gender_tr,
431
+ "age_group": age_tr,
432
  })
433
 
434
  # ================= Sidebar (teşhis) =================
435
  with st.sidebar:
436
  st.caption(f"Resolved API_BASE: {API_BASE}")
437
  st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
438
+ st.caption(f"Tile size: {TILE}px (env: TILE_SIZE)")
439
 
440
  if st.button("Ping /health"):
441
  try:
442
  r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
443
  st.write(r.status_code)
444
+ try: st.json(r.json())
445
+ except Exception: st.code((r.text or "")[:1000])
 
 
446
  except Exception as e:
447
  st.error(f"Health error: {e}")
448