Gaurav vashistha commited on
Commit
bcae6dd
·
1 Parent(s): 0644f3e

Upgrade: Structured JSON Analysis & UI Scroll Fix

Browse files
Files changed (3) hide show
  1. agent.py +29 -40
  2. server.py +16 -6
  3. stitch_continuity_dashboard/code.html +28 -2
agent.py CHANGED
@@ -3,6 +3,7 @@ import time
3
  import logging
4
  import tempfile
5
  import hashlib
 
6
  from google import genai
7
  from google.genai import types
8
  from config import Settings
@@ -22,67 +23,60 @@ def get_file_hash(filepath):
22
  def get_or_upload_file(client, filepath):
23
  """Uploads file only if it doesn't already exist in Gemini (deduplication)."""
24
  file_hash = get_file_hash(filepath)
25
-
26
- # Check if file with this hash name already exists
27
  try:
28
  for f in client.files.list(config={'page_size': 50}):
29
  if f.display_name == file_hash and f.state.name == "ACTIVE":
30
- logger.info(f"♻️ Smart Cache Hit: Using existing file {file_hash}")
31
  return f
32
  except Exception:
33
  pass
34
-
35
- logger.info(f"wm Uploading new file: {filepath} (Hash: {file_hash})")
36
  return client.files.upload(file=filepath, config={'display_name': file_hash})
37
 
38
- def analyze_videos(state):
39
- # Note: This function is kept for LangGraph compatibility if needed,
40
- # but the main logic is now in analyze_only below.
41
- return analyze_only(state['video_a_local_path'], state['video_c_local_path'], state.get('job_id'))
42
-
43
  def analyze_only(path_a, path_c, job_id=None):
44
  update_job_status(job_id, "analyzing", 10, "Director checking file cache...")
45
  client = genai.Client(api_key=Settings.GOOGLE_API_KEY)
46
 
47
  try:
48
- # 1. Smart Upload
49
  file_a = get_or_upload_file(client, path_a)
50
  file_c = get_or_upload_file(client, path_c)
51
 
52
- # 2. Wait for processing
53
  while file_a.state.name == "PROCESSING" or file_c.state.name == "PROCESSING":
54
- update_job_status(job_id, "analyzing", 20, "Google is processing video geometry...")
55
  time.sleep(2)
56
  file_a = client.files.get(name=file_a.name)
57
  file_c = client.files.get(name=file_c.name)
58
-
59
- # 3. THE V2.7 'VFX DIRECTOR' PROMPT
60
  prompt = """
61
- You are a VFX Director specializing in surreal, seamless video morphing.
62
-
63
- Task: Analyze the visual composition, lighting, and primary shapes of Video A (Start) and Video C (End).
64
- Goal: Write a visual prompt for a 2-second intermediate video (Video B) that semantically transforms A into C.
65
-
66
- Strict Rules:
67
- 1. DO NOT use words like "dissolve", "fade", "cut", or "transition".
68
- 2. Describe a PHYSICAL transformation. How does the texture of A become the texture of C?
69
- 3. Match the lighting evolution (e.g., "The golden hour light creates deep shadows that morph into...")
70
- 4. Find a connecting shape (e.g., "The curve of the river flows upwards to match the curve of the jawline").
71
-
72
- Output: ONLY the final visual prompt text. Keep it under 40 words.
73
  """
74
-
75
  update_job_status(job_id, "analyzing", 30, "Director drafting creative morph...")
76
 
77
- res = client.models.generate_content(model="gemini-2.0-flash-exp", contents=[prompt, file_a, file_c])
78
- return {"prompt": res.text, "status": "success"}
 
 
 
 
 
 
 
 
 
 
 
 
79
  except Exception as e:
80
- logger.error(f"Analysis Error: {e}")
81
  return {"detail": str(e), "status": "error"}
82
 
83
  def generate_only(prompt, path_a, path_c, job_id, style, audio, neg, guidance, motion):
