renderfy commited on
Commit
84bfdd1
·
verified ·
1 Parent(s): 4950fa7

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +243 -233
streamlit_app.py CHANGED
@@ -1,30 +1,38 @@
1
- # streamlit_app.py — LamboVision (Car) UI
2
- # Pre-run Sample Preview kartları + Run sonrası sonuç kartları (Packshot/Padded/Composite/Video)
 
 
 
 
 
 
 
 
3
 
4
  import os, io, base64, zipfile, requests, streamlit as st
5
  from PIL import Image
6
 
7
- # ===== Theme (dark + Lamborghini orange) =====
8
  try:
9
  st._config.set_option("theme.base", "dark")
10
- st._config.set_option("theme.primaryColor", "#ff7a1a") # dark orange
11
  st._config.set_option("theme.backgroundColor", "#0e0f12")
12
  st._config.set_option("theme.secondaryBackgroundColor", "#161a1f")
13
  st._config.set_option("theme.textColor", "#e7e9ed")
14
  except Exception:
15
  pass
16
 
17
- # ================= Env & API =================
18
- API_BASE = (os.getenv("AI_LamboVision_API", "http://127.0.0.1:7860") or "http://127.0.0.1:7860").strip().strip("'\"").rstrip("/")
19
- HF_TOKEN = (os.getenv("AI_LamboVision_TOKEN", "") or "").strip().strip("'\"")
20
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
21
 
22
  VIDEO_MAX_PX_DEFAULT = int(os.getenv("VIDEO_MAX_PX", "720"))
23
  VIDEO_RES_OPTIONS = ["512P", "768P"]
24
  VIDEO_DUR_OPTIONS = ["6", "10"]
25
- ASPECT_OPTIONS = ["16:9", "9:16", "1:1", "4:3"]
26
 
27
- # ================= Session =================
28
  if "results" not in st.session_state:
29
  st.session_state["results"] = None
30
  if "job_counter" not in st.session_state:
@@ -32,19 +40,15 @@ if "job_counter" not in st.session_state:
32
  if "video_ui_max_px" not in st.session_state:
33
  st.session_state["video_ui_max_px"] = VIDEO_MAX_PX_DEFAULT
34
 
35
- # ================= Helpers =================
36
  def _needs_auth(url: str) -> bool:
37
  return url.startswith(API_BASE) or url.startswith("outputs/") or url.startswith("/outputs/")
38
 
39
  def _abs_backend_url(url_or_path: str) -> str:
40
- if not url_or_path:
41
- return ""
42
- if url_or_path.startswith(("http://","https://")):
43
- return url_or_path
44
- if url_or_path.startswith("/outputs/"):
45
- return f"{API_BASE}{url_or_path}"
46
- if url_or_path.startswith("outputs/"):
47
- return f"{API_BASE}/{url_or_path}"
48
  return f"{API_BASE}/{url_or_path.lstrip('/')}"
49
 
50
  def backend_ok() -> bool:
@@ -61,15 +65,16 @@ def post_chain(data: dict, files_payload):
61
  r.raise_for_status()
62
  return r.json()
63
 
64
- def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=False, preview_aspect="16:9", prompt_text=""):
65
  payload = {
66
  "image_url": image_url,
67
  "duration": duration,
68
  "resolution": resolution,
69
- "preview_aspect": preview_aspect,
70
  "prompt_optimizer": "true" if prompt_optimizer else "false",
71
- "prompt": prompt_text or "", # backend boşsa kendi defaults'unu kullanır
72
  }
 
 
73
  r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload, headers=HEADERS, timeout=600)
74
  r.raise_for_status()
75
  return r.json()
@@ -123,40 +128,43 @@ def make_zip(named_bytes: list[tuple[str, bytes]]) -> bytes:
123
  if b: z.writestr(fname, b)
124
  buf.seek(0); return buf.read()
125
 
 
126
  def _parse_aspect_str(s: str):
127
  try:
128
  a, b = s.split(":"); return max(1, int(a)), max(1, int(b))
129
  except Exception:
130
- return (16, 9)
131
 
132
  def inject_base_css():
133
  st.markdown("""
134
  <style>
135
  .stApp { background: #0e0f12; }
136
  header[data-testid="stHeader"] { background: transparent; }
137
- .block-container { padding-top: 1.25rem; }
138
- h1, .stMarkdown h1 { font-size: 1.85rem; line-height: 1.3; margin-bottom: 0.15rem; }
139
  .stMarkdown p { margin-top: 0.15rem; }
140
  label, .stTextInput label, .stSelectbox label, .stNumberInput label, .stFileUploader label,
141
  .stCheckbox label, .stSlider label, .stRadio label {
142
- color: #ff8a33 !important; font-weight: 600 !important;
 
143
  }
144
  .stButton > button[kind="primary"]{
145
  background: #ff7a1a !important; border: 0 !important; color: #111 !important;
146
  }
147
  .stButton > button:hover { filter: brightness(1.05); }
148
- .lb-card-title { font-weight: 700; font-size: 1.05rem; margin: 2px 0 8px 0; color: #eaecef; }
149
- .lb-frame {
 
150
  position: relative; width: 100%;
151
  border-radius: 12px; overflow: hidden;
152
  background: #111419;
153
  display:flex; align-items:center; justify-content:center;
154
  box-shadow: 0 0 0 1px rgba(255,122,26,0.15) inset;
155
  }
156
- .lb-frame img { width:100%; height:100%; display:block; }
157
- .lb-video { display:flex; justify-content:center; }
158
- .lb-video video { border-radius:8px; max-width: 540px; width:100%; height:auto; }
159
- .stCaption { color: #ffb07a !important; }
160
  </style>
161
  """, unsafe_allow_html=True)
162
 
@@ -164,49 +172,13 @@ def inject_preview_css(aspect: str, fit_mode="contain"):
164
  aw, ah = _parse_aspect_str(aspect)
165
  st.markdown(f"""
166
  <style>
167
- .lb-frame {{ aspect-ratio: {aw} / {ah}; }}
168
- .lb-frame img {{ object-fit:{fit_mode}; }}
169
  </style>
170
  """, unsafe_allow_html=True)
171
 
172
- def show_tile_card_display_download(
173
- col, title,
174
- display_url: str | None = None,
175
- display_bytes: bytes | None = None,
176
- download_url: str | None = None,
177
- download_bytes: bytes | None = None,
178
- filename_stub: str = "image",
179
- key_prefix: str = "",
180
- ):
181
- col.markdown(f'<div class="lb-card-title">{title}</div>', unsafe_allow_html=True)
182
- if not display_url and not display_bytes:
183
- col.markdown('<div class="lb-frame"></div>', unsafe_allow_html=True)
184
- col.caption("—"); return None, None
185
- if display_bytes is not None:
186
- src = image_bytes_to_data_url(display_bytes)
187
- col.markdown(f'<div class="lb-frame"><img src="{src}"/></div>', unsafe_allow_html=True)
188
- else:
189
- abs_url = _abs_backend_url(display_url)
190
- col.markdown(f'<div class="lb-frame"><img src="{abs_url}"/></div>', unsafe_allow_html=True)
191
-
192
- native = download_bytes or (fetch_bytes(download_url) if download_url else None)
193
- if not native:
194
- native = display_bytes
195
- if native:
196
- sz = image_size_from_bytes(native)
197
- if sz: col.caption(f"Real Res: {sz[0]}×{sz[1]} px")
198
- ext = infer_ext(native)
199
- mime = "image/jpeg" if ext.lower() in [".jpg",".jpeg"] else ("image/png" if ext.lower()==".png" else "image/webp")
200
- col.download_button("Download (Full Quality)", data=native,
201
- file_name=f"{filename_stub}{ext if ext!='.bin' else '.jpg'}",
202
- mime=mime, key=f"{key_prefix}_{filename_stub}_dl")
203
- else:
204
- col.caption("—")
205
- return display_url or "data", native
206
-
207
  def rgba_to_rgb_on_bg(img_rgba: Image.Image, bg_rgb=(255,255,255)) -> Image.Image:
