MogensR commited on
Commit
dd0209f
·
verified ·
1 Parent(s): a409c45

Update streamlit_app.py

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