Gaurav vashistha commited on
Commit
eb4c41a
·
1 Parent(s): 7341198

feat: split analyze/generate endpoints and add SVD catch-all

Browse files
Files changed (2) hide show
  1. server.py +46 -20
  2. stitch_continuity_dashboard/code.html +108 -32
server.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import FastAPI, HTTPException, UploadFile, Form, File
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.staticfiles import StaticFiles
4
  from fastapi.responses import FileResponse
@@ -6,7 +6,7 @@ import uvicorn
6
  import os
7
  import shutil
8
  import uuid
9
- from continuity_agent.agent import app as continuity_graph
10
 
11
  app = FastAPI(title="Continuity", description="AI Video Bridging Service")
12
 
@@ -26,11 +26,10 @@ app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs")
26
  async def read_root():
27
  return FileResponse("stitch_continuity_dashboard/code.html")
28
 
29
- @app.post("/generate-transition")
30
- async def generate_transition(
31
  video_a: UploadFile = File(...),
32
- video_c: UploadFile = File(...),
33
- prompt: str = Form("Cinematic transition")
34
  ):
35
  try:
36
  request_id = str(uuid.uuid4())
@@ -45,28 +44,55 @@ async def generate_transition(
45
  with open(path_c, "wb") as buffer:
46
  shutil.copyfileobj(video_c.file, buffer)
47
 
48
- initial_state = {
49
- "video_a_url": "local_upload",
50
- "video_c_url": "local_upload",
51
- "user_notes": prompt,
52
- "veo_prompt": prompt,
53
- "video_a_local_path": os.path.abspath(path_a),
54
- "video_c_local_path": os.path.abspath(path_c),
55
- "generated_video_url": "",
56
- "status": "started"
57
- }
58
 
59
- result = continuity_graph.invoke(initial_state)
60
- gen_path = result.get("generated_video_url")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  if not gen_path or "Error" in gen_path:
63
  raise HTTPException(status_code=500, detail=f"Generation failed: {gen_path}")
64
 
65
- final_filename = f"{request_id}_bridge.mp4"
 
66
  final_output_path = os.path.join(OUTPUT_DIR, final_filename)
67
- shutil.move(gen_path, final_output_path)
 
 
 
 
 
 
 
68
 
69
  return {"video_url": f"/outputs/{final_filename}"}
 
 
 
 
70
  except Exception as e:
71
  print(f"Server Error: {e}")
72
  raise HTTPException(status_code=500, detail=str(e))
 
1
+ from fastapi import FastAPI, HTTPException, UploadFile, Form, File, Body
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.staticfiles import StaticFiles
4
  from fastapi.responses import FileResponse
 
6
  import os
7
  import shutil
8
  import uuid
9
+ from continuity_agent.agent import analyze_only, generate_only
10
 
11
  app = FastAPI(title="Continuity", description="AI Video Bridging Service")
12
 
 
26
  async def read_root():
27
  return FileResponse("stitch_continuity_dashboard/code.html")
28
 
29
+ @app.post("/analyze")
30
+ async def analyze_endpoint(
31
  video_a: UploadFile = File(...),
32
+ video_c: UploadFile = File(...)
 
33
  ):
34
  try:
35
  request_id = str(uuid.uuid4())
 
44
  with open(path_c, "wb") as buffer:
45
  shutil.copyfileobj(video_c.file, buffer)
46
 
47
+ # Call Agent
48
+ result = analyze_only(os.path.abspath(path_a), os.path.abspath(path_c))
 
 
 
 
 
 
 
 
49
 
50
+ if result.get("status") == "error":
51
+ raise HTTPException(status_code=500, detail=result.get("detail"))
52
+
53
+ return {
54
+ "prompt": result["prompt"],
55
+ "video_a_path": os.path.abspath(path_a),
56
+ "video_c_path": os.path.abspath(path_c)
57
+ }
58
+ except Exception as e:
59
+ print(f"Server Error (Analyze): {e}")
60
+ raise HTTPException(status_code=500, detail=str(e))
61
+
62
+ @app.post("/generate")
63
+ async def generate_endpoint(
64
+ prompt: str = Body(...),
65
+ video_a_path: str = Body(...),
66
+ video_c_path: str = Body(...)
67
+ ):
68
+ try:
69
+ if not os.path.exists(video_a_path) or not os.path.exists(video_c_path):
70
+ raise HTTPException(status_code=400, detail="Video files not found on server.")
71
+
72
+ # Call Agent
73
+ result = generate_only(prompt, video_a_path, video_c_path)
74
+ gen_path = result.get("video_url")
75
 
76
  if not gen_path or "Error" in gen_path:
77
  raise HTTPException(status_code=500, detail=f"Generation failed: {gen_path}")
78
 
79
+ # Move final file to output dir if it's not already there (SVD might return temp path)
80
+ final_filename = f"{uuid.uuid4()}_bridge.mp4"
81
  final_output_path = os.path.join(OUTPUT_DIR, final_filename)
82
+
83
+ # If gen_path is a URL (some providers), we might need to handle differently
84
+ # But our agent functions return local paths (SVD) or temp paths (Wan)
85
+ if os.path.exists(gen_path):
86
+ shutil.move(gen_path, final_output_path)
87
+ else:
88
+ # Assume it's an error message or invalid
89
+ raise HTTPException(status_code=500, detail="Generated file missing.")
90
 
91
  return {"video_url": f"/outputs/{final_filename}"}
92
+
93
+ except Exception as e:
94
+ print(f"Server Error (Generate): {e}")
95
+ raise HTTPException(status_code=500, detail=str(e))
96
  except Exception as e:
97
  print(f"Server Error: {e}")
98
  raise HTTPException(status_code=500, detail=str(e))
stitch_continuity_dashboard/code.html CHANGED
@@ -245,23 +245,39 @@
245
  </main>
246
  <!-- Floating Action Bar (Director Controls) -->
247
  <div class="fixed bottom-10 left-1/2 -translate-x-1/2 z-50 w-full max-w-2xl px-4">
248
- <div class="glass-panel rounded-full p-2 pl-6 flex items-center shadow-neon">
249
- <div class="flex-1 flex items-center gap-3">
250
- <span class="material-symbols-outlined text-gray-400">edit_note</span>
251
- <input id="director-notes"
252
- class="w-full bg-transparent border-none text-white placeholder-gray-500 focus:ring-0 text-sm font-medium"
253
- placeholder="Add Director Notes (e.g., 'Slow dissolve', 'Cyber glitch')..." type="text" />
 
 
 
 
 
 
 
 
 
254
  </div>
255
- <div class="h-8 w-[1px] bg-white/10 mx-2"></div>
 
 
 
256
  <button id="generate-btn"
257
- class="flex items-center gap-2 bg-primary hover:bg-[#6b0bc9] text-white px-6 py-3 rounded-full font-bold text-sm transition-all shadow-[0_0_15px_rgba(127,13,242,0.4)] hover:shadow-[0_0_25px_rgba(127,13,242,0.6)] whitespace-nowrap">
258
- <span class="material-symbols-outlined text-[20px]">movie_filter</span>
259
- Generate Transition
260
  </button>
261
  </div>
262
  </div>
263
 
264
  <script>
 
 
 
 
265
  // UI Helper: Update filename text when file is selected
266
  function handleFileSelect(input, labelId) {
267
  if (input.files && input.files[0]) {
@@ -271,14 +287,21 @@
271
  }
272
  }
273
 
274
- // Main Logic: Send to Agent
275
- const generateBtn = document.getElementById("generate-btn");
276
- if (generateBtn) {
277
- generateBtn.addEventListener("click", async () => {
 
 
 
 
 
 
 
 
278
  const fileA = document.getElementById("video-upload-a").files[0];
279
  const fileC = document.getElementById("video-upload-c").files[0];
280
- const notes = document.getElementById("director-notes").value;
281
- const btn = document.getElementById("generate-btn");
282
 
283
  // 1. Validation
284
  if (!fileA || !fileC) {
@@ -288,45 +311,98 @@
288
 
289
  // 2. Loading State
290
  const originalContent = btn.innerHTML;
291
- btn.innerHTML = `<span class="material-symbols-outlined animate-spin text-[20px] mr-2">progress_activity</span> Director is working...`;
292
  btn.disabled = true;
293
  btn.classList.add("opacity-70", "cursor-not-allowed");
294
 
295
  const formData = new FormData();
296
  formData.append("video_a", fileA);
297
  formData.append("video_c", fileC);
298
- formData.append("prompt", notes || "Cinematic transition between scenes");
299
 
300
  try {
301
  // 3. Send to Server
302
- const response = await fetch("/generate-transition", {
303
  method: "POST",
304
  body: formData
305
  });
306
 
307
- if (!response.ok) throw new Error("Agent backend returned an error.");
308
 
309
  const data = await response.json();
310
 
311
- // 4. Success: Inject Video into the Middle Card
312
- const bridgeCard = document.getElementById("bridge-card");
 
 
 
 
 
313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  if (bridgeCard) {
315
- // Ensure we are replacing the inner content completely
316
  bridgeCard.innerHTML = `
317
- <video controls autoplay loop class="w-full h-full object-cover rounded-2xl border-2 border-primary shadow-neon">
318
- <source src="${data.video_url}" type="video/mp4">
319
- Your browser does not support the video tag.
320
- </video>
321
- `;
322
- } else {
323
- console.error("Could not find the bridge card");
324
- alert("Video generated, but could not place it in UI. Check console.");
 
325
  }
326
 
327
  } catch (error) {
328
  console.error(error);
329
- alert("❌ Generation Failed: " + error.message + "\n\nMake sure 'python server.py' is running!");
330
  } finally {
331
  btn.innerHTML = originalContent;
332
  btn.disabled = false;
 
245
  </main>
246
  <!-- Floating Action Bar (Director Controls) -->
247
  <div class="fixed bottom-10 left-1/2 -translate-x-1/2 z-50 w-full max-w-2xl px-4">
248
+ <!-- Analysis Panel (Step 1) -->
249
+ <div id="analysis-panel" class="glass-panel rounded-full p-2 pl-6 flex items-center justify-center shadow-neon">
250
+ <button id="analyze-btn"
251
+ class="flex items-center gap-2 bg-primary hover:bg-[#6b0bc9] text-white px-8 py-3 rounded-full font-bold text-sm transition-all shadow-[0_0_15px_rgba(127,13,242,0.4)] hover:shadow-[0_0_25px_rgba(127,13,242,0.6)] whitespace-nowrap">
252
+ <span class="material-symbols-outlined text-[20px]">analytics</span>
253
+ Upload & Analyze Scenes
254
+ </button>
255
+ </div>
256
+
257
+ <!-- Review Panel (Step 2 - Hidden Initially) -->
258
+ <div id="review-panel" class="hidden glass-panel rounded-2xl p-4 flex flex-col gap-4 shadow-neon w-full">
259
+ <div class="flex items-center justify-between pb-2 border-b border-white/10">
260
+ <h3 class="text-white font-display font-bold">🎬 Director's Cut: Review Prompt</h3>
261
+ <button onclick="resetUI()"
262
+ class="text-gray-400 hover:text-white text-xs uppercase tracking-widest">Reset</button>
263
  </div>
264
+ <textarea id="prompt-box" rows="3"
265
+ class="w-full bg-surface-dark/50 border border-white/10 rounded-lg p-3 text-white text-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none"
266
+ placeholder="AI generated transition prompt will appear here..."></textarea>
267
+
268
  <button id="generate-btn"
269
+ class="w-full flex items-center justify-center gap-2 bg-gradient-to-r from-primary to-purple-600 hover:from-[#6b0bc9] hover:to-purple-700 text-white px-6 py-3 rounded-xl font-bold text-lg transition-all shadow-lg">
270
+ <span class="material-symbols-outlined text-[24px]">movie_filter</span>
271
+ ACTION! (Generate Video)
272
  </button>
273
  </div>
274
  </div>
275
 
276
  <script>
277
+ // State
278
+ let currentVideoAPath = "";
279
+ let currentVideoCPath = "";
280
+
281
  // UI Helper: Update filename text when file is selected
282
  function handleFileSelect(input, labelId) {
283
  if (input.files && input.files[0]) {
 
287
  }
288
  }
289
 
290
+ function resetUI() {
291
+ document.getElementById("analysis-panel").classList.remove("hidden");
292
+ document.getElementById("review-panel").classList.add("hidden");
293
+ document.getElementById("prompt-box").value = "";
294
+ currentVideoAPath = "";
295
+ currentVideoCPath = "";
296
+ }
297
+
298
+ // Step 1: Analyze
299
+ const analyzeBtn = document.getElementById("analyze-btn");
300
+ if (analyzeBtn) {
301
+ analyzeBtn.addEventListener("click", async () => {
302
  const fileA = document.getElementById("video-upload-a").files[0];
303
  const fileC = document.getElementById("video-upload-c").files[0];
304
+ const btn = document.getElementById("analyze-btn");
 
305
 
306
  // 1. Validation
307
  if (!fileA || !fileC) {
 
311
 
312
  // 2. Loading State
313
  const originalContent = btn.innerHTML;
314
+ btn.innerHTML = `<span class="material-symbols-outlined animate-spin text-[20px] mr-2">progress_activity</span> Analyzing Scenes...`;
315
  btn.disabled = true;
316
  btn.classList.add("opacity-70", "cursor-not-allowed");
317
 
318
  const formData = new FormData();
319
  formData.append("video_a", fileA);
320
  formData.append("video_c", fileC);
 
321
 
322
  try {
323
  // 3. Send to Server
324
+ const response = await fetch("/analyze", {
325
  method: "POST",
326
  body: formData
327
  });
328
 
329
+ if (!response.ok) throw new Error("Analysis failed.");
330
 
331
  const data = await response.json();
332
 
333
+ // 4. Success: Show Review Panel
334
+ document.getElementById("prompt-box").value = data.prompt;
335
+ currentVideoAPath = data.video_a_path;
336
+ currentVideoCPath = data.video_c_path;
337
+
338
+ document.getElementById("analysis-panel").classList.add("hidden");
339
+ document.getElementById("review-panel").classList.remove("hidden");
340
 
341
+ } catch (error) {
342
+ console.error(error);
343
+ alert("❌ Analysis Failed: " + error.message);
344
+ } finally {
345
+ btn.innerHTML = originalContent;
346
+ btn.disabled = false;
347
+ btn.classList.remove("opacity-70", "cursor-not-allowed");
348
+ }
349
+ });
350
+ }
351
+
352
+ // Step 2: Generate
353
+ const generateBtn = document.getElementById("generate-btn");
354
+ if (generateBtn) {
355
+ generateBtn.addEventListener("click", async () => {
356
+ const prompt = document.getElementById("prompt-box").value;
357
+ const btn = document.getElementById("generate-btn");
358
+
359
+ if (!currentVideoAPath || !currentVideoCPath) {
360
+ alert("Session lost. Please re-upload videos.");
361
+ resetUI();
362
+ return;
363
+ }
364
+
365
+ // Loading State
366
+ const originalContent = btn.innerHTML;
367
+ btn.innerHTML = `<span class="material-symbols-outlined animate-spin text-[20px] mr-2">progress_activity</span> Generating (This may take ~60s)...`;
368
+ btn.disabled = true;
369
+ btn.classList.add("opacity-70", "cursor-not-allowed");
370
+
371
+ try {
372
+ const response = await fetch("/generate", {
373
+ method: "POST",
374
+ headers: {
375
+ "Content-Type": "application/json"
376
+ },
377
+ body: JSON.stringify({
378
+ prompt: prompt,
379
+ video_a_path: currentVideoAPath,
380
+ video_c_path: currentVideoCPath
381
+ })
382
+ });
383
+
384
+ if (!response.ok) throw new Error("Generation failed.");
385
+
386
+ const data = await response.json();
387
+
388
+ // Success: Inject Video
389
+ const bridgeCard = document.getElementById("bridge-card");
390
  if (bridgeCard) {
 
391
  bridgeCard.innerHTML = `
392
+ <video controls autoplay loop class="w-full h-full object-cover rounded-2xl border-2 border-primary shadow-neon">
393
+ <source src="${data.video_url}" type="video/mp4">
394
+ Your browser does not support the video tag.
395
+ </video>
396
+ `;
397
+ // Reset UI to allow new creation but keep video
398
+ document.getElementById("analysis-panel").classList.remove("hidden");
399
+ document.getElementById("review-panel").classList.add("hidden");
400
+ btn.innerHTML = originalContent; // Restore button just in case
401
  }
402
 
403
  } catch (error) {
404
  console.error(error);
405
+ alert("❌ Generation Failed: " + error.message);
406
  } finally {
407
  btn.innerHTML = originalContent;
408
  btn.disabled = false;