renderfy commited on
Commit
b8567ec
·
verified ·
1 Parent(s): a533fe1

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +13 -0
  2. requirements.txt +12 -0
  3. streamlit_app.py +417 -0
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"]
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,417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # streamlit_app.py — AI LightBox · Beauty (packshot + on-face + SR + video)
2
+ import os, io, zipfile, requests, streamlit as st
3
+ from PIL import Image
4
+
5
+ # ================= Env & API =================
6
+ API_BASE = (os.getenv("AI_LIGHTBOX_API", "http://127.0.0.1:8000") or "http://127.0.0.1:8000").strip().strip("'\"").rstrip("/")
7
+ HF_TOKEN = (os.getenv("AI_LIGHTBOX_TOKEN", "") or "").strip().strip("'\"")
8
+ HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
9
+ PREVIEW_SIZE = int(os.getenv("PREVIEW_SIZE", "512"))
10
+
11
+ # ================= Session State =================
12
+ if "results" not in st.session_state:
13
+ st.session_state["results"] = None
14
+ if "job_counter" not in st.session_state:
15
+ st.session_state["job_counter"] = 0
16
+ if "duration" not in st.session_state:
17
+ st.session_state["duration"] = "6"
18
+
19
+ # ================= Yardımcılar =================
20
+ def _needs_auth(url: str) -> bool:
21
+ return url.startswith(API_BASE) or url.startswith("outputs/")
22
+
23
+ def backend_ok() -> bool:
24
+ try:
25
+ r = requests.get(f"{API_BASE}/health", headers=HEADERS, timeout=10)
26
+ r.raise_for_status()
27
+ return True
28
+ except Exception:
29
+ return False
30
+
31
+ def post_chain(data: dict, files_payload):
32
+ data = {**data, "to_video": "false"} # videoyu ayrı tetikleriz
33
+ r = requests.post(f"{API_BASE}/v1/tryon/chain", data=data, files=files_payload or None,
34
+ headers=HEADERS, timeout=600)
35
+ r.raise_for_status()
36
+ return r.json()
37
+
38
+ def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=False):
39
+ payload = {
40
+ "image_url": image_url,
41
+ "duration": duration,
42
+ "resolution": resolution,
43
+ "prompt_optimizer": "true" if prompt_optimizer else "false",
44
+ }
45
+ r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload, headers=HEADERS, timeout=600)
46
+ r.raise_for_status()
47
+ return r.json()
48
+
49
+ @st.cache_data(ttl=300, show_spinner=False)
50
+ def fetch_bytes(url_or_path: str):
51
+ if not url_or_path:
52
+ return None
53
+ try:
54
+ url = f"{API_BASE}/{url_or_path}" if url_or_path.startswith("outputs/") else url_or_path
55
+ headers = HEADERS if _needs_auth(url) else None
56
+ r = requests.get(url, headers=headers, timeout=180)
57
+ r.raise_for_status()
58
+ return r.content
59
+ except Exception:
60
+ return None
61
+
62
+ @st.cache_data(ttl=300, show_spinner=False)
63
+ def image_size_from_bytes(b: bytes):
64
+ try:
65
+ im = Image.open(io.BytesIO(b))
66
+ return im.size
67
+ except Exception:
68
+ return None
69
+
70
+ def infer_ext(data: bytes) -> str:
71
+ try:
72
+ im = Image.open(io.BytesIO(data))
73
+ fmt = (im.format or "JPEG").lower()
74
+ return "." + {"jpeg": "jpg", "jpg": "jpg", "png": "png", "webp": "webp"}.get(fmt, "jpg")
75
+ except Exception:
76
+ return ".jpg"
77
+
78
+ @st.cache_data(ttl=300, show_spinner=False)
79
+ def square_preview_bytes(img_bytes: bytes, size: int = PREVIEW_SIZE, bg_rgb=(255, 255, 255)) -> bytes:
80
+ im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")
81
+ w, h = im.size
82
+ scale = min(size / w, size / h)
83
+ new_w, new_h = max(1, int(w * scale)), max(1, int(h * scale))
84
+ im_resized = im.resize((new_w, new_h), Image.LANCZOS)
85
+ canvas = Image.new("RGBA", (size, size), (*bg_rgb, 255))
86
+ off = ((size - new_w) // 2, (size - new_h) // 2)
87
+ canvas.paste(im_resized, off, im_resized)
88
+ buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()
89
+
90
+ def clamp(v: float, lo: float, hi: float) -> float:
91
+ try:
92
+ v = float(v)
93
+ except Exception:
94
+ v = (lo + hi) / 2.0
95
+ return max(lo, min(hi, v))
96
+
97
+ def first_present(d: dict, keys: list, default=None):
98
+ for k in keys:
99
+ v = d.get(k)
100
+ if v:
101
+ return v
102
+ return default
103
+
104
+ def show_tile(col, title, url: str | None, bg_rgb=(255,255,255), filename_stub="image", key_prefix=""):
105
+ col.subheader(title)
106
+ if not url:
107
+ placeholder = Image.new("RGBA", (PREVIEW_SIZE, PREVIEW_SIZE), (0, 0, 0, 0))
108
+ col.image(placeholder, use_container_width=True)
109
+ col.caption("—")
110
+ return None, None
111
+
112
+ b = fetch_bytes(url)
113
+ if not b:
114
+ col.warning("Görsel yüklenemedi")
115
+ return None, None
116
+
117
+ pv = square_preview_bytes(b, size=PREVIEW_SIZE, bg_rgb=bg_rgb)
118
+ col.image(pv, use_container_width=True)
119
+ sz = image_size_from_bytes(b)
120
+ if sz:
121
+ col.caption(f"Gerçek çözünürlük: {sz[0]}×{sz[1]} px")
122
+
123
+ ext = infer_ext(b)
124
+ mime = "image/jpeg" if ext.lower() in [".jpg", ".jpeg"] else ("image/png" if ext.lower()==".png" else "image/webp")
125
+ col.download_button(
126
+ "İndir (tam kalite)",
127
+ data=b,
128
+ file_name=f"{filename_stub}{ext}",
129
+ mime=mime,
130
+ key=f"{key_prefix}_{filename_stub}_dl"
131
+ )
132
+ return url, b
133
+
134
+ def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
135
+ buf = io.BytesIO()
136
+ with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:
137
+ for fname, b in named_bytes:
138
+ if b:
139
+ z.writestr(fname, b)
140
+ buf.seek(0)
141
+ return buf.read()
142
+
143
+ # ================= Sayfa =================
144
+ st.set_page_config(page_title="AI LightBox · Beauty", layout="wide", page_icon="💄")
145
+ st.title("💄 AI LightBox · Beauty")
146
+ st.caption("Packshot → Padding (kare) → On-Face yerleştirme · Super Resolution · (opsiyonel) Video")
147
+
148
+ # ================= Üst Kontrol Şeridi =================
149
+ ok = backend_ok()
150
+ col1, col2, col3, col4, col5, col6, col7 = st.columns([1.7, 1.6, 1.2, 1.3, 0.9, 1.1, 0.9])
151
+
152
+ with col1:
153
+ file_list = st.file_uploader("Product image (upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
154
+ with col2:
155
+ image_url = st.text_input("or Product URL", key="image_url")
156
+ with col3:
157
+ category = st.selectbox(
158
+ "Category",
159
+ [
160
+ "auto","lipstick","lipgloss","lipliner",
161
+ "eyeshadow","eyeliner","mascara",
162
+ "foundation","concealer","blush","bronzer","contour","highlighter",
163
+ "nail_polish","skincare","perfume","tools","set"
164
+ ],
165
+ index=0, key="category"
166
+ )
167
+ with col4:
168
+ padding_ratio_val = st.number_input(
169
+ "Content scale (0.30–0.95)",
170
+ min_value=0.30, max_value=0.95, value=0.50, step=0.01,
171
+ help="Kare tuvalde ürünün uzun kenarı = tuval × bu oran.",
172
+ key="padding_ratio"
173
+ )
174
+ with col5:
175
+ upscale = st.checkbox("Super Resolution", True, key="upscale")
176
+ with col6:
177
+ upscale_stage = st.selectbox("SR stage", ["both","final","packshot"], index=0, help="Default: both", key="upscale_stage")
178
+ with col7:
179
+ upscale_factor = st.selectbox("SR factor", ["2","4"], index=0, key="upscale_factor")
180
+
181
+ colA, colB, colC, colD, colE = st.columns([1.0, 1.1, 1.2, 0.9, 1.0])
182
+ with colA:
183
+ st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")
184
+ with colB:
185
+ identity_lock = st.checkbox("Identity lock", True, key="identity_lock")
186
+ with colC:
187
+ to_video = st.checkbox("Enable video (later)", False, key="to_video")
188
+ with colD:
189
+ duration = st.selectbox("Video sec", ["6","10"], index=0, key="duration")
190
+ st.session_state["duration"] = duration
191
+ with colE:
192
+ run = st.button("Run", type="primary", use_container_width=True, key="run_btn")
193
+
194
+ # == Tuval seçenekleri ==
195
+ colT1, colT2 = st.columns([1.3, 1.0])
196
+ with colT1:
197
+ canvas_policy = st.selectbox("Canvas", ["fixed_1200","match_long_edge","keep_input"], index=0, key="canvas_policy")
198
+ with colT2:
199
+ canvas_size = st.number_input("Canvas size (px)", min_value=256, max_value=8192, value=1200, step=64, key="canvas_size")
200
+
201
+ face_url = st.text_input("Face image URL (optional)", key="face_url")
202
+
203
+ # ================= Girdi Önizleme =================
204
+ c_in, c_pack, c_pad, c_face = st.columns(4)
205
+ white_bg = (255, 255, 255)
206
+
207
+ input_preview_bytes = None
208
+ if file_list:
209
+ try:
210
+ input_preview_bytes = file_list[0].getvalue()
211
+ except Exception:
212
+ input_preview_bytes = None
213
+ elif image_url:
214
+ input_preview_bytes = fetch_bytes(image_url)
215
+
216
+ if input_preview_bytes:
217
+ c_in.subheader("Input")
218
+ c_in.image(square_preview_bytes(input_preview_bytes, size=PREVIEW_SIZE, bg_rgb=white_bg), use_container_width=True)
219
+ try:
220
+ sz = Image.open(io.BytesIO(input_preview_bytes)).size
221
+ c_in.caption(f"Input resolution: {sz[0]}×{sz[1]} px")
222
+ except Exception:
223
+ c_in.caption("—")
224
+ else:
225
+ show_tile(c_in, "Input", None)
226
+
227
+ # ================ Koş & Sonuçları Kaydet ================
228
+ if run:
229
+ if not ok:
230
+ st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
231
+ elif not (file_list or image_url):
232
+ st.error("En az bir product görseli girin (upload veya URL).")
233
+ else:
234
+ try:
235
+ padding_ratio = clamp(padding_ratio_val, 0.30, 0.95)
236
+ data = {
237
+ "category": category,
238
+ "edit_prompt": "",
239
+ "num_images": "1",
240
+ "image_urls": (image_url or ""),
241
+ "mannequin_image_url": (face_url or ""), # face ref
242
+ "padding_ratio": str(padding_ratio),
243
+ "identity_lock": "true" if identity_lock else "false",
244
+ # SR
245
+ "upscale": "true" if upscale else "false",
246
+ "upscale_factor": upscale_factor,
247
+ "upscale_stage": upscale_stage,
248
+ # Canvas
249
+ "canvas_policy": canvas_policy,
250
+ "canvas_size": str(int(canvas_size or 1200)),
251
+ }
252
+ files_payload = []
253
+ if file_list:
254
+ for f in file_list:
255
+ files_payload.append(("files", (f.name, f.getvalue(), f.type or "image/jpeg")))
256
+
257
+ with st.spinner("Running chain…"):
258
+ out = post_chain(data, files_payload)
259
+
260
+ # ---- Çıktıları topla ----
261
+ packshot_url = None
262
+ try:
263
+ imgs = out["packshot_step"]["result"]["images"]
264
+ if imgs:
265
+ packshot_url = imgs[0].get("url")
266
+ except Exception:
267
+ pass
268
+
269
+ padded_url = None
270
+ try:
271
+ padded_url = out.get("padding_step", {}).get("saved_file")
272
+ if not padded_url:
273
+ pads = out.get("packshot_step", {}).get("padded_urls") or out.get("padded_urls") or []
274
+ if pads:
275
+ padded_url = pads[0]
276
+ except Exception:
277
+ pass
278
+
279
+ packshot_sr_list = first_present(out.get("packshot_step", {}), [
280
+ "upscaled_packshot_urls", "upscaled_packshot_files"
281
+ ], default=[]) or []
282
+ packshot_sr_url = packshot_sr_list[0] if packshot_sr_list else None
283
+
284
+ placed_url = None
285
+ try:
286
+ imgs = out["image_step"]["result"]["images"]
287
+ if imgs:
288
+ placed_url = imgs[0].get("url")
289
+ except Exception:
290
+ pass
291
+
292
+ final_sr_list = first_present(out.get("image_step", {}), [
293
+ "upscaled_final_urls", "upscaled_final_files"
294
+ ], default=[]) or []
295
+ placed_sr_url = final_sr_list[0] if final_sr_list else None
296
+
297
+ st.session_state["job_counter"] += 1
298
+ st.session_state["results"] = {
299
+ "job_id": out.get("job_id"),
300
+ "schema_version": out.get("schema_version"),
301
+ "final_canvas": (
302
+ out.get("packshot_step", {}).get("final_canvas")
303
+ or out.get("padding_step", {}).get("final_canvas")
304
+ ),
305
+ "packshot_url": packshot_url,
306
+ "packshot_sr_url": packshot_sr_url,
307
+ "padded_url": padded_url,
308
+ "placed_url": placed_url,
309
+ "placed_sr_url": placed_sr_url,
310
+ "video_url": None,
311
+ "api_base": API_BASE,
312
+ "has_auth_header": bool(HEADERS),
313
+ }
314
+
315
+ except requests.HTTPError as e:
316
+ st.error(f"HTTP {e.response.status_code}")
317
+ except Exception as e:
318
+ st.error(f"Hata: {e}")
319
+
320
+ # ================ Sonuçları Göster (kalıcı) ================
321
+ res = st.session_state.get("results")
322
+ vbytes = None
323
+ if res:
324
+ key_prefix = f"job{st.session_state['job_counter']}"
325
+
326
+ p_title = "Packshot (SR)" if res.get("packshot_sr_url") else "Packshot"
327
+ p_url, p_bytes = show_tile(
328
+ c_pack, p_title, res.get("packshot_sr_url") or res.get("packshot_url"),
329
+ bg_rgb=white_bg, filename_stub="packshot", key_prefix=key_prefix
330
+ )
331
+
332
+ pad_url, pad_bytes = show_tile(
333
+ c_pad, "Padded", res.get("padded_url"),
334
+ bg_rgb=white_bg, filename_stub="padded", key_prefix=key_prefix
335
+ )
336
+
337
+ shown_final_url = res.get("placed_sr_url") or res.get("placed_url")
338
+ m_title = "On-Face (SR)" if res.get("placed_sr_url") else "On-Face"
339
+ m_url, m_bytes = show_tile(
340
+ c_face, m_title, shown_final_url,
341
+ bg_rgb=white_bg, filename_stub="onface", key_prefix=key_prefix
342
+ )
343
+
344
+ # ---- Video (final görselden) ----
345
+ if st.checkbox("Generate video from this result", value=False, key="video_toggle"):
346
+ src_for_video = res.get("placed_sr_url") or res.get("placed_url")
347
+ if not src_for_video:
348
+ st.warning("Video için final görsel yok.")
349
+ else:
350
+ with st.spinner("Generating video…"):
351
+ v = post_video(src_for_video, duration=st.session_state.get("duration", "6"), resolution="768P", prompt_optimizer=False)
352
+ vurl = (v.get("result") or {}).get("video", {}).get("url")
353
+ if vurl:
354
+ st.session_state["results"]["video_url"] = vurl
355
+ else:
356
+ st.info("Video URL gelmedi.")
357
+
358
+ vurl = st.session_state["results"].get("video_url")
359
+ if vurl:
360
+ st.subheader("Video")
361
+ st.video(vurl, format="video/mp4")
362
+ vb = fetch_bytes(vurl)
363
+ if vb:
364
+ vbytes = vb
365
+ st.download_button(
366
+ "Download video (MP4)",
367
+ data=vb,
368
+ file_name="ai_lightbox_video.mp4",
369
+ mime="video/mp4",
370
+ key=f"{key_prefix}_video_dl"
371
+ )
372
+ else:
373
+ st.info("Video URL var ama içerik indirilemedi.")
374
+
375
+ # ---- ZIP indir ----
376
+ named = []
377
+ if p_bytes: named.append((f"packshot{infer_ext(p_bytes)}", p_bytes))
378
+ if pad_bytes: named.append((f"padded{infer_ext(pad_bytes)}", pad_bytes))
379
+ if m_bytes: named.append((f"onface{infer_ext(m_bytes)}", m_bytes))
380
+ if vbytes: named.append(("video.mp4", vbytes))
381
+ if named:
382
+ zip_bytes = make_zip(named)
383
+ st.download_button(
384
+ "Download all (ZIP)",
385
+ data=zip_bytes,
386
+ file_name="ai_lightbox_beauty_outputs.zip",
387
+ mime="application/zip",
388
+ key=f"{key_prefix}_zip_dl"
389
+ )
390
+
391
+ # Debug
392
+ with st.expander("Debug"):
393
+ st.json({
394
+ "job_id": res.get("job_id"),
395
+ "schema_version": res.get("schema_version"),
396
+ "final_canvas": res.get("final_canvas"),
397
+ "api_base": res.get("api_base"),
398
+ "has_auth_header": res.get("has_auth_header"),
399
+ "packshot_url": res.get("packshot_url"),
400
+ "packshot_sr_url": res.get("packshot_sr_url"),
401
+ "padded_url": res.get("padded_url"),
402
+ "placed_url": res.get("placed_url"),
403
+ "placed_sr_url": res.get("placed_sr_url"),
404
+ "video_url": res.get("video_url"),
405
+ })
406
+
407
+ # ================= Sidebar =================
408
+ with st.sidebar:
409
+ if st.button("Clear cache"):
410
+ st.cache_data.clear()
411
+ st.success("Cache cleared.")
412
+ if st.button("Clear results"):
413
+ st.session_state["results"] = None
414
+ st.success("Results cleared.")
415
+ st.caption(f"API_BASE: {API_BASE}")
416
+ if HF_TOKEN:
417
+ st.caption("Auth: Bearer (active)")