renderfy/BeautyBoxAI
0
1# streamlit_app.py — AI LightBox · Beauty (v0.3 UI)2# Packshot → Padding (square/canvas) → Try-on (lips/eyes/face/nails/hair/brow/body/lifestyle) · SR · (optional) Video3 4import os, io, zipfile, requests, streamlit as st5from PIL import Image6from pathlib import Path7 8# ================= Env & API (robust) =================9def _env(k):10 return (os.getenv(k) or "").strip().strip("'\"")11 12# Desteklenen env anahtarları (öncelik sırası)13API_BASE = (14 _env("AI_BEAUTYBOX_API") or15 _env("AI_LIGHTBOX_API") or16 _env("BeautyBoxAI_API") or17 _env("AI_BEAUTY_API")18).rstrip("/")19 20HF_TOKEN = (21 _env("AI_BEAUTYBOX_TOKEN") or22 _env("AI_LIGHTBOX_TOKEN") or23 _env("BeautyBoxAI_TOKEN") or24 _env("AI_BEAUTY_TOKEN")25)26 27if not API_BASE:28 st.error("API_BASE bulunamadı. Settings > Variables: AI_BEAUTYBOX_API veya AI_LIGHTBOX_API tanımlayın.")29 st.stop()30 31HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}32PREVIEW_SIZE = int(os.getenv("PREVIEW_SIZE", "512"))33 34# ================= Session State (ilk yükte) =================35defaults = {36 "results": None,37 "job_counter": 0,38 "duration": "6",39}40for k, v in defaults.items():41 if k not in st.session_state:42 st.session_state[k] = v43 44# ================= Kategori/Region eşleme =================45LIPS = {"lipstick","lipgloss","lipliner"}46EYES = {"eyeshadow","eyeliner","mascara","brow","false_lashes"}47FACE = {"foundation","concealer","blush","bronzer","contour","highlighter","body_makeup"}48HANDS = {"nail_polish"}49LIFESTYLE = {"skincare","perfume","tools","set","haircare","shampoo","conditioner","body_lotion","body_care"}50HAIR = {"hair_color"}51 52REGIONS = ["auto","lips","eyes","face","nails","hand","hair","brow","body","lifestyle"]53 54def tryon_label(category: str, region_override: str, sr: bool) -> str:55 # Region override önce gelir56 r = (region_override or "auto").lower()57 if r != "auto":58 base = {59 "lips":"On-Lips","eyes":"On-Eyes","face":"On-Face","nails":"On-Nails",60 "hand":"On-Hand","hair":"On-Hair","brow":"On-Brows","body":"On-Body",61 "lifestyle":"Lifestyle"62 }.get(r, "Try-on")63 return f"{base} (SR)" if sr else base64 65 c = (category or "auto").lower()66 if c in LIPS: base = "On-Lips"67 elif c in EYES: base = "On-Eyes"68 elif c in HANDS: base = "On-Nails"69 elif c in HAIR: base = "On-Hair"70 elif c in FACE: base = "On-Face"71 elif c in LIFESTYLE: base = "Lifestyle"72 else: base = "Try-on"73 return f"{base} (SR)" if sr else base74 75# ================= Yardımcılar =================76def _needs_auth(url: str) -> bool:77 return url.startswith(API_BASE) or url.startswith("outputs/")78 79@st.cache_data(ttl=300, show_spinner=False)80def fetch_bytes(url_or_path: str):81 """URL, outputs/ yolu ya da mevcut klasördeki yerel dosyayı okuyabilir."""82 if not url_or_path:83 return None84 try:85 # 1) Mevcut klasörde yerel dosya?86 p = Path(url_or_path)87 if p.is_file():88 return p.read_bytes()89 # 2) outputs/ için API_BASE ön ekini uygula90 url = f"{API_BASE}/{url_or_path}" if url_or_path.startswith("outputs/") else url_or_path91 headers = (HEADERS if (_needs_auth(url) and HF_TOKEN) else None)92 r = requests.get(url, headers=headers, timeout=180)93 r.raise_for_status()94 return r.content95 except Exception:96 return None97 98def backend_ok() -> bool:99 try:100 r = requests.get(f"{API_BASE}/health", headers=HEADERS if HF_TOKEN else None, timeout=10)101 r.raise_for_status()102 return True103 except Exception:104 return False105 106def post_chain(data: dict, files_payload):107 data = {**data, "to_video": "false"}108 r = requests.post(f"{API_BASE}/v1/tryon/chain", data=data, files=files_payload or None,109 headers=HEADERS if HF_TOKEN else None, timeout=600)110 r.raise_for_status()111 return r.json()112 113def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=False):114 payload = {115 "image_url": image_url,116 "duration": duration,117 "resolution": resolution,118 "prompt_optimizer": "true" if prompt_optimizer else "false",119 }120 r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload,121 headers=HEADERS if HF_TOKEN else None, timeout=600)122 r.raise_for_status()123 return r.json()124 125@st.cache_data(ttl=300, show_spinner=False)126def image_size_from_bytes(b: bytes):127 try:128 im = Image.open(io.BytesIO(b))129 return im.size130 except Exception:131 return None132 133def infer_ext(data: bytes) -> str:134 try:135 im = Image.open(io.BytesIO(data))136 fmt = (im.format or "JPEG").lower()137 return "." + {"jpeg": "jpg", "jpg": "jpg", "png": "png", "webp": "webp"}.get(fmt, "jpg")138 except Exception:139 return ".jpg"140 141@st.cache_data(ttl=300, show_spinner=False)142def square_preview_bytes(img_bytes: bytes, size: int = PREVIEW_SIZE, bg_rgb=(255, 255, 255)) -> bytes:143 im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")144 w, h = im.size145 scale = min(size / w, size / h)146 new_w, new_h = max(1, int(w * scale)), max(1, int(h * scale))147 im_resized = im.resize((new_w, new_h), Image.LANCZOS)148 canvas = Image.new("RGBA", (size, size), (*bg_rgb, 255))149 off = ((size - new_w) // 2, (size - new_h) // 2)150 canvas.paste(im_resized, off, im_resized)151 buf = io.BytesIO(); canvas.save(buf, format="PNG"); return buf.getvalue()152 153def clamp(v: float, lo: float, hi: float) -> float:154 try:155 v = float(v)156 except Exception:157 v = (lo + hi) / 2.0158 return max(lo, min(hi, v))159 160def first_present(d: dict, keys: list, default=None):161 for k in keys:162 v = d.get(k)163 if v:164 return v165 return default166 167def show_tile(col, title, url: str | None, bg_rgb=(255,255,255), filename_stub="image", key_prefix=""):168 col.subheader(title)169 if not url:170 placeholder = Image.new("RGBA", (PREVIEW_SIZE, PREVIEW_SIZE), (0, 0, 0, 0))171 col.image(placeholder, use_container_width=True)172 col.caption("—")173 return None, None174 175 b = fetch_bytes(url)176 if not b:177 col.warning("Görsel yüklenemedi")178 return None, None179 180 pv = square_preview_bytes(b, size=PREVIEW_SIZE, bg_rgb=bg_rgb)181 col.image(pv, use_container_width=True)182 sz = image_size_from_bytes(b)183 if sz:184 col.caption(f"Gerçek çözünürlük: {sz[0]}×{sz[1]} px")185 186 ext = infer_ext(b)187 mime = "image/jpeg" if ext.lower() in [".jpg", ".jpeg"] else ("image/png" if ext.lower()==".png" else "image/webp")188 col.download_button(189 "İndir (tam kalite)",190 data=b,191 file_name=f"{filename_stub}{ext}",192 mime=mime,193 key=f"{key_prefix}_{filename_stub}_dl"194 )195 return url, b196 197def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:198 buf = io.BytesIO()199 with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as z:200 for fname, b in named_bytes:201 if b:202 z.writestr(fname, b)203 buf.seek(0)204 return buf.read()205 206# ================= Sayfa =================207st.set_page_config(page_title="AI LightBox · Beauty", layout="wide", page_icon="💄")208st.title("💄 AI LightBox · Beauty")209st.caption("Packshot → Padding (canvas) → Try-on (lips/eyes/face/nails/hair/brow/body/lifestyle) · Super Resolution · (optional) Video")210 211# ================= Üst Kontrol Şeridi =================212ok = backend_ok()213col1, col2, col3, col4, col5, col6, col7 = st.columns([1.8, 1.6, 1.4, 1.3, 0.9, 1.1, 0.9])214 215with col1:216 file_list = st.file_uploader("Product image (upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")217with col2:218 image_url = st.text_input("or Product URL", key="image_url")219with col3:220 category = st.selectbox(221 "Category",222 [223 # Lips224 "auto","lipstick","lipgloss","lipliner",225 # Eyes226 "eyeshadow","eyeliner","mascara","brow","false_lashes",227 # Face/complexion228 "foundation","concealer","blush","bronzer","contour","highlighter","body_makeup",229 # Nails230 "nail_polish",231 # Lifestyle232 "skincare","perfume","tools","set",233 # Hair/Body care234 "hair_color","haircare","shampoo","conditioner","body_lotion","body_care",235 ],236 index=0, key="category"237 )238with col4:239 padding_ratio_val = st.number_input(240 "Content scale (0.30–0.95)",241 min_value=0.30, max_value=0.95, value=0.50, step=0.01,242 help="Kare tuvalde ürünün uzun kenarı = tuval × bu oran.",243 key="padding_ratio"244 )245with col5:246 upscale = st.checkbox("Super Resolution", False, key="upscale")247with col6:248 upscale_stage = st.selectbox("SR stage", ["both","final","packshot"], index=0, help="Default: both", key="upscale_stage")249with col7:250 upscale_factor = st.selectbox("SR factor", ["2","4"], index=0, key="upscale_factor")251 252# == Tuval & Try-on kontrolleri ==253t1, t2, t3, t4 = st.columns([1.15, 1.15, 1.2, 1.5])254with t1:255 canvas_policy = st.selectbox("Canvas", ["fixed_1200","match_long_edge","keep_input"], index=0, key="canvas_policy")256with t2:257 canvas_size = st.number_input("Canvas size (px)", min_value=256, max_value=8192, value=1200, step=64, key="canvas_size")258with t3:259 apply_region = st.selectbox("Apply region", REGIONS, index=0, key="apply_region")260with t4:261 shade_hex = st.text_input("Shade HEX (optional, e.g. #C6A27A)", value="", key="shade_hex")262 263b1, b2, b3 = st.columns([1.6, 1.6, 1.0])264with b1:265 shade_swatch_url = st.text_input("Shade swatch URL (optional)", value="", key="shade_swatch_url")266with b2:267 model_ref_url = st.text_input("Model image URL (face/hand/eyes/hair/body — optional)", key="face_url")268with b3:269 identity_lock = st.checkbox("Identity lock", True, key="identity_lock")270 271# ============ DEMO PREVIEW (mevcut klasörden input.jpg & tryon.jpg) ============272st.markdown("---")273st.subheader("Demo preview (çalıştırmadan önce örnek)")274 275d_in, d_pack, d_pad, d_try = st.columns(4)276 277# Input preview278in_bytes = fetch_bytes("input.jpg")279if in_bytes:280 d_in.subheader("Input")281 d_in.image(square_preview_bytes(in_bytes, size=PREVIEW_SIZE, bg_rgb=(255,255,255)), use_container_width=True)282 sz = image_size_from_bytes(in_bytes)283 if sz:284 d_in.caption(f"Input resolution: {sz[0]}×{sz[1]} px")285 d_in.download_button(286 "İndir (tam kalite)", data=in_bytes,287 file_name=f"input{infer_ext(in_bytes)}",288 mime="image/jpeg", key="demo_input_dl"289 )290else:291 show_tile(d_in, "Input", None)292 293# Packshot & Padded columnları bu demoda boş294show_tile(d_pack, "Packshot", None)295show_tile(d_pad, "Padded", None)296 297# On-Hair / final try-on preview298try_bytes = fetch_bytes("tryon.jpg")299if try_bytes:300 d_try.subheader("On-Hair")301 d_try.image(square_preview_bytes(try_bytes, size=PREVIEW_SIZE, bg_rgb=(255,255,255)), use_container_width=True)302 sz2 = image_size_from_bytes(try_bytes)303 if sz2:304 d_try.caption(f"Gerçek çözünürlük: {sz2[0]}×{sz2[1]} px")305 d_try.download_button(306 "İndir (tam kalite)", data=try_bytes,307 file_name=f"tryon{infer_ext(try_bytes)}",308 mime="image/jpeg", key="demo_tryon_dl"309 )310else:311 show_tile(d_try, "On-Hair", None)312 313# ================= Alt kontrol şeridi =================314cA, cB = st.columns([1.0, 1.0])315with cA:316 st.info(f"Backend: {'Online' if ok else 'Offline'}", icon="🔌")317with cB:318 run = st.button("Run", type="primary", use_container_width=True, key="run_btn")319 320# ================= Girdi Önizleme =================321c_in, c_pack, c_pad, c_try = st.columns(4)322white_bg = (255, 255, 255)323 324input_preview_bytes = None325if file_list:326 try:327 input_preview_bytes = file_list[0].getvalue()328 except Exception:329 input_preview_bytes = None330elif image_url:331 input_preview_bytes = fetch_bytes(image_url)332 333if input_preview_bytes:334 c_in.subheader("Input")335 c_in.image(square_preview_bytes(input_preview_bytes, size=PREVIEW_SIZE, bg_rgb=white_bg), use_container_width=True)336 try:337 sz = Image.open(io.BytesIO(input_preview_bytes)).size338 c_in.caption(f"Input resolution: {sz[0]}×{sz[1]} px")339 except Exception:340 c_in.caption("—")341else:342 show_tile(c_in, "Input", None)343 344# ================ Koş & Sonuçları Kaydet ================345if run:346 if not ok:347 st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")348 elif not (file_list or image_url):349 st.error("En az bir product görseli girin (upload veya URL).")350 else:351 try:352 padding_ratio = clamp(padding_ratio_val, 0.30, 0.95)353 data = {354 "category": category,355 "edit_prompt": "",356 "num_images": "1",357 "image_urls": (image_url or ""),358 "mannequin_image_url": (model_ref_url or ""),359 "padding_ratio": str(padding_ratio),360 "identity_lock": "true" if identity_lock else "false",361 # Canvas362 "canvas_policy": canvas_policy,363 "canvas_size": str(int(canvas_size or 1200)),364 # Shade / Region365 "shade_hex": (shade_hex or "").strip() or None,366 "shade_swatch_url": (shade_swatch_url or "").strip() or None,367 "apply_region": apply_region,368 # SR369 "upscale": "true" if upscale else "false",370 "upscale_factor": st.session_state["upscale_factor"],371 "upscale_stage": st.session_state["upscale_stage"],372 }373 files_payload = []374 if file_list:375 for f in file_list:376 files_payload.append(("files", (f.name, f.getvalue(), f.type or "image/jpeg")))377 378 with st.spinner("Running chain…"):379 out = post_chain(data, files_payload)380 381 # ---- Çıktıları topla ----382 packshot_url = None383 try:384 imgs = out["packshot_step"]["result"]["images"]385 if imgs:386 packshot_url = imgs[0].get("url")387 except Exception:388 pass389 390 padded_url = None391 try:392 padded_url = out.get("padding_step", {}).get("saved_file")393 if not padded_url:394 pads = out.get("packshot_step", {}).get("padded_urls") or out.get("padded_urls") or []395 if pads:396 padded_url = pads[0]397 except Exception:398 pass399 400 packshot_sr_list = first_present(out.get("packshot_step", {}), [401 "upscaled_packshot_urls", "upscaled_packshot_files"402 ], default=[]) or []403 packshot_sr_url = packshot_sr_list[0] if packshot_sr_list else None404 405 placed_url = None406 try:407 imgs = out["image_step"]["result"]["images"]408 if imgs:409 placed_url = imgs[0].get("url")410 except Exception:411 pass412 413 final_sr_list = first_present(out.get("image_step", {}), [414 "upscaled_final_urls", "upscaled_final_files"415 ], default=[]) or []416 placed_sr_url = final_sr_list[0] if final_sr_list else None417 418 st.session_state["job_counter"] += 1419 st.session_state["results"] = {420 "job_id": out.get("job_id"),421 "schema_version": out.get("schema_version"),422 "final_canvas": (423 out.get("packshot_step", {}).get("final_canvas")424 or out.get("padding_step", {}).get("final_canvas")425 ),426 "packshot_url": packshot_url,427 "packshot_sr_url": packshot_sr_url,428 "padded_url": padded_url,429 "placed_url": placed_url,430 "placed_sr_url": placed_sr_url,431 "video_url": None,432 "api_base": API_BASE,433 "has_auth_header": bool(HEADERS),434 # UI state snapshot (etiket/isimlendirme için)435 "category": category,436 "apply_region": apply_region,437 }438 439 except requests.HTTPError as e:440 st.error(f"HTTP {e.response.status_code}")441 except Exception as e:442 st.error(f"Hata: {e}")443 444# ================ Sonuçları Göster (kalıcı) ================445res = st.session_state.get("results")446vbytes = None447if res:448 key_prefix = f"job{st.session_state['job_counter']}"449 450 p_title = "Packshot (SR)" if res.get("packshot_sr_url") else "Packshot"451 p_url, p_bytes = show_tile(452 c_pack, p_title, res.get("packshot_sr_url") or res.get("packshot_url"),453 bg_rgb=white_bg, filename_stub="packshot", key_prefix=key_prefix454 )455 456 pad_url, pad_bytes = show_tile(457 c_pad, "Padded", res.get("padded_url"),458 bg_rgb=white_bg, filename_stub="padded", key_prefix=key_prefix459 )460 461 shown_final_url = res.get("placed_sr_url") or res.get("placed_url")462 m_title = tryon_label(res.get("category","auto"), res.get("apply_region","auto"), bool(res.get("placed_sr_url")))463 m_url, m_bytes = show_tile(464 c_try, m_title, shown_final_url,465 bg_rgb=white_bg, filename_stub="tryon", key_prefix=key_prefix466 )467 468 # ---- Video (final görselden) ----469 st.markdown("---")470 colv1, colv2, colv3 = st.columns([1.1, 1.0, 1.2])471 with colv1:472 gen_video = st.checkbox("Generate video from this result", value=False, key="video_toggle")473 with colv2:474 st.selectbox("Video sec", ["6","10"], index=(0 if st.session_state["duration"]=="6" else 1), key="duration_select")475 with colv3:476 go_video = st.button("Create video", use_container_width=True, key="video_btn")477 478 if gen_video and go_video:479 src_for_video = res.get("placed_sr_url") or res.get("placed_url")480 if not src_for_video:481 st.warning("Video için final görsel yok.")482 else:483 with st.spinner("Generating video…"):484 v = post_video(485 src_for_video,486 duration=st.session_state.get("duration_select","6"),487 resolution="768P",488 prompt_optimizer=False489 )490 vurl = (v.get("result") or {}).get("video", {}).get("url")491 if vurl:492 st.session_state["results"]["video_url"] = vurl493 else:494 st.info("Video URL gelmedi.")495 496 vurl = st.session_state["results"].get("video_url")497 if vurl:498 st.subheader("Video")499 st.video(vurl, format="video/mp4")500 vb = fetch_bytes(vurl)501 if vb:502 vbytes = vb503 st.download_button(504 "Download video (MP4)",505 data=vb,506 file_name="ai_lightbox_video.mp4",507 mime="video/mp4",508 key=f"{key_prefix}_video_dl"509 )510 else:511 st.info("Video URL var ama içerik indirilemedi.")512 513 # ---- ZIP indir ----514 named = []515 if p_bytes: named.append((f"packshot{infer_ext(p_bytes)}", p_bytes))516 if pad_bytes: named.append((f"padded{infer_ext(pad_bytes)}", pad_bytes))517 if m_bytes: named.append((f"tryon{infer_ext(m_bytes)}", m_bytes))518 if vbytes: named.append(("video.mp4", vbytes))519 if named:520 zip_bytes = make_zip(named)521 st.download_button(522 "Download all (ZIP)",523 data=zip_bytes,524 file_name="ai_lightbox_beauty_outputs.zip",525 mime="application/zip",526 key=f"{key_prefix}_zip_dl"527 )528 529 # Debug530 with st.expander("Debug"):531 st.json({532 "job_id": res.get("job_id"),533 "schema_version": res.get("schema_version"),534 "final_canvas": res.get("final_canvas"),535 "api_base": res.get("api_base"),536 "has_auth_header": res.get("has_auth_header"),537 "category": res.get("category"),538 "apply_region": res.get("apply_region"),539 "packshot_url": res.get("packshot_url"),540 "packshot_sr_url": res.get("packshot_sr_url"),541 "padded_url": res.get("padded_url"),542 "placed_url": res.get("placed_url"),543 "placed_sr_url": res.get("placed_sr_url"),544 "video_url": res.get("video_url"),545 })546 547# ================= Sidebar (teşhis) =================548with st.sidebar:549 st.caption(f"Resolved API_BASE: {API_BASE}")550 st.caption("Auth header: " + ("ON" if HF_TOKEN else "OFF"))551 if st.button("Ping /health"):552 try:553 r = requests.get(f"{API_BASE}/health",554 headers=HEADERS if HF_TOKEN else None,555 timeout=10)556 st.write(r.status_code)557 try:558 st.json(r.json())559 except Exception:560 st.code((r.text or "")[:1000])561 except Exception as e:562 st.error(f"Health error: {e}")563 if st.button("Clear cache"):564 st.cache_data.clear(); st.success("Cache cleared.")565 if st.button("Clear results"):566 st.session_state["results"] = None; st.success("Results cleared.")567 