Gaurav vashistha commited on
Commit
87e3cb1
·
1 Parent(s): b0901ce

feat: add audio mood customization

Browse files
Files changed (3) hide show
  1. agent.py +11 -6
  2. server.py +8 -12
  3. stitch_continuity_dashboard/code.html +395 -450
agent.py CHANGED
@@ -109,8 +109,12 @@ def analyze_videos(state: ContinuityState) -> dict:
109
  def generate_video(state: ContinuityState) -> dict:
110
  logger.info("--- 🎥 Generator Node ---")
111
  job_id = state.get("job_id")
112
- prompt = state.get('veo_prompt', "")
 
113
 
 
 
 
114
  update_job_status(job_id, "generating", 50, "Veo initializing...")
115
 
116
  # Check GCP Project ID
@@ -126,13 +130,13 @@ def generate_video(state: ContinuityState) -> dict:
126
  logger.info("⚡ Initializing Google Veo 3.1 (Unified SDK)...")
127
  client = genai.Client(vertexai=True, project=Settings.GCP_PROJECT_ID, location=Settings.GCP_LOCATION)
128
 
129
- logger.info(f"Generating with Veo 3.1... Prompt: {prompt[:30]}...")
130
- update_job_status(job_id, "generating", 60, "Veo 3.1 generating video+audio (this takes ~60s)...")
131
 
132
  # Veo 3.1 supports native audio generation
