renderfy commited on
Commit
9b31739
·
verified ·
1 Parent(s): adf8ad5

Upload 5 files

Browse files
Files changed (6) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +13 -0
  3. input.jpg +0 -0
  4. requirements.txt +12 -0
  5. streamlit_app.py +428 -0
  6. tryon.jpg +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tryon.jpg filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+ WORKDIR /app
7
+
8
+ COPY --chown=user requirements.txt .
9
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
10
+
11
+ COPY --chown=user . .
12
+ EXPOSE 7860
13
+ CMD ["streamlit", "run", "streamlit_app.py", "--server.address=0.0.0.0", "--server.port=7860"]
input.jpg ADDED
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit
2
+ httpx>=0.27
3
+ pyjwt>=2.9
4
+ fastapi
5
+ uvicorn[standard]
6
+ pillow
7
+ requests
8
+ fal-client
9
+ python-multipart
10
+ aiofiles
11
+ websockets
12
+ websocket-client
streamlit_app.py ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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) ve "tryon.jpg" (örnek çıktı)
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")
15
+ or _env("AI_LIGHTBOX_API")
16
+ or _env("LUXFIT_API")
17
+ or _env("AI_BEAUTYBOX_API")
18
+ or _env("AI_BEAUTY_API")
19
+ ).rstrip("/") if (
20
+ _env("FITSTUDIO_API")
21
+ or _env("AI_LIGHTBOX_API")
22
+ or _env("LUXFIT_API")
23
+ or _env("AI_BEAUTYBOX_API")
24
+ or _env("AI_BEAUTY_API")
25
+ ) else ""
26
+
27
+ HF_TOKEN = (
28
+ _env("FITSTUDIO_TOKEN")
29
+ or _env("AI_LIGHTBOX_TOKEN")
30
+ or _env("LUXFIT_TOKEN")
31
+ or _env("AI_BEAUTYBOX_TOKEN")
32
+ or _env("AI_BEAUTY_TOKEN")
33
+ )
34
+
35
+ if not API_BASE:
36
+ st.error("API_BASE bulunamadı. Settings > Variables: FITSTUDIO_API (veya AI_LIGHTBOX_API / LUXFIT_API) tanımlayın.")
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, # son çalıştırmanın sonuçları
45
+ "job_counter": 0, # indirme butonları için unique key
46
+ "duration": "6", # video süresi varsayılan
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:
54
+ return (url.startswith(API_BASE) or url.startswith("outputs/"))
55
+
56
+ @st.cache_data(ttl=300, show_spinner=False)
57
+ 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():
66
+ return p.read_bytes()
67
+ url = f"{API_BASE}/{url_or_path}" if url_or_path.startswith("outputs/") else url_or_path
68
+ headers = (HEADERS if (_needs_auth(url) and HF_TOKEN) else None)
69
+ r = requests.get(url, headers=headers, timeout=180)
70
+ r.raise_for_status()
71
+ return r.content
72
+ except Exception:
73
+ return None
74
+
75
+ @st.cache_data(ttl=300, show_spinner=False)
76
+ def image_size_from_bytes(b: bytes):
77
+ try:
78
+ im = Image.open(io.BytesIO(b))
79
+ return im.size
80
+ except Exception:
81
+ return None
82
+
83
+ def infer_ext(data: bytes) -> str:
84
+ try:
85
+ im = Image.open(io.BytesIO(data))
86
+ fmt = (im.format or "JPEG").lower()
87
+ return "." + {"jpeg":"jpg","jpg":"jpg","png":"png","webp":"webp"}.get(fmt, "jpg")
88
+ except Exception:
89
+ return ".jpg"
90
+
91
+ @st.cache_data(ttl=300, show_spinner=False)
92
+ def square_preview_bytes(img_bytes: bytes, size: int = PREVIEW_SIZE, bg_rgb=(255,255,255)) -> bytes:
93
+ im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")
94
+ w, h = im.size
95
+ scale = min(size / w, size / h)
96
+ new_w, new_h = max(1, int(w*scale)), max(1, int(h*scale))
97
+ im_resized = im.resize((new_w, new_h), Image.LANCZOS)
98
+ canvas = Image.new("RGBA", (size, size), (*bg_rgb, 255))
99
+ off = ((size - new_w)//2, (size - new_h)//2)
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 make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
141
+ buf = io.BytesIO()
142
+ with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
143
+ for fname, b in named_bytes:
144
+ if b:
145
+ z.writestr(fname, b)
146
+ buf.seek(0)
147
+ return buf.read()
148
+
149
+ def backend_ok() -> bool:
150
+ try:
151
+ r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
152
+ r.raise_for_status()
153
+ return True
154
+ except Exception:
155
+ return False
156
+
157
+ def post_image_edit(data: dict, files_payload):
158
+ r = requests.post(f"{API_BASE}/v1/image/edit", data=data, files=files_payload or None,
159
+ headers=HEADERS if HF_TOKEN else None, timeout=600)
160
+ r.raise_for_status()
161
+ return r.json()
162
+
163
+ def post_chain(data: dict, files_payload):
164
+ payload = {**data, "to_video": "false"} # video UI'da ayrı akış
165
+ r = requests.post(f"{API_BASE}/v1/tryon/chain", data=payload, files=files_payload or None,
166
+ headers=HEADERS if HF_TOKEN else None, timeout=600)
167
+ r.raise_for_status()
168
+ return r.json()
169
+
170
+ def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=True):
171
+ payload = {"image_url": image_url, "duration": duration, "resolution": resolution,
172
+ "prompt_optimizer": "true" if prompt_optimizer else "false"}
173
+ r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload,
174
+ headers=HEADERS if HF_TOKEN else None, timeout=600)
175
+ r.raise_for_status()
176
+ return r.json()
177
+
178
+ # ================= Sayfa =================
179
+ st.set_page_config(page_title="Fit Studio AI", layout="wide", page_icon="🧵")
180
+ st.title("🧵 Fit Studio AI")
181
+ st.caption("Fashion & Apparel — Still Image (VTO) → (optional) Runway Video")
182
+
183
+ # ================= Üst Kontrol Şeridi =================
184
+ ok = backend_ok()
185
+ c0, c1, c2, c3, c4 = st.columns([1.0, 1.8, 1.8, 1.4, 1.6])
186
+
187
+ with c0:
188
+ category = st.selectbox("Kategori", ["general","underwear"], index=0, key="category")
189
+
190
+ with c1:
191
+ file_list = st.file_uploader("Garment image (upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
192
+
193
+ with c2:
194
+ image_url = st.text_input("veya Garment URL", key="image_url")
195
+
196
+ with c3:
197
+ num_images = st.selectbox("Çıktı adedi", [1,2,3,4], index=0, key="num_images")
198
+
199
+ with c4:
200
+ custom_prompt = st.text_area("Özel prompt (boş bırak → varsayılan)", value="", height=80, key="custom_prompt")
201
+
202
+ bA, bB = st.columns([1.0, 1.0])
203
+ with bA:
204
+ st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
205
+ with bB:
206
+ # İki farklı çalıştırma seçeneği: sadece foto veya zincir
207
+ run_image = st.button("Çalıştır (Sadece Foto)", type="primary", use_container_width=True, key="run_img_btn")
208
+ run_chain = st.button("Zincir (Foto → Video hazırlık)", use_container_width=True, key="run_chain_btn")
209
+
210
+ # ================= Demo Preview (mevcut klasörden) =================
211
+ st.markdown("---")
212
+ st.subheader("Demo preview (çalıştırmadan önce örnek)")
213
+ st.caption("Bu bölüm sadece örnek; aynı klasörden **input.jpg** ve **tryon.jpg** okunur.")
214
+
215
+ dp_in, dp_sp, dp_tp = st.columns(3)
216
+ in_bytes = fetch_bytes("input.jpg")
217
+ if in_bytes:
218
+ dp_in.subheader("Input")
219
+ dp_in.image(square_preview_bytes(in_bytes, size=PREVIEW_SIZE, bg_rgb=(255,255,255)), use_container_width=True)
220
+ sz = image_size_from_bytes(in_bytes)
221
+ if sz: dp_in.caption(f"Input: {sz[0]}×{sz[1]} px")
222
+ dp_in.download_button("İndir (tam kalite)", data=in_bytes, file_name=f"input{infer_ext(in_bytes)}",
223
+ mime="image/jpeg", key="demo_input_dl")
224
+ else:
225
+ show_tile(dp_in, "Input", None)
226
+
227
+ try_bytes = fetch_bytes("tryon.jpg")
228
+ if try_bytes:
229
+ dp_sp.subheader("Sample Output")
230
+ dp_sp.image(square_preview_bytes(try_bytes, size=PREVIEW_SIZE, bg_rgb=(255,255,255)), use_container_width=True)
231
+ sz2 = image_size_from_bytes(try_bytes)
232
+ if sz2: dp_sp.caption(f"Output: {sz2[0]}×{sz2[1]} px")
233
+ dp_sp.download_button("İndir (tam kalite)", data=try_bytes, file_name=f"tryon{infer_ext(try_bytes)}",
234
+ mime="image/jpeg", key="demo_try_dl")
235
+ else:
236
+ show_tile(dp_sp, "Sample Output", None)
237
+
238
+ show_tile(dp_tp, "Video (örnek yok)", None)
239
+
240
+ # ================= Girdi Önizleme =================
241
+ st.markdown("---")
242
+ g1, g2, g3, g4 = st.columns(4)
243
+ white_bg = (255,255,255)
244
+
245
+ input_preview = None
246
+ if file_list:
247
+ try:
248
+ input_preview = file_list[0].getvalue()
249
+ except Exception:
250
+ input_preview = None
251
+ elif image_url:
252
+ input_preview = fetch_bytes(image_url)
253
+
254
+ if input_preview:
255
+ g1.subheader("Input")
256
+ g1.image(square_preview_bytes(input_preview, size=PREVIEW_SIZE, bg_rgb=white_bg), use_container_width=True)
257
+ try:
258
+ sz = Image.open(io.BytesIO(input_preview)).size
259
+ g1.caption(f"{sz[0]}×{sz[1]} px")
260
+ except Exception:
261
+ g1.caption("—")
262
+ else:
263
+ show_tile(g1, "Input", None)
264
+
265
+ # ================ Çalıştır ================
266
+ def _build_files_payload():
267
+ files_payload = []
268
+ if file_list:
269
+ for f in file_list:
270
+ files_payload.append(("files", (f.name, f.getvalue(), f.type or "image/jpeg")))
271
+ return files_payload
272
+
273
+ def _common_form_data():
274
+ data = {
275
+ "category": st.session_state["category"],
276
+ "num_images": str(st.session_state["num_images"]),
277
+ }
278
+ if st.session_state.get("custom_prompt","").strip():
279
+ data["prompt"] = st.session_state["custom_prompt"].strip()
280
+ if image_url:
281
+ data["image_urls"] = image_url.strip()
282
+ return data
283
+
284
+ if run_image or run_chain:
285
+ if not ok:
286
+ st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
287
+ elif not (file_list or image_url):
288
+ st.error("En az bir garment görseli girin (upload veya URL).")
289
+ else:
290
+ try:
291
+ files_payload = _build_files_payload()
292
+ form = _common_form_data()
293
+
294
+ with st.spinner("Çalışıyor…"):
295
+ if run_image:
296
+ out = post_image_edit(form, files_payload)
297
+ image_json = out
298
+ else:
299
+ out = post_chain(form, files_payload) # to_video=false
300
+ image_json = out.get("image_step") or out # Fallback
301
+
302
+ # ---- Çıktıları çıkar ----
303
+ imgs = (image_json.get("result") or {}).get("images") or []
304
+ urls = [i.get("url") for i in imgs if isinstance(i, dict) and i.get("url")]
305
+ # UI state
306
+ st.session_state["job_counter"] += 1
307
+ st.session_state["results"] = {
308
+ "job_id": (image_json.get("job_id") or out.get("job_id")),
309
+ "schema_version": (image_json.get("schema_version") or out.get("schema_version")),
310
+ "image_urls": urls,
311
+ "video_url": None,
312
+ }
313
+
314
+ except requests.HTTPError as e:
315
+ st.error(f"HTTP {e.response.status_code}")
316
+ except Exception as e:
317
+ st.error(f"Hata: {e}")
318
+
319
+ # ================ Sonuçları Göster ================
320
+ res = st.session_state.get("results") or {}
321
+ vbytes = None
322
+ if res:
323
+ key_prefix = f"job{st.session_state['job_counter']}"
324
+ img_urls = res.get("image_urls") or []
325
+
326
+ # İlk 4 görseli göster
327
+ cols = st.columns(4)
328
+ named = []
329
+ if img_urls:
330
+ for idx in range(min(4, len(img_urls))):
331
+ u = img_urls[idx]
332
+ c = cols[idx] if idx < len(cols) else st
333
+ title = "Look" if idx == 0 else f"Look #{idx+1}"
334
+ _, b = show_tile(c, title, u, bg_rgb=white_bg, filename_stub=f"look_{idx+1}", key_prefix=f"{key_prefix}_{idx}")
335
+ if b:
336
+ named.append((f"look_{idx+1}{infer_ext(b)}", b))
337
+ else:
338
+ show_tile(cols[0], "Look", None)
339
+
340
+ # ---- Video (ilk görselden) ----
341
+ st.markdown("---")
342
+ colv1, colv2, colv3 = st.columns([1.1, 1.0, 1.2])
343
+ with colv1:
344
+ gen_video = st.checkbox("Bu sonuçtan video üret", value=False, key="video_toggle")
345
+ with colv2:
346
+ st.selectbox("Video süresi", ["6","10"], index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")
347
+ with colv3:
348
+ go_video = st.button("Video Oluştur", use_container_width=True, key="video_btn")
349
+
350
+ if gen_video and go_video:
351
+ src_for_video = img_urls[0] if img_urls else None
352
+ if not src_for_video:
353
+ st.warning("Video için kaynak görsel yok.")
354
+ else:
355
+ with st.spinner("Video üretiliyor…"):
356
+ v = post_video(
357
+ src_for_video,
358
+ duration=st.session_state.get("duration_select","6"),
359
+ resolution="768P",
360
+ prompt_optimizer=True
361
+ )
362
+ vurl = (v.get("result") or {}).get("video", {}).get("url")
363
+ if vurl:
364
+ st.session_state["results"]["video_url"] = vurl
365
+ else:
366
+ st.info("Video URL gelmedi.")
367
+
368
+ vurl = st.session_state["results"].get("video_url")
369
+ if vurl:
370
+ st.subheader("Video")
371
+ st.video(vurl, format="video/mp4")
372
+ vb = fetch_bytes(vurl)
373
+ if vb:
374
+ vbytes = vb
375
+ st.download_button(
376
+ "Video indir (MP4)",
377
+ data=vb,
378
+ file_name="fitstudio_video.mp4",
379
+ mime="video/mp4",
380
+ key=f"{key_prefix}_video_dl"
381
+ )
382
+ else:
383
+ st.info("Video URL var ama içerik indirilemedi.")
384
+
385
+ # ---- ZIP indir (isteğe bağlı) ----
386
+ if named or vbytes:
387
+ if vbytes:
388
+ named.append(("video.mp4", vbytes))
389
+ zip_bytes = make_zip(named)
390
+ st.download_button(
391
+ "Tümünü indir (ZIP)",
392
+ data=zip_bytes,
393
+ file_name="fitstudio_outputs.zip",
394
+ mime="application/zip",
395
+ key=f"{key_prefix}_zip_dl"
396
+ )
397
+
398
+ # Debug
399
+ with st.expander("Debug"):
400
+ st.json({
401
+ "job_id": res.get("job_id"),
402
+ "schema_version": res.get("schema_version"),
403
+ "api_base": API_BASE,
404
+ "has_auth_header": bool(HF_TOKEN),
405
+ "image_urls": img_urls,
406
+ "video_url": res.get("video_url"),
407
+ })
408
+
409
+ # ================= Sidebar (teşhis) =================
410
+ with st.sidebar:
411
+ st.caption(f"Resolved API_BASE: {API_BASE}")
412
+ st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))
413
+
414
+ if st.button("Ping /health"):
415
+ try:
416
+ r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)
417
+ st.write(r.status_code)
418
+ try:
419
+ st.json(r.json())
420
+ except Exception:
421
+ st.code((r.text or "")[:1000])
422
+ except Exception as e:
423
+ st.error(f"Health error: {e}")
424
+
425
+ if st.button("Clear cache"):
426
+ st.cache_data.clear(); st.success("Cache cleared.")
427
+ if st.button("Clear results"):
428
+ st.session_state["results"] = None; st.success("Results cleared.")
tryon.jpg ADDED

Git LFS Details

  • SHA256: d496e6bc4f0b8e8c0eb097031e7713f0416ce879a586a993b8d1c2da81402e7a
  • Pointer size: 132 Bytes
  • Size of remote file: 1.12 MB