MogensR commited on
Commit
8e80ab9
·
verified ·
1 Parent(s): f10d25c

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +87 -423
streamlit_app.py CHANGED
@@ -1,65 +1,61 @@
1
- # ==================================================================================
2
- # streamlit_ui.py
3
- # STREAMLIT VIDEO BACKGROUND REPLACER - MAIN APPLICATION (HF-Ready Logging)
4
- # ==================================================================================
5
- # Single-button workflow: Upload video + background → Process → Download result
6
- # Uses SAM2 + MatAnyone pipeline with temporal smoothing
7
- # Adds robust logging (stdout + rotating file), in-app log tail viewer,
8
- # and a "Download logs" button for /tmp/app.log
9
- # ==================================================================================
10
-
11
- # =========================================
12
- # CHAPTER 1: IMPORTS AND SETUP
13
- # =========================================
14
-
15
- import streamlit as st
16
  import os
17
  import sys
18
  import time
19
  from pathlib import Path
20
- import cv2
21
- import numpy as np
22
- from PIL import Image
23
  import logging
24
  import logging.handlers
25
- import io
26
- import torch
27
  import traceback
28
  import uuid
29
  from datetime import datetime
30
  from tempfile import NamedTemporaryFile
31
  import subprocess
 
 
 
 
 
32
 
33
- # Project Setup
34
- sys.path.append(str(Path(__file__).parent.absolute()))
35
 
36
  # Import pipeline functions
37
- from pipeline.video_pipeline import stage1_create_transparent_video, stage2_composite_background
 
 
 
 
 
 
38
 
 
39
  APP_NAME = "Advanced Video Background Replacer"
40
- LOG_FILE = "/tmp/app.log" # HF Spaces: writable, survives session
41
  LOG_MAX_BYTES = 5 * 1024 * 1024
42
  LOG_BACKUPS = 5
43
 
44
- # =========================================
45
- # CHAPTER 2: LOGGING
46
- # =========================================
47
-
48
  def setup_logging(level: int = logging.INFO) -> logging.Logger:
49
  logger = logging.getLogger(APP_NAME)
50
  logger.setLevel(level)
51
- logger.propagate = False # avoid double logs in Streamlit
52
 
53
  # Clear previous handlers on rerun
54
  for h in list(logger.handlers):
55
  logger.removeHandler(h)
56
 
57
- # Console handler (stdout → visible in HF logs)
58
  ch = logging.StreamHandler(sys.stdout)
59
  ch.setLevel(level)
60
  ch.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
61
 
62
- # Rotating file handler (local tailing inside the app)
63
  fh = logging.handlers.RotatingFileHandler(
64
  LOG_FILE, maxBytes=LOG_MAX_BYTES, backupCount=LOG_BACKUPS, encoding="utf-8"
65
  )
@@ -72,133 +68,13 @@ def setup_logging(level: int = logging.INFO) -> logging.Logger:
72
 
73
  logger = setup_logging()
74
 
75
- # Global Exception Hook
76
  def custom_excepthook(type, value, tb):
77
  logger.error(f"Unhandled: {type.__name__}: {value}\n{''.join(traceback.format_tb(tb))}", exc_info=True)
78
- sys.excepthook = custom_excepthook
79
 