133
  operation = client.models.generate_videos(
134
  model='veo-3.1-generate-preview',
135
- prompt=prompt,
136
  config=types.GenerateVideosConfig(
137
  number_of_videos=1,
138
  )
@@ -199,7 +203,7 @@ def analyze_only(state_or_path_a, path_c=None, job_id=None, style="Cinematic"):
199
  result = analyze_videos(state)
200
  return {"prompt": result.get("scene_analysis"), "status": "success"}
201
 
202
- def generate_only(prompt, path_a, path_c, job_id=None, style="Cinematic"):
203
  state = {
204
  "job_id": job_id,
205
  "video_a_url": "local",
@@ -207,6 +211,7 @@ def generate_only(prompt, path_a, path_c, job_id=None, style="Cinematic"):
207
  "video_a_local_path": path_a,
208
  "video_c_local_path": path_c,
209
  "veo_prompt": prompt,
210
- "style": style
 
211
  }
212
  return generate_video(state)
 
109
  def generate_video(state: ContinuityState) -> dict:
110
  logger.info("--- 🎥 Generator Node ---")
111
  job_id = state.get("job_id")
112
+ visual_prompt = state.get('veo_prompt', "")
113
+ audio_context = state.get('audio_prompt', "Realistic ambient sound")
114
 
115
+ # Merge Prompts for Veo 3.1
116
+ # Veo 3.1 understands audio instructions within the main prompt
117
+ full_prompt = f"{visual_prompt} Soundtrack: {audio_context}"
118
  update_job_status(job_id, "generating", 50, "Veo initializing...")
119
 
120
  # Check GCP Project ID
 
130
  logger.info("⚡ Initializing Google Veo 3.1 (Unified SDK)...")
131
  client = genai.Client(vertexai=True, project=Settings.GCP_PROJECT_ID, location=Settings.GCP_LOCATION)
132
 
133
+ logger.info(f"Generating with Veo 3.1... Prompt: {full_prompt[:50]}...")
134
+ update_job_status(job_id, "generating", 60, f"Veo 3.1 generating with audio style: '{audio_context}'...")
135
 
136
  # Veo 3.1 supports native audio generation
137
  operation = client.models.generate_videos(
138
  model='veo-3.1-generate-preview',
139
+ prompt=full_prompt,
140
  config=types.GenerateVideosConfig(
141
  number_of_videos=1,
142
  )
 
203
  result = analyze_videos(state)
204
  return {"prompt": result.get("scene_analysis"), "status": "success"}
205
 
206
+ def generate_only(prompt, path_a, path_c, job_id=None, style="Cinematic", audio_prompt="Cinematic ambient sound"):
207
  state = {
208
  "job_id": job_id,
209
  "video_a_url": "local",
 
211
  "video_a_local_path": path_a,
212
  "video_c_local_path": path_c,
213
  "veo_prompt": prompt,
214
+ "style": style,
215
+ "audio_prompt": audio_prompt # Pass the new parameter to state
216
  }
217
  return generate_video(state)
server.py CHANGED
@@ -33,7 +33,6 @@ def analyze_endpoint(
33
  video_a: UploadFile = File(...),
34
  video_c: UploadFile = File(...)
35
  ):
36
- # Changed to 'def' to run in threadpool (prevent blocking)
37
  try:
38
  request_id = str(uuid.uuid4())
39
  ext_a = os.path.splitext(video_a.filename)[1] or ".mp4"
@@ -41,15 +40,14 @@ def analyze_endpoint(
41
 
42
  path_a = os.path.join(OUTPUT_DIR, f"{request_id}_a{ext_a}")
43
  path_c = os.path.join(OUTPUT_DIR, f"{request_id}_c{ext_c}")
44
-
45
  with open(path_a, "wb") as buffer:
46
  shutil.copyfileobj(video_a.file, buffer)
47
  with open(path_c, "wb") as buffer:
48
  shutil.copyfileobj(video_c.file, buffer)
49
 
50
- # This is blocking, so 'def' is required
51
  result = analyze_only(os.path.abspath(path_a), os.path.abspath(path_c), job_id=request_id)
52
-
53
  if result.get("status") == "error":
54
  raise HTTPException(status_code=500, detail=result.get("detail"))
55
 
@@ -67,23 +65,22 @@ def generate_endpoint(
67
  background_tasks: BackgroundTasks,
68
  prompt: str = Body(...),
69
  style: str = Body("Cinematic"),
 
70
  video_a_path: str = Body(...),
71
  video_c_path: str = Body(...)
72
  ):
73
- # Changed to 'def' for safety
74
  try:
75
  if not os.path.exists(video_a_path) or not os.path.exists(video_c_path):
76
- raise HTTPException(status_code=400, detail="Video files not found on server.")
77
 
78
  job_id = str(uuid.uuid4())
79
 
80
- # Initialize job status
81
  status_file = os.path.join(OUTPUT_DIR, f"{job_id}.json")
82
  with open(status_file, "w") as f:
83
  json.dump({"status": "queued", "progress": 0, "log": "Job queued..."}, f)
84
-
85
- # Add to background tasks
86
- background_tasks.add_task(generate_only, prompt, video_a_path, video_c_path, job_id, style)
87
 
88
  return {"job_id": job_id}
89
 
@@ -96,7 +93,7 @@ def get_status(job_id: str):
96
  file_path = os.path.join(OUTPUT_DIR, f"{job_id}.json")
97
  if not os.path.exists(file_path):
98
  raise HTTPException(status_code=404, detail="Job not found")
99
-
100
  try:
101
  with open(file_path, "r") as f:
102
  data = json.load(f)
@@ -106,7 +103,6 @@ def get_status(job_id: str):
106
 
107
  @app.get("/history")
108
  def get_history():
109
- """Returns list of past generated videos from GCS."""
110
  return get_history_from_gcs()
111
 
112
  if __name__ == "__main__":
 
33
  video_a: UploadFile = File(...),
34
  video_c: UploadFile = File(...)
35
  ):
 
36
  try:
37
  request_id = str(uuid.uuid4())
38
  ext_a = os.path.splitext(video_a.filename)[1] or ".mp4"
 
40
 
41
  path_a = os.path.join(OUTPUT_DIR, f"{request_id}_a{ext_a}")
42
  path_c = os.path.join(OUTPUT_DIR, f"{request_id}_c{ext_c}")
43
+
44
  with open(path_a, "wb") as buffer:
45
  shutil.copyfileobj(video_a.file, buffer)
46
  with open(path_c, "wb") as buffer:
47
  shutil.copyfileobj(video_c.file, buffer)
48
 
 
49
  result = analyze_only(os.path.abspath(path_a), os.path.abspath(path_c), job_id=request_id)
50
+
51
  if result.get("status") == "error":
52
  raise HTTPException(status_code=500, detail=result.get("detail"))
53
 
 
65
  background_tasks: BackgroundTasks,
66
  prompt: str = Body(...),
67
  style: str = Body("Cinematic"),
68
+ audio_prompt: str = Body("Cinematic ambient sound"), # <--- NEW: Capture Audio Mood
69
  video_a_path: str = Body(...),
70
  video_c_path: str = Body(...)
71
  ):
 
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.")
75
 
76
  job_id = str(uuid.uuid4())
77
 
 
78
  status_file = os.path.join(OUTPUT_DIR, f"{job_id}.json")
79
  with open(status_file, "w") as f:
80
  json.dump({"status": "queued", "progress": 0, "log": "Job queued..."}, f)
81
+
82
+ # Pass audio_prompt to the agent function
83
+ background_tasks.add_task(generate_only, prompt, video_a_path, video_c_path, job_id, style, audio_prompt)
84
 
85
  return {"job_id": job_id}
86
 
 
93
  file_path = os.path.join(OUTPUT_DIR, f"{job_id}.json")
94
  if not os.path.exists(file_path):
95
  raise HTTPException(status_code=404, detail="Job not found")
96
+
97
  try:
98
  with open(file_path, "r") as f:
99
  data = json.load(f)
 
103
 
104
  @app.get("/history")
105
  def get_history():
 
106
  return get_history_from_gcs()
107
 
108
  if __name__ == "__main__":
stitch_continuity_dashboard/code.html CHANGED
@@ -1,474 +1,419 @@
1
  <!DOCTYPE html>
2
  <html class="dark" lang="en">
3
-
4
  <head>
5
- <meta charset="utf-8" />
6
- <meta content="width=device-width, initial-scale=1.0" name="viewport" />
7
- <title>Continuity Dashboard</title>
8
- <!-- Google Fonts -->
9
- <link href="https://fonts.googleapis.com" rel="preconnect" />
10
- <link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect" />
11
- <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&amp;display=swap"
12
- rel="stylesheet" />
13
- <link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@300;400;500;600;700&amp;display=swap"
14
- rel="stylesheet" />
15
- <!-- Material Symbols -->
16
- <link
17
- href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap"
18
- rel="stylesheet" />
19
- <!-- Tailwind CSS -->
20
- <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
21
- <script id="tailwind-config">
22
- tailwind.config = {
23
- darkMode: "class",
24
- theme: {
25
- extend: {
26
- colors: {
27
- "primary": "#7f0df2",
28
- "background-light": "#f7f5f8",
29
- "background-dark": "#191022",
30
- "surface-dark": "#2a1d35",
31
- "border-dark": "#4d3168",
32
- },
33
- fontFamily: {
34
- "display": ["Space Grotesk", "sans-serif"],
35
- "body": ["Noto Sans", "sans-serif"],
36
- },
37
- borderRadius: { "DEFAULT": "0.25rem", "lg": "0.5rem", "xl": "0.75rem", "2xl": "1rem", "full": "9999px" },
38
- boxShadow: {
39
- "neon": "0 0 20px rgba(127, 13, 242, 0.4)",
40
- "neon-strong": "0 0 35px rgba(127, 13, 242, 0.6)",
41
- }
42
- },
43
- },
44
- }
45
- </script>
46
- <style>
47
- /* Custom scrollbar for dark theme */
48
- ::-webkit-scrollbar {
49
- width: 8px;
50
- height: 8px;
51
- }
52
-
53
- ::-webkit-scrollbar-track {
54
- background: #191022;
55
- }
56
-
57
- ::-webkit-scrollbar-thumb {
58
- background: #4d3168;
59
- border-radius: 4px;
60
- }
61
-
62
- ::-webkit-scrollbar-thumb:hover {
63
- background: #7f0df2;
64
- }
65
-
66
- .glass-panel {
67
- background: rgba(25, 16, 34, 0.6);
68
- backdrop-filter: blur(12px);
69
- -webkit-backdrop-filter: blur(12px);
70
- border: 1px solid rgba(255, 255, 255, 0.05);
71
- }
72
-
73
- /* Footer fade for the floating button */
74
- .footer-fade {
75
- background: linear-gradient(to top, #191022 20%, transparent 100%);
76
- pointer-events: none;
77
- }
78
-
79
- .connection-line {
80
- height: 2px;
81
- flex-grow: 1;
82
- background: linear-gradient(90deg, #4d3168 0%, #7f0df2 50%, #4d3168 100%);
83
- opacity: 0.5;
84
- position: relative;
85
- }
86
-
87
- .connection-line::after {
88
- content: '';
89
- position: absolute;
90
- right: 0;
91
- top: 50%;
92
- transform: translateY(-50%);
93
- width: 6px;
94
- height: 6px;
95
- background: #7f0df2;
96
- border-radius: 50%;
97
- box-shadow: 0 0 10px #7f0df2;
98
- }
99
- </style>
100
  </head>
 
101
 
102
- <body
103
- class="relative flex h-screen w-full flex-col bg-background-light dark:bg-background-dark font-display overflow-hidden text-white selection:bg-primary selection:text-white">
104
- <!-- Background Ambient Glows -->
105
- <div class="fixed top-0 left-0 w-full h-full pointer-events-none overflow-hidden z-0">
106
- <div class="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-primary/20 rounded-full blur-[120px]"></div>
107
- <div class="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-blue-600/10 rounded-full blur-[120px]"></div>
108
- </div>
109
- <!-- Header -->
110
- <header
111
- class="fixed top-0 left-0 right-0 z-50 flex items-center justify-between whitespace-nowrap border-b border-white/5 glass-panel px-8 py-4">
112
- <div class="flex items-center gap-4 text-white">
113
- <div class="size-8 flex items-center justify-center text-primary">
114
- <span class="material-symbols-outlined text-3xl">emergency_recording</span>
115
- </div>
116
- <h2
117
- class="text-white text-xl font-bold leading-tight tracking-[-0.015em] bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-400">
118
- Continuity <span class="text-xs font-normal text-gray-400 ml-2 opacity-60 tracking-widest uppercase">The Missing
119
- Link</span>
120
- </h2>
121
- </div>
122
- <div class="flex gap-3">
123
- <button
124
- class="flex size-10 cursor-pointer items-center justify-center overflow-hidden rounded-lg bg-surface-dark hover:bg-primary/20 transition-colors border border-white/5 text-white">
125
- <span class="material-symbols-outlined text-[20px]">settings</span>
126
- </button>
127
- <button
128
- class="flex size-10 cursor-pointer items-center justify-center overflow-hidden rounded-lg bg-surface-dark hover:bg-primary/20 transition-colors border border-white/5 text-white">
129
- <span class="material-symbols-outlined text-[20px]">account_circle</span>
130
- </button>
131
- </div>
132
- </header>
133
- <!-- Main Workspace -->
134
- <main class="relative z-10 flex flex-1 flex-col items-center justify-center w-full px-8 pt-20 pb-28">
135
- <!-- Timeline Container -->
136
- <div class="w-full max-w-[1400px] flex items-center justify-center gap-6 xl:gap-10">
137
- <!-- SCENE A: Upload State -->
138
- <div class="flex flex-col gap-4 flex-1 max-w-[320px] group">
139
- <!-- HEADER FIX A: Centered + Gap + Separator -->
140
- <div class="flex items-center justify-center gap-3 px-1">
141
- <span class="text-xs font-bold tracking-widest text-gray-400 uppercase">Input Source</span>
142
- <span class="text-gray-600">|</span>
143
- <span class="text-xs font-bold tracking-widest text-primary">SCENE A</span>
144
- </div>
145
- <div
146
- class="relative flex flex-col items-center justify-center gap-6 rounded-2xl border-2 border-dashed border-border-dark bg-surface-dark/30 hover:bg-surface-dark/50 hover:border-primary/50 transition-all duration-300 px-6 py-12 h-[360px]">
147
- <div
148
- class="size-16 rounded-full bg-surface-dark flex items-center justify-center mb-2 shadow-lg group-hover:scale-110 transition-transform">
149
- <span class="material-symbols-outlined text-3xl text-gray-300 group-hover:text-white">video_file</span>
150
- </div>
151
- <div class="flex flex-col items-center gap-2 text-center">
152
- <p class="text-white text-lg font-bold">Start Scene</p>
153
- <p class="text-gray-400 text-sm max-w-[200px]">Drag &amp; drop your starting video clip here</p>
154
- </div>
155
- <input type="file" id="video-upload-a" accept="video/*,image/*" class="hidden"
156
- onchange="handleFileSelect(this, 'label-a')">
157
- <button onclick="document.getElementById('video-upload-a').click()"
158
- class="mt-2 flex items-center justify-center rounded-lg h-9 px-4 bg-surface-dark hover:bg-primary hover:text-white border border-white/10 transition-all text-sm font-bold tracking-wide text-gray-200 shadow-lg">
159
- <span class="material-symbols-outlined text-[18px] mr-2">upload</span>
160
- <span id="label-a">Select File</span>
161
- </button>
162
- </div>
163
- </div>
164
- <!-- Connector Left -->
165
- <div class="hidden md:flex flex-col items-center justify-center w-16 xl:w-24 opacity-40">
166
- <div class="w-full h-[2px] bg-gradient-to-r from-transparent via-primary to-transparent relative">
167
- <div class="absolute right-0 -top-1.5 text-primary animate-pulse">
168
- <span class="material-symbols-outlined text-lg">chevron_right</span>
169
- </div>
170
  </div>
171
- </div>
172
- <!-- THE BRIDGE: AI Generation Core -->
173
- <div class="flex flex-col gap-4 flex-[1.2] max-w-[480px] relative z-20">
174
- <div class="flex items-center justify-center px-1">
175
- <span
176
- class="text-xs font-bold tracking-[0.2em] text-primary drop-shadow-[0_0_8px_rgba(127,13,242,0.8)] uppercase animate-pulse">The
177
- Bridge</span>
 
 
178
  </div>
179
- <!-- Active Card Container -->
180
- <div id="bridge-card"
181
- class="relative w-full aspect-[4/3] rounded-2xl overflow-hidden shadow-neon border border-primary/30 group bg-black">
182
- <!-- Placeholder Gradient Background -->
183
- <div class="absolute inset-0 bg-gradient-to-br from-surface-dark to-[#0f0a16] z-0"></div>
184
- <!-- Abstract Visualization -->
185
- <div
186
- class="absolute inset-0 opacity-40 mix-blend-overlay bg-[url('https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&amp;w=1000&amp;auto=format&amp;fit=crop')] bg-cover bg-center"
187
- data-alt="Abstract digital waves representing AI processing"></div>
188
- <!-- Glowing Core Animation (CSS Only representation) -->
189
- <div class="absolute inset-0 flex items-center justify-center">
190
- <div class="relative size-32">
191
- <div class="absolute inset-0 rounded-full border-2 border-primary/20 animate-[spin_4s_linear_infinite]">
192
- </div>
193
- <div
194
- class="absolute inset-2 rounded-full border-2 border-t-primary border-r-transparent border-b-primary/50 border-l-transparent animate-[spin_3s_linear_infinite_reverse]">
195
- </div>
196
- <div class="absolute inset-0 flex items-center justify-center">
197
- <span
198
- class="material-symbols-outlined text-4xl text-primary drop-shadow-[0_0_10px_rgba(127,13,242,1)]">auto_awesome</span>
199
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  </div>
201
- </div>
202
- <!-- Content Overlay -->
203
- <div
204
- class="absolute bottom-0 left-0 w-full p-6 bg-gradient-to-t from-black/90 via-black/50 to-transparent flex items-end justify-between">
205
- <div class="flex flex-col gap-1">
206
- <p class="text-white text-xl font-bold leading-tight">AI Transition</p>
207
- <div class="flex items-center gap-2">
208
- <div class="size-2 rounded-full bg-yellow-500 animate-pulse"></div>
209
- <p class="text-gray-300 text-xs font-medium uppercase tracking-wide">Waiting for inputs...</p>
210
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  </div>
212
- </div>
213
- </div>
214
- </div>
215
- <!-- Connector Right -->
216
- <div class="hidden md:flex flex-col items-center justify-center w-16 xl:w-24 opacity-40">
217
- <div class="w-full h-[2px] bg-gradient-to-r from-transparent via-primary to-transparent relative">
218
- <div class="absolute right-0 -top-1.5 text-primary animate-pulse">
219
- <span class="material-symbols-outlined text-lg">chevron_right</span>
220
- </div>
221
- </div>
222
- </div>
223
- <!-- SCENE C: Upload State -->
224
- <div class="flex flex-col gap-4 flex-1 max-w-[320px] group">
225
- <!-- HEADER FIX C: Centered + Gap + Separator + Order Check -->
226
- <div class="flex items-center justify-center gap-3 px-1">
227
- <span class="text-xs font-bold tracking-widest text-primary">SCENE C</span>
228
- <span class="text-gray-600">|</span>
229
- <span class="text-xs font-bold tracking-widest text-gray-400 uppercase">Target Source</span>
230
- </div>
231
- <div
232
- class="relative flex flex-col items-center justify-center gap-6 rounded-2xl border-2 border-dashed border-border-dark bg-surface-dark/30 hover:bg-surface-dark/50 hover:border-primary/50 transition-all duration-300 px-6 py-12 h-[360px]">
233
- <div
234
- class="size-16 rounded-full bg-surface-dark flex items-center justify-center mb-2 shadow-lg group-hover:scale-110 transition-transform">
235
- <span class="material-symbols-outlined text-3xl text-gray-300 group-hover:text-white">movie_edit</span>
236
- </div>
237
- <div class="flex flex-col items-center gap-2 text-center">
238
- <p class="text-white text-lg font-bold">End Scene</p>
239
- <p class="text-gray-400 text-sm max-w-[200px]">Drag &amp; drop your target video clip here</p>
240
- </div>
241
- <input type="file" id="video-upload-c" accept="video/*,image/*" class="hidden"
242
- onchange="handleFileSelect(this, 'label-c')">
243
- <button onclick="document.getElementById('video-upload-c').click()"
244
- class="mt-2 flex items-center justify-center rounded-lg h-9 px-4 bg-surface-dark hover:bg-primary hover:text-white border border-white/10 transition-all text-sm font-bold tracking-wide text-gray-200 shadow-lg">
245
- <span class="material-symbols-outlined text-[18px] mr-2">upload</span>
246
- <span id="label-c">Select File</span>
247
- </button>
248
  </div>
249
- </div>
250
- </div>
251
- </main>
252
-
253
- <!-- Footer Fade Mask -->
254
- <div class="fixed bottom-0 left-0 w-full h-32 footer-fade z-40"></div>
255
-
256
- <!-- Floating Action Bar (Director Controls) -->
257
- <div class="fixed bottom-10 left-1/2 -translate-x-1/2 z-50 w-full max-w-2xl px-4">
258
- <!-- Analysis Panel (Step 1) -->
259
- <div id="analysis-panel" class="glass-panel rounded-full p-2 pl-6 flex items-center justify-center shadow-neon">
260
- <button id="analyze-btn"
261
- 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">
262
- <span class="material-symbols-outlined text-[20px]">analytics</span>
263
- Upload & Analyze Scenes
264
- </button>
265
- </div>
266
-
267
- <!-- Review Panel (Step 2 - Hidden Initially) -->
268
- <div id="review-panel" class="hidden glass-panel rounded-2xl p-4 flex flex-col gap-4 shadow-neon w-full">
269
- <div class="flex items-center justify-between pb-2 border-b border-white/10">
270
- <h3 class="text-white font-display font-bold">🎬 Director's Cut: Review Prompt</h3>
271
- <button onclick="resetUI()"
272
- class="text-gray-400 hover:text-white text-xs uppercase tracking-widest">Reset</button>
273
- </div>
274
- <textarea id="prompt-box" rows="3"
275
- 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"
276
- placeholder="AI generated transition prompt will appear here..."></textarea>
277
 
278
- <div class="flex flex-col gap-2">
279
- <label for="style-select" class="text-xs font-bold text-gray-400 uppercase tracking-widest pl-1">Creative
280
- Style</label>
281
- <select id="style-select"
282
- 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">
283
- <option value="Cinematic">Cinematic (Default)</option>
284
- <option value="Anime">Anime</option>
285
- <option value="Cyberpunk">Cyberpunk</option>
286
- <option value="VHS Glitch">VHS Glitch</option>
287
- <option value="Claymation">Claymation</option>
288
- <option value="Noir">Noir</option>
289
- </select>
290
- </div>
291
-
292
- <button id="generate-btn"
293
- 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">
294
- <span class="material-symbols-outlined text-[24px]">movie_filter</span>
295
- ACTION! (Generate Video)
296
- </button>
297
- </div>
298
- </div>
299
-
300
- <script>
301
- // State
302
- let currentVideoAPath = "";
303
- let currentVideoCPath = "";
304
-
305
- // UI Helper: Update filename text when file is selected
306
- function handleFileSelect(input, labelId) {
307
- if (input.files && input.files[0]) {
308
- const label = document.getElementById(labelId);
309
- label.innerText = input.files[0].name;
310
- label.classList.add("text-primary"); // Turn purple to indicate success
311
- }
312
- }
313
-
314
- function resetUI() {
315
- document.getElementById("analysis-panel").classList.remove("hidden");
316
- document.getElementById("review-panel").classList.add("hidden");
317
- document.getElementById("prompt-box").value = "";
318
- currentVideoAPath = "";
319
- currentVideoCPath = "";
320
- }
321
 
322
- // Step 1: Analyze
323
- const analyzeBtn = document.getElementById("analyze-btn");
324
- if (analyzeBtn) {
325
- analyzeBtn.addEventListener("click", async () => {
326
- const fileA = document.getElementById("video-upload-a").files[0];
327
- const fileC = document.getElementById("video-upload-c").files[0];
328
- const btn = document.getElementById("analyze-btn");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
- // 1. Validation
331
- if (!fileA || !fileC) {
332
- alert("⚠️ Action Required: Please upload both Scene A and Scene C videos.");
333
- return;
 
 
 
 
 
 
 
 
 
 
 
334
  }
335
-
336
- // 2. Loading State
337
- const originalContent = btn.innerHTML;
338
- btn.innerHTML = `<span class="material-symbols-outlined animate-spin text-[20px] mr-2">progress_activity</span> Analyzing Scenes...`;
339
- btn.disabled = true;
340
- btn.classList.add("opacity-70", "cursor-not-allowed");
341
-
342
- const formData = new FormData();
343
- formData.append("video_a", fileA);
344
- formData.append("video_c", fileC);
345
-
346
- try {
347
- // 3. Send to Server
348
- const response = await fetch("/analyze", {
349
- method: "POST",
350
- body: formData
351
- });
352
-
353
- if (!response.ok) throw new Error("Analysis failed.");
354
-
355
- const data = await response.json();
356
-
357
- // 4. Success: Show Review Panel
358
- document.getElementById("prompt-box").value = data.prompt;
359
- currentVideoAPath = data.video_a_path;
360
- currentVideoCPath = data.video_c_path;
361
-
362
- document.getElementById("analysis-panel").classList.add("hidden");
363
- document.getElementById("review-panel").classList.remove("hidden");
364
-
365
- } catch (error) {
366
- console.error(error);
367
- alert("❌ Analysis Failed: " + error.message);
368
- } finally {
369
- btn.innerHTML = originalContent;
370
- btn.disabled = false;
371
- btn.classList.remove("opacity-70", "cursor-not-allowed");
372
  }
373
- });
374
- }
375
 
376
- // Step 2: Generate
377
- const generateBtn = document.getElementById("generate-btn");
378
- if (generateBtn) {
379
- generateBtn.addEventListener("click", async () => {
380
- const prompt = document.getElementById("prompt-box").value;
381
- const style = document.getElementById("style-select").value;
382
- const btn = document.getElementById("generate-btn");
 
 
 
 
 
 
383
 
384
- if (!currentVideoAPath || !currentVideoCPath) {
385
- alert("Session lost. Please re-upload videos.");
386
- resetUI();
387
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  }
389
 
390
- // Loading State
391
- const originalContent = btn.innerHTML;
392
- btn.disabled = true;
393
- btn.classList.add("opacity-70", "cursor-not-allowed");
394
-
395
- try {
396
- // 1. Start Job
397
- const response = await fetch("/generate", {
398
- method: "POST",
399
- headers: {
400
- "Content-Type": "application/json"
401
- },
402
- body: JSON.stringify({
403
- prompt: prompt,
404
- style: style,
405
- video_a_path: currentVideoAPath,
406
- video_c_path: currentVideoCPath
407
- })
408
- });
409
-
410
- if (!response.ok) throw new Error("Generation failed to start.");
411
-
412
- const data = await response.json();
413
- const jobId = data.job_id;
414
-
415
- // 2. Poll for Status
416
- const pollInterval = setInterval(async () => {
417
- try {
418
- // ADDED TIMESTAMP TO PREVENT CACHING
419
- const statusRes = await fetch(`/status/${jobId}?t=${Date.now()}`);
420
- if (!statusRes.ok) return;
421
-
422
- const statusData = await statusRes.json();
423
-
424
- // Update Button Text
425
- btn.innerHTML = `<span class="material-symbols-outlined animate-spin text-[20px] mr-2">progress_activity</span> ${statusData.log} (${statusData.progress}%)`;
426
-
427
- if (statusData.status === "completed") {
428
- clearInterval(pollInterval);
429
-
430
- // Show Video
431
- const bridgeCard = document.getElementById("bridge-card");
432
- if (bridgeCard) {
433
- if (statusData.video_url) {
434
- bridgeCard.innerHTML = `
435
- <video controls autoplay loop class="w-full h-full object-cover rounded-2xl border-2 border-primary shadow-neon">
436
- <source src="${statusData.video_url}" type="video/mp4">
437
- Your browser does not support the video tag.
438
- </video>
439
- `;
440
- document.getElementById("analysis-panel").classList.remove("hidden");
441
- document.getElementById("review-panel").classList.add("hidden");
442
- } else {
443
- alert("Video generated but URL missing.");
444
- }
445
- btn.innerHTML = originalContent;
446
- btn.disabled = false;
447
- btn.classList.remove("opacity-70", "cursor-not-allowed");
448
  }
449
- } else if (statusData.status === "error") {
450
- clearInterval(pollInterval);
451
- alert("❌ Generation Error: " + statusData.log);
452
- btn.innerHTML = originalContent;
453
- btn.disabled = false;
454
- btn.classList.remove("opacity-70", "cursor-not-allowed");
455
- }
456
 
457
- } catch (e) {
458
- console.error("Polling error", e);
459
- }
460
- }, 1000);
461
-
462
- } catch (error) {
463
- console.error(error);
464
- alert("❌ Request Failed: " + error.message);
465
- btn.innerHTML = originalContent;
466
- btn.disabled = false;
467
- btn.classList.remove("opacity-70", "cursor-not-allowed");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  }
469
- });
470
- }
471
- </script>
472
  </body>
473
-
474
  </html>
 
1
  <!DOCTYPE html>
2
  <html class="dark" lang="en">
 
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Continuity</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
10
+ <script>
11
+ tailwind.config = {
12
+ darkMode: 'class',
13
+ theme: {
14
+ extend: {
15
+ colors: {
16
+ primary: '#6b0bc9', // Deep Purple
17
+ secondary: '#10b981', // Emerald
18
+ background: '#0d0612', // Very Dark Purple/Black
19
+ surface: '#1c1326', // Dark Purple Surface
20
+ 'surface-dark': '#130a1a',
21
+ accent: '#d946ef', // Fuchsia
22
+ },
23
+ fontFamily: {
24
+ sans: ['Outfit', 'sans-serif'],
25
+ display: ['Space Grotesk', 'sans-serif'],
26
+ },
27
+ boxShadow: {
28
+ 'neon': '0 0 20px rgba(107, 11, 201, 0.3)',
29
+ 'glow': '0 0 15px rgba(217, 70, 239, 0.4)',
30
+ }
31
+ }
32
+ }
33
+ }
34
+ </script>
35
+ <style>
36
+ body {
37
+ background-color: #0d0612;
38
+ background-image: radial-gradient(circle at 15% 50%, rgba(107, 11, 201, 0.15), transparent 25%),
39
+ radial-gradient(circle at 85% 30%, rgba(16, 185, 129, 0.1), transparent 25%);
40
+ color: #e2e8f0;
41
+ }
42
+
43
+ ::-webkit-scrollbar {
44
+ width: 8px;
45
+ }
46
+ ::-webkit-scrollbar-track {
47
+ background: #191022;
48
+ }
49
+ ::-webkit-scrollbar-thumb {
50
+ background: #4d3168;
51
+ border-radius: 4px;
52
+ }
53
+ ::-webkit-scrollbar-thumb:hover {
54
+ background: #7f0df2;
55
+ }
56
+ .glass-panel {
57
+ background: rgba(25, 16, 34, 0.6);
58
+ backdrop-filter: blur(12px);
59
+ -webkit-backdrop-filter: blur(12px);
60
+ border: 1px solid rgba(255, 255, 255, 0.05);
61
+ }
62
+ /* Footer fade for the floating button */
63
+ .footer-fade {
64
+ background: linear-gradient(to top, #191022 20%, transparent 100%);
65
+ pointer-events: none;
66
+ }
67
+ .connection-line {
68
+ height: 2px;
69
+ flex-grow: 1;
70
+ background: linear-gradient(90deg, #4d3168 0%, #7f0df2 50%, #4d3168 100%);
71
+ opacity: 0.5;
72
+ position: relative;
73
+ }
74
+ .connection-line::after {
75
+ content: '';
76
+ position: absolute;
77
+ right: 0;
78
+ top: 50%;
79
+ transform: translateY(-50%);
80
+ width: 6px;
81
+ height: 6px;
82
+ background: #7f0df2;
83
+ border-radius: 50%;
84
+ box-shadow: 0 0 10px #7f0df2;
85
+ }
86
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
87
  </head>
88
+ <body class="h-screen flex flex-col overflow-hidden">
89
 
90
+ <!-- Header -->
91
+ <header class="h-16 border-b border-white/5 bg-surface/50 backdrop-blur-md flex items-center justify-between px-6 z-20 shrink-0">
92
+ <div class="flex items-center gap-3">
93
+ <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-lg">
94
+ <span class="material-symbols-outlined text-white text-md">all_inclusive</span>
95
+ </div>
96
+ <h1 class="text-xl font-display font-bold tracking-tight text-white">Continuity</h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  </div>
98
+
99
+ <div class="flex items-center gap-4">
100
+ <div class="hidden md:flex items-center gap-2 px-3 py-1.5 rounded-full bg-white/5 border border-white/5">
101
+ <div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
102
+ <span class="text-xs font-medium text-gray-300">System Online</span>
103
+ </div>
104
+ <a href="https://github.com/Bhishaj9/Continuity" target="_blank" class="p-2 text-gray-400 hover:text-white transition-colors">
105
+ <svg viewBox="0 0 24 24" class="w-5 h-5 fill-current" aria-hidden="true"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path></svg>
106
+ </a>
107
  </div>
108
+ </header>
109
+
110
+ <!-- Main Workspace -->
111
+ <main class="flex-1 flex overflow-hidden">
112
+
113
+ <!-- Left Panel: Scene Inputs (Scrollable) -->
114
+ <div class="w-1/3 min-w-[320px] max-w-[400px] border-r border-white/5 bg-surface/30 flex flex-col h-full">
115
+ <div class="p-6 overflow-y-auto custom-scrollbar flex-1 pb-32">
116
+ <h2 class="text-sm font-bold text-gray-400 uppercase tracking-wider mb-6">Scene Setup</h2>
117
+
118
+ <!-- Scene A Upload -->
119
+ <div class="mb-8 group">
120
+ <div class="flex items-center justify-between mb-2">
121
+ <label class="text-white font-medium text-sm">Scene A (Start)</label>
122
+ <span class="text-[10px] px-2 py-0.5 rounded bg-primary/20 text-primary border border-primary/30">Input</span>
123
+ </div>
124
+ <div class="relative w-full aspect-video rounded-xl bg-surface-dark border-2 border-dashed border-white/10 group-hover:border-primary/50 transition-colors overflow-hidden flex flex-col items-center justify-center cursor-pointer hover:bg-white/5" onclick="document.getElementById('video-upload-a').click()">
125
+ <span class="material-symbols-outlined text-4xl text-gray-600 mb-2 group-hover:text-primary transition-colors">movie</span>
126
+ <span id="label-a" class="text-xs text-gray-500 font-medium">Click to Upload Video A</span>
127
+ <input type="file" id="video-upload-a" class="hidden" accept="video/*" onchange="handleFileSelect(this, 'label-a')">
128
+ </div>
129
+ </div>
130
+
131
+ <!-- Connection Arrow -->
132
+ <div class="flex items-center justify-center mb-8 opacity-50">
133
+ <div class="h-12 w-[1px] bg-gradient-to-b from-transparent via-primary to-transparent"></div>
134
+ </div>
135
+
136
+ <!-- Scene C Upload -->
137
+ <div class="mb-4 group">
138
+ <div class="flex items-center justify-between mb-2">
139
+ <label class="text-white font-medium text-sm">Scene C (End)</label>
140
+ <span class="text-[10px] px-2 py-0.5 rounded bg-primary/20 text-primary border border-primary/30">Input</span>
141
+ </div>
142
+ <div class="relative w-full aspect-video rounded-xl bg-surface-dark border-2 border-dashed border-white/10 group-hover:border-primary/50 transition-colors overflow-hidden flex flex-col items-center justify-center cursor-pointer hover:bg-white/5" onclick="document.getElementById('video-upload-c').click()">
143
+ <span class="material-symbols-outlined text-4xl text-gray-600 mb-2 group-hover:text-primary transition-colors">movie</span>
144
+ <span id="label-c" class="text-xs text-gray-500 font-medium">Click to Upload Video C</span>
145
+ <input type="file" id="video-upload-c" class="hidden" accept="video/*" onchange="handleFileSelect(this, 'label-c')">
146
+ </div>
147
+ </div>
148
  </div>
149
+
150
+ <!-- Bottom Action Area (Fixed) -->
151
+ <div class="p-6 border-t border-white/5 bg-surface/50 backdrop-blur-md absolute bottom-0 left-0 w-full z-10">
152
+ <!-- 1. Analyze Panel -->
153
+ <div id="analysis-panel" class="glass-panel rounded-full p-2 pl-6 flex items-center justify-center shadow-neon">
154
+ <button id="analyze-btn"
155
+ 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">
156
+ <span class="material-symbols-outlined text-[20px]">analytics</span>
157
+ Upload & Analyze Scenes
158
+ </button>
159
+ </div>
160
+
161
+ <!-- 2. Review Panel (Hidden Initially) -->
162
+ <div id="review-panel" class="hidden glass-panel rounded-2xl p-4 flex flex-col gap-4 shadow-neon w-full">
163
+ <div class="flex items-center justify-between pb-2 border-b border-white/10">
164
+ <h3 class="text-white font-display font-bold">🎬 Director's Cut: Review Prompt</h3>
165
+ <button onclick="resetUI()" class="text-gray-400 hover:text-white text-xs uppercase tracking-widest">Reset</button>
166
+ </div>
167
+
168
+ <textarea id="prompt-box" rows="3"
169
+ 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"
170
+ placeholder="AI generated transition prompt will appear here..."></textarea>
171
+
172
+ <div class="grid grid-cols-2 gap-4">
173
+ <div class="flex flex-col gap-2">
174
+ <label for="style-select" class="text-xs font-bold text-gray-400 uppercase tracking-widest pl-1">Visual Style</label>
175
+ <select id="style-select" 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">
176
+ <option value="Cinematic">Cinematic (Default)</option>
177
+ <option value="Anime">Anime</option>
178
+ <option value="Cyberpunk">Cyberpunk</option>
179
+ <option value="VHS Glitch">VHS Glitch</option>
180
+ <option value="Claymation">Claymation</option>
181
+ <option value="Noir">Noir</option>
182
+ </select>
183
+ </div>
184
+
185
+ <div class="flex flex-col gap-2">
186
+ <label for="audio-input" class="text-xs font-bold text-gray-400 uppercase tracking-widest pl-1">Audio Mood</label>
187
+ <input id="audio-input" type="text"
188
+ 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"
189
+ placeholder="e.g. Epic orchestral, Cyberpunk synth..." value="Cinematic ambient sound">
190
+ </div>
191
+ </div>
192
+
193
+ <button id="generate-btn"
194
+ 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">
195
+ <span class="material-symbols-outlined text-[24px]">movie_filter</span>
196
+ ACTION! (Generate Video)
197
+ </button>
198
+ </div>
199
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
+ <!-- Right Panel: The Bridge (Output) -->
203
+ <div class="flex-1 bg-[#050308] relative flex flex-col items-center justify-center p-12">
204
+
205
+ <!-- Background Decorations -->
206
+ <div class="absolute inset-0 overflow-hidden pointer-events-none">
207
+ <div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-primary/5 rounded-full blur-3xl"></div>
208
+ <div class="absolute grid grid-cols-[repeat(20,minmax(0,1fr))] w-full h-full opacity-10">
209
+ <!-- Grid lines generated via script or just consistent bg -->
210
+ </div>
211
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
+ <!-- Main Content Card -->
214
+ <div class="relative z-10 w-full max-w-4xl flex flex-col items-center">
215
+
216
+ <h2 class="text-center font-display text-4xl font-bold text-white mb-2 tracking-tight">The Bridge</h2>
217
+ <p class="text-center text-gray-400 mb-10 max-w-lg">AI-generated seamless transition between Scene A and Scene C.</p>
218
+
219
+ <div class="w-full aspect-video rounded-3xl bg-surface/40 border border-white/5 shadow-2xl backdrop-blur-sm p-4 relative flex items-center justify-center group" id="bridge-card">
220
+ <!-- Placeholder State -->
221
+ <div class="text-center">
222
+ <div class="w-20 h-20 rounded-full bg-white/5 flex items-center justify-center mb-4 mx-auto group-hover:scale-110 transition-transform duration-500">
223
+ <span class="material-symbols-outlined text-4xl text-gray-600">movie_edit</span>
224
+ </div>
225
+ <p class="text-gray-500 font-medium">Waiting for inputs...</p>
226
+ </div>
227
+
228
+ <!-- Loading Overlay (Hidden by default) -->
229
+ <div id="loader" class="hidden absolute inset-0 bg-black/80 backdrop-blur-md rounded-2xl flex flex-col items-center justify-center z-20">
230
+ <div class="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mb-4"></div>
231
+ <p class="text-white font-medium animate-pulse">Forging the bridge...</p>
232
+ </div>
233
+ </div>
234
+
235
+ <!-- Timeline / Visualization (Static for now) -->
236
+ <div class="w-full mt-8 flex items-center justify-between px-4 opacity-50">
237
+ <span class="text-xs font-mono text-gray-500">00:00</span>
238
+ <div class="connection-line mx-4"></div>
239
+ <span class="text-xs font-mono text-gray-500">00:05</span>
240
+ </div>
241
 
242
+ </div>
243
+ </div>
244
+ </main>
245
+
246
+ <script>
247
+ let currentVideoAPath = "";
248
+ let currentVideoCPath = "";
249
+
250
+ // UI Helper: Update filename text when file is selected
251
+ function handleFileSelect(input, labelId) {
252
+ if (input.files && input.files[0]) {
253
+ const label = document.getElementById(labelId);
254
+ label.innerText = input.files[0].name;
255
+ label.classList.add("text-primary"); // Turn purple to indicate success
256
+ }
257
  }
258
+
259
+ function resetUI() {
260
+ document.getElementById("analysis-panel").classList.remove("hidden");
261
+ document.getElementById("review-panel").classList.add("hidden");
262
+ document.getElementById("prompt-box").value = "";
263
+ currentVideoAPath = "";
264
+ currentVideoCPath = "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  }
 
 
266
 
267
+ // Step 1: Analyze
268
+ const analyzeBtn = document.getElementById("analyze-btn");
269
+ if (analyzeBtn) {
270
+ analyzeBtn.addEventListener("click", async () => {
271
+ const fileA = document.getElementById("video-upload-a").files[0];
272
+ const fileC = document.getElementById("video-upload-c").files[0];
273
+ const btn = document.getElementById("analyze-btn");
274
+
275
+ // 1. Validation
276
+ if (!fileA || !fileC) {
277
+ alert("⚠️ Action Required: Please upload both Scene A and Scene C videos.");
278
+ return;
279
+ }
280
 
281
+ // 2. Loading State
282
+ const originalContent = btn.innerHTML;
283
+ btn.innerHTML = `<span class="material-symbols-outlined animate-spin text-[20px] mr-2">progress_activity</span> Analyzing Scenes...`;
284
+ btn.disabled = true;
285
+ btn.classList.add("opacity-70", "cursor-not-allowed");
286
+
287
+ const formData = new FormData();
288
+ formData.append("video_a", fileA);
289
+ formData.append("video_c", fileC);
290
+
291
+ try {
292
+ // 3. Send to Server
293
+ const response = await fetch("/analyze", {
294
+ method: "POST",
295
+ body: formData
296
+ });
297
+
298
+ if (!response.ok) throw new Error("Analysis failed.");
299
+
300
+ const data = await response.json();
301
+
302
+ // 4. Success: Show Review Panel
303
+ document.getElementById("prompt-box").value = data.prompt;
304
+ currentVideoAPath = data.video_a_path;
305
+ currentVideoCPath = data.video_c_path;
306
+
307
+ document.getElementById("analysis-panel").classList.add("hidden");
308
+ document.getElementById("review-panel").classList.remove("hidden");
309
+
310
+ } catch (error) {
311
+ console.error(error);
312
+ alert("❌ Analysis Failed: " + error.message);
313
+ } finally {
314
+ btn.innerHTML = originalContent;
315
+ btn.disabled = false;
316
+ btn.classList.remove("opacity-70", "cursor-not-allowed");
317
+ }
318
+ });
319
  }
320
 
321
+ // Step 2: Generate
322
+ const generateBtn = document.getElementById("generate-btn");
323
+ if (generateBtn) {
324
+ generateBtn.addEventListener("click", async () => {
325
+ const prompt = document.getElementById("prompt-box").value;
326
+ const style = document.getElementById("style-select").value;
327
+ // Capture Audio Prompt
328
+ const audioPrompt = document.getElementById("audio-input").value;
329
+ const btn = document.getElementById("generate-btn");
330
+
331
+ if (!currentVideoAPath || !currentVideoCPath) {
332
+ alert("Session lost. Please re-upload videos.");
333
+ resetUI();
334
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  }
 
 
 
 
 
 
 
336
 
337
+ // Loading State
338
+ const originalContent = btn.innerHTML;
339
+ btn.disabled = true;
340
+ btn.classList.add("opacity-70", "cursor-not-allowed");
341
+
342
+ try {
343
+ // 1. Start Job
344
+ const response = await fetch("/generate", {
345
+ method: "POST",
346
+ headers: {
347
+ "Content-Type": "application/json"
348
+ },
349
+ body: JSON.stringify({
350
+ prompt: prompt,
351
+ style: style,
352
+ audio_prompt: audioPrompt, // Send to server
353
+ video_a_path: currentVideoAPath,
354
+ video_c_path: currentVideoCPath
355
+ })
356
+ });
357
+
358
+ if (!response.ok) throw new Error("Generation failed to start.");
359
+
360
+ const data = await response.json();
361
+ const jobId = data.job_id;
362
+
363
+ // 2. Poll for Status
364
+ const pollInterval = setInterval(async () => {
365
+ try {
366
+ // ADDED TIMESTAMP TO PREVENT CACHING
367
+ const statusRes = await fetch(`/status/${jobId}?t=${Date.now()}`);
368
+ if (!statusRes.ok) return;
369
+
370
+ const statusData = await statusRes.json();
371
+
372
+ // Update Button Text
373
+ btn.innerHTML = `<span class="material-symbols-outlined animate-spin text-[20px] mr-2">progress_activity</span> ${statusData.log} (${statusData.progress}%)`;
374
+
375
+ if (statusData.status === "completed") {
376
+ clearInterval(pollInterval);
377
+ // Show Video
378
+ const bridgeCard = document.getElementById("bridge-card");
379
+ if (bridgeCard) {
380
+ if (statusData.video_url) {
381
+ bridgeCard.innerHTML = `
382
+ <video controls autoplay loop class="w-full h-full object-cover rounded-2xl border-2 border-primary shadow-neon">
383
+ <source src="${statusData.video_url}" type="video/mp4">
384
+ Your browser does not support the video tag.
385
+ </video>
386
+ `;
387
+ document.getElementById("analysis-panel").classList.remove("hidden");
388
+ document.getElementById("review-panel").classList.add("hidden");
389
+ } else {
390
+ alert("Video generated but URL missing.");
391
+ }
392
+ btn.innerHTML = originalContent;
393
+ btn.disabled = false;
394
+ btn.classList.remove("opacity-70", "cursor-not-allowed");
395
+ }
396
+ } else if (statusData.status === "error") {
397
+ clearInterval(pollInterval);
398
+ alert("❌ Generation Error: " + statusData.log);
399
+ btn.innerHTML = originalContent;
400
+ btn.disabled = false;
401
+ btn.classList.remove("opacity-70", "cursor-not-allowed");
402
+ }
403
+ } catch (e) {
404
+ console.error("Polling error", e);
405
+ }
406
+ }, 1000);
407
+
408
+ } catch (error) {
409
+ console.error(error);
410
+ alert("❌ Request Failed: " + error.message);
411
+ btn.innerHTML = originalContent;
412
+ btn.disabled = false;
413
+ btn.classList.remove("opacity-70", "cursor-not-allowed");
414
+ }
415
+ });
416
  }
417
+ </script>
 
 
418
  </body>
 
419
  </html>