Spaces:
Sleeping
Sleeping
Gaurav vashistha
commited on
Commit
·
78eeeb4
1
Parent(s):
e9456a0
fix: resolve server blocking and frontend status caching issues
Browse files- server.py +7 -24
- stitch_continuity_dashboard/code.html +11 -58
server.py
CHANGED
|
@@ -7,7 +7,6 @@ import os
|
|
| 7 |
import shutil
|
| 8 |
import uuid
|
| 9 |
import json
|
| 10 |
-
# FIXED IMPORT: Importing from root agent.py instead of continuity_agent
|
| 11 |
from agent import analyze_only, generate_only
|
| 12 |
|
| 13 |
app = FastAPI(title="Continuity", description="AI Video Bridging Service")
|
|
@@ -25,16 +24,15 @@ os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
| 25 |
app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs")
|
| 26 |
|
| 27 |
@app.get("/")
|
| 28 |
-
|
| 29 |
-
# Serve the dashboard HTML
|
| 30 |
return FileResponse("stitch_continuity_dashboard/code.html")
|
| 31 |
|
| 32 |
@app.post("/analyze")
|
| 33 |
-
|
| 34 |
-
background_tasks: BackgroundTasks,
|
| 35 |
video_a: UploadFile = File(...),
|
| 36 |
video_c: UploadFile = File(...)
|
| 37 |
):
|
|
|
|
| 38 |
try:
|
| 39 |
request_id = str(uuid.uuid4())
|
| 40 |
ext_a = os.path.splitext(video_a.filename)[1] or ".mp4"
|
|
@@ -48,23 +46,7 @@ async def analyze_endpoint(
|
|
| 48 |
with open(path_c, "wb") as buffer:
|
| 49 |
shutil.copyfileobj(video_c.file, buffer)
|
| 50 |
|
| 51 |
-
#
|
| 52 |
-
# For now keeping it sync as per user previous flows, but could be made async easily.
|
| 53 |
-
# However, user request specifically focused on "generate" being async.
|
| 54 |
-
# But wait, analyze also calls Gemini which can be slow.
|
| 55 |
-
# Refactoring to also include job_id for analyze might be good practice but not explicitly requested for "analyze" in prompt detail,
|
| 56 |
-
# BUT the user request says: "Update analyze_videos node: Call utils.update_job_status...".
|
| 57 |
-
# So we should probably treat analyze as async too OR just pass the job_id.
|
| 58 |
-
# But the frontend flow for analyze currently awaits the response to get the prompt.
|
| 59 |
-
# If we make it async, we break the frontend flow unless we refactor that too.
|
| 60 |
-
# The prompt says: "Update code.html ... Update the generate button JavaScript."
|
| 61 |
-
# It doesn't explicitly say to update the analyze button logic to be async.
|
| 62 |
-
# However, update_job_status IS called in analyze_videos.
|
| 63 |
-
# So, we can pass a job_id if we want status updates, but if we await it, the status updates are only useful if polled in parallel.
|
| 64 |
-
# For now, I will keep analyze synchronous but pass a dummy job_id if we want logging, or just let it block.
|
| 65 |
-
# Actually, let's keep it blocking as per original server code, but pass a job_id so at least logs are written.
|
| 66 |
-
|
| 67 |
-
# Call Agent with local paths
|
| 68 |
result = analyze_only(os.path.abspath(path_a), os.path.abspath(path_c), job_id=request_id)
|
| 69 |
|
| 70 |
if result.get("status") == "error":
|
|
@@ -80,12 +62,13 @@ async def analyze_endpoint(
|
|
| 80 |
raise HTTPException(status_code=500, detail=str(e))
|
| 81 |
|
| 82 |
@app.post("/generate")
|
| 83 |
-
|
| 84 |
background_tasks: BackgroundTasks,
|
| 85 |
prompt: str = Body(...),
|
| 86 |
video_a_path: str = Body(...),
|
| 87 |
video_c_path: str = Body(...)
|
| 88 |
):
|
|
|
|
| 89 |
try:
|
| 90 |
if not os.path.exists(video_a_path) or not os.path.exists(video_c_path):
|
| 91 |
raise HTTPException(status_code=400, detail="Video files not found on server.")
|
|
@@ -107,7 +90,7 @@ async def generate_endpoint(
|
|
| 107 |
raise HTTPException(status_code=500, detail=str(e))
|
| 108 |
|
| 109 |
@app.get("/status/{job_id}")
|
| 110 |
-
|
| 111 |
file_path = os.path.join(OUTPUT_DIR, f"{job_id}.json")
|
| 112 |
if not os.path.exists(file_path):
|
| 113 |
raise HTTPException(status_code=404, detail="Job not found")
|
|
|
|
| 7 |
import shutil
|
| 8 |
import uuid
|
| 9 |
import json
|
|
|
|
| 10 |
from agent import analyze_only, generate_only
|
| 11 |
|
| 12 |
app = FastAPI(title="Continuity", description="AI Video Bridging Service")
|
|
|
|
| 24 |
app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs")
|
| 25 |
|
| 26 |
@app.get("/")
|
| 27 |
+
def read_root():
|
|
|
|
| 28 |
return FileResponse("stitch_continuity_dashboard/code.html")
|
| 29 |
|
| 30 |
@app.post("/analyze")
|
| 31 |
+
def analyze_endpoint(
|
|
|
|
| 32 |
video_a: UploadFile = File(...),
|
| 33 |
video_c: UploadFile = File(...)
|
| 34 |
):
|
| 35 |
+
# Changed to 'def' to run in threadpool (prevent blocking)
|
| 36 |
try:
|
| 37 |
request_id = str(uuid.uuid4())
|
| 38 |
ext_a = os.path.splitext(video_a.filename)[1] or ".mp4"
|
|
|
|
| 46 |
with open(path_c, "wb") as buffer:
|
| 47 |
shutil.copyfileobj(video_c.file, buffer)
|
| 48 |
|
| 49 |
+
# This is blocking, so 'def' is required
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
result = analyze_only(os.path.abspath(path_a), os.path.abspath(path_c), job_id=request_id)
|
| 51 |
|
| 52 |
if result.get("status") == "error":
|
|
|
|
| 62 |
raise HTTPException(status_code=500, detail=str(e))
|
| 63 |
|
| 64 |
@app.post("/generate")
|
| 65 |
+
def generate_endpoint(
|
| 66 |
background_tasks: BackgroundTasks,
|
| 67 |
prompt: str = Body(...),
|
| 68 |
video_a_path: str = Body(...),
|
| 69 |
video_c_path: str = Body(...)
|
| 70 |
):
|
| 71 |
+
# Changed to 'def' for safety
|
| 72 |
try:
|
| 73 |
if not os.path.exists(video_a_path) or not os.path.exists(video_c_path):
|
| 74 |
raise HTTPException(status_code=400, detail="Video files not found on server.")
|
|
|
|
| 90 |
raise HTTPException(status_code=500, detail=str(e))
|
| 91 |
|
| 92 |
@app.get("/status/{job_id}")
|
| 93 |
+
def get_status(job_id: str):
|
| 94 |
file_path = os.path.join(OUTPUT_DIR, f"{job_id}.json")
|
| 95 |
if not os.path.exists(file_path):
|
| 96 |
raise HTTPException(status_code=404, detail="Job not found")
|
stitch_continuity_dashboard/code.html
CHANGED
|
@@ -389,7 +389,8 @@
|
|
| 389 |
// 2. Poll for Status
|
| 390 |
const pollInterval = setInterval(async () => {
|
| 391 |
try {
|
| 392 |
-
|
|
|
|
| 393 |
if (!statusRes.ok) return;
|
| 394 |
|
| 395 |
const statusData = await statusRes.json();
|
|
@@ -403,66 +404,18 @@
|
|
| 403 |
// Show Video
|
| 404 |
const bridgeCard = document.getElementById("bridge-card");
|
| 405 |
if (bridgeCard) {
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
// The backend returns full local path generally, but server logic for update_job_status
|
| 409 |
-
// kept the local path. We need to serve it via the /outputs/ mount.
|
| 410 |
-
// Wait, utils.save_video_bytes returns absolute path.
|
| 411 |
-
// server.py: app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs")
|
| 412 |
-
// config.py/utils.py save to temp folder?
|
| 413 |
-
// Agent returns local path.
|
| 414 |
-
// In server.py for generate_only, we don't have the final file move logic anymore locally in server.py?
|
| 415 |
-
// Ah, right. Agent calls update_job_status with the video_url (local path).
|
| 416 |
-
// The frontend needs a URL it can access.
|
| 417 |
-
// We need to move the file to OUTPUT_DIR or make sure Agent saves to OUTPUT_DIR.
|
| 418 |
-
// Agent uses download_to_temp / save_video_bytes which use tempfile.
|
| 419 |
-
// So the file in 'video_url' is in /tmp/...
|
| 420 |
-
// This won't be accessible via /outputs/.
|
| 421 |
-
// We need to fix this.
|
| 422 |
-
|
| 423 |
-
// This requires a minor fix in server or agent.
|
| 424 |
-
// Since I can't edit server.py inside this JS block logic easily to change python logic...
|
| 425 |
-
// I should probably have updated the Agent to save to OUTPUT_DIR or Server to handle the move?
|
| 426 |
-
// BUT, the server.py I just wrote launches generate_only in background.
|
| 427 |
-
// generate_only returns the result, but since it's in background, we ignore return.
|
| 428 |
-
// The only communication channel is the status file.
|
| 429 |
-
// The status file contains 'video_url' which is the temp path.
|
| 430 |
-
// Frontend cannot access temp path.
|
| 431 |
-
|
| 432 |
-
// Quick FIX:
|
| 433 |
-
// The frontend can't move files.
|
| 434 |
-
// The AGENT needs to know where to save, OR Utils.
|
| 435 |
-
// OR, we update the logic in agent.py to try to move it? No that's messy.
|
| 436 |
-
// Let's look at where static files are served. `/outputs`.
|
| 437 |
-
// If the file is in /tmp, we can't serve it.
|
| 438 |
-
|
| 439 |
-
// Maybe I should modify `utils.save_video_bytes` to accept a directory?
|
| 440 |
-
// Or I can modify `server.py` to launch a wrapper that moves the file?
|
| 441 |
-
|
| 442 |
-
// I will implement a wrapper in server.py in the NEXT step or modify server.py now if I catch it?
|
| 443 |
-
// I already wrote server.py.
|
| 444 |
-
// I will apply a fix to `agent.py` or `utils.py` to ensure it saves to `outputs/` OR
|
| 445 |
-
// modify `server.py` to wrap the task.
|
| 446 |
-
// Wrapping in server.py is cleanest for separation.
|
| 447 |
-
// "generate_only" is imported.
|
| 448 |
-
|
| 449 |
-
// Wait, `utils.update_job_status` writes to `outputs/{job_id}.json`.
|
| 450 |
-
// If I change `utils.py` to also move the video if provided?
|
| 451 |
-
// That seems like a good side effect for a "helper" dealing with job status in this specific app context.
|
| 452 |
-
// Or `agent.py` can move it.
|
| 453 |
-
|
| 454 |
-
// Let's handle this in the JS for now assuming the URL *might* work if I fix it in backend in a sec.
|
| 455 |
-
// I'll assume the backend will provide a valid relative URL like `/outputs/filename.mp4`.
|
| 456 |
-
|
| 457 |
-
bridgeCard.innerHTML = `
|
| 458 |
<video controls autoplay loop class="w-full h-full object-cover rounded-2xl border-2 border-primary shadow-neon">
|
| 459 |
-
<source src="${
|
| 460 |
Your browser does not support the video tag.
|
| 461 |
</video>
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
|
|
|
|
|
|
| 466 |
btn.innerHTML = originalContent;
|
| 467 |
btn.disabled = false;
|
| 468 |
btn.classList.remove("opacity-70", "cursor-not-allowed");
|
|
|
|
| 389 |
// 2. Poll for Status
|
| 390 |
const pollInterval = setInterval(async () => {
|
| 391 |
try {
|
| 392 |
+
// ADDED TIMESTAMP TO PREVENT CACHING
|
| 393 |
+
const statusRes = await fetch(`/status/${jobId}?t=${Date.now()}`);
|
| 394 |
if (!statusRes.ok) return;
|
| 395 |
|
| 396 |
const statusData = await statusRes.json();
|
|
|
|
| 404 |
// Show Video
|
| 405 |
const bridgeCard = document.getElementById("bridge-card");
|
| 406 |
if (bridgeCard) {
|
| 407 |
+
if (statusData.video_url) {
|
| 408 |
+
bridgeCard.innerHTML = `
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
<video controls autoplay loop class="w-full h-full object-cover rounded-2xl border-2 border-primary shadow-neon">
|
| 410 |
+
<source src="${statusData.video_url}" type="video/mp4">
|
| 411 |
Your browser does not support the video tag.
|
| 412 |
</video>
|
| 413 |
+
`;
|
| 414 |
+
document.getElementById("analysis-panel").classList.remove("hidden");
|
| 415 |
+
document.getElementById("review-panel").classList.add("hidden");
|
| 416 |
+
} else {
|
| 417 |
+
alert("Video generated but URL missing.");
|
| 418 |
+
}
|
| 419 |
btn.innerHTML = originalContent;
|
| 420 |
btn.disabled = false;
|
| 421 |
btn.classList.remove("opacity-70", "cursor-not-allowed");
|