208
- if img_rgba.mode != "RGBA":
209
- return img_rgba.convert("RGB")
210
  bg = Image.new("RGBA", img_rgba.size, (*bg_rgb, 255))
211
  bg.paste(img_rgba, (0,0), img_rgba)
212
  return bg.convert("RGB")
@@ -220,11 +192,9 @@ def make_small_jpeg_from_native(native: bytes, target_long_edge: int = 1280, bg_
220
  im = im.convert("RGBA")
221
  w, h = im.size
222
  if w >= h:
223
- new_w = min(target_long_edge, w)
224
- new_h = max(1, int(h * (new_w / max(1, w))))
225
  else:
226
- new_h = min(target_long_edge, h)
227
- new_w = max(1, int(w * (new_h / max(1, h))))
228
  im2 = im.resize((new_w, new_h), Image.LANCZOS)
229
  im2_rgb = rgba_to_rgb_on_bg(im2, bg_rgb=bg_rgb)
230
  out = io.BytesIO(); im2_rgb.save(out, format="JPEG", quality=88, subsampling=1)
@@ -232,13 +202,13 @@ def make_small_jpeg_from_native(native: bytes, target_long_edge: int = 1280, bg_
232
 
233
  def make_preview_frame(img_bytes: bytes, aspect: str, long_edge: int, ratio: float, bg_rgb=(255,255,255)) -> bytes:
234
  im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")
235
- a, b = _parse_aspect_str(aspect)
236
- if a >= b:
237
- W = max(256, min(8192, int(long_edge))); H = max(1, int(W * b / a))
238
  else:
239
- H = max(256, min(8192, int(long_edge))); W = max(1, int(H * a / b))
240
  ratio = max(0.10, min(0.99, float(ratio)))
241
- target = int(min(W, H) * ratio if a == b else (H * ratio if a < b else W * ratio))
242
  w, h = im.size
243
  if w >= h: new_w, new_h = target, max(1, int(h * (target / max(1, w))))
244
  else: new_h, new_w = target, max(1, int(w * (target / max(1, h))))
@@ -248,126 +218,107 @@ def make_preview_frame(img_bytes: bytes, aspect: str, long_edge: int, ratio: flo
248
  out = io.BytesIO(); canvas.convert("RGB").save(out, format="JPEG", quality=90, subsampling=1)
249
  return out.getvalue()
250
 
251
- # ================= Local demo assets (optional) =================
252
  def load_local_demo():
253
  demo = {}
254
- for fname in ["input.jpg", "packshot.jpg", "model.jpg", "tryon.mp4"]:
255
  if os.path.exists(fname):
256
  with open(fname, "rb") as f: demo[fname] = f.read()
257
  return demo
258
-
259
  demo_files = load_local_demo()
260
 
261
- # ================= Prompt builders =================
262
- def build_car_prompt(model_name: str, body_color: str, finish: str, wheel_style: str, caliper_color: str, extra: str = "") -> str:
263
- base = (
264
- "Catalog-ready studio photo of a Lamborghini from a three-quarter front view. "
265
- "Neutral studio background, soft rim light, realistic reflections, no text/logos in background. "
266
- "Keep true proportions, lens look (35–50mm), and paint micro-highlights. "
267
- "IDENTITY & GEOMETRY LOCK: Treat the car as a fixed real object. "
268
- "Do NOT redraw or stylize body panels; preserve body lines, panel gaps, reflections and lenses; "
269
- "no rotation/mirroring (Δyaw/pitch/roll ≤ 1°); keep wheel circles true, tire sidewalls, brake discs & calipers, ride height; "
270
- "preserve badges/logo integrity; no warp or fake text."
271
- )
272
- cfg = (
273
- f" Subject: {model_name}. "
274
- f"Change body color to {body_color} with {finish} finish. "
275
- f"Wheels: {wheel_style}; brake calipers {caliper_color}. "
276
- "Maintain original stance; no suspension changes; no aero additions."
277
- )
278
- if extra.strip():
279
- cfg += " " + extra.strip()
280
- return base + " " + cfg
281
-
282
  SCENE_PRESETS = {
283
- "Monaco Sunset": "Monaco coastal boulevard at golden hour, subtle dolly-in, realistic paint reflections, no artifacts.",
284
- "Dubai Night": "Dubai Marina night city lights, gentle lateral parallax, crisp highlights, no motion blur.",
285
- "Studio Cinematic": "High-end studio cyclorama, soft rim light, tripod-like stability, micro-motion only."
 
 
286
  }
287
 
288
- def build_video_prompt(scene_name: str, extra: str = "") -> str:
289
- base = SCENE_PRESETS.get(scene_name, "High-end studio, subtle parallax, natural camera micro-motion.")
290
- lock = " Keep logos and wheel circles intact; no wheel warping; no added text; realistic reflections."
291
- if extra.strip():
292
- return base + " " + lock + " " + extra.strip()
293
- return base + " " + lock
294
-
295
- # ================= Page =================
296
- st.set_page_config(page_title="LamboVision · Car Configurator", layout="wide", page_icon="🏎️")
297
  inject_base_css()
298
 
299
  st.title("🏎️ LamboVision · AI Car Studio")
300
- st.caption("Configure your Lamborghini and generate instant cinematic previews for high-intent leads.")
301
 
302
  ok = backend_ok()
303
 
304
- # -------- Top control bar --------
305
- col1, col2, col3, col4, col5, col6, col7 = st.columns([1.6, 1.6, 1.0, 1.2, 0.9, 1.1, 0.9])
306
  with col1:
307
  file_list = st.file_uploader("Car Image (Upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
308
  with col2:
309
  image_url = st.text_input("Or URL", key="image_url", placeholder="https://... (optional)")
310
  with col3:
311
- category = st.selectbox("Category", ["auto","car","vehicle"], index=0, key="category")
312
  with col4:
313
- preview_ratio = st.number_input("Preview Scale (0.10–0.99)", min_value=0.10, max_value=0.99, value=0.60, step=0.01,
314
- help="Sadece önizleme kadrajında büyüklüğü etkiler.", key="preview_ratio")
315
  with col5:
316
- upscale = st.checkbox("Super Res", False, key="upscale")
317
  with col6:
318
- upscale_stage = st.selectbox("SR Step", ["both","final","packshot"], index=0, help="Varsayılan: both", key="upscale_stage")
319
  with col7:
320
- upscale_factor = st.selectbox("SR Factor", ["2","4"], index=0, key="upscale_factor")
321
 
322
- colA, colB, colC, colD, colE, colF = st.columns([1.0, 1.0, 1.1, 0.9, 0.9, 1.2])
323
  with colA:
324
- st.info(f"Connection: {'Online' if ok else 'Offline'}", icon="🔌")
325
  with colB:
326
- identity_lock = st.checkbox("Identity lock", True, key="identity_lock")
327
  with colC:
328
- to_video = st.checkbox("One Click Generate Video", False, key="to_video")
329
  with colD:
330
- duration = st.selectbox("Duration ", VIDEO_DUR_OPTIONS, index=0, key="duration_sel")
331
  with colE:
332
- resolution = st.selectbox("Video Res", VIDEO_RES_OPTIONS, index=1, key="resolution_sel")
333
  with colF:
334
- run = st.button("Run 🏁", type="primary", use_container_width=True, key="run_btn")
335
-
336
- # -------- Configurator row --------
337
- st.markdown("### Configurator")
338
- c1, c2, c3, c4, c5, c6 = st.columns([1.4, 1.0, 0.9, 1.2, 1.0, 1.2])
339
- with c1:
340
- model_name = st.text_input("Model", value="Lamborghini Revuelto", key="cfg_model")
341
- with c2:
342
- body_color = st.text_input("Body color", value="Verde Ithaca", key="cfg_color")
343
- with c3:
344
- finish = st.selectbox("Finish", ["gloss","matte","satin"], index=0, key="cfg_finish")
345
- with c4:
346
- wheel_style = st.text_input("Wheel style", value="lightweight forged", key="cfg_wheels")
347
- with c5:
348
- caliper_color = st.text_input("Caliper", value="yellow", key="cfg_caliper")
349
- with c6:
350
- extra_prompt = st.text_input("Extra (optional)", value="", placeholder="stripe delete, studio floor reflection, ...", key="cfg_extra")
351
-
352
- # -------- Scene presets for video --------
353
- st.markdown("### Scene Presets")
354
- s1, s2, s3, s4 = st.columns([1.2, 1.2, 1.2, 1.2])
355
- with s1:
356
- scene_name = st.selectbox("Scene", list(SCENE_PRESETS.keys()), index=0, key="scene_name")
357
- with s2:
358
- scene_extra = st.text_input("Scene extra (optional)", value="", key="scene_extra")
359
- with s3:
360
  preview_aspect = st.selectbox("Preview Frame Ratio", ASPECT_OPTIONS, index=0, key="preview_aspect")
361
- with s4:
362
  preview_long_edge = st.number_input("Preview Long Edge (px)", min_value=256, max_value=8192, value=1920, step=64, key="preview_long_edge")
 
 
 
 
363
 
364
- fit_mode = st.selectbox("Preview Layout", ["fit","fill"], index=0, help="Sadece önizlemeyi etkiler.", key="fit_mode")
365
  inject_preview_css(preview_aspect, fit_mode="contain" if fit_mode=="fit" else "cover")
366
 
367
- # ================= PRE-RUN SAMPLE PREVIEWS =================
368
- st.subheader("Sample Previews")
369
- col_in, col_pack, col_pad, col_vid = st.columns(4)
370
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  input_bytes = None
372
  if file_list:
373
  try: input_bytes = file_list[0].getvalue()
@@ -377,8 +328,46 @@ elif image_url:
377
  elif demo_files.get("input.jpg"):
378
  input_bytes = demo_files["input.jpg"]
379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  show_tile_card_display_download(
381
- col_in, "Sample Input preview",
382
  display_bytes=input_bytes if input_bytes else demo_files.get("input.jpg"),
383
  download_bytes=input_bytes if input_bytes else demo_files.get("input.jpg"),
384
  filename_stub="input", key_prefix="pre"
@@ -386,7 +375,7 @@ show_tile_card_display_download(
386
 
387
  pack_bytes_demo = demo_files.get("packshot.jpg") or input_bytes
388
  show_tile_card_display_download(
389
- col_pack, "Sample Preview",
390
  display_bytes=pack_bytes_demo,
391
  download_bytes=pack_bytes_demo,
392
  filename_stub="packshot", key_prefix="pre"
@@ -401,84 +390,101 @@ if pack_bytes_demo:
401
  except Exception:
402
  padded_bytes = None
403
  show_tile_card_display_download(
404
- col_pad, "Sample Composite Preview",
405
  display_bytes=padded_bytes or pack_bytes_demo,
406
  download_bytes=padded_bytes or pack_bytes_demo,
407
  filename_stub="padded_ui", key_prefix="pre"
408
  )
409
 
 
 
 
 
 
 
 
 
 
410
  vid_demo = demo_files.get("tryon.mp4")
411
- col_vid.markdown('<div class="lb-card-title">AI Generated Video Preview</div>', unsafe_allow_html=True)
412
  if vid_demo:
413
  data_url = "data:video/mp4;base64," + base64.b64encode(vid_demo).decode("ascii")
414
- col_vid.markdown(
415
  f"""
416
- <div class="lb-frame" style="display:flex;align-items:center;justify-content:center;">
417
  <video autoplay loop muted playsinline style="width:100%;height:100%;object-fit:cover;">
418
  <source src="{data_url}" type="video/mp4">
419
  </video>
420
  </div>
421
  """, unsafe_allow_html=True
422
  )
423
- col_vid.download_button("Download Video (MP4)", data=vid_demo, file_name="tryon.mp4", mime="video/mp4", key="pre_video_dl")
424
  else:
425
- col_vid.markdown('<div class="lb-frame"></div>', unsafe_allow_html=True)
426
- col_vid.caption("—")
427
 
428
- # ================= RUN =================
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  if run:
430
  if not ok:
431
- st.error("Backend erişilemiyor. API_BASE / TOKEN kontrol edin.")
432
  elif not (file_list or image_url or demo_files.get("input.jpg")):
433
- st.error("En az bir görsel girişi yapın (dosya yükle veya URL).")
434
  else:
435
  try:
436
- # Build edit prompt from configurator
437
- edit_prompt = build_car_prompt(
438
- model_name=model_name,
439
- body_color=body_color,
440
- finish=finish,
441
- wheel_style=wheel_style,
442
- caliper_color=caliper_color,
443
- extra=extra_prompt
444
- )
445
-
446
  img_urls = image_url or ""
 
447
  files_payload = []
448
  if file_list:
449
  for f in file_list:
450
  files_payload.append(("files", (f.name, f.getvalue(), f.type or "image/jpeg")))
451
 
452
  data = {
453
- "category": category,
454
- "edit_prompt": edit_prompt, # backend doluysa bunu kullanır
455
  "num_images": "1",
456
  "image_urls": img_urls,
457
- "mannequin_image_url": "", # otomotivde kullanılmıyor
 
 
 
458
  "identity_lock": "true" if identity_lock else "false",
459
- # Preview controls
460
  "preview_aspect": preview_aspect,
461
  "preview_long_edge": str(int(preview_long_edge)),
462
  "preview_ratio": str(float(preview_ratio)),
463
- "padding_ratio": str(float(preview_ratio)),
464
  # SR
465
  "upscale": "true" if upscale else "false",
466
- "upscale_factor": st.session_state.get("upscale_factor","2"),
467
  "upscale_stage": st.session_state.get("upscale_stage","both"),
468
- # Video chain (optional)
469
  "to_video": "true" if to_video else "false",
470
- "video_prompt": build_video_prompt(scene_name, scene_extra) if to_video else "",
471
  "duration": st.session_state.get("duration_sel", VIDEO_DUR_OPTIONS[0]),
472
  "resolution": st.session_state.get("resolution_sel", VIDEO_RES_OPTIONS[1]),
473
  "prompt_optimizer": "false",
474
  }
475
 
476
- with st.spinner("AI Chain Running…"):
477
  out = post_chain(data, files_payload if files_payload else None)
478
 
479
  st.session_state["job_counter"] += 1
480
 
481
- # ---- Outputs ----
482
  pack_native_url = None; pack_display_jpeg = None
483
  try:
484
  pack_native_url = (out["packshot_step"]["result"]["images_native"] or [{}])[0].get("url")
@@ -495,12 +501,13 @@ if run:
495
  if nb: padded_display_jpeg = make_small_jpeg_from_native(nb, 1280, bg_rgb=(255,255,255))
496
  except Exception: pass
497
 
498
- placed_native_url = None; placed_display_jpeg = None
499
  try:
500
- placed_native_url = (out.get("image_step") or {}).get("result", {}).get("images_native", [{}])[0].get("url")
501
- if placed_native_url:
502
- mn = fetch_bytes(placed_native_url)
503
- if mn: placed_display_jpeg = make_small_jpeg_from_native(mn, 1280, bg_rgb=(255,255,255))
 
504
  except Exception: pass
505
 
506
  vurl = None
@@ -520,28 +527,31 @@ if run:
520
  "packshot_display_jpeg": pack_display_jpeg,
521
  "padded_native_url": padded_native_url,
522
  "padded_display_jpeg": padded_display_jpeg,
523
- "placed_native_url": placed_native_url,
524
- "placed_display_jpeg": placed_display_jpeg,
525
  "video_url": vurl,
526
  "api_base": API_BASE,
527
  "has_auth_header": bool(HEADERS),
528
  "preview_aspect": preview_aspect,
 
 
 
529
  }
530
 
531
  except requests.HTTPError as e:
532
  st.error(f"HTTP {e.response.status_code}")
533
  except Exception as e:
534
- st.error(f"Hata: {e}")
535
 
536
- # ================= RESULTS =================
537
  res = st.session_state.get("results")
538
  vbytes = None
539
  if res:
540
  st.subheader("Results")
541
- c_pack, c_pad, c_model, c_vid = st.columns(4)
542
 
543
  show_tile_card_display_download(
544
- c_pack, "Packshot (Studio)",
545
  display_bytes=res.get("packshot_display_jpeg"),
546
  download_url=res.get("packshot_native_url"),
547
  filename_stub="packshot_native",
@@ -549,7 +559,7 @@ if res:
549
  )
550
 
551
  show_tile_card_display_download(
552
- c_pad, "Padded",
553
  display_bytes=res.get("padded_display_jpeg"),
554
  download_url=res.get("padded_native_url"),
555
  filename_stub="padded_backend",
@@ -557,20 +567,21 @@ if res:
557
  )
558
 
559
  show_tile_card_display_download(
560
- c_model, "Composite",
561
- display_bytes=res.get("placed_display_jpeg"),
562
- download_url=res.get("placed_native_url"),
563
- filename_stub="composite_native",
564
  key_prefix=f"job{st.session_state['job_counter']}"
565
  )
566
 
567
- c_vid.markdown('<div class="lb-card-title">AI Generated Video</div>', unsafe_allow_html=True)
568
  vurl = res.get("video_url")
 
569
  if vurl:
570
  abs_v = _abs_backend_url(vurl)
571
- c_vid.markdown(
572
  f"""
573
- <div class="lb-frame" style="display:flex;align-items:center;justify-content:center;">
574
  <video autoplay loop muted controls playsinline style="width:100%;height:100%;object-fit:cover;">
575
  <source src="{abs_v}" type="video/mp4">
576
  </video>
@@ -580,26 +591,25 @@ if res:
580
  vb = fetch_bytes(abs_v)
581
  if vb:
582
  vbytes = vb
583
- c_vid.download_button("Download Video (MP4)", data=vb, file_name="lambovision_video.mp4",
584
- mime="video/mp4", key=f"job{st.session_state['job_counter']}_video_dl")
585
  else:
586
- c_vid.markdown('<div class="lb-frame"></div>', unsafe_allow_html=True)
 
587
  vid_checkbox_key = f"video_toggle_job{st.session_state['job_counter']}"
588
- if st.checkbox("Generate Video From This Result", value=False, key=vid_checkbox_key):
589
- src_for_video = res.get("packshot_native_url") or res.get("placed_native_url")
590
  if not src_for_video:
591
- st.warning("Video için kullanılacak native görsel bulunamadı.")
592
  else:
593
- with st.spinner("Generating Video …"):
594
  try:
595
  v = post_video(
596
  src_for_video,
597
  duration=st.session_state.get("duration_sel", VIDEO_DUR_OPTIONS[0]),
598
  resolution=st.session_state.get("resolution_sel", VIDEO_RES_OPTIONS[1]),
599
- preview_aspect=st.session_state.get("preview_aspect","16:9"),
600
- prompt_text=build_video_prompt(st.session_state.get("scene_name","Studio Cinematic"),
601
- st.session_state.get("scene_extra","")),
602
- prompt_optimizer=False,
603
  )
604
  except requests.HTTPError as e:
605
  st.error(f"HTTP {e.response.status_code}")
@@ -612,29 +622,29 @@ if res:
612
  if isinstance(ui_hints, dict):
613
  st.session_state["video_ui_max_px"] = int(ui_hints.get("suggested_video_max_px", VIDEO_MAX_PX_DEFAULT))
614
  st.session_state.pop(vid_checkbox_key, None)
615
- st.success("Video is Ready! Showing preview…")
616
  st.rerun()
617
  else:
618
- st.info("Video URL gelmedi.")
619
 
620
- # ZIP
621
  named = []
622
  pn = fetch_bytes(res.get("packshot_native_url")) if res.get("packshot_native_url") else None
623
- mn = fetch_bytes(res.get("placed_native_url")) if res.get("placed_native_url") else None
624
  if pn: named.append((f"packshot_native{infer_ext(pn)}", pn))
625
- if mn: named.append((f"composite_native{infer_ext(mn)}", mn))
626
  if vbytes: named.append(("video.mp4", vbytes))
627
  if named:
628
  zip_bytes = make_zip(named)
629
  st.download_button("Download All (ZIP)", data=zip_bytes,
630
- file_name="lambovision_outputs.zip", mime="application/zip",
631
  key=f"job{st.session_state['job_counter']}_zip_dl")
632
 
633
- # ================= Sidebar =================
634
  with st.sidebar:
635
  st.caption(f"API_BASE: {API_BASE}")
636
- if HF_TOKEN: st.caption("Auth: Bearer (aktif)")
637
  st.slider("Video preview width (px)", 360, 1080, st.session_state["video_ui_max_px"],
638
  step=10, key="video_ui_max_px")
639
  if st.button("Clear cache"): st.cache_data.clear(); st.success("Cache cleared.")
640
- if st.button("Clear results"): st.session_state["results"] = None; st.success("Cleared.")
 
1
+ # streamlit_app.py — LamboVision · AI Car Studio (frontend) v0.6.3
2
+ # Previews: 9:16 cards (Input / Packshot / Padded / Scene / Video)
3
+ # Configurator: model, body color, finish, wheel style, caliper color + Extra prompt
4
+ # Endpoints used:
5
+ # POST /v1/tryon/chain (recommended full pipeline)
6
+ # POST /v1/video/from-image (on-demand video)
7
+ #
8
+ # Env:
9
+ # AI_LIGHTBOX_API -> http://host:port (default: http://127.0.0.1:8000)
10
+ # AI_LIGHTBOX_TOKEN -> Bearer (optional)
11
 
12
  import os, io, base64, zipfile, requests, streamlit as st
13
  from PIL import Image
14
 
15
+ # ==== Theming (dark + orange) ====
16
  try:
17
  st._config.set_option("theme.base", "dark")
18
+ st._config.set_option("theme.primaryColor", "#ff7a1a")
19
  st._config.set_option("theme.backgroundColor", "#0e0f12")
20
  st._config.set_option("theme.secondaryBackgroundColor", "#161a1f")
21
  st._config.set_option("theme.textColor", "#e7e9ed")
22
  except Exception:
23
  pass
24
 
25
+ # ==== Env & API ====
26
+ API_BASE = (os.getenv("AI_LIGHTBOX_API", "http://127.0.0.1:8000") or "http://127.0.0.1:8000").strip().strip("'\"").rstrip("/")
27
+ HF_TOKEN = (os.getenv("AI_LIGHTBOX_TOKEN", "") or "").strip().strip("'\"")
28
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
29
 
30
  VIDEO_MAX_PX_DEFAULT = int(os.getenv("VIDEO_MAX_PX", "720"))
31
  VIDEO_RES_OPTIONS = ["512P", "768P"]
32
  VIDEO_DUR_OPTIONS = ["6", "10"]
33
+ ASPECT_OPTIONS = ["9:16", "3:4", "1:1"]
34
 
35
+ # ==== Session ====
36
  if "results" not in st.session_state:
37
  st.session_state["results"] = None
38
  if "job_counter" not in st.session_state:
 
40
  if "video_ui_max_px" not in st.session_state:
41
  st.session_state["video_ui_max_px"] = VIDEO_MAX_PX_DEFAULT
42
 
43
+ # ==== Backend helpers ====
44
  def _needs_auth(url: str) -> bool:
45
  return url.startswith(API_BASE) or url.startswith("outputs/") or url.startswith("/outputs/")
46
 
47
  def _abs_backend_url(url_or_path: str) -> str:
48
+ if not url_or_path: return ""
49
+ if url_or_path.startswith(("http://","https://")): return url_or_path
50
+ if url_or_path.startswith("/outputs/"): return f"{API_BASE}{url_or_path}"
51
+ if url_or_path.startswith("outputs/"): return f"{API_BASE}/{url_or_path}"
 
 
 
 
52
  return f"{API_BASE}/{url_or_path.lstrip('/')}"
53
 
54
  def backend_ok() -> bool:
 
65
  r.raise_for_status()
66
  return r.json()
67
 
68
+ def post_video(image_url: str, duration="6", resolution="768P", prompt_optimizer=False, preview_aspect="9:16", prompt=None):
69
  payload = {
70
  "image_url": image_url,
71
  "duration": duration,
72
  "resolution": resolution,
 
73
  "prompt_optimizer": "true" if prompt_optimizer else "false",
74
+ "preview_aspect": preview_aspect,
75
  }
76
+ if prompt is not None:
77
+ payload["prompt"] = prompt
78
  r = requests.post(f"{API_BASE}/v1/video/from-image", data=payload, headers=HEADERS, timeout=600)
79
  r.raise_for_status()
80
  return r.json()
 
128
  if b: z.writestr(fname, b)
129
  buf.seek(0); return buf.read()
130
 
131
+ # ==== Preview helpers (uniform 9:16 cards) ====
132
  def _parse_aspect_str(s: str):
133
  try:
134
  a, b = s.split(":"); return max(1, int(a)), max(1, int(b))
135
  except Exception:
136
+ return (9, 16)
137
 
138
  def inject_base_css():
139
  st.markdown("""
140
  <style>
141
  .stApp { background: #0e0f12; }
142
  header[data-testid="stHeader"] { background: transparent; }
143
+ .block-container { padding-top: 1.1rem; }
144
+ h1, .stMarkdown h1 { font-size: 1.85rem; line-height: 1.25; margin-bottom: 0.1rem; }
145
  .stMarkdown p { margin-top: 0.15rem; }
146
  label, .stTextInput label, .stSelectbox label, .stNumberInput label, .stFileUploader label,
147
  .stCheckbox label, .stSlider label, .stRadio label {
148
+ color: #ff8a33 !important;
149
+ font-weight: 600 !important;
150
  }
151
  .stButton > button[kind="primary"]{
152
  background: #ff7a1a !important; border: 0 !important; color: #111 !important;
153
  }
154
  .stButton > button:hover { filter: brightness(1.05); }
155
+ .stAlert { border-radius: 10px; }
156
+ .lv-card-title { font-weight: 700; font-size: 1.05rem; margin: 2px 0 8px 0; color: #eaecef; }
157
+ .lv-frame {
158
  position: relative; width: 100%;
159
  border-radius: 12px; overflow: hidden;
160
  background: #111419;
161
  display:flex; align-items:center; justify-content:center;
162
  box-shadow: 0 0 0 1px rgba(255,122,26,0.15) inset;
163
  }
164
+ .lv-frame img { width:100%; height:100%; display:block; }
165
+ .lv-video { display:flex; justify-content:center; }
166
+ .lv-video video { border-radius:8px; max-width: 540px; width:100%; height:auto; }
167
+ .stCaption, .st-emotion-cache-1r6slb0 { color: #ffb07a !important; }
168
  </style>
169
  """, unsafe_allow_html=True)
170
 
 
172
  aw, ah = _parse_aspect_str(aspect)
173
  st.markdown(f"""
174
  <style>
175
+ .lv-frame {{ aspect-ratio: {aw} / {ah}; }}
176
+ .lv-frame img {{ object-fit:{fit_mode}; }}
177
  </style>
178
  """, unsafe_allow_html=True)
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  def rgba_to_rgb_on_bg(img_rgba: Image.Image, bg_rgb=(255,255,255)) -> Image.Image:
181
+ if img_rgba.mode != "RGBA": return img_rgba.convert("RGB")
 
182
  bg = Image.new("RGBA", img_rgba.size, (*bg_rgb, 255))
183
  bg.paste(img_rgba, (0,0), img_rgba)
184
  return bg.convert("RGB")
 
192
  im = im.convert("RGBA")
193
  w, h = im.size
194
  if w >= h:
195
+ new_w = min(target_long_edge, w); new_h = max(1, int(h * (new_w / max(1, w))))
 
196
  else:
197
+ new_h = min(target_long_edge, h); new_w = max(1, int(w * (new_h / max(1, h))))
 
198
  im2 = im.resize((new_w, new_h), Image.LANCZOS)
199
  im2_rgb = rgba_to_rgb_on_bg(im2, bg_rgb=bg_rgb)
200
  out = io.BytesIO(); im2_rgb.save(out, format="JPEG", quality=88, subsampling=1)
 
202
 
203
  def make_preview_frame(img_bytes: bytes, aspect: str, long_edge: int, ratio: float, bg_rgb=(255,255,255)) -> bytes:
204
  im = Image.open(io.BytesIO(img_bytes)).convert("RGBA")
205
+ aw, ah = _parse_aspect_str(aspect)
206
+ if aw >= ah:
207
+ W = max(256, min(8192, int(long_edge))); H = max(1, int(W * ah / aw))
208
  else:
209
+ H = max(256, min(8192, int(long_edge))); W = max(1, int(H * aw / ah))
210
  ratio = max(0.10, min(0.99, float(ratio)))
211
+ target = int(min(W, H) * ratio if aw == ah else (H * ratio if aw < ah else W * ratio))
212
  w, h = im.size
213
  if w >= h: new_w, new_h = target, max(1, int(h * (target / max(1, w))))
214
  else: new_h, new_w = target, max(1, int(w * (target / max(1, h))))
 
218
  out = io.BytesIO(); canvas.convert("RGB").save(out, format="JPEG", quality=90, subsampling=1)
219
  return out.getvalue()
220
 
221
+ # ==== Local demo files (optional) ====
222
  def load_local_demo():
223
  demo = {}
224
+ for fname in ["input.jpg", "packshot.jpg", "scene.jpg", "tryon.mp4"]:
225
  if os.path.exists(fname):
226
  with open(fname, "rb") as f: demo[fname] = f.read()
227
  return demo
 
228
  demo_files = load_local_demo()
229
 
230
+ # ==== Configurator data ====
231
+ LAMBOMODELS = [
232
+ "Revuelto", "Huracán Tecnica", "Huracán Sterrato", "Huracán EVO",
233
+ "Urus S", "Urus Performante", "Sián", "Aventador SVJ (legacy)"
234
+ ]
235
+ FINISHES = ["Gloss", "Matte", "Satin", "Pearl"]
236
+ WHEELS = ["Hexagon Forged", "Narvi", "Minotauro", "Taigete", "Shiny Black", "Graphite Gray"]
237
+ CALIPERS = ["Yellow", "Black", "Red", "Green", "Orange", "Silver"]
238
+ BODY_COLORS = [
239
+ "Giallo Auge", "Arancio Borealis", "Verde Mantis", "Blu Nila",
240
+ "Grigio Telesto", "Nero Noctis", "Bianco Monocerus", "Rosso Mars"
241
+ ]
 
 
 
 
 
 
 
 
 
242
  SCENE_PRESETS = {
243
+ "Studio Clean": "premium studio, neutral gradient backdrop, soft rim light, glossy floor reflections",
244
+ "Monaco Sunset": "Monaco harbor at golden hour, warm reflections on paint, soft shadows",
245
+ "Dubai Night": "Dubai skyline at night, neon reflections, high-contrast lighting, luxurious vibe",
246
+ "Track Day": "race track pit lane, subtle motion cues, realistic asphalt reflections",
247
+ "Mountain Pass": "alpine pass road, crisp air, cool daylight, cinematic depth"
248
  }
249
 
250
+ # ==== App ====
251
+ st.set_page_config(page_title="LamboVision · AI Car Studio", layout="wide", page_icon="🏎️")
 
 
 
 
 
 
 
252
  inject_base_css()
253
 
254
  st.title("🏎️ LamboVision · AI Car Studio")
255
+ st.caption("Upload → Configure → Scene → Cinematic")
256
 
257
  ok = backend_ok()
258
 
259
+ # ---- Top controls ----
260
+ col1, col2, col3, col4, col5, col6, col7 = st.columns([1.6, 1.6, 1.2, 1.4, 1.0, 1.0, 1.2])
261
  with col1:
262
  file_list = st.file_uploader("Car Image (Upload)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True, key="file_upl")
263
  with col2:
264
  image_url = st.text_input("Or URL", key="image_url", placeholder="https://... (optional)")
265
  with col3:
266
+ model_sel = st.selectbox("Model", LAMBOMODELS, index=0, key="lv_model")
267
  with col4:
268
+ body_color = st.selectbox("Body Color", BODY_COLORS, index=0, key="lv_color")
 
269
  with col5:
270
+ finish = st.selectbox("Finish", FINISHES, index=0, key="lv_finish")
271
  with col6:
272
+ wheel = st.selectbox("Wheel Style", WHEELS, index=0, key="lv_wheel")
273
  with col7:
274
+ caliper = st.selectbox("Caliper Color", CALIPERS, index=0, key="lv_caliper")
275
 
276
+ colA, colB, colC, colD, colE, colF = st.columns([1.4, 1.4, 1.2, 1.0, 1.2, 1.2])
277
  with colA:
278
+ extra_prompt = st.text_input("Extra (optional)", placeholder="carbon mirrors, dual stripe delete, etc.", key="extra_prompt")
279
  with colB:
280
+ scene_name = st.selectbox("Scene Preset", list(SCENE_PRESETS.keys()), index=0, key="scene_preset")
281
  with colC:
282
+ preview_ratio = st.number_input("Preview Scale (0.10–0.99)", min_value=0.10, max_value=0.99, value=0.90, step=0.01, key="preview_ratio")
283
  with colD:
284
+ upscale = st.checkbox("Super Res", False, key="upscale")
285
  with colE:
286
+ upscale_stage = st.selectbox("SR Step", ["both","final","packshot"], index=0, help="Default: both", key="upscale_stage")
287
  with colF:
288
+ identity_lock = st.checkbox("Identity lock", True, key="identity_lock")
289
+
290
+ colT1, colT2, colT3, colT4 = st.columns([1.0, 1.0, 1.0, 1.2])
291
+ with colT1:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  preview_aspect = st.selectbox("Preview Frame Ratio", ASPECT_OPTIONS, index=0, key="preview_aspect")
293
+ with colT2:
294
  preview_long_edge = st.number_input("Preview Long Edge (px)", min_value=256, max_value=8192, value=1920, step=64, key="preview_long_edge")
295
+ with colT3:
296
+ fit_mode = st.selectbox("Preview Layout", ["fit","fill"], index=0, help="Affects previews only.", key="fit_mode")
297
+ with colT4:
298
+ st.info(f"Connection: {'Online' if ok else 'Offline'}", icon="🔌")
299
 
300
+ # Inject CSS for aspect
301
  inject_preview_css(preview_aspect, fit_mode="contain" if fit_mode=="fit" else "cover")
302
 
303
+ # ==== Build edit prompt (cars) ====
304
+ def build_edit_prompt(model, color, finish, wheel, caliper, extra):
305
+ parts = [
306
+ f"Lamborghini {model}",
307
+ f"body color: {color}",
308
+ f"finish: {finish}",
309
+ f"wheel style: {wheel}",
310
+ f"brake calipers: {caliper}",
311
+ "catalog-clean alignment, keep true geometry, badges and proportions, realistic reflections"
312
+ ]
313
+ if extra and extra.strip():
314
+ parts.append(extra.strip())
315
+ return ", ".join(parts)
316
+
317
+ # ==== Pre-run PREVIEWS (5 cards) ====
318
+ st.subheader("Previews (9:16)")
319
+ c_in, c_pack, c_pad, c_scene, c_video = st.columns(5)
320
+
321
+ # Input bytes
322
  input_bytes = None
323
  if file_list:
324
  try: input_bytes = file_list[0].getvalue()
 
328
  elif demo_files.get("input.jpg"):
329
  input_bytes = demo_files["input.jpg"]
330
 
331
+ def show_tile_card_display_download(
332
+ col, title,
333
+ display_url: str | None = None,
334
+ display_bytes: bytes | None = None,
335
+ download_url: str | None = None,
336
+ download_bytes: bytes | None = None,
337
+ filename_stub: str = "image",
338
+ key_prefix: str = "",
339
+ ):
340
+ col.markdown(f'<div class="lv-card-title">{title}</div>', unsafe_allow_html=True)
341
+ if not display_url and not display_bytes:
342
+ col.markdown('<div class="lv-frame"></div>', unsafe_allow_html=True)
343
+ col.caption("—"); return None, None
344
+
345
+ if display_bytes is not None:
346
+ src = image_bytes_to_data_url(display_bytes)
347
+ col.markdown(f'<div class="lv-frame"><img src="{src}"/></div>', unsafe_allow_html=True)
348
+ else:
349
+ abs_url = _abs_backend_url(display_url)
350
+ col.markdown(f'<div class="lv-frame"><img src="{abs_url}"/></div>', unsafe_allow_html=True)
351
+
352
+ native = download_bytes or (fetch_bytes(download_url) if download_url else None)
353
+ if not native:
354
+ native = display_bytes
355
+
356
+ if native:
357
+ sz = image_size_from_bytes(native)
358
+ if sz: col.caption(f"Real Res: {sz[0]}×{sz[1]} px")
359
+ ext = infer_ext(native)
360
+ mime = "image/jpeg" if ext.lower() in [".jpg",".jpeg"] else ("image/png" if ext.lower()==".png" else "image/webp")
361
+ col.download_button("Download (Full Quality)", data=native,
362
+ file_name=f"{filename_stub}{ext if ext!='.bin' else '.jpg'}",
363
+ mime=mime, key=f"{key_prefix}_{filename_stub}_dl")
364
+ else:
365
+ col.caption("—")
366
+
367
+ return display_url or "data", native
368
+
369
  show_tile_card_display_download(
370
+ c_in, "Sample Input preview",
371
  display_bytes=input_bytes if input_bytes else demo_files.get("input.jpg"),
372
  download_bytes=input_bytes if input_bytes else demo_files.get("input.jpg"),
373
  filename_stub="input", key_prefix="pre"
 
375
 
376
  pack_bytes_demo = demo_files.get("packshot.jpg") or input_bytes
377
  show_tile_card_display_download(
378
+ c_pack, "Sample Packshot (Clean/Configured)",
379
  display_bytes=pack_bytes_demo,
380
  download_bytes=pack_bytes_demo,
381
  filename_stub="packshot", key_prefix="pre"
 
390
  except Exception:
391
  padded_bytes = None
392
  show_tile_card_display_download(
393
+ c_pad, "Sample Padded (9:16 UI)",
394
  display_bytes=padded_bytes or pack_bytes_demo,
395
  download_bytes=padded_bytes or pack_bytes_demo,
396
  filename_stub="padded_ui", key_prefix="pre"
397
  )
398
 
399
+ scene_bytes_demo = demo_files.get("scene.jpg") or pack_bytes_demo
400
+ show_tile_card_display_download(
401
+ c_scene, "Sample Scene Composite",
402
+ display_bytes=scene_bytes_demo,
403
+ download_bytes=scene_bytes_demo,
404
+ filename_stub="scene_composite", key_prefix="pre"
405
+ )
406
+
407
+ c_video.markdown('<div class="lv-card-title">AI Generated Video Preview</div>', unsafe_allow_html=True)
408
  vid_demo = demo_files.get("tryon.mp4")
 
409
  if vid_demo:
410
  data_url = "data:video/mp4;base64," + base64.b64encode(vid_demo).decode("ascii")
411
+ c_video.markdown(
412
  f"""
413
+ <div class="lv-frame" style="display:flex;align-items:center;justify-content:center;">
414
  <video autoplay loop muted playsinline style="width:100%;height:100%;object-fit:cover;">
415
  <source src="{data_url}" type="video/mp4">
416
  </video>
417
  </div>
418
  """, unsafe_allow_html=True
419
  )
420
+ c_video.download_button("Download Video (MP4)", data=vid_demo, file_name="sample_tryon.mp4", mime="video/mp4", key="pre_video_dl")
421
  else:
422
+ c_video.markdown('<div class="lv-frame"></div>', unsafe_allow_html=True)
423
+ c_video.caption("—")
424
 
425
+ # ==== Actions ====
426
+ colAct1, colAct2, colAct3, colAct4, colAct5 = st.columns([1.0, 1.0, 1.2, 1.0, 1.2])
427
+ with colAct1:
428
+ to_video = st.checkbox("One Click Generate Video", False, key="to_video")
429
+ with colAct2:
430
+ duration = st.selectbox("Duration", VIDEO_DUR_OPTIONS, index=0, key="duration_sel")
431
+ with colAct3:
432
+ resolution = st.selectbox("Video Res", VIDEO_RES_OPTIONS, index=1, key="resolution_sel")
433
+ with colAct4:
434
+ do_scene_pass = st.checkbox("Scene Composite", True, key="do_scene_pass")
435
+ with colAct5:
436
+ run = st.button("Run ▶", type="primary", use_container_width=True, key="run_btn")
437
+
438
+ # ==== Run pipeline ====
439
  if run:
440
  if not ok:
441
+ st.error("Backend unreachable. Check API_BASE / TOKEN.")
442
  elif not (file_list or image_url or demo_files.get("input.jpg")):
443
+ st.error("Provide at least one image (upload or URL).")
444
  else:
445
  try:
446
+ # Build prompts
447
+ edit_prompt = build_edit_prompt(model_sel, body_color, finish, wheel, caliper, extra_prompt)
448
+ scene_text = SCENE_PRESETS.get(scene_name, "premium studio scene")
 
 
 
 
 
 
 
449
  img_urls = image_url or ""
450
+
451
  files_payload = []
452
  if file_list:
453
  for f in file_list:
454
  files_payload.append(("files", (f.name, f.getvalue(), f.type or "image/jpeg")))
455
 
456
  data = {
457
+ "category": "auto",
458
+ "edit_prompt": edit_prompt,
459
  "num_images": "1",
460
  "image_urls": img_urls,
461
+ # scene pass
462
+ "do_scene_pass": "true" if do_scene_pass else "false",
463
+ "scene_prompt_text": scene_text,
464
+ # identity lock (backendside rules)
465
  "identity_lock": "true" if identity_lock else "false",
466
+ # canvas/preview
467
  "preview_aspect": preview_aspect,
468
  "preview_long_edge": str(int(preview_long_edge)),
469
  "preview_ratio": str(float(preview_ratio)),
 
470
  # SR
471
  "upscale": "true" if upscale else "false",
472
+ "upscale_factor": "2",
473
  "upscale_stage": st.session_state.get("upscale_stage","both"),
474
+ # video
475
  "to_video": "true" if to_video else "false",
476
+ "video_prompt": scene_text, # same vibe for camera move
477
  "duration": st.session_state.get("duration_sel", VIDEO_DUR_OPTIONS[0]),
478
  "resolution": st.session_state.get("resolution_sel", VIDEO_RES_OPTIONS[1]),
479
  "prompt_optimizer": "false",
480
  }
481
 
482
+ with st.spinner("LamboVision pipeline running…"):
483
  out = post_chain(data, files_payload if files_payload else None)
484
 
485
  st.session_state["job_counter"] += 1
486
 
487
+ # Parse outputs
488
  pack_native_url = None; pack_display_jpeg = None
489
  try:
490
  pack_native_url = (out["packshot_step"]["result"]["images_native"] or [{}])[0].get("url")
 
501
  if nb: padded_display_jpeg = make_small_jpeg_from_native(nb, 1280, bg_rgb=(255,255,255))
502
  except Exception: pass
503
 
504
+ scene_native_url = None; scene_display_jpeg = None
505
  try:
506
+ if out.get("scene_step"):
507
+ scene_native_url = (out["scene_step"]["result"]["images_native"] or [{}])[0].get("url")
508
+ if scene_native_url:
509
+ sb = fetch_bytes(scene_native_url)
510
+ if sb: scene_display_jpeg = make_small_jpeg_from_native(sb, 1280, bg_rgb=(255,255,255))
511
  except Exception: pass
512
 
513
  vurl = None
 
527
  "packshot_display_jpeg": pack_display_jpeg,
528
  "padded_native_url": padded_native_url,
529
  "padded_display_jpeg": padded_display_jpeg,
530
+ "scene_native_url": scene_native_url,
531
+ "scene_display_jpeg": scene_display_jpeg,
532
  "video_url": vurl,
533
  "api_base": API_BASE,
534
  "has_auth_header": bool(HEADERS),
535
  "preview_aspect": preview_aspect,
536
+ "scene_vendor_url": out.get("scene_vendor_url"),
537
+ "scene_prompt_text": scene_text,
538
+ "edit_prompt": edit_prompt,
539
  }
540
 
541
  except requests.HTTPError as e:
542
  st.error(f"HTTP {e.response.status_code}")
543
  except Exception as e:
544
+ st.error(f"Error: {e}")
545
 
546
+ # ==== Results ====
547
  res = st.session_state.get("results")
548
  vbytes = None
549
  if res:
550
  st.subheader("Results")
551
+ c_pack2, c_pad2, c_scene2, c_video2 = st.columns(4)
552
 
553
  show_tile_card_display_download(
554
+ c_pack2, "Configured Packshot",
555
  display_bytes=res.get("packshot_display_jpeg"),
556
  download_url=res.get("packshot_native_url"),
557
  filename_stub="packshot_native",
 
559
  )
560
 
561
  show_tile_card_display_download(
562
+ c_pad2, "Padded (9:16 Canvas)",
563
  display_bytes=res.get("padded_display_jpeg"),
564
  download_url=res.get("padded_native_url"),
565
  filename_stub="padded_backend",
 
567
  )
568
 
569
  show_tile_card_display_download(
570
+ c_scene2, "Scene Composite",
571
+ display_bytes=res.get("scene_display_jpeg"),
572
+ download_url=res.get("scene_native_url"),
573
+ filename_stub="scene_native",
574
  key_prefix=f"job{st.session_state['job_counter']}"
575
  )
576
 
577
+ # Video
578
  vurl = res.get("video_url")
579
+ c_video2.markdown('<div class="lv-card-title">AI Cinematic</div>', unsafe_allow_html=True)
580
  if vurl:
581
  abs_v = _abs_backend_url(vurl)
582
+ c_video2.markdown(
583
  f"""
584
+ <div class="lv-frame" style="display:flex;align-items:center;justify-content:center;">
585
  <video autoplay loop muted controls playsinline style="width:100%;height:100%;object-fit:cover;">
586
  <source src="{abs_v}" type="video/mp4">
587
  </video>
 
591
  vb = fetch_bytes(abs_v)
592
  if vb:
593
  vbytes = vb
594
+ c_video2.download_button("Download Video (MP4)", data=vb, file_name="lambo_vision_video.mp4",
595
+ mime="video/mp4", key=f"job{st.session_state['job_counter']}_video_dl")
596
  else:
597
+ c_video2.markdown('<div class="lv-frame"></div>', unsafe_allow_html=True)
598
+ # On-demand video from scene or packshot
599
  vid_checkbox_key = f"video_toggle_job{st.session_state['job_counter']}"
600
+ if st.checkbox("Generate Video From Scene", value=False, key=vid_checkbox_key):
601
+ src_for_video = res.get("scene_native_url") or res.get("packshot_native_url")
602
  if not src_for_video:
603
+ st.warning("No image found for video.")
604
  else:
605
+ with st.spinner("Generating video …"):
606
  try:
607
  v = post_video(
608
  src_for_video,
609
  duration=st.session_state.get("duration_sel", VIDEO_DUR_OPTIONS[0]),
610
  resolution=st.session_state.get("resolution_sel", VIDEO_RES_OPTIONS[1]),
611
+ preview_aspect=st.session_state.get("preview_aspect","9:16"),
612
+ prompt=res.get("scene_prompt_text")
 
 
613
  )
614
  except requests.HTTPError as e:
615
  st.error(f"HTTP {e.response.status_code}")
 
622
  if isinstance(ui_hints, dict):
623
  st.session_state["video_ui_max_px"] = int(ui_hints.get("suggested_video_max_px", VIDEO_MAX_PX_DEFAULT))
624
  st.session_state.pop(vid_checkbox_key, None)
625
+ st.success("Video is ready.")
626
  st.rerun()
627
  else:
628
+ st.info("No video URL returned.")
629
 
630
+ # ZIP bundle
631
  named = []
632
  pn = fetch_bytes(res.get("packshot_native_url")) if res.get("packshot_native_url") else None
633
+ sn = fetch_bytes(res.get("scene_native_url")) if res.get("scene_native_url") else None
634
  if pn: named.append((f"packshot_native{infer_ext(pn)}", pn))
635
+ if sn: named.append((f"scene_native{infer_ext(sn)}", sn))
636
  if vbytes: named.append(("video.mp4", vbytes))
637
  if named:
638
  zip_bytes = make_zip(named)
639
  st.download_button("Download All (ZIP)", data=zip_bytes,
640
+ file_name="lambo_vision_outputs.zip", mime="application/zip",
641
  key=f"job{st.session_state['job_counter']}_zip_dl")
642
 
643
+ # ==== Sidebar ====
644
  with st.sidebar:
645
  st.caption(f"API_BASE: {API_BASE}")
646
+ if HF_TOKEN: st.caption("Auth: Bearer ✓")
647
  st.slider("Video preview width (px)", 360, 1080, st.session_state["video_ui_max_px"],
648
  step=10, key="video_ui_max_px")
649
  if st.button("Clear cache"): st.cache_data.clear(); st.success("Cache cleared.")
650
+ if st.button("Clear last results"): st.session_state["results"] = None; st.success("Results cleared.")