80
- # =========================================
81
- # CHAPTER 3: DIAGNOSTICS HELPERS
82
- # =========================================
83
-
84
- def try_run(cmd, timeout=5):
85
- """Run a shell command safely and return (ok, stdout|stderr)."""
86
- try:
87
- out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, timeout=timeout, text=True)
88
- return True, out.strip()
89
- except Exception as e:
90
- return False, str(e)
91
-
92
- def tail_file(path: str, lines: int = 400) -> str:
93
- """Read last N lines from a text file efficiently."""
94
- if not os.path.exists(path):
95
- return "(log file not found)"
96
- try:
97
- # A simple, safe tail (files are small due to rotation)
98
- with open(path, "r", encoding="utf-8", errors="replace") as f:
99
- content = f.readlines()
100
- return "".join(content[-lines:])
101
- except Exception as e:
102
- return f"(failed to read log: {e})"
103
-
104
- def read_file_bytes(path: str) -> bytes:
105
- """Read a file as bytes; return b'' if missing/error."""
106
- try:
107
- if not os.path.exists(path):
108
- return b""
109
- with open(path, "rb") as f:
110
- return f.read()
111
- except Exception:
112
- return b""
113
-
114
- def check_gpu(logger: logging.Logger):
115
- """Check GPU availability and log details."""
116
- logger.info("=" * 60)
117
- logger.info("GPU DIAGNOSTIC")
118
- logger.info("=" * 60)
119
-
120
- logger.info(f"Python: {sys.version.split()[0]}")
121
- logger.info(f"Torch: {torch.__version__}")
122
- logger.info(f"CUDA compiled version in torch: {torch.version.cuda}")
123
-
124
- cuda_available = torch.cuda.is_available()
125
- logger.info(f"torch.cuda.is_available(): {cuda_available}")
126
-
127
- # Try to probe nvidia-smi if present
128
- ok, out = try_run(["bash", "-lc", "command -v nvidia-smi && nvidia-smi -L && nvidia-smi -q -d MEMORY | head -n 40"], timeout=6)
129
- if ok:
130
- logger.info("nvidia-smi probe:\n" + out)
131
- else:
132
- logger.info(f"nvidia-smi probe not available: {out}")
133
-
134
- if cuda_available:
135
- try:
136
- count = torch.cuda.device_count()
137
- logger.info(f"CUDA Device Count: {count}")
138
- for i in range(count):
139
- logger.info(f"Device {i}: {torch.cuda.get_device_name(i)}")
140
- torch.cuda.set_device(0)
141
- logger.info("Set CUDA device 0 as default")
142
- test_tensor = torch.randn(64, 64).cuda()
143
- logger.info(f"GPU Test: SUCCESS on {test_tensor.device}")
144
- del test_tensor
145
- torch.cuda.empty_cache()
146
- except Exception as e:
147
- logger.error(f"GPU test failed: {e}", exc_info=True)
148
- else:
149
- logger.warning("CUDA NOT AVAILABLE")
150
-
151
- logger.info("=" * 60)
152
- return cuda_available
153
-
154
- def dump_env(logger: logging.Logger):
155
- keys_of_interest = [
156
- "HF_HOME", "HF_TOKEN", "PYTORCH_CUDA_ALLOC_CONF", "CUDA_VISIBLE_DEVICES",
157
- "TORCH_ALLOW_TF32_CUBLAS_OVERRIDE", "OMP_NUM_THREADS", "NUMEXPR_MAX_THREADS",
158
- "PYTHONPATH", "PATH"
159
- ]
160
- logger.info("=== ENV VARS (sanitized) ===")
161
- for k in keys_of_interest:
162
- v = os.environ.get(k)
163
- if not v:
164
- continue
165
- # Mask tokens/secrets
166
- if "TOKEN" in k or "KEY" in k or "SECRET" in k or "PASSWORD" in k:
167
- v = "[REDACTED]"
168
- logger.info(f"{k}={v}")
169
- logger.info("============================")
170
-
171
- # =========================================
172
- # CHAPTER 4: STREAMLIT CONFIGURATION
173
- # =========================================
174
-
175
- st.set_page_config(
176
- page_title=APP_NAME,
177
- page_icon="🎥",
178
- layout="wide",
179
- initial_sidebar_state="expanded"
180
- )
181
-
182
- # Custom CSS
183
- st.markdown("""
184
- <style>
185
- .main .block-container { padding-top: 1.0rem; padding-bottom: 2rem; }
186
- .stButton>button {
187
- width: 100%;
188
- background-color: #4CAF50;
189
- color: white;
190
- font-weight: bold;
191
- transition: all 0.3s;
192
- }
193
- .stButton>button:hover { background-color: #45a049; }
194
- .small-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; white-space: pre-wrap; }
195
- </style>
196
- """, unsafe_allow_html=True)
197
-
198
- # =========================================
199
- # CHAPTER 5: SESSION STATE
200
- # =========================================
201
 
 
202
  def initialize_session_state():
