Gaurav vashistha commited on
Commit
5e07b31
·
1 Parent(s): 7451451

Implement Fault Tolerance: UI polling timeout and backend FFmpeg detection

Browse files
Files changed (2) hide show
  1. stitch_continuity_dashboard/code.html +40 -22
  2. utils.py +16 -11
stitch_continuity_dashboard/code.html CHANGED
@@ -102,6 +102,10 @@
102
  <div class="size-1.5 rounded-full bg-green-500 animate-pulse"></div>
103
  <span class="text-[10px] font-bold text-green-500 uppercase tracking-wider">Online</span>
104
  </div>
 
 
 
 
105
  </div>
106
 
107
  <div class="flex items-center gap-3">
@@ -455,31 +459,45 @@
455
  if (!res.ok) throw new Error(res.statusText);
456
  const data = await res.json();
457
  const poll = setInterval(async () => {
458
- const sRes = await fetch(`/status/${data.job_id}?t=${Date.now()}`);
459
- if (sRes.ok) {
460
- const s = await sRes.json();
461
- if (s.status === "completed") {
462
- clearInterval(poll);
463
- document.getElementById("bridge-content").innerHTML = `<video controls autoplay loop class="w-full h-full object-contain bg-black"><source src="${s.video_url}" type="video/mp4"></video>`;
464
- document.getElementById("bridge-card-outer").classList.replace("border-primary/20", "border-primary");
465
- document.getElementById("bridge-border").classList.replace("border-primary/50", "border-transparent");
466
- if (s.merged_video_url) {
467
- const dlBtn = document.getElementById("merged-download-btn");
468
- dlBtn.href = s.merged_video_url;
469
- document.getElementById("merged-download-container").classList.remove("hidden");
470
- }
471
- btn.innerHTML = "Done!";
472
- setTimeout(() => {
 
 
 
 
 
 
 
 
 
473
  btn.disabled = false;
474
- btn.innerHTML = `<span class="material-symbols-outlined">auto_fix_high</span> Generate Video`;
475
- }, 3000);
476
- } else if (s.status === "error") {
 
 
 
 
 
 
 
477
  clearInterval(poll);
478
- alert(s.log);
479
  btn.disabled = false;
480
- btn.innerHTML = "Try Again";
481
- } else {
482
- btn.innerHTML = `${s.log} (${s.progress}%)`;
483
  }
484
  }
485
  }, 1500);
 
102
  <div class="size-1.5 rounded-full bg-green-500 animate-pulse"></div>
103
  <span class="text-[10px] font-bold text-green-500 uppercase tracking-wider">Online</span>
104
  </div>
105
+ <div class="flex items-center gap-2 px-2 py-1 bg-green-500/10 border border-green-500/20 rounded-full ml-2">
106
+ <div class="size-1.5 rounded-full bg-green-500 animate-pulse"></div>
107
+ <span class="text-[10px] font-bold text-green-500 uppercase tracking-wider">Online</span>
108
+ </div>
109
  </div>
110
 
111
  <div class="flex items-center gap-3">
 
459
  if (!res.ok) throw new Error(res.statusText);
460
  const data = await res.json();
461
  const poll = setInterval(async () => {
462
+ failureCount = window.fc || 0;
463
+ try {
464
+ const sRes = await fetch(`/status/${data.job_id}?t=${Date.now()}`);
465
+ if (sRes.ok) {
466
+ window.fc = 0;
467
+ const s = await sRes.json();
468
+ if (s.status === "completed") {
469
+ clearInterval(poll);
470
+ document.getElementById("bridge-content").innerHTML = `<video controls autoplay loop class="w-full h-full object-contain bg-black"><source src="${s.video_url}" type="video/mp4"></video>`;
471
+ document.getElementById("bridge-card-outer").classList.replace("border-primary/20", "border-primary");
472
+ document.getElementById("bridge-border").classList.replace("border-primary/50", "border-transparent");
473
+ if (s.merged_video_url) {
474
+ const dlBtn = document.getElementById("merged-download-btn");
475
+ dlBtn.href = s.merged_video_url;
476
+ document.getElementById("merged-download-container").classList.remove("hidden");
477
+ }
478
+ btn.innerHTML = "Done!";
479
+ setTimeout(() => {
480
+ btn.disabled = false;
481
+ btn.innerHTML = `<span class="material-symbols-outlined">auto_fix_high</span> Generate Video`;
482
+ }, 3000);
483
+ } else if (s.status === "error") {
484
+ clearInterval(poll);
485
+ alert(s.log);
486
  btn.disabled = false;
487
+ btn.innerHTML = "Try Again";
488
+ } else {
489
+ btn.innerHTML = `${s.log} (${s.progress}%)`;
490
+ }
491
+ } else {
492
+ throw new Error("Server Error");
493
+ }
494
+ } catch (e) {
495
+ window.fc = (window.fc || 0) + 1;
496
+ if (window.fc > 5) {
497
  clearInterval(poll);
498
+ alert("Connection lost. The active job may get lost but you can try refreshing.");
499
  btn.disabled = false;
500
+ btn.innerHTML = "Connection Failed";
 
 
501
  }
502
  }
503
  }, 1500);
utils.py CHANGED
@@ -48,42 +48,49 @@ def save_video_bytes(bytes_data, suffix=".mp4") -> str:
48
  return f.name
49
 
50
  def normalize_video(input_path):
51
- """Converts any video to a standard 1080p, 24fps, silent MP4 intermediate."""
 
 
52
  output_path = input_path.replace(".mp4", "_norm.mp4")
53
  cmd = [
54
  "ffmpeg", "-y", "-i", input_path,
55
  "-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=24,format=yuv420p",
56
- "-c:v", "libx264", "-preset", "ultrafast", "-crf", "28", "-an", # Remove audio to prevent mixing crashes
 
57
  output_path
58
  ]
