Spaces:
Running
Running
Upload streamlit_app.py
Browse files- streamlit_app.py +200 -173
streamlit_app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
# streamlit_app.py — AI LightBox · Jewelry (Frontend v0.6.0
|
| 2 |
-
# Not: "Previews" kartlarının
|
| 3 |
import os, io, base64, zipfile, requests, streamlit as st
|
| 4 |
from PIL import Image
|
| 5 |
|
|
@@ -7,14 +7,14 @@ from PIL import Image
|
|
| 7 |
def apply_theme():
|
| 8 |
THEME = {
|
| 9 |
"base": "dark",
|
| 10 |
-
"primaryColor": "#ff7a1a",
|
| 11 |
-
"backgroundColor": "#0e0f12",
|
| 12 |
-
"secondaryBackgroundColor": "#
|
| 13 |
-
"panelBackgroundColor": "#1a1d23", # velvet panel
|
| 14 |
"textColor": "#e7eaee",
|
| 15 |
-
"mutedColor": "#
|
| 16 |
-
"
|
| 17 |
-
"
|
|
|
|
| 18 |
}
|
| 19 |
try:
|
| 20 |
st._config.set_option("theme.base", THEME["base"])
|
|
@@ -30,58 +30,102 @@ def apply_theme():
|
|
| 30 |
:root {{
|
| 31 |
--bg: {THEME["backgroundColor"]};
|
| 32 |
--sb: {THEME["secondaryBackgroundColor"]};
|
| 33 |
-
--panel: {THEME["panelBackgroundColor"]};
|
| 34 |
--text: {THEME["textColor"]};
|
| 35 |
--muted: {THEME["mutedColor"]};
|
| 36 |
--primary: {THEME["primaryColor"]};
|
| 37 |
-
--outline: {THEME["
|
| 38 |
-
--
|
|
|
|
| 39 |
}}
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}}
|
| 51 |
-
.
|
| 52 |
-
.j-sub {{ color: var(--muted); margin-top: 4px; }}
|
| 53 |
|
| 54 |
-
/*
|
| 55 |
-
.
|
| 56 |
-
background: var(--
|
| 57 |
-
|
| 58 |
}}
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
color: var(--primary); font-weight: 700; font-size: 1.0rem;
|
| 62 |
}}
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
| 67 |
}}
|
| 68 |
|
| 69 |
-
/* ---
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
/* --- Divider --- */
|
| 80 |
-
.j-divider {{ height: 1px; background: linear-gradient(90deg, transparent, rgba(255,122,26,0.7), transparent);
|
| 81 |
-
margin: 12px 0 2px 0; }}
|
| 82 |
-
|
| 83 |
-
/* Keep PREVIEW cards intact; only adjust titles color via class used below */
|
| 84 |
-
.lb-card-title {{ color: var(--primary); font-weight: 800; }}
|
| 85 |
</style>
|
| 86 |
""", unsafe_allow_html=True)
|
| 87 |
|
|
@@ -215,16 +259,19 @@ def inject_preview_css(aspect: str, fit_mode="contain"):
|
|
| 215 |
st.markdown(f"""
|
| 216 |
<style>
|
| 217 |
.lb-card-title {{
|
| 218 |
-
font-weight:
|
| 219 |
}}
|
| 220 |
.lb-frame {{
|
| 221 |
position: relative; width: 100%;
|
| 222 |
aspect-ratio: {aw} / {ah};
|
| 223 |
-
border-radius: 12px; overflow: hidden; background:
|
| 224 |
display:flex; align-items:center; justify-content:center;
|
| 225 |
-
box-shadow: 0 0 0 1px
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
}}
|
| 227 |
-
.lb-frame img {{ width:100%; height:100%; object-fit:{fit_mode}; display:block; }}
|
| 228 |
.lb-video {{ display:flex; justify-content:center; }}
|
| 229 |
.lb-video video {{ border-radius:8px; max-width: 540px; width:100%; height:auto; }}
|
| 230 |
</style>
|
|
@@ -268,122 +315,108 @@ def show_tile_card_display_download(
|
|
| 268 |
|
| 269 |
return display_url or "data", native
|
| 270 |
|
| 271 |
-
# =======================
|
| 272 |
-
def load_local_demo():
|
| 273 |
-
demo = {}
|
| 274 |
-
for fname in ["input.jpg", "packshot.jpg", "model.jpg", "tryon.mp4"]:
|
| 275 |
-
if os.path.exists(fname):
|
| 276 |
-
with open(fname, "rb") as f: demo[fname] = f.read()
|
| 277 |
-
return demo
|
| 278 |
-
|
| 279 |
-
demo_files = load_local_demo()
|
| 280 |
-
|
| 281 |
-
# ======================= PAGE =======================
|
| 282 |
-
st.set_page_config(page_title="AI LightBox · Jewelry", layout="wide", page_icon="💎")
|
| 283 |
-
|
| 284 |
-
# --- HERO ---
|
| 285 |
st.markdown("""
|
| 286 |
-
<div class="
|
| 287 |
-
<
|
| 288 |
-
<
|
| 289 |
-
<div class="j-divider"></div>
|
| 290 |
</div>
|
| 291 |
""", unsafe_allow_html=True)
|
| 292 |
|
|
|
|
|
|
|
| 293 |
ok = backend_ok()
|
| 294 |
-
use_rmbg = (st.session_state["health"].get("use_rmbg") if isinstance(st.session_state["health"], dict) else None) or "
|
| 295 |
version = st.session_state["health"].get("version", "—")
|
| 296 |
|
| 297 |
-
# ---
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
with left:
|
| 307 |
-
# INPUT PANEL
|
| 308 |
-
st.markdown('<div class="j-card"><div class="j-head">📥 Product Input</div>', unsafe_allow_html=True)
|
| 309 |
-
col1, col2 = st.columns([1.4, 1.1])
|
| 310 |
-
with col1:
|
| 311 |
-
file_list = st.file_uploader("Product Image (Upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
|
| 312 |
-
with col2:
|
| 313 |
-
image_url = st.text_input("Or URL", key="image_url")
|
| 314 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
| 315 |
-
|
| 316 |
-
# CANVAS PANEL
|
| 317 |
-
st.markdown('<div class="j-card"><div class="j-head">🖼️ Canvas / Preview</div>', unsafe_allow_html=True)
|
| 318 |
-
colT1, colT2, colT3 = st.columns([1.0, 1.0, 1.0])
|
| 319 |
-
with colT1:
|
| 320 |
-
preview_aspect = st.selectbox("Preview Frame Ratio", ASPECT_OPTIONS, index=0, key="preview_aspect")
|
| 321 |
-
with colT2:
|
| 322 |
-
preview_long_edge = st.number_input("Preview Long Edge (px)", min_value=256, max_value=8192, value=1920, step=64, key="preview_long_edge")
|
| 323 |
-
with colT3:
|
| 324 |
-
fit_mode = st.selectbox("Preview Layout", ["fit","fill"], index=0, help="Sadece önizlemeyi etkiler.", key="fit_mode")
|
| 325 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
| 326 |
-
|
| 327 |
-
# ACTION PANEL
|
| 328 |
-
st.markdown('<div class="j-card"><div class="j-head">🚀 Generate</div>', unsafe_allow_html=True)
|
| 329 |
-
colA, colB, colC = st.columns([1.2, 1.2, 1.0])
|
| 330 |
-
with colA:
|
| 331 |
-
run = st.button("Run 💎", type="primary", use_container_width=True, key="run_btn")
|
| 332 |
-
with colB:
|
| 333 |
-
st.caption("Tip: First run will warm up models.")
|
| 334 |
-
with colC:
|
| 335 |
-
st.caption("All outputs served from /outputs/*")
|
| 336 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
| 337 |
-
|
| 338 |
-
with right:
|
| 339 |
-
# PRODUCT/GENERATION PANEL
|
| 340 |
-
st.markdown('<div class="j-card"><div class="j-head">⚙️ Product · Generation</div>', unsafe_allow_html=True)
|
| 341 |
-
col3, col4 = st.columns([1.0, 1.0])
|
| 342 |
-
with col3:
|
| 343 |
-
jewel_type = st.selectbox("Type", ["auto","earring","ring","necklace","bracelet","pendant","brooch","set","watch"], index=0, key="jewel_type")
|
| 344 |
-
preview_ratio = st.number_input("Preview Scale (0.10–0.99)", min_value=0.10, max_value=0.99, value=0.50, step=0.01,
|
| 345 |
-
help="Sadece önizleme kadrajında büyüklüğü etkiler.", key="preview_ratio")
|
| 346 |
-
identity_lock = st.checkbox("Identity lock", True, key="identity_lock")
|
| 347 |
-
with col4:
|
| 348 |
-
upscale = st.checkbox("Super Res", False, key="upscale")
|
| 349 |
-
upscale_stage = st.selectbox("SR Step", ["both","final","packshot"], index=0, help="Varsayılan: both", key="upscale_stage")
|
| 350 |
-
upscale_factor = st.selectbox("SR Factor", ["2","4"], index=0, key="upscale_factor")
|
| 351 |
-
st.markdown('<div class="j-divider"></div>', unsafe_allow_html=True)
|
| 352 |
-
col5, col6, col7 = st.columns([1.0, 1.0, 1.0])
|
| 353 |
-
with col5:
|
| 354 |
-
to_video = st.checkbox("One Click Generate Video", False, key="to_video")
|
| 355 |
-
with col6:
|
| 356 |
-
duration = st.selectbox("Duration ", VIDEO_DUR_OPTIONS, index=0, key="duration_sel")
|
| 357 |
-
with col7:
|
| 358 |
-
resolution = st.selectbox("Video Res", VIDEO_RES_OPTIONS, index=1, key="resolution_sel")
|
| 359 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
| 360 |
-
|
| 361 |
-
# Inject CSS for PREVIEW CARDS ONLY (structure intact)
|
| 362 |
-
inject_preview_css(preview_aspect, fit_mode="contain" if fit_mode=="fit" else "cover")
|
| 363 |
|
| 364 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
st.subheader("Previews")
|
| 366 |
col_in, col_pack, col_pad, col_vid = st.columns(4)
|
| 367 |
|
| 368 |
-
# Input bytes
|
| 369 |
input_bytes = None
|
| 370 |
if file_list:
|
| 371 |
try: input_bytes = file_list[0].getvalue()
|
| 372 |
except Exception: input_bytes = None
|
| 373 |
elif image_url:
|
| 374 |
input_bytes = fetch_bytes(image_url)
|
| 375 |
-
|
| 376 |
-
|
|
|
|
|
|
|
| 377 |
|
| 378 |
show_tile_card_display_download(
|
| 379 |
col_in, "Sample Input preview",
|
| 380 |
-
display_bytes=input_bytes
|
| 381 |
-
download_bytes=input_bytes
|
| 382 |
filename_stub="input", key_prefix="pre"
|
| 383 |
)
|
| 384 |
|
| 385 |
-
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
show_tile_card_display_download(
|
| 388 |
col_pack, "Sample Packshot (BG Removed)",
|
| 389 |
display_bytes=pack_bytes_demo,
|
|
@@ -391,19 +424,18 @@ show_tile_card_display_download(
|
|
| 391 |
filename_stub="packshot", key_prefix="pre"
|
| 392 |
)
|
| 393 |
|
| 394 |
-
# Padded preview (UI-only)
|
| 395 |
padded_bytes = None
|
| 396 |
if pack_bytes_demo:
|
| 397 |
try:
|
| 398 |
im = Image.open(io.BytesIO(pack_bytes_demo)).convert("RGBA")
|
| 399 |
-
aw, ah = (lambda s: (int(s.split(":")[0]), int(s.split(":")[1])))(preview_aspect)
|
| 400 |
if aw >= ah:
|
| 401 |
-
W = int(preview_long_edge); H = max(1, int(W * ah / aw))
|
| 402 |
else:
|
| 403 |
-
H = int(preview_long_edge); W = max(1, int(H * aw / ah))
|
| 404 |
-
|
| 405 |
w, h = im.size
|
| 406 |
-
target = int(min(W, H) *
|
| 407 |
if w >= h: new_w, new_h = target, max(1, int(h * (target / max(1, w))))
|
| 408 |
else: new_h, new_w = target, max(1, int(w * (target / max(1, h))))
|
| 409 |
im_resized = im.resize((new_w, new_h), Image.LANCZOS)
|
|
@@ -421,8 +453,10 @@ show_tile_card_display_download(
|
|
| 421 |
filename_stub="padded_ui", key_prefix="pre"
|
| 422 |
)
|
| 423 |
|
| 424 |
-
|
| 425 |
-
|
|
|
|
|
|
|
| 426 |
col_vid.markdown('<div class="lb-card-title">AI Generated Video Preview</div>', unsafe_allow_html=True)
|
| 427 |
if vid_demo:
|
| 428 |
data_url = "data:video/mp4;base64," + base64.b64encode(vid_demo).decode("ascii")
|
|
@@ -443,8 +477,8 @@ else:
|
|
| 443 |
# ======================= RUN =======================
|
| 444 |
if run:
|
| 445 |
if not ok:
|
| 446 |
-
st.error("
|
| 447 |
-
elif not (file_list or image_url or
|
| 448 |
st.error("En az bir görsel girişi yapın (dosya yükle veya URL).")
|
| 449 |
else:
|
| 450 |
try:
|
|
@@ -456,21 +490,18 @@ if run:
|
|
| 456 |
|
| 457 |
data = {
|
| 458 |
"category": st.session_state.get("jewel_type","auto"),
|
| 459 |
-
"edit_prompt": "",
|
| 460 |
"num_images": "1",
|
| 461 |
"image_urls": img_urls,
|
| 462 |
"mannequin_image_url": "",
|
| 463 |
"identity_lock": "true" if st.session_state.get("identity_lock", True) else "false",
|
| 464 |
-
# Preview/Pad controls
|
| 465 |
"preview_aspect": st.session_state.get("preview_aspect","9:16"),
|
| 466 |
"preview_long_edge": str(int(st.session_state.get("preview_long_edge", 1920))),
|
| 467 |
"preview_ratio": str(float(st.session_state.get("preview_ratio", 0.5))),
|
| 468 |
-
"padding_ratio": str(float(st.session_state.get("preview_ratio", 0.5))),
|
| 469 |
-
# SR
|
| 470 |
"upscale": "true" if st.session_state.get("upscale", False) else "false",
|
| 471 |
"upscale_factor": st.session_state.get("upscale_factor","2"),
|
| 472 |
"upscale_stage": st.session_state.get("upscale_stage","both"),
|
| 473 |
-
# Video (optional inside chain)
|
| 474 |
"to_video": "true" if st.session_state.get("to_video", False) else "false",
|
| 475 |
"video_prompt": "",
|
| 476 |
"duration": st.session_state.get("duration_sel", VIDEO_DUR_OPTIONS[0]),
|
|
@@ -478,18 +509,16 @@ if run:
|
|
| 478 |
"prompt_optimizer": "false",
|
| 479 |
}
|
| 480 |
|
| 481 |
-
with st.spinner("AI
|
| 482 |
out = post_chain(data, files_payload if files_payload else None)
|
| 483 |
|
| 484 |
st.session_state["job_counter"] += 1
|
| 485 |
|
| 486 |
-
# Parse outputs (v0.6.0)
|
| 487 |
pack_native_url = (out.get("packshot_step", {}).get("result", {}).get("images_native") or [{}])[0].get("url")
|
| 488 |
padded_native_url = (out.get("packshot_step", {}).get("result", {}).get("padded_native") or [{}])[0].get("url")
|
| 489 |
placed_native_url = (out.get("image_step", {}).get("result", {}).get("images_native") or [{}])[0].get("url")
|
| 490 |
vurl = (out.get("video_step") or {}).get("result", {}).get("video", {}).get("url")
|
| 491 |
|
| 492 |
-
# DISPLAY small JPGs (dark-friendly)
|
| 493 |
pack_display_jpeg = None
|
| 494 |
if pack_native_url:
|
| 495 |
pn = fetch_bytes(pack_native_url)
|
|
@@ -527,7 +556,7 @@ if run:
|
|
| 527 |
res = st.session_state.get("results")
|
| 528 |
vbytes = None
|
| 529 |
if res:
|
| 530 |
-
st.markdown('<div class="
|
| 531 |
c_pack, c_pad, c_model, c_vid = st.columns(4)
|
| 532 |
|
| 533 |
show_tile_card_display_download(
|
|
@@ -588,13 +617,12 @@ if res:
|
|
| 588 |
vurl2 = (v.get("result") or {}).get("video", {}).get("url")
|
| 589 |
if vurl2:
|
| 590 |
st.session_state["results"]["video_url"] = vurl2
|
| 591 |
-
st.success("Video
|
| 592 |
else:
|
| 593 |
st.info("Video URL gelmedi.")
|
| 594 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 595 |
|
| 596 |
-
#
|
| 597 |
-
st.markdown('<div class="j-card"><div class="j-head">📦 Export</div>', unsafe_allow_html=True)
|
| 598 |
named = []
|
| 599 |
pn = fetch_bytes(res.get("packshot_native_url")) if res.get("packshot_native_url") else None
|
| 600 |
mn = fetch_bytes(res.get("placed_native_url")) if res.get("placed_native_url") else None
|
|
@@ -610,13 +638,12 @@ if res:
|
|
| 610 |
st.download_button("Download All (ZIP)", data=buf.read(),
|
| 611 |
file_name="ai_lightbox_outputs.zip", mime="application/zip",
|
| 612 |
key=f"job{st.session_state['job_counter']}_zip_dl")
|
| 613 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
| 614 |
|
| 615 |
-
# ======================= Sidebar =======================
|
| 616 |
with st.sidebar:
|
|
|
|
| 617 |
st.caption(f"API_BASE: {API_BASE}")
|
| 618 |
if HF_TOKEN: st.caption("Auth: Bearer (aktif)")
|
| 619 |
-
st.caption(f"Backend RMBG policy: {use_rmbg}")
|
| 620 |
st.slider("Video önizleme genişliği (px)", 360, 1080, st.session_state["video_ui_max_px"],
|
| 621 |
step=10, key="video_ui_max_px")
|
| 622 |
if st.button("Önbelleği temizle"): st.cache_data.clear(); st.success("Önbellek temizlendi.")
|
|
|
|
| 1 |
+
# streamlit_app.py — AI LightBox · Jewelry (Frontend · v0.6.0)
|
| 2 |
+
# Not: "Previews" kartlarının yapısı korunmuştur. Diğer UI bölümleri jewelry uyumlu modern tasarımla güncellendi.
|
| 3 |
import os, io, base64, zipfile, requests, streamlit as st
|
| 4 |
from PIL import Image
|
| 5 |
|
|
|
|
| 7 |
def apply_theme():
|
| 8 |
THEME = {
|
| 9 |
"base": "dark",
|
| 10 |
+
"primaryColor": "#ff7a1a", # jewelry orange
|
| 11 |
+
"backgroundColor": "#0e0f12", # deep onyx
|
| 12 |
+
"secondaryBackgroundColor": "#14161b",# smoky quartz
|
|
|
|
| 13 |
"textColor": "#e7eaee",
|
| 14 |
+
"mutedColor": "#9aa1ab",
|
| 15 |
+
"cardOutline": "rgba(255,255,255,0.06)",
|
| 16 |
+
"accent2": "#ffd6b0", # soft amber
|
| 17 |
+
"accent3": "#2a2f3a", # hematite
|
| 18 |
}
|
| 19 |
try:
|
| 20 |
st._config.set_option("theme.base", THEME["base"])
|
|
|
|
| 30 |
:root {{
|
| 31 |
--bg: {THEME["backgroundColor"]};
|
| 32 |
--sb: {THEME["secondaryBackgroundColor"]};
|
|
|
|
| 33 |
--text: {THEME["textColor"]};
|
| 34 |
--muted: {THEME["mutedColor"]};
|
| 35 |
--primary: {THEME["primaryColor"]};
|
| 36 |
+
--outline: {THEME["cardOutline"]};
|
| 37 |
+
--accent2: {THEME["accent2"]};
|
| 38 |
+
--accent3: {THEME["accent3"]};
|
| 39 |
}}
|
| 40 |
+
|
| 41 |
+
/* Background with subtle vignette */
|
| 42 |
+
.stApp {{
|
| 43 |
+
background:
|
| 44 |
+
radial-gradient(1200px 800px at 0% 0%, rgba(255,122,26,0.06), transparent 50%),
|
| 45 |
+
radial-gradient(1000px 700px at 100% 100%, rgba(255,122,26,0.04), transparent 50%),
|
| 46 |
+
var(--bg) !important;
|
| 47 |
+
}}
|
| 48 |
+
.block-container {{ padding-top: 1.25rem !important; }}
|
| 49 |
+
|
| 50 |
+
/* Headline hero */
|
| 51 |
+
.hero {{
|
| 52 |
+
padding: 16px 18px;
|
| 53 |
+
border-radius: 16px;
|
| 54 |
+
background: linear-gradient(180deg, rgba(255,122,26,0.12) 0%, rgba(255,122,26,0.06) 100%);
|
| 55 |
+
box-shadow: 0 0 0 1px rgba(255,122,26,0.18) inset, 0 6px 30px rgba(0,0,0,0.35);
|
| 56 |
+
}}
|
| 57 |
+
.hero h1 {{
|
| 58 |
+
margin: 0; padding: 0;
|
| 59 |
+
font-size: 1.4rem; line-height: 1.3; letter-spacing: .2px;
|
| 60 |
+
color: var(--accent2);
|
| 61 |
+
}}
|
| 62 |
+
.hero p {{ margin: 4px 0 0 0; color: var(--text); opacity:.9; }}
|
| 63 |
+
|
| 64 |
+
/* Panels (controls) */
|
| 65 |
+
.panel {{
|
| 66 |
+
background: linear-gradient(180deg, rgba(20,22,27,0.9) 0%, rgba(20,22,27,0.75) 100%);
|
| 67 |
+
border-radius: 16px;
|
| 68 |
+
padding: 14px 14px 8px 14px;
|
| 69 |
+
margin-top: 12px;
|
| 70 |
+
box-shadow: 0 0 0 1px var(--outline) inset, 0 10px 30px rgba(0,0,0,0.35);
|
| 71 |
+
}}
|
| 72 |
+
.panel h3 {{
|
| 73 |
+
margin: 0 0 10px 0; font-size: 1.05rem; font-weight: 700; color: var(--primary);
|
| 74 |
+
display: inline-flex; align-items: center; gap: 8px;
|
| 75 |
+
}}
|
| 76 |
+
.chipbar {{ display:flex; flex-wrap: wrap; gap:8px; margin:6px 0 2px 0; }}
|
| 77 |
+
.chip {{
|
| 78 |
+
display:inline-flex; align-items:center; gap:8px; padding:6px 10px; border-radius:999px;
|
| 79 |
+
background: rgba(255,122,26,0.10); color: var(--accent2); border: 1px solid rgba(255,122,26,0.25);
|
| 80 |
+
font-size:.85rem; font-weight:600; letter-spacing:.2px;
|
| 81 |
+
}}
|
| 82 |
+
.chip .dot {{ width:8px; height:8px; border-radius:50%; background:#2ee06b; box-shadow:0 0 0 2px rgba(46,224,107,0.15); }}
|
| 83 |
+
.chip.muted {{ background: rgba(154,161,171,0.10); color: var(--text); border-color: rgba(154,161,171,0.25); }}
|
| 84 |
+
.chip.badge {{ background: rgba(42,47,58,0.9); color: var(--text); border-color: rgba(255,255,255,0.08); }}
|
| 85 |
+
|
| 86 |
+
/* Make labels and captions jewelry-orange */
|
| 87 |
+
label, [data-testid="stWidgetLabel"] p, .lb-card-title, h2, h3, .stCaption, .stMarkdown > p {{
|
| 88 |
+
color: var(--primary) !important;
|
| 89 |
+
}}
|
| 90 |
+
|
| 91 |
+
/* Buttons */
|
| 92 |
+
.stButton > button {{
|
| 93 |
+
background: var(--primary) !important; color: #101215 !important; font-weight: 800;
|
| 94 |
+
border: none; border-radius: 12px; padding: .55rem .9rem;
|
| 95 |
+
box-shadow: 0 0 0 1px #00000033 inset, 0 8px 18px rgba(255,122,26,0.25);
|
| 96 |
+
transition: transform .08s ease;
|
| 97 |
+
}}
|
| 98 |
+
.stButton > button:hover {{ filter: brightness(1.06); transform: translateY(-1px); }}
|
| 99 |
+
.stDownloadButton > button {{
|
| 100 |
+
background: transparent !important; color: var(--primary) !important;
|
| 101 |
+
border: 1px solid var(--primary); border-radius: 12px; font-weight:700;
|
| 102 |
}}
|
| 103 |
+
.stDownloadButton > button:hover {{ filter: brightness(1.1); }}
|
|
|
|
| 104 |
|
| 105 |
+
/* Inputs look crisper on dark */
|
| 106 |
+
.stTextInput, .stNumberInput, .stSelectbox, .stFileUploader, .stCheckbox, .stSlider, .stRadio {{
|
| 107 |
+
background: var(--accent3) !important; border-radius: 12px !important;
|
| 108 |
+
box-shadow: 0 0 0 1px var(--outline) inset !important; padding: 6px 8px !important;
|
| 109 |
}}
|
| 110 |
+
[data-testid="stFileUploaderDropzone"] {{
|
| 111 |
+
background: rgba(42,47,58,0.6); border: 1px dashed rgba(255,255,255,0.15); border-radius: 12px;
|
|
|
|
| 112 |
}}
|
| 113 |
+
|
| 114 |
+
/* Alerts -> on-brand */
|
| 115 |
+
.stAlert, .stAlert p, .stAlert span {{
|
| 116 |
+
color: var(--accent2) !important; background: rgba(255,122,26,0.08) !important;
|
| 117 |
+
border-radius: 12px; box-shadow: 0 0 0 1px rgba(255,122,26,0.25) inset;
|
| 118 |
}}
|
| 119 |
|
| 120 |
+
/* ---- Previews cards (UNCHANGED structure) ---- */
|
| 121 |
+
.lb-card-title {{
|
| 122 |
+
font-weight:700; font-size:1.15rem; margin:0 0 8px 0; color: var(--primary);
|
| 123 |
+
}}
|
| 124 |
+
.lb-frame {{
|
| 125 |
+
border-radius: 12px; overflow: hidden; background: var(--sb);
|
| 126 |
+
display:flex; align-items:center; justify-content:center;
|
| 127 |
+
box-shadow: 0 0 0 1px var(--outline) inset;
|
| 128 |
+
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
</style>
|
| 130 |
""", unsafe_allow_html=True)
|
| 131 |
|
|
|
|
| 259 |
st.markdown(f"""
|
| 260 |
<style>
|
| 261 |
.lb-card-title {{
|
| 262 |
+
font-weight:700; font-size:1.15rem; margin:0 0 8px 0; color: var(--primary);
|
| 263 |
}}
|
| 264 |
.lb-frame {{
|
| 265 |
position: relative; width: 100%;
|
| 266 |
aspect-ratio: {aw} / {ah};
|
| 267 |
+
border-radius: 12px; overflow: hidden; background: var(--sb);
|
| 268 |
display:flex; align-items:center; justify-content:center;
|
| 269 |
+
box-shadow: 0 0 0 1px var(--outline) inset;
|
| 270 |
+
}}
|
| 271 |
+
.lb-frame img {{
|
| 272 |
+
width:100%; height:100%; object-fit:{fit_mode};
|
| 273 |
+
display:block;
|
| 274 |
}}
|
|
|
|
| 275 |
.lb-video {{ display:flex; justify-content:center; }}
|
| 276 |
.lb-video video {{ border-radius:8px; max-width: 540px; width:100%; height:auto; }}
|
| 277 |
</style>
|
|
|
|
| 315 |
|
| 316 |
return display_url or "data", native
|
| 317 |
|
| 318 |
+
# ======================= Hero =======================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
st.markdown("""
|
| 320 |
+
<div class="hero">
|
| 321 |
+
<h1>💎 AI LightBox · Jewelry Studio</h1>
|
| 322 |
+
<p>Product packshot, mannequin placement ve kısa video — tek akışta.</p>
|
|
|
|
| 323 |
</div>
|
| 324 |
""", unsafe_allow_html=True)
|
| 325 |
|
| 326 |
+
# ======================= Page =======================
|
| 327 |
+
st.set_page_config(page_title="AI LightBox · Jewelry", layout="wide", page_icon="💎")
|
| 328 |
ok = backend_ok()
|
| 329 |
+
use_rmbg = (st.session_state["health"].get("use_rmbg") if isinstance(st.session_state["health"], dict) else None) or "—"
|
| 330 |
version = st.session_state["health"].get("version", "—")
|
| 331 |
|
| 332 |
+
# -------- Status chips (compact) --------
|
| 333 |
+
st.markdown(f"""
|
| 334 |
+
<div class="chipbar">
|
| 335 |
+
<div class="chip"><span class="dot"></span>{'Online' if ok else 'Offline'}</div>
|
| 336 |
+
<div class="chip badge">Backend v{version}</div>
|
| 337 |
+
<div class="chip muted">RMBG: {use_rmbg}</div>
|
| 338 |
+
</div>
|
| 339 |
+
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
|
| 341 |
+
# -------- Panels: Inputs · Studio Controls · Video --------
|
| 342 |
+
st.markdown('<div class="panel"> <h3>🧰 Inputs</h3>', unsafe_allow_html=True)
|
| 343 |
+
col1, col2, col3 = st.columns([1.6, 1.6, 1.0])
|
| 344 |
+
with col1:
|
| 345 |
+
file_list = st.file_uploader("Product Image (Upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
|
| 346 |
+
with col2:
|
| 347 |
+
image_url = st.text_input("Or URL", key="image_url")
|
| 348 |
+
with col3:
|
| 349 |
+
jewel_type = st.selectbox("Type", ["auto","earring","ring","necklace","bracelet","pendant","brooch","set","watch"], index=0, key="jewel_type")
|
| 350 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 351 |
+
|
| 352 |
+
st.markdown('<div class="panel"> <h3>🪄 Studio Controls</h3>', unsafe_allow_html=True)
|
| 353 |
+
colS1, colS2, colS3, colS4 = st.columns([1.1, 1.1, 1.1, 1.1])
|
| 354 |
+
with colS1:
|
| 355 |
+
preview_ratio = st.number_input("Preview Scale (0.10–0.99)", min_value=0.10, max_value=0.99, value=0.50, step=0.01,
|
| 356 |
+
help="Sadece önizleme kadrajında büyüklüğü etkiler.", key="preview_ratio")
|
| 357 |
+
with colS2:
|
| 358 |
+
preview_aspect = st.selectbox("Preview Frame Ratio", ASPECT_OPTIONS, index=0, key="preview_aspect")
|
| 359 |
+
with colS3:
|
| 360 |
+
preview_long_edge = st.number_input("Preview Long Edge (px)", min_value=256, max_value=8192, value=1920, step=64, key="preview_long_edge")
|
| 361 |
+
with colS4:
|
| 362 |
+
fit_mode = st.selectbox("Preview Layout", ["fit","fill"], index=0, help="Sadece önizlemeyi etkiler.", key="fit_mode")
|
| 363 |
+
|
| 364 |
+
colS5, colS6, colS7, colS8 = st.columns([1.0, 1.0, 1.0, 1.0])
|
| 365 |
+
with colS5:
|
| 366 |
+
upscale = st.checkbox("Super Res", False, key="upscale")
|
| 367 |
+
with colS6:
|
| 368 |
+
upscale_stage = st.selectbox("SR Step", ["both","final","packshot"], index=0, help="Varsayılan: both", key="upscale_stage")
|
| 369 |
+
with colS7:
|
| 370 |
+
upscale_factor = st.selectbox("SR Factor", ["2","4"], index=0, key="upscale_factor")
|
| 371 |
+
with colS8:
|
| 372 |
+
identity_lock = st.checkbox("Identity lock", True, key="identity_lock")
|
| 373 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 374 |
+
|
| 375 |
+
st.markdown('<div class="panel"> <h3>🎬 Video</h3>', unsafe_allow_html=True)
|
| 376 |
+
colV1, colV2, colV3, colV4 = st.columns([1.0, 1.0, 1.0, 1.0])
|
| 377 |
+
with colV1:
|
| 378 |
+
to_video = st.checkbox("One Click Generate Video", False, key="to_video")
|
| 379 |
+
with colV2:
|
| 380 |
+
duration = st.selectbox("Duration ", VIDEO_DUR_OPTIONS, index=0, key="duration_sel")
|
| 381 |
+
with colV3:
|
| 382 |
+
resolution = st.selectbox("Video Res", VIDEO_RES_OPTIONS, index=1, key="resolution_sel")
|
| 383 |
+
with colV4:
|
| 384 |
+
run = st.button("Run 💎", type="primary", use_container_width=True, key="run_btn")
|
| 385 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 386 |
+
|
| 387 |
+
# Inject CSS for preview frames (kept as is)
|
| 388 |
+
inject_preview_css(st.session_state.get("preview_aspect","9:16"),
|
| 389 |
+
fit_mode="contain" if st.session_state.get("fit_mode","fit")=="fit" else "cover")
|
| 390 |
+
|
| 391 |
+
# ======================= PREVIEWS (UNCHANGED STRUCTURE) =======================
|
| 392 |
st.subheader("Previews")
|
| 393 |
col_in, col_pack, col_pad, col_vid = st.columns(4)
|
| 394 |
|
|
|
|
| 395 |
input_bytes = None
|
| 396 |
if file_list:
|
| 397 |
try: input_bytes = file_list[0].getvalue()
|
| 398 |
except Exception: input_bytes = None
|
| 399 |
elif image_url:
|
| 400 |
input_bytes = fetch_bytes(image_url)
|
| 401 |
+
else:
|
| 402 |
+
demo = "input.jpg"
|
| 403 |
+
if os.path.exists(demo):
|
| 404 |
+
with open(demo, "rb") as f: input_bytes = f.read()
|
| 405 |
|
| 406 |
show_tile_card_display_download(
|
| 407 |
col_in, "Sample Input preview",
|
| 408 |
+
display_bytes=input_bytes,
|
| 409 |
+
download_bytes=input_bytes,
|
| 410 |
filename_stub="input", key_prefix="pre"
|
| 411 |
)
|
| 412 |
|
| 413 |
+
pack_bytes_demo = None
|
| 414 |
+
demo_pack = "packshot.jpg"
|
| 415 |
+
if os.path.exists(demo_pack):
|
| 416 |
+
with open(demo_pack, "rb") as f: pack_bytes_demo = f.read()
|
| 417 |
+
if not pack_bytes_demo:
|
| 418 |
+
pack_bytes_demo = input_bytes
|
| 419 |
+
|
| 420 |
show_tile_card_display_download(
|
| 421 |
col_pack, "Sample Packshot (BG Removed)",
|
| 422 |
display_bytes=pack_bytes_demo,
|
|
|
|
| 424 |
filename_stub="packshot", key_prefix="pre"
|
| 425 |
)
|
| 426 |
|
|
|
|
| 427 |
padded_bytes = None
|
| 428 |
if pack_bytes_demo:
|
| 429 |
try:
|
| 430 |
im = Image.open(io.BytesIO(pack_bytes_demo)).convert("RGBA")
|
| 431 |
+
aw, ah = (lambda s: (int(s.split(":")[0]), int(s.split(":")[1])))(st.session_state.get("preview_aspect","9:16"))
|
| 432 |
if aw >= ah:
|
| 433 |
+
W = int(st.session_state.get("preview_long_edge", 1920)); H = max(1, int(W * ah / aw))
|
| 434 |
else:
|
| 435 |
+
H = int(st.session_state.get("preview_long_edge", 1920)); W = max(1, int(H * aw / ah))
|
| 436 |
+
ratio_val = max(0.10, min(0.99, float(st.session_state.get("preview_ratio", 0.5))))
|
| 437 |
w, h = im.size
|
| 438 |
+
target = int(min(W, H) * ratio_val if aw == ah else (H * ratio_val if aw < ah else W * ratio_val))
|
| 439 |
if w >= h: new_w, new_h = target, max(1, int(h * (target / max(1, w))))
|
| 440 |
else: new_h, new_w = target, max(1, int(w * (target / max(1, h))))
|
| 441 |
im_resized = im.resize((new_w, new_h), Image.LANCZOS)
|
|
|
|
| 453 |
filename_stub="padded_ui", key_prefix="pre"
|
| 454 |
)
|
| 455 |
|
| 456 |
+
vid_demo = None
|
| 457 |
+
demo_vid = "tryon.mp4"
|
| 458 |
+
if os.path.exists(demo_vid):
|
| 459 |
+
with open(demo_vid, "rb") as f: vid_demo = f.read()
|
| 460 |
col_vid.markdown('<div class="lb-card-title">AI Generated Video Preview</div>', unsafe_allow_html=True)
|
| 461 |
if vid_demo:
|
| 462 |
data_url = "data:video/mp4;base64," + base64.b64encode(vid_demo).decode("ascii")
|
|
|
|
| 477 |
# ======================= RUN =======================
|
| 478 |
if run:
|
| 479 |
if not ok:
|
| 480 |
+
st.error("Bağlantı sorunu. API_BASE / TOKEN kontrol edin.")
|
| 481 |
+
elif not (file_list or image_url or input_bytes):
|
| 482 |
st.error("En az bir görsel girişi yapın (dosya yükle veya URL).")
|
| 483 |
else:
|
| 484 |
try:
|
|
|
|
| 490 |
|
| 491 |
data = {
|
| 492 |
"category": st.session_state.get("jewel_type","auto"),
|
| 493 |
+
"edit_prompt": "",
|
| 494 |
"num_images": "1",
|
| 495 |
"image_urls": img_urls,
|
| 496 |
"mannequin_image_url": "",
|
| 497 |
"identity_lock": "true" if st.session_state.get("identity_lock", True) else "false",
|
|
|
|
| 498 |
"preview_aspect": st.session_state.get("preview_aspect","9:16"),
|
| 499 |
"preview_long_edge": str(int(st.session_state.get("preview_long_edge", 1920))),
|
| 500 |
"preview_ratio": str(float(st.session_state.get("preview_ratio", 0.5))),
|
| 501 |
+
"padding_ratio": str(float(st.session_state.get("preview_ratio", 0.5))),
|
|
|
|
| 502 |
"upscale": "true" if st.session_state.get("upscale", False) else "false",
|
| 503 |
"upscale_factor": st.session_state.get("upscale_factor","2"),
|
| 504 |
"upscale_stage": st.session_state.get("upscale_stage","both"),
|
|
|
|
| 505 |
"to_video": "true" if st.session_state.get("to_video", False) else "false",
|
| 506 |
"video_prompt": "",
|
| 507 |
"duration": st.session_state.get("duration_sel", VIDEO_DUR_OPTIONS[0]),
|
|
|
|
| 509 |
"prompt_optimizer": "false",
|
| 510 |
}
|
| 511 |
|
| 512 |
+
with st.spinner("AI chain çalışıyor…"):
|
| 513 |
out = post_chain(data, files_payload if files_payload else None)
|
| 514 |
|
| 515 |
st.session_state["job_counter"] += 1
|
| 516 |
|
|
|
|
| 517 |
pack_native_url = (out.get("packshot_step", {}).get("result", {}).get("images_native") or [{}])[0].get("url")
|
| 518 |
padded_native_url = (out.get("packshot_step", {}).get("result", {}).get("padded_native") or [{}])[0].get("url")
|
| 519 |
placed_native_url = (out.get("image_step", {}).get("result", {}).get("images_native") or [{}])[0].get("url")
|
| 520 |
vurl = (out.get("video_step") or {}).get("result", {}).get("video", {}).get("url")
|
| 521 |
|
|
|
|
| 522 |
pack_display_jpeg = None
|
| 523 |
if pack_native_url:
|
| 524 |
pn = fetch_bytes(pack_native_url)
|
|
|
|
| 556 |
res = st.session_state.get("results")
|
| 557 |
vbytes = None
|
| 558 |
if res:
|
| 559 |
+
st.markdown('<div class="panel"><h3>🧾 Product Results</h3>', unsafe_allow_html=True)
|
| 560 |
c_pack, c_pad, c_model, c_vid = st.columns(4)
|
| 561 |
|
| 562 |
show_tile_card_display_download(
|
|
|
|
| 617 |
vurl2 = (v.get("result") or {}).get("video", {}).get("url")
|
| 618 |
if vurl2:
|
| 619 |
st.session_state["results"]["video_url"] = vurl2
|
| 620 |
+
st.success("Video hazır.")
|
| 621 |
else:
|
| 622 |
st.info("Video URL gelmedi.")
|
| 623 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 624 |
|
| 625 |
+
# Bundle ZIP
|
|
|
|
| 626 |
named = []
|
| 627 |
pn = fetch_bytes(res.get("packshot_native_url")) if res.get("packshot_native_url") else None
|
| 628 |
mn = fetch_bytes(res.get("placed_native_url")) if res.get("placed_native_url") else None
|
|
|
|
| 638 |
st.download_button("Download All (ZIP)", data=buf.read(),
|
| 639 |
file_name="ai_lightbox_outputs.zip", mime="application/zip",
|
| 640 |
key=f"job{st.session_state['job_counter']}_zip_dl")
|
|
|
|
| 641 |
|
| 642 |
+
# ======================= Sidebar (compact) =======================
|
| 643 |
with st.sidebar:
|
| 644 |
+
st.markdown("### ⚙️ Studio Settings")
|
| 645 |
st.caption(f"API_BASE: {API_BASE}")
|
| 646 |
if HF_TOKEN: st.caption("Auth: Bearer (aktif)")
|
|
|
|
| 647 |
st.slider("Video önizleme genişliği (px)", 360, 1080, st.session_state["video_ui_max_px"],
|
| 648 |
step=10, key="video_ui_max_px")
|
| 649 |
if st.button("Önbelleği temizle"): st.cache_data.clear(); st.success("Önbellek temizlendi.")
|