Spaces:
Running
Running
Gaurav vashistha commited on
Commit ·
5e07b31
1
Parent(s): 7451451
Implement Fault Tolerance: UI polling timeout and backend FFmpeg detection
Browse files- stitch_continuity_dashboard/code.html +40 -22
- 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 |
-
|
| 459 |
-
|
| 460 |
-
const
|
| 461 |
-
if (
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
btn.disabled = false;
|
| 474 |
-
btn.innerHTML =
|
| 475 |
-
}
|
| 476 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
clearInterval(poll);
|
| 478 |
-
alert(
|
| 479 |
btn.disabled = false;
|
| 480 |
-
btn.innerHTML = "
|
| 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 |
-
"""
|
|
|
|
|
|
|
| 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",
|
|
|
|
| 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 |
-
"""
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 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)
|