59
  subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30)
60
  return output_path
61
 
62
  def stitch_videos(path_a, path_b, path_c, output_path):
63
- """ Robust Stitching: Normalizes clips individually first, then concats. This avoids the 'complex filter' crashes common with mismatched inputs. """
64
- logger.info(f"🧵 Stitching: {path_a} + {path_b} + {path_c}")
 
 
 
65
 
 
66
  try:
67
- # 1. Normalize all inputs to identical format
68
  norm_a = normalize_video(path_a)
69
  norm_b = normalize_video(path_b)
70
  norm_c = normalize_video(path_c)
71
 
72
- # 2. Create list file for concat
 
 
73
  list_file = "concat_list.txt"
74
  with open(list_file, "w") as f:
75
  f.write(f"file '{norm_a}'\n")
76
  f.write(f"file '{norm_b}'\n")
77
  f.write(f"file '{norm_c}'\n")
78
 
79
- # 3. Concatenate using stream copy (fast & safe)
80
  cmd = [
81
  "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file,
82
  "-c", "copy", output_path
83
  ]
84
  subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30)
85
 
86
- # Cleanup temps
87
  for p in [norm_a, norm_b, norm_c, list_file]:
88
  if os.path.exists(p): os.remove(p)
89
 
@@ -91,7 +98,7 @@ def stitch_videos(path_a, path_b, path_c, output_path):
91
 
92
  except Exception as e:
93
  logger.error(f"Stitch Logic Failed: {e}")
94
- raise e
95
 
96
  def update_job_status(job_id, status, progress, log=None, video_url=None, merged_video_url=None):
97
  if not job_id: return
@@ -99,14 +106,12 @@ def update_job_status(job_id, status, progress, log=None, video_url=None, merged
99
 
100
  final_url = video_url
101
  final_merged_url = merged_video_url
102
- # Move Bridge
103
  if video_url and os.path.exists(video_url) and status == "completed":
104
  final_filename = f"{job_id}_bridge.mp4"
105
  dest = os.path.join("outputs", final_filename)
106
  if os.path.abspath(video_url) != os.path.abspath(dest): shutil.move(video_url, dest)
107
  final_url = f"/outputs/{final_filename}"
108
  if Settings.GCP_BUCKET_NAME: upload_to_gcs(dest, final_filename)
109
- # Move Merged
110
  if merged_video_url and os.path.exists(merged_video_url) and status == "completed":
111
  merged_filename = f"{job_id}_merged.mp4"
112
  merged_dest = os.path.join("outputs", merged_filename)
 
48
  return f.name
49
 
50
  def normalize_video(input_path):
51
+ """Helper to normalize video. Returns None if ffmpeg missing."""
52
+ if not shutil.which("ffmpeg"): return None
53
+
54
  output_path = input_path.replace(".mp4", "_norm.mp4")
55
  cmd = [
56
  "ffmpeg", "-y", "-i", input_path,
57
  "-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=24,format=yuv420p",
58
+ "-c:v", "libx264", "-preset", "ultrafast", "-crf", "28",
59
+ "-an",
60
  output_path
61
  ]
62
  subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30)
63
  return output_path
64
 
65
  def stitch_videos(path_a, path_b, path_c, output_path):
66
+ """ Attempts to stitch videos. RETURNS: output_path if successful, NONE if ffmpeg is missing/fails. """
67
+ # 1. CHECK IF FFMPEG EXISTS
68
+ if not shutil.which("ffmpeg"):
69
+ logger.warning("⚠️ FFmpeg not found. Skipping stitch.")
70
+ return None
71
 
72
+ logger.info(f"🧵 Stitching: {path_a} + {path_b} + {path_c}")
73
  try:
 
74
  norm_a = normalize_video(path_a)
75
  norm_b = normalize_video(path_b)
76
  norm_c = normalize_video(path_c)
77
 
78
+ if not all([norm_a, norm_b, norm_c]):
79
+ raise Exception("Normalization failed")
80
+
81
  list_file = "concat_list.txt"
82
  with open(list_file, "w") as f:
83
  f.write(f"file '{norm_a}'\n")
84
  f.write(f"file '{norm_b}'\n")
85
  f.write(f"file '{norm_c}'\n")
86
 
 
87
  cmd = [
88
  "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file,
89
  "-c", "copy", output_path
90
  ]
91
  subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30)
92
 
93
+ # Cleanup
94
  for p in [norm_a, norm_b, norm_c, list_file]:
95
  if os.path.exists(p): os.remove(p)
96
 
 
98
 
99
  except Exception as e:
100
  logger.error(f"Stitch Logic Failed: {e}")
101
+ return None # Return None so the pipeline continues without crashing
102
 
103
  def update_job_status(job_id, status, progress, log=None, video_url=None, merged_video_url=None):
104
  if not job_id: return
 
106
 
107
  final_url = video_url
108
  final_merged_url = merged_video_url
 
109
  if video_url and os.path.exists(video_url) and status == "completed":
110
  final_filename = f"{job_id}_bridge.mp4"
111
  dest = os.path.join("outputs", final_filename)
112
  if os.path.abspath(video_url) != os.path.abspath(dest): shutil.move(video_url, dest)
113
  final_url = f"/outputs/{final_filename}"
114
  if Settings.GCP_BUCKET_NAME: upload_to_gcs(dest, final_filename)
 
115
  if merged_video_url and os.path.exists(merged_video_url) and status == "completed":
116
  merged_filename = f"{job_id}_merged.mp4"
117
  merged_dest = os.path.join("outputs", merged_filename)