203
  defaults = {
204
  'uploaded_video': None,
@@ -224,296 +100,84 @@ def initialize_session_state():
224
  st.session_state[k] = v
225
 
226
  if st.session_state.gpu_available is None:
227
- # On first load, run diagnostics once
228
  dump_env(logger)
229
  st.session_state.gpu_available = check_gpu(logger)
230
 
 
231
  def set_log_level(name: str):
232
  name = (name or "INFO").upper()
233
  lvl = getattr(logging, name, logging.INFO)
234
- setup_logging(lvl) # rebuild handlers at new level
235
  global logger
236
  logger = logging.getLogger(APP_NAME)
237
  logger.setLevel(lvl)
238
  st.session_state.log_level_name = name
239
  logger.info(f"Log level set to {name}")
240
 
241
- # =========================================
242
- # CHAPTER 6: MAIN APP
243
- # =========================================
 
 
 
244
 
245
- def main():
246
- st.title(APP_NAME)
247
- st.caption("HF-ready with robust logs, live tail, and log download")
248
- st.markdown("---")
249
 
250
- initialize_session_state()
251
-
252
- # ------------- Sidebar: System & Logs -----------------
253
- with st.sidebar:
254
- st.subheader("System Status")
255
-
256
- # Log level control
257
- log_level = st.selectbox(
258
- "Log level",
259
- ["DEBUG", "INFO", "WARNING", "ERROR"],
260
- index=["DEBUG", "INFO", "WARNING", "ERROR"].index(st.session_state.log_level_name),
261
- key="log_level_select"
262
- )
263
- if log_level != st.session_state.log_level_name:
264
- set_log_level(log_level)
265
-
266
- if st.session_state.gpu_available:
267
- try:
268
- dev = torch.cuda.get_device_name(0)
269
- except Exception:
270
- dev = "Detected (name unavailable)"
271
- st.success(f"GPU: {dev}")
272
- else:
273
- st.error("GPU: Not Available")
274
-
275
- st.markdown("**Log file:** `/tmp/app.log`")
276
- st.checkbox("Auto-refresh log tail (every 2s)", key="auto_refresh_logs")
277
-
278
- # --- Download logs button ---
279
- log_bytes = read_file_bytes(LOG_FILE)
280
- st.download_button(
281
- "Download logs",
282
- data=log_bytes if log_bytes else b"Log file not available yet.",
283
- file_name="app.log",
284
- mime="text/plain",
285
- use_container_width=True,
286
- disabled=not bool(log_bytes)
287
- )
288
-
289
- st.number_input("Tail last N lines", min_value=50, max_value=5000, step=50, key="log_tail_lines")
290
- if st.button("Refresh Logs"):
291
- st.session_state._force_log_refresh = True # trigger rerun
292
-
293
- with st.expander("View Log Tail", expanded=True):
294
- # Auto-refresh: trigger reruns
295
- if st.session_state.auto_refresh_logs:
296
- time.sleep(2)
297
- st.rerun()
298
- log_text = tail_file(LOG_FILE, st.session_state.log_tail_lines)
299
- st.code(log_text, language="text")
300
-
301
- # ------------- Error Display -----------------
302
- if st.session_state.last_error:
303
- with st.expander("⚠️ Last Error", expanded=False):
304
- st.error(st.session_state.last_error)
305
- if st.button("Clear Error"):
306
- st.session_state.last_error = None
307
- st.rerun()
308
-
309
- col1, col2 = st.columns([1, 1], gap="large")
310
-
311
- # ------------- Column 1: Video Upload -----------------
312
- with col1:
313
- st.header("1. Upload Video")
314
- uploaded = st.file_uploader(
315
- "Upload Video",
316
- type=["mp4", "mov", "avi", "mkv", "webm"],
317
- key="video_uploader"
318
- )
319
-
320
- current_video_id = id(uploaded)
321
- if uploaded is not None and current_video_id != st.session_state.last_video_id:
322
- logger.info(f"[UI] New video selected: name={uploaded.name}, size≈{getattr(uploaded, 'size', 'n/a')} bytes")
323
- st.session_state.uploaded_video = uploaded
324
- st.session_state.last_video_id = current_video_id
325
- st.session_state.video_bytes_cache = None
326
- st.session_state.processed_video_bytes = None
327
- st.session_state.last_error = None
328
-
329
- st.markdown("### Video Preview")
330
- if st.session_state.video_preview_placeholder is None:
331
- st.session_state.video_preview_placeholder = st.empty()
332
-
333
- if st.session_state.uploaded_video is not None:
334
- try:
335
- if st.session_state.video_bytes_cache is None:
336
- logger.info("[UI] Caching uploaded video for preview...")
337
- st.session_state.uploaded_video.seek(0)
338
- st.session_state.video_bytes_cache = st.session_state.uploaded_video.read()
339
- logger.info(f"[UI] Cached {len(st.session_state.video_bytes_cache)/1e6:.2f} MB for preview")
340
-
341
- with st.session_state.video_preview_placeholder.container():
342
- st.video(st.session_state.video_bytes_cache)
343
-
344
- except Exception as e:
345
- logger.error(f"[UI] Video preview error: {e}", exc_info=True)
346
- st.session_state.video_preview_placeholder.error(f"Cannot display video: {e}")
347
- else:
348
- st.session_state.video_preview_placeholder.empty()
349
-
350
- # ------------- Column 2: Background + Processing -----------------
351
- with col2:
352
- st.header("2. Background Settings")
353
-
354
- bg_type = st.radio(
355
- "Select Background Type:",
356
- ["Image", "Color"],
357
- horizontal=True,
358
- key="bg_type_radio"
359
- )
360
-
361
- # Background Image
362
- if bg_type == "Image":
363
- bg_image = st.file_uploader(
364
- "Upload Background Image",
365
- type=["jpg", "png", "jpeg"],
366
- key="bg_image_uploader"
367
- )
368
- current_bg_id = id(bg_image)
369
- if bg_image is not None and current_bg_id != st.session_state.last_bg_image_id:
370
- logger.info(f"[UI] New background image: {bg_image.name}")
371
- st.session_state.last_bg_image_id = current_bg_id
372
- try:
373
- st.session_state.bg_image_cache = Image.open(bg_image)
374
- # Lazy convert to display; real convert is just before Stage 2
375
- except Exception as e:
376
- st.session_state.bg_image_cache = None
377
- logger.error(f"[UI] Failed to load background image: {e}", exc_info=True)
378
-
379
- if st.session_state.bg_preview_placeholder is None:
380
- st.session_state.bg_preview_placeholder = st.empty()
381
-
382
- if st.session_state.bg_image_cache is not None:
383
- with st.session_state.bg_preview_placeholder.container():
384
- st.image(st.session_state.bg_image_cache, caption="Selected Background", use_container_width=True)
385
- else:
386
- st.session_state.bg_preview_placeholder.empty()
387
-
388
- # Background Color
389
- else:
390
- selected_color = st.color_picker(
391
- "Choose Background Color",
392
- st.session_state.bg_color,
393
- key="color_picker"
394
- )
395
- if selected_color != st.session_state.cached_color:
396
- logger.info(f"[UI] New background color: {selected_color}")
397
- st.session_state.bg_color = selected_color
398
- st.session_state.cached_color = selected_color
399
-
400
- color_rgb = tuple(int(selected_color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
401
- color_display = np.zeros((100, 100, 3), dtype=np.uint8)
402
- color_display[:, :] = color_rgb
403
- st.session_state.color_display_cache = color_display
404
-
405
- if st.session_state.bg_preview_placeholder is None:
406
- st.session_state.bg_preview_placeholder = st.empty()
407
-
408
- if st.session_state.color_display_cache is not None:
409
- with st.session_state.bg_preview_placeholder.container():
410
- st.image(st.session_state.color_display_cache, caption="Selected Color", width=200)
411
- else:
412
- st.session_state.bg_preview_placeholder.empty()
413
-
414
- # ------------- Processing -------------
415
- st.header("3. Process Video")
416
- can_process = (st.session_state.uploaded_video is not None and not st.session_state.processing)
417
-
418
- if st.button("Process Video", disabled=not can_process, use_container_width=True):
419
- run_id = uuid.uuid4().hex[:8]
420
- logger.info("=" * 80)
421
- logger.info(f"[RUN {run_id}] VIDEO PROCESSING STARTED at {datetime.utcnow().isoformat()}Z")
422
- logger.info("=" * 80)
423
-
424
- st.session_state.processing = True
425
- st.session_state.processed_video_bytes = None
426
- st.session_state.last_error = None
427
-
428
- t0 = time.time()
429
- try:
430
- # Validate background
431
- if bg_type == "Image":
432
- if st.session_state.bg_image_cache is None:
433
- raise RuntimeError("Background type is Image, but no image was provided.")
434
- background = st.session_state.bg_image_cache.convert("RGB")
435
- logger.info(f"[RUN {run_id}] Using IMAGE background (RGB)")
436
- else:
437
- if not st.session_state.bg_color:
438
- raise RuntimeError("Background type is Color, but no color was selected.")
439
- background = st.session_state.bg_color
440
- logger.info(f"[RUN {run_id}] Using COLOR background: {background}")
441
-
442
- # Materialize uploaded video to temp file
443
- if st.session_state.video_bytes_cache is None:
444
- logger.info(f"[RUN {run_id}] Reading uploaded video into cache for processing...")
445
- st.session_state.uploaded_video.seek(0)
446
- st.session_state.video_bytes_cache = st.session_state.uploaded_video.read()
447
- logger.info(f"[RUN {run_id}] Cached {len(st.session_state.video_bytes_cache)/1e6:.2f} MB for processing")
448
-
449
- suffix = Path(st.session_state.uploaded_video.name).suffix or ".mp4"
450
- with NamedTemporaryFile(delete=False, suffix=suffix) as tmp_vid:
451
- tmp_vid.write(st.session_state.video_bytes_cache)
452
- tmp_vid_path = tmp_vid.name
453
-
454
- # Explicit debug about Stage 1 input bytes
455
- input_bytes_len = len(st.session_state.video_bytes_cache) if st.session_state.video_bytes_cache else 0
456
- logger.debug(f"[RUN {run_id}] Stage 1 input bytes: {input_bytes_len}")
457
-
458
- logger.info(f"[RUN {run_id}] Temp video path: {tmp_vid_path}")
459
-
460
- # Stage 1
461
- t1 = time.time()
462
- logger.info(f"[RUN {run_id}] Stage 1: Transparent video creation START")
463
- transparent_path = stage1_create_transparent_video(tmp_vid_path)
464
- if not transparent_path or not os.path.exists(transparent_path):
465
- raise RuntimeError("Stage 1 failed: Transparent video not created")
466
- logger.info(f"[RUN {run_id}] Stage 1: DONE → {transparent_path} (Δ {time.time()-t1:.2f}s)")
467
 
468
- # Stage 2
469
- t2 = time.time()
470
- logger.info(f"[RUN {run_id}] Stage 2: Compositing START (bg_type={bg_type.lower()})")
471
- final_path = stage2_composite_background(transparent_path, background, bg_type.lower())
472
- if not final_path or not os.path.exists(final_path):
473
- raise RuntimeError("Stage 2 failed: Final video not created")
474
- logger.info(f"[RUN {run_id}] Stage 2: DONE → {final_path} (Δ {time.time()-t2:.2f}s)")
475
 
476
- # Load final into memory (Streamlit download)
477
- with open(final_path, 'rb') as f:
478
- st.session_state.processed_video_bytes = f.read()
479
 
480
- total = time.time() - t0
481
- logger.info(f"[RUN {run_id}] SUCCESS size={len(st.session_state.processed_video_bytes)/1e6:.2f}MB, total Δ={total:.2f}s")
482
- st.success("Video processing complete!")
483
- st.session_state.last_error = None
484
 
485
- except Exception as e:
486
- total = time.time() - t0
487
- error_msg = f"[RUN {run_id}] Processing Error: {str(e)} (Δ {total:.2f}s)\n\nCheck logs for details."
488
- logger.error(error_msg)
489
- logger.error(traceback.format_exc())
490
- st.session_state.last_error = error_msg
491
- st.error(error_msg)
492
 
493
- finally:
494
- st.session_state.processing = False
495
- logger.info(f"[RUN {run_id}] Processing finished (processing flag cleared)")
496
 
497
- # Results
498
- if st.session_state.processed_video_bytes is not None:
499
- st.markdown("---")
500
- st.markdown("### Processed Video")
501
- try:
502
- st.video(st.session_state.processed_video_bytes)
503
- st.download_button(
504
- label="Download Processed Video",
505
- data=st.session_state.processed_video_bytes,
506
- file_name="processed_video.mp4",
507
- mime="video/mp4",
508
- use_container_width=True
509
- )
510
- except Exception as e:
511
- logger.error(f"[UI] Display error: {e}", exc_info=True)
512
- st.error(f"Display error: {e}")
 
 
 
 
513
 
514
- # =========================================
515
- # CHAPTER 7: ENTRY POINT
516
- # =========================================
517
 
518
  if __name__ == "__main__":
 
519
  main()
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Core Application Logic for Video Background Replacer
4
+ - Handles video processing pipeline (SAM2 + MatAnyone + FFmpeg)
5
+ - Integrates with UI (imported from ui.py)
6
+ - Manages session state and logging
7
+ """
 
 
 
 
 
 
 
 
8
  import os
9
  import sys
10
  import time
11
  from pathlib import Path
 
 
 
12
  import logging
13
  import logging.handlers
 
 
14
  import traceback
15
  import uuid
16
  from datetime import datetime
17
  from tempfile import NamedTemporaryFile
18
  import subprocess
19
+ import streamlit as st
20
+ import cv2
21
+ import numpy as np
22
+ from PIL import Image
23
+ import torch
24
 
25
+ # Import UI components
26
+ from ui import render_ui
27
 
28
  # Import pipeline functions
29
+ from pipeline.video_pipeline import (
30
+ stage1_create_transparent_video,
31
+ stage2_composite_background,
32
+ setup_t4_environment,
33
+ check_gpu,
34
+ dump_env
35
+ )
36
 
37
+ # --- Constants ---
38
  APP_NAME = "Advanced Video Background Replacer"
39
+ LOG_FILE = "/tmp/app.log"
40
  LOG_MAX_BYTES = 5 * 1024 * 1024
41
  LOG_BACKUPS = 5
42
 
43
+ # --- Logging Setup ---
 
 
 
44
  def setup_logging(level: int = logging.INFO) -> logging.Logger:
45
  logger = logging.getLogger(APP_NAME)
46
  logger.setLevel(level)
47
+ logger.propagate = False
48
 
49
  # Clear previous handlers on rerun
50
  for h in list(logger.handlers):
51
  logger.removeHandler(h)
52
 
53
+ # Console handler
54
  ch = logging.StreamHandler(sys.stdout)
55
  ch.setLevel(level)
56
  ch.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
57
 
58
+ # Rotating file handler
59
  fh = logging.handlers.RotatingFileHandler(
60
  LOG_FILE, maxBytes=LOG_MAX_BYTES, backupCount=LOG_BACKUPS, encoding="utf-8"
61
  )
 
68
 
69
  logger = setup_logging()
70
 
71
+ # --- Global Exception Hook ---
72
  def custom_excepthook(type, value, tb):
73
  logger.error(f"Unhandled: {type.__name__}: {value}\n{''.join(traceback.format_tb(tb))}", exc_info=True)
 
74
 
75
+ sys.excepthook = custom_excepthook
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ # --- Session State Initialization ---
78
  def initialize_session_state():
79
  defaults = {
80
  'uploaded_video': None,
 
100
  st.session_state[k] = v
101
 
102
  if st.session_state.gpu_available is None:
 
103
  dump_env(logger)
104
  st.session_state.gpu_available = check_gpu(logger)
105
 
106
+ # --- Set Log Level ---
107
  def set_log_level(name: str):
108
  name = (name or "INFO").upper()
109
  lvl = getattr(logging, name, logging.INFO)
110
+ setup_logging(lvl)
111
  global logger
112
  logger = logging.getLogger(APP_NAME)
113
  logger.setLevel(lvl)
114
  st.session_state.log_level_name = name
115
  logger.info(f"Log level set to {name}")
116
 
117
+ # --- Main Processing Function ---
118
+ def process_video(uploaded_video, bg_image, bg_color, bg_type):
119
+ run_id = uuid.uuid4().hex[:8]
120
+ logger.info("=" * 80)
121
+ logger.info(f"[RUN {run_id}] VIDEO PROCESSING STARTED at {datetime.utcnow().isoformat()}Z")
122
+ logger.info("=" * 80)
123
 
124
+ st.session_state.processing = True
125
+ st.session_state.processed_video_bytes = None
126
+ st.session_state.last_error = None
127
+ t0 = time.time()
128
 
129
+ try:
130
+ # Materialize uploaded video to temp file
131
+ suffix = Path(uploaded_video.name).suffix or ".mp4"
132
+ with NamedTemporaryFile(delete=False, suffix=suffix) as tmp_vid:
133
+ uploaded_video.seek(0)
134
+ tmp_vid.write(uploaded_video.read())
135
+ tmp_vid_path = tmp_vid.name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
+ # Stage 1: Create transparent video and extract audio
138
+ transparent_path, audio_path = stage1_create_transparent_video(tmp_vid_path)
139
+ if not transparent_path or not os.path.exists(transparent_path):
140
+ raise RuntimeError("Stage 1 failed: Transparent video not created")
 
 
 
141
 
142
+ # Stage 2: Composite with background and restore audio
143
+ background = bg_image.convert("RGB") if bg_type == "Image" else bg_color
144
+ final_path = stage2_composite_background(transparent_path, audio_path, background, bg_type.lower())
145
 
146
+ if not final_path or not os.path.exists(final_path):
147
+ raise RuntimeError("Stage 2 failed: Final video not created")
 
 
148
 
149
+ # Load final video into memory for download
150
+ with open(final_path, 'rb') as f:
151
+ st.session_state.processed_video_bytes = f.read()
 
 
 
 
152
 
153
+ total = time.time() - t0
154
+ logger.info(f"[RUN {run_id}] SUCCESS size={len(st.session_state.processed_video_bytes)/1e6:.2f}MB, total Δ={total:.2f}s")
155
+ return True
156
 
157
+ except Exception as e:
158
+ total = time.time() - t0
159
+ error_msg = f"[RUN {run_id}] Processing Error: {str(e)} (Δ {total:.2f}s)\n\nCheck logs for details."
160
+ logger.error(error_msg)
161
+ logger.error(traceback.format_exc())
162
+ st.session_state.last_error = error_msg
163
+ return False
164
+
165
+ finally:
166
+ st.session_state.processing = False
167
+ logger.info(f"[RUN {run_id}] Processing finished")
168
+
169
+ # --- Main App Entry Point ---
170
+ def main():
171
+ st.set_page_config(
172
+ page_title=APP_NAME,
173
+ page_icon="🎥",
174
+ layout="wide",
175
+ initial_sidebar_state="expanded"
176
+ )
177
 
178
+ initialize_session_state()
179
+ render_ui(process_video)
 
180
 
181
  if __name__ == "__main__":
182
+ setup_t4_environment()
183
  main()