Team Ai
Apppublic

renderfy/BeautyBoxAI

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
streamlit_app.py567 linesDownload Raw Back to root
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