84
  update_job_status(job_id, "generating", 50, "Production started (Veo 3.1)...")
85
-
86
  full_prompt = f"{style} style. {prompt} Soundtrack: {audio}"
87
  if neg:
88
  full_prompt += f" --no {neg}"
@@ -91,19 +85,17 @@ def generate_only(prompt, path_a, path_c, job_id, style, audio, neg, guidance, m
91
  if Settings.GCP_PROJECT_ID:
92
  client = genai.Client(vertexai=True, project=Settings.GCP_PROJECT_ID, location=Settings.GCP_LOCATION)
93
  op = client.models.generate_videos(
94
- model='veo-3.1-generate-preview',
95
- prompt=full_prompt,
96
  config=types.GenerateVideosConfig(number_of_videos=1)
97
  )
98
 
99
  while not op.done:
100
  time.sleep(5)
101
- # Ideally check status here if possible, or just wait loop
102
-
103
  if op.result and op.result.generated_videos:
104
  vid = op.result.generated_videos[0]
105
  bridge_path = None
106
-
107
  if vid.video.uri:
108
  bridge_path = tempfile.mktemp(suffix=".mp4")
109
  download_blob(vid.video.uri, bridge_path)
@@ -117,12 +109,9 @@ def generate_only(prompt, path_a, path_c, job_id, style, audio, neg, guidance, m
117
  final_output = stitch_videos(path_a, bridge_path, path_c, final_cut_path)
118
  update_job_status(job_id, "completed", 100, "Done!", video_url=bridge_path, merged_video_url=final_output)
119
  except Exception as e:
120
- logger.error(f"Stitch failed: {e}")
121
  update_job_status(job_id, "completed", 100, "Stitch failed, showing bridge.", video_url=bridge_path)
122
  return
123
-
124
  except Exception as e:
125
  update_job_status(job_id, "error", 0, f"Error: {e}")
126
  return
127
-
128
  update_job_status(job_id, "error", 0, "Generation failed.")
 
3
  import logging
4
  import tempfile
5
  import hashlib
6
+ import json
7
  from google import genai
8
  from google.genai import types
9
  from config import Settings
 
23
  def get_or_upload_file(client, filepath):
24
  """Uploads file only if it doesn't already exist in Gemini (deduplication)."""
25
  file_hash = get_file_hash(filepath)
 
 
26
  try:
27
  for f in client.files.list(config={'page_size': 50}):
28
  if f.display_name == file_hash and f.state.name == "ACTIVE":
29
+ logger.info(f"♻️ Smart Cache Hit: {file_hash}")
30
  return f
31
  except Exception:
32
  pass
33
+ logger.info(f"wm Uploading new file: {file_hash}")
 
34
  return client.files.upload(file=filepath, config={'display_name': file_hash})
35
 
 
 
 
 
 
36
  def analyze_only(path_a, path_c, job_id=None):
37
  update_job_status(job_id, "analyzing", 10, "Director checking file cache...")
38
  client = genai.Client(api_key=Settings.GOOGLE_API_KEY)
39
 
40
  try:
 
41
  file_a = get_or_upload_file(client, path_a)
42
  file_c = get_or_upload_file(client, path_c)
43
 
 
44
  while file_a.state.name == "PROCESSING" or file_c.state.name == "PROCESSING":
45
+ update_job_status(job_id, "analyzing", 20, "Google processing video...")
46
  time.sleep(2)
47
  file_a = client.files.get(name=file_a.name)
48
  file_c = client.files.get(name=file_c.name)
49
+
 
50
  prompt = """
51
+ You are a VFX Director. Analyze Video A and Video C.
52
+ Return a JSON object with exactly these keys:
53
+ {
54
+ "analysis_a": "Brief description of Video A's lighting, subject, and camera movement.",
55
+ "analysis_c": "Brief description of Video C's lighting, subject, and camera movement.",
56
+ "visual_prompt_b": "A surreal, seamless morphing prompt that transforms A into C. DO NOT use words like 'dissolve' or 'cut'. Focus on shape and texture transformation."
57
+ }
 
 
 
 
 
58
  """
 
59
  update_job_status(job_id, "analyzing", 30, "Director drafting creative morph...")
60
 
61
+ # Request JSON output
62
+ res = client.models.generate_content(
63
+ model="gemini-2.0-flash-exp",
64
+ contents=[prompt, file_a, file_c],
65
+ config=types.GenerateContentConfig(response_mime_type="application/json")
66
+ )
67
+
68
+ data = json.loads(res.text)
69
+ return {
70
+ "analysis_a": data.get("analysis_a", ""),
71
+ "analysis_c": data.get("analysis_c", ""),
72
+ "prompt": data.get("visual_prompt_b", res.text),
73
+ "status": "success"
74
+ }
75
  except Exception as e:
 
76
  return {"detail": str(e), "status": "error"}
77
 
78
  def generate_only(prompt, path_a, path_c, job_id, style, audio, neg, guidance, motion):
79
  update_job_status(job_id, "generating", 50, "Production started (Veo 3.1)...")
 
80
  full_prompt = f"{style} style. {prompt} Soundtrack: {audio}"
81
  if neg:
82
  full_prompt += f" --no {neg}"
 
85
  if Settings.GCP_PROJECT_ID:
86
  client = genai.Client(vertexai=True, project=Settings.GCP_PROJECT_ID, location=Settings.GCP_LOCATION)
87
  op = client.models.generate_videos(
88
+ model='veo-3.1-generate-preview',
89
+ prompt=full_prompt,
90
  config=types.GenerateVideosConfig(number_of_videos=1)
91
  )
92
 
93
  while not op.done:
94
  time.sleep(5)
95
+
 
96
  if op.result and op.result.generated_videos:
97
  vid = op.result.generated_videos[0]
98
  bridge_path = None
 
99
  if vid.video.uri:
100
  bridge_path = tempfile.mktemp(suffix=".mp4")
101
  download_blob(vid.video.uri, bridge_path)
 
109
  final_output = stitch_videos(path_a, bridge_path, path_c, final_cut_path)
110
  update_job_status(job_id, "completed", 100, "Done!", video_url=bridge_path, merged_video_url=final_output)
111
  except Exception as e:
 
112
  update_job_status(job_id, "completed", 100, "Stitch failed, showing bridge.", video_url=bridge_path)
113
  return
 
114
  except Exception as e:
115
  update_job_status(job_id, "error", 0, f"Error: {e}")
116
  return
 
117
  update_job_status(job_id, "error", 0, "Generation failed.")
server.py CHANGED
@@ -8,6 +8,7 @@ from utils import get_history_from_gcs
8
 
9
  app = FastAPI(title="Continuity", description="AI Video Bridging Service")
10
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
 
11
  os.makedirs("outputs", exist_ok=True)
12
  app.mount("/outputs", StaticFiles(directory="outputs"), name="outputs")
13
 
@@ -42,19 +43,26 @@ def read_root():
42
  def analyze_endpoint(video_a: UploadFile = File(...), video_c: UploadFile = File(...)):
43
  try:
44
  rid = str(uuid.uuid4())
45
- ext_a = os.path.splitext(video_a.filename)[1] or ".mp4"
46
- ext_c = os.path.splitext(video_c.filename)[1] or ".mp4"
47
- pa = os.path.join("outputs", f"{rid}_a{ext_a}")
48
- pc = os.path.join("outputs", f"{rid}_c{ext_c}")
49
  with open(pa, "wb") as b:
50
  shutil.copyfileobj(video_a.file, b)
51
  with open(pc, "wb") as b:
52
  shutil.copyfileobj(video_c.file, b)
53
-
54
  res = analyze_only(os.path.abspath(pa), os.path.abspath(pc), job_id=rid)
 
55
  if res.get("status") == "error":
56
  raise HTTPException(500, res.get("detail"))
57
- return {"prompt": res["prompt"], "video_a_path": os.path.abspath(pa), "video_c_path": os.path.abspath(pc)}
 
 
 
 
 
 
 
58
  except Exception as e:
59
  raise HTTPException(500, str(e))
60
 
@@ -71,9 +79,11 @@ async def generate_endpoint(
71
  ):
72
  if not os.path.exists(video_a_path) or not os.path.exists(video_c_path):
73
  raise HTTPException(400, "Videos not found.")
 
74
  job_id = str(uuid.uuid4())
75
  with open(f"outputs/{job_id}.json", "w") as f:
76
  json.dump({"status": "queued", "progress": 0, "log": "Queued..."}, f)
 
77
  await job_queue.add_job(generate_only, prompt, video_a_path, video_c_path, job_id, style, audio_prompt, negative_prompt, guidance_scale, motion_strength)
78
  return {"job_id": job_id}
79
 
 
8
 
9
  app = FastAPI(title="Continuity", description="AI Video Bridging Service")
10
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
11
+
12
  os.makedirs("outputs", exist_ok=True)
13
  app.mount("/outputs", StaticFiles(directory="outputs"), name="outputs")
14
 
 
43
  def analyze_endpoint(video_a: UploadFile = File(...), video_c: UploadFile = File(...)):
44
  try:
45
  rid = str(uuid.uuid4())
46
+ pa = os.path.join("outputs", f"{rid}_a.mp4")
47
+ pc = os.path.join("outputs", f"{rid}_c.mp4")
48
+
 
49
  with open(pa, "wb") as b:
50
  shutil.copyfileobj(video_a.file, b)
51
  with open(pc, "wb") as b:
52
  shutil.copyfileobj(video_c.file, b)
53
+
54
  res = analyze_only(os.path.abspath(pa), os.path.abspath(pc), job_id=rid)
55
+
56
  if res.get("status") == "error":
57
  raise HTTPException(500, res.get("detail"))
58
+
59
+ return {
60
+ "analysis_a": res.get("analysis_a"),
61
+ "analysis_c": res.get("analysis_c"),
62
+ "prompt": res["prompt"],
63
+ "video_a_path": os.path.abspath(pa),
64
+ "video_c_path": os.path.abspath(pc)
65
+ }
66
  except Exception as e:
67
  raise HTTPException(500, str(e))
68
 
 
79
  ):
80
  if not os.path.exists(video_a_path) or not os.path.exists(video_c_path):
81
  raise HTTPException(400, "Videos not found.")
82
+
83
  job_id = str(uuid.uuid4())
84
  with open(f"outputs/{job_id}.json", "w") as f:
85
  json.dump({"status": "queued", "progress": 0, "log": "Queued..."}, f)
86
+
87
  await job_queue.add_job(generate_only, prompt, video_a_path, video_c_path, job_id, style, audio_prompt, negative_prompt, guidance_scale, motion_strength)
88
  return {"job_id": job_id}
89
 
stitch_continuity_dashboard/code.html CHANGED
@@ -113,7 +113,8 @@
113
  </header>
114
 
115
  <!-- Main Stage: Scrollable Content -->
116
- <main class="flex-1 w-full overflow-y-auto relative flex flex-col items-center pt-8 pb-32">
 
117
  <div class="w-full max-w-6xl mx-auto flex items-center justify-center gap-4 md:gap-8 lg:gap-12 px-4">
118
 
119
  <!-- SCENE A -->
@@ -224,8 +225,23 @@
224
  class="text-xs text-gray-500 hover:text-white uppercase tracking-wider">Reset</button>
225
  </div>
226
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  <div><label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Visual
228
- Direction</label><textarea id="prompt-box" rows="4"
229
  class="w-full bg-black/20 border border-white/10 rounded-lg p-3 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary outline-none resize-none"></textarea>
230
  </div>
231
  <div class="grid grid-cols-2 gap-4">
@@ -370,6 +386,11 @@
370
  document.getElementById("analysis-panel").classList.remove("hidden");
371
  document.getElementById("review-panel").classList.add("hidden");
372
  document.getElementById("prompt-box").value = "";
 
 
 
 
 
373
  currentVideoAPath = "";
374
  currentVideoCPath = "";
375
 
@@ -393,6 +414,11 @@
393
  const res = await fetch("/analyze", { method: "POST", body: fd });
394
  const data = await res.json();
395
  document.getElementById("prompt-box").value = data.prompt;
 
 
 
 
 
396
  currentVideoAPath = data.video_a_path;
397
  currentVideoCPath = data.video_c_path;
398
  document.getElementById("analysis-panel").classList.add("hidden");
 
113
  </header>
114
 
115
  <!-- Main Stage: Scrollable Content -->
116
+ <!-- FIX: Increased padding bottom to pb-[32rem] (~512px) to clear floating controls -->
117
+ <main class="flex-1 w-full overflow-y-auto relative flex flex-col items-center pt-8 pb-[32rem]">
118
  <div class="w-full max-w-6xl mx-auto flex items-center justify-center gap-4 md:gap-8 lg:gap-12 px-4">
119
 
120
  <!-- SCENE A -->
 
225
  class="text-xs text-gray-500 hover:text-white uppercase tracking-wider">Reset</button>
226
  </div>
227
  </div>
228
+
229
+ <!-- NEW: Structured Analysis Display -->
230
+ <div class="grid grid-cols-2 gap-4 mb-2">
231
+ <div class="bg-white/5 p-2 rounded-lg border border-white/10">
232
+ <span class="text-[9px] font-bold text-primary uppercase">Scene A Analysis</span>
233
+ <p id="analysis-a-text" class="text-[10px] text-gray-300 h-10 overflow-y-auto mt-1 leading-tight">
234
+ Waiting for analysis...</p>
235
+ </div>
236
+ <div class="bg-white/5 p-2 rounded-lg border border-white/10">
237
+ <span class="text-[9px] font-bold text-primary uppercase">Scene C Analysis</span>
238
+ <p id="analysis-c-text" class="text-[10px] text-gray-300 h-10 overflow-y-auto mt-1 leading-tight">
239
+ Waiting for analysis...</p>
240
+ </div>
241
+ </div>
242
+
243
  <div><label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Visual
244
+ Direction (Bridge B)</label><textarea id="prompt-box" rows="3"
245
  class="w-full bg-black/20 border border-white/10 rounded-lg p-3 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary outline-none resize-none"></textarea>
246
  </div>
247
  <div class="grid grid-cols-2 gap-4">
 
386
  document.getElementById("analysis-panel").classList.remove("hidden");
387
  document.getElementById("review-panel").classList.add("hidden");
388
  document.getElementById("prompt-box").value = "";
389
+
390
+ // Reset Analysis Fields
391
+ document.getElementById("analysis-a-text").innerText = "Waiting for analysis...";
392
+ document.getElementById("analysis-c-text").innerText = "Waiting for analysis...";
393
+
394
  currentVideoAPath = "";
395
  currentVideoCPath = "";
396
 
 
414
  const res = await fetch("/analyze", { method: "POST", body: fd });
415
  const data = await res.json();
416
  document.getElementById("prompt-box").value = data.prompt;
417
+
418
+ // Populate Analysis Fields
419
+ document.getElementById("analysis-a-text").innerText = data.analysis_a || "No details found.";
420
+ document.getElementById("analysis-c-text").innerText = data.analysis_c || "No details found.";
421
+
422
  currentVideoAPath = data.video_a_path;
423
  currentVideoCPath = data.video_c_path;
424
  document.getElementById("analysis-panel").classList.add("hidden");