Gaurav vashistha commited on
Commit
4e8e87d
·
1 Parent(s): 1197046

feat: smart caching for analysis and ui cleanup

Browse files
Files changed (2) hide show
  1. agent.py +46 -25
  2. stitch_continuity_dashboard/code.html +142 -318
agent.py CHANGED
@@ -2,34 +2,65 @@ import os
2
  import time
3
  import logging
4
  import tempfile
5
- import json
6
  from google import genai
7
  from google.genai import types
8
  from config import Settings
9
  from utils import download_to_temp, download_blob, save_video_bytes, update_job_status, stitch_videos
10
 
11
  logging.basicConfig(level=logging.INFO)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  def analyze_only(path_a, path_c, job_id=None):
14
- update_job_status(job_id, "analyzing", 10, "Director starting analysis...")
15
  client = genai.Client(api_key=Settings.GOOGLE_API_KEY)
16
-
17
  try:
18
- file_a = client.files.upload(file=path_a)
19
- file_c = client.files.upload(file=path_c)
 
20
 
 
21
  while file_a.state.name == "PROCESSING" or file_c.state.name == "PROCESSING":
22
- time.sleep(1)
23
-
 
 
 
24
  prompt = "You are a director. Analyze Video A and Video C. Write a visual prompt for a 2-second transition (Video B) connecting A to C. Output ONLY the prompt."
25
  update_job_status(job_id, "analyzing", 30, "Director drafting transition...")
26
 
27
- res = client.models.generate_content(
28
- model="gemini-2.0-flash-exp",
29
- contents=[prompt, file_a, file_c]
30
- )
31
  return {"prompt": res.text, "status": "success"}
32
  except Exception as e:
 
33
  return {"detail": str(e), "status": "error"}
34
 
35
  def generate_only(prompt, path_a, path_c, job_id, style, audio, neg, guidance, motion):
@@ -38,26 +69,20 @@ def generate_only(prompt, path_a, path_c, job_id, style, audio, neg, guidance, m
38
  full_prompt = f"{style} style. {prompt} Soundtrack: {audio}"
39
  if neg:
40
  full_prompt += f" --no {neg}"
41
-
42
  try:
43
  if Settings.GCP_PROJECT_ID:
44
  client = genai.Client(vertexai=True, project=Settings.GCP_PROJECT_ID, location=Settings.GCP_LOCATION)
45
-
46
- # Note: Guidance and Motion strength parameters would be used here if the model config supported them directly in this SDK version
47
  op = client.models.generate_videos(
48
- model='veo-3.1-generate-preview',
49
- prompt=full_prompt,
50
  config=types.GenerateVideosConfig(number_of_videos=1)
51
  )
52
-
53
  while not op.done:
54
  time.sleep(5)
55
- op = client.operations.get(op)
56
 
57
  if op.result and op.result.generated_videos:
58
  vid = op.result.generated_videos[0]
59
  bridge_path = None
60
-
61
  if vid.video.uri:
62
  bridge_path = tempfile.mktemp(suffix=".mp4")
63
  download_blob(vid.video.uri, bridge_path)
@@ -65,21 +90,17 @@ def generate_only(prompt, path_a, path_c, job_id, style, audio, neg, guidance, m
65
  bridge_path = save_video_bytes(vid.video.video_bytes)
66
 
67
  if bridge_path:
68
- # --- PHASE 3: THE FINAL CUT ---
69
  update_job_status(job_id, "stitching", 80, "Stitching Director's Cut (A+B+C)...", video_url=bridge_path)
70
  final_cut_path = os.path.join("outputs", f"{job_id}_merged_temp.mp4")
71
-
72
  try:
73
  final_output = stitch_videos(path_a, bridge_path, path_c, final_cut_path)
74
- # Success: Send BOTH Bridge URL (for player) and Merged URL (for download button)
75
  update_job_status(job_id, "completed", 100, "Done!", video_url=bridge_path, merged_video_url=final_output)
76
  except Exception as e:
77
- # If stitch fails, fallback to just the bridge
78
- logging.error(f"Stitch failed: {e}")
79
  update_job_status(job_id, "completed", 100, "Stitch failed, showing bridge.", video_url=bridge_path)
80
  return
81
  except Exception as e:
82
  update_job_status(job_id, "error", 0, f"Error: {e}")
83
  return
84
-
85
  update_job_status(job_id, "error", 0, "Generation failed.")
 
2
  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
9
  from utils import download_to_temp, download_blob, save_video_bytes, update_job_status, stitch_videos
10
 
11
  logging.basicConfig(level=logging.INFO)
12
+ logger = logging.getLogger(__name__)
13
+
14
+ def get_file_hash(filepath):
15
+ """Calculates MD5 hash of file to prevent duplicate uploads."""
16
+ hash_md5 = hashlib.md5()
17
+ with open(filepath, "rb") as f:
18
+ for chunk in iter(lambda: f.read(4096), b""):
19
+ hash_md5.update(chunk)
20
+ return hash_md5.hexdigest()
21
+
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
+ # 1. Check if file with this hash name already exists
27
+ # Note: In production with many files, you'd store this mapping in a DB.
28
+ # For this scale, iterating recent files is acceptable.
29
+ try:
30
+ for f in client.files.list(config={'page_size': 50}):
31
+ if f.display_name == file_hash and f.state.name == "ACTIVE":
32
+ logger.info(f"♻️ Smart Cache Hit: Using existing file for {os.path.basename(filepath)}")
33
+ return f
34
+ except Exception:
35
+ pass # If list fails, just proceed to upload
36
+
37
+ # 2. Upload if not found
38
+ logger.info(f"wm Uploading new file: {filepath} (Hash: {file_hash})")
39
+ return client.files.upload(file=filepath, config={'display_name': file_hash})
40
 
41
  def analyze_only(path_a, path_c, job_id=None):
42
+ update_job_status(job_id, "analyzing", 10, "Director checking file cache...")
43
  client = genai.Client(api_key=Settings.GOOGLE_API_KEY)
44
+
45
  try:
46
+ # Use Smart Upload (Cache Check)
47
+ file_a = get_or_upload_file(client, path_a)
48
+ file_c = get_or_upload_file(client, path_c)
49
 
50
+ # Wait for processing (usually instant if cached)
51
  while file_a.state.name == "PROCESSING" or file_c.state.name == "PROCESSING":
52
+ update_job_status(job_id, "analyzing", 20, "Google is processing video geometry...")
53
+ time.sleep(2)
54
+ file_a = client.files.get(name=file_a.name)
55
+ file_c = client.files.get(name=file_c.name)
56
+
57
  prompt = "You are a director. Analyze Video A and Video C. Write a visual prompt for a 2-second transition (Video B) connecting A to C. Output ONLY the prompt."
58
  update_job_status(job_id, "analyzing", 30, "Director drafting transition...")
59
 
60
+ res = client.models.generate_content(model="gemini-2.0-flash-exp", contents=[prompt, file_a, file_c])
 
 
 
61
  return {"prompt": res.text, "status": "success"}
62
  except Exception as e:
63
+ logger.error(f"Analysis Error: {e}")
64
  return {"detail": str(e), "status": "error"}
65
 
66
  def generate_only(prompt, path_a, path_c, job_id, style, audio, neg, guidance, motion):
 
69
  full_prompt = f"{style} style. {prompt} Soundtrack: {audio}"
70
  if neg:
71
  full_prompt += f" --no {neg}"
 
72
  try:
73
  if Settings.GCP_PROJECT_ID:
74
  client = genai.Client(vertexai=True, project=Settings.GCP_PROJECT_ID, location=Settings.GCP_LOCATION)
 
 
75
  op = client.models.generate_videos(
76
+ model='veo-3.1-generate-preview',
77
+ prompt=full_prompt,
78
  config=types.GenerateVideosConfig(number_of_videos=1)
79
  )
 
80
  while not op.done:
81
  time.sleep(5)
 
82
 
83
  if op.result and op.result.generated_videos:
84
  vid = op.result.generated_videos[0]
85
  bridge_path = None
 
86
  if vid.video.uri:
87
  bridge_path = tempfile.mktemp(suffix=".mp4")
88
  download_blob(vid.video.uri, bridge_path)
 
90
  bridge_path = save_video_bytes(vid.video.video_bytes)
91
 
92
  if bridge_path:
 
93
  update_job_status(job_id, "stitching", 80, "Stitching Director's Cut (A+B+C)...", video_url=bridge_path)
94
  final_cut_path = os.path.join("outputs", f"{job_id}_merged_temp.mp4")
 
95
  try:
96
  final_output = stitch_videos(path_a, bridge_path, path_c, final_cut_path)
 
97
  update_job_status(job_id, "completed", 100, "Done!", video_url=bridge_path, merged_video_url=final_output)
98
  except Exception as e:
99
+ logger.error(f"Stitch failed: {e}")
 
100
  update_job_status(job_id, "completed", 100, "Stitch failed, showing bridge.", video_url=bridge_path)
101
  return
102
  except Exception as e:
103
  update_job_status(job_id, "error", 0, f"Error: {e}")
104
  return
105
+
106
  update_job_status(job_id, "error", 0, "Generation failed.")
stitch_continuity_dashboard/code.html CHANGED
@@ -48,7 +48,6 @@
48
 
49
  #gallery-drawer {
50
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
51
- z-index: 50;
52
  }
53
 
54
  .drawer-open {
@@ -82,354 +81,179 @@
82
  background: #362445;
83
  border-radius: 2px;
84
  }
85
-
86
- /* Custom Scrollbar */
87
- .custom-scrollbar::-webkit-scrollbar {
88
- width: 4px;
89
- }
90
-
91
- .custom-scrollbar::-webkit-scrollbar-track {
92
- background: rgba(255, 255, 255, 0.05);
93
- }
94
-
95
- .custom-scrollbar::-webkit-scrollbar-thumb {
96
- background: rgba(127, 13, 242, 0.5);
97
- border-radius: 2px;
98
- }
99
  </style>
100
  </head>
101
 
102
  <body
103
  class="bg-background-dark font-body text-white h-screen w-screen overflow-hidden flex flex-col selection:bg-primary selection:text-white">
104
-
105
- <!-- HEADER (Fixed height, no shrink) -->
106
- <div
107
- class="h-16 shrink-0 flex items-center justify-between px-6 border-b border-border-dark bg-background-dark/80 backdrop-blur-md z-40 relative">
108
- <div class="flex items-center gap-3">
109
- <div
110
- class="size-8 flex items-center justify-center bg-primary/20 rounded-lg border border-primary/30 text-primary">
111
- <span class="material-symbols-outlined">movie_filter</span>
112
- </div>
113
- <h1 class="text-xl font-display font-bold tracking-tight">Continuity</h1>
114
- </div>
115
- <div class="flex items-center gap-3">
116
- <button onclick="toggleDrawer(true)"
117
- class="flex items-center gap-2 px-4 py-2 bg-primary hover:bg-[#6b0bc9] text-white rounded-lg transition-colors text-xs font-bold uppercase tracking-wider shadow-neon">
118
- <span class="material-symbols-outlined text-lg">history</span> Gallery
119
- </button>
120
  </div>
 
121
  </div>
122
-
123
- <!-- GALLERY DRAWER (Fixed absolute overlay) -->
124
- <div id="drawer-overlay"
125
- class="fixed inset-0 bg-black/80 backdrop-blur-sm z-40 hidden transition-opacity duration-300 pointer-events-auto"
126
- onclick="toggleDrawer(false)"></div>
127
- <div id="gallery-drawer"
128
- class="fixed top-0 right-0 h-full w-full max-w-md bg-surface-dark border-l border-border-dark shadow-2xl drawer-closed flex flex-col pointer-events-auto">
129
- <div class="p-6 border-b border-white/5 flex items-center justify-between bg-surface-dark">
130
- <h2 class="text-lg font-bold text-white flex items-center gap-2"><span
131
- class="material-symbols-outlined text-primary">history</span> History</h2>
132
- <button onclick="toggleDrawer(false)" class="text-gray-400 hover:text-white transition-colors"><span
133
- class="material-symbols-outlined">close</span></button>
134
- </div>
135
- <div id="gallery-content" class="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar">
136
- <div class="text-center text-gray-500 mt-10">Loading...</div>
137
- </div>
138
  </div>
139
-
140
- <!-- MAIN CONTENT (Flex grow, scrollable) -->
141
- <div class="flex-1 overflow-y-auto w-full relative z-0 flex flex-col items-center pt-8 pb-32 px-4">
142
- <div class="w-full max-w-6xl flex flex-col md:flex-row items-center justify-center gap-4 md:gap-8 lg:gap-12">
143
-
144
- <!-- Scene A -->
145
- <div class="flex flex-col gap-3 flex-1 w-full max-w-[320px] group">
146
- <div class="flex justify-between px-1"><span
147
- class="text-[10px] font-bold tracking-widest text-gray-500 uppercase">Scene A (Start)</span>
148
- </div>
149
- <div class="relative aspect-[9/16] md:aspect-[3/4] bg-surface-dark border-2 border-dashed border-border-dark rounded-2xl flex flex-col items-center justify-center gap-4 hover:border-primary/50 hover:bg-surface-dark/80 transition-all cursor-pointer shadow-lg overflow-hidden"
150
- onclick="document.getElementById('video-upload-a').click()">
151
- <div
152
- class="size-12 rounded-full bg-white/5 flex items-center justify-center group-hover:scale-110 transition-transform relative z-10">
153
- <span
154
- class="material-symbols-outlined text-2xl text-gray-400 group-hover:text-white">upload</span>
155
- </div>
156
- <p id="label-a" class="text-xs font-medium text-gray-400 text-center px-4 relative z-10">Upload
157
- Start Clip</p>
158
- <input type="file" id="video-upload-a" accept="video/*" class="hidden"
159
- onchange="handleFileSelect(this, 'label-a')">
160
  </div>
 
 
 
 
161
  </div>
162
-
163
- <!-- Bridge -->
164
- <div class="flex flex-col gap-3 flex-[1.5] w-full max-w-[500px] relative z-20">
165
- <div class="flex justify-center px-1"><span
166
- class="text-[10px] font-bold tracking-[0.2em] text-primary uppercase animate-pulse">Generated
167
- Bridge</span></div>
168
- <div id="bridge-card-outer"
169
- class="relative aspect-video rounded-2xl shadow-neon transition-all duration-500 border border-primary/20">
170
- <div id="bridge-card-inner" class="force-clip w-full h-full bg-black relative">
171
- <div id="bridge-content" class="w-full h-full">
172
- <div
173
- class="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1614850523060-8da1d56ae167?q=80&w=1000&auto=format&fit=crop')] bg-cover bg-center opacity-20 mix-blend-overlay">
174
- </div>
175
- <div class="absolute inset-0 flex flex-col items-center justify-center text-center p-6">
176
- <div
177
- class="size-16 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center mb-3">
178
- <span class="material-symbols-outlined text-3xl text-primary">auto_awesome</span>
179
- </div>
180
- <p class="text-sm text-gray-300">Ready to bridge the gap</p>
181
- </div>
182
  </div>
183
- <div id="bridge-border"
184
- class="absolute inset-0 rounded-2xl border-2 border-transparent pointer-events-none z-30">
 
 
 
185
  </div>
186
  </div>
187
- </div>
188
-
189
- <div id="merged-download-container"
190
- class="hidden flex justify-center mt-4 animate-in fade-in slide-in-from-top-2">
191
- <a id="merged-download-btn" href="#" download
192
- class="flex items-center gap-2 px-6 py-3 bg-surface-dark hover:bg-white/5 border border-primary/30 hover:border-primary text-primary hover:text-white rounded-xl font-bold text-xs uppercase tracking-widest transition-all shadow-lg group">
193
- <span class="material-symbols-outlined group-hover:animate-bounce">download</span>
194
- Download Merged Video
195
- </a>
196
  </div>
197
  </div>
198
-
199
- <!-- Scene C -->
200
- <div class="flex flex-col gap-3 flex-1 w-full max-w-[320px] group">
201
- <div class="flex justify-end px-1"><span
202
- class="text-[10px] font-bold tracking-widest text-gray-500 uppercase">Scene C (End)</span></div>
203
- <div class="relative aspect-[9/16] md:aspect-[3/4] bg-surface-dark border-2 border-dashed border-border-dark rounded-2xl flex flex-col items-center justify-center gap-4 hover:border-primary/50 hover:bg-surface-dark/80 transition-all cursor-pointer shadow-lg overflow-hidden"
204
- onclick="document.getElementById('video-upload-c').click()">
205
- <div
206
- class="size-12 rounded-full bg-white/5 flex items-center justify-center group-hover:scale-110 transition-transform relative z-10">
207
- <span
208
- class="material-symbols-outlined text-2xl text-gray-400 group-hover:text-white">upload</span>
209
- </div>
210
- <p id="label-c" class="text-xs font-medium text-gray-400 text-center px-4 relative z-10">Upload End
211
- Clip</p>
212
- <input type="file" id="video-upload-c" accept="video/*" class="hidden"
213
- onchange="handleFileSelect(this, 'label-c')">
 
214
  </div>
 
 
 
 
215
  </div>
216
  </div>
217
  </div>
218
-
219
- <!-- FLOATING PANELS (Fixed Bottom) -->
220
- <div class="fixed bottom-6 left-1/2 -translate-x-1/2 w-full max-w-2xl px-4 z-30">
221
-
222
- <!-- ANALYSIS PANEL -->
223
- <div id="analysis-panel"
224
- class="glass-panel p-2 rounded-full shadow-neon flex items-center justify-between pl-6 pr-2 mb-4">
225
- <div class="flex flex-col"><span class="text-sm font-bold text-white">Continuity Engine</span><span
226
- class="text-[10px] text-gray-400 uppercase tracking-wide">Ready for analysis</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  <div class="flex gap-2">
228
  <button onclick="toggleDrawer(true)"
229
- class="bg-surface-dark hover:bg-white/10 text-white p-3 rounded-full transition-all flex items-center justify-center border border-white/10"><span
230
- class="material-symbols-outlined text-lg">history</span></button>
231
- <button id="analyze-btn"
232
- class="bg-primary hover:bg-[#6b0bc9] text-white px-6 py-3 rounded-full font-bold text-sm transition-all flex items-center gap-2 shadow-lg"><span
233
- class="material-symbols-outlined text-lg">analytics</span> Analyze Scenes</button>
234
  </div>
235
  </div>
236
-
237
- <!-- REVIEW PANEL -->
238
- <div id="review-panel"
239
- class="hidden glass-panel rounded-2xl p-5 shadow-2xl flex flex-col gap-4 animate-in slide-in-from-bottom-4 duration-300 max-h-[80vh] overflow-y-auto custom-scrollbar">
240
- <div class="flex items-center justify-between border-b border-white/10 pb-3">
241
- <h3 class="text-sm font-bold text-white flex items-center gap-2"><span
242
- class="material-symbols-outlined text-primary">movie_edit</span> Director's Configuration</h3>
243
- <div class="flex gap-2">
244
- <button onclick="toggleDrawer(true)"
245
- class="text-xs text-gray-400 hover:text-white uppercase tracking-wider flex items-center gap-1"><span
246
- class="material-symbols-outlined text-sm">history</span> History</button>
247
- <span class="text-white/10">|</span>
248
- <button onclick="resetUI()"
249
- class="text-xs text-gray-500 hover:text-white uppercase tracking-wider">Reset</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  </div>
251
- </div>
252
- <div>
253
- <label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Visual
254
- Direction</label>
255
- <textarea id="prompt-box" rows="2"
256
- 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>
257
- </div>
258
- <div class="grid grid-cols-2 gap-4">
259
  <div>
260
- <label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Visual
261
- Style</label>
262
- <select id="style-select" onchange="savePreference('style', this.value)"
263
- class="w-full bg-black/20 border border-white/10 rounded-lg p-2.5 text-sm text-white focus:border-primary outline-none cursor-pointer">
264
- <option value="Cinematic">Cinematic</option>
265
- <option value="Anime">Anime</option>
266
- <option value="Cyberpunk">Cyberpunk</option>
267
- <option value="VHS">VHS Glitch</option>
268
- <option value="Noir">Noir</option>
269
- </select>
270
  </div>
271
  <div>
272
- <label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Audio
273
- Mood</label>
274
- <select id="audio-input" onchange="savePreference('audio', this.value)"
275
- class="w-full bg-black/20 border border-white/10 rounded-lg p-2.5 text-sm text-white focus:border-primary outline-none cursor-pointer">
276
- <option value="Cinematic orchestral score">Cinematic</option>
277
- <option value="Industrial synthwave">Cyberpunk</option>
278
- <option value="Nature sounds">Nature</option>
279
- <option value="Tense atmosphere">Horror</option>
280
- <option value="High energy rock">Action</option>
281
- </select>
282
- </div>
283
- </div>
284
- <div class="border-t border-white/10 pt-3">
285
- <button onclick="document.getElementById('advanced-settings').classList.toggle('hidden')"
286
- class="flex items-center gap-2 text-xs font-bold text-gray-400 uppercase tracking-widest hover:text-white transition-colors w-full">
287
- <span class="material-symbols-outlined text-sm">tune</span> Advanced Physics & Controls <span
288
- class="material-symbols-outlined text-sm ml-auto">expand_more</span>
289
- </button>
290
- <div id="advanced-settings"
291
- class="hidden pt-3 grid grid-cols-1 md:grid-cols-2 gap-4 animate-in fade-in slide-in-from-top-2">
292
- <div class="col-span-1 md:col-span-2">
293
- <label class="text-[10px] text-gray-500 uppercase font-bold">Negative Prompt</label>
294
- <input id="negative-prompt" type="text" placeholder="text, blurry, watermark, distorted"
295
- class="w-full bg-black/20 border border-white/10 rounded-lg p-2 text-xs text-white focus:border-red-500/50 outline-none mt-1">
296
- </div>
297
- <div>
298
- <div class="flex justify-between"><label
299
- class="text-[10px] text-gray-500 uppercase font-bold">Guidance Scale</label><span
300
- id="guidance-val" class="text-[10px] text-primary">5.0</span></div>
301
- <input id="guidance-scale" type="range" min="1" max="20" value="5" step="0.5"
302
- class="w-full mt-2"
303
- oninput="document.getElementById('guidance-val').innerText = this.value">
304
- </div>
305
- <div>
306
- <div class="flex justify-between"><label
307
- class="text-[10px] text-gray-500 uppercase font-bold">Motion Strength</label><span
308
- id="motion-val" class="text-[10px] text-primary">5</span></div>
309
- <input id="motion-strength" type="range" min="1" max="10" value="5" class="w-full mt-2"
310
- oninput="document.getElementById('motion-val').innerText = this.value">
311
- </div>
312
  </div>
313
  </div>
314
- <button id="generate-btn"
315
- class="w-full bg-gradient-to-r from-primary to-[#9d4edd] hover:brightness-110 text-white py-3.5 rounded-xl font-bold text-sm tracking-wide shadow-lg flex items-center justify-center gap-2 mt-1">
316
- <span class="material-symbols-outlined">auto_fix_high</span> Generate Video
317
- </button>
318
  </div>
 
 
 
319
  </div>
320
-
321
- <script>
322
- function savePreference(key, value) { localStorage.setItem('continuity_' + key, value); }
323
- function loadPreferences() {
324
- const s = localStorage.getItem('continuity_style'); const a = localStorage.getItem('continuity_audio');
325
- if (s) document.getElementById('style-select').value = s;
326
- if (a) document.getElementById('audio-input').value = a;
327
- }
328
- loadPreferences();
329
- const drawer = document.getElementById('gallery-drawer');
330
- const overlay = document.getElementById('drawer-overlay');
331
- function toggleDrawer(show) {
332
- if (show) { drawer.classList.replace('drawer-closed', 'drawer-open'); overlay.classList.remove('hidden'); fetchHistory(); }
333
- else { drawer.classList.replace('drawer-open', 'drawer-closed'); overlay.classList.add('hidden'); }
334
- }
335
- async function fetchHistory() {
336
- const c = document.getElementById('gallery-content'); c.innerHTML = '<div class="text-center mt-10"><span class="material-symbols-outlined animate-spin">progress_activity</span></div>';
337
- try {
338
- const res = await fetch('/history'); const data = await res.json();
339
- if (!data || !data.length) { c.innerHTML = '<div class="text-center text-gray-500 mt-10 text-xs">No history found.</div>'; return; }
340
- c.innerHTML = data.map(item => `
341
- <div class="bg-black/40 rounded-lg overflow-hidden border border-white/5 hover:border-primary/50 transition-colors mb-4">
342
- <video src="${item.url}" class="w-full aspect-video object-cover" controls></video>
343
- <div class="p-2 flex justify-between items-center"><span class="text-[10px] text-gray-400 truncate w-32">${item.name}</span><a href="${item.url}" download class="text-gray-400 hover:text-white"><span class="material-symbols-outlined text-sm">download</span></a></div>
344
- </div>`).join('');
345
- } catch (e) { c.innerHTML = '<div class="text-center text-red-400 mt-10">Error loading history.</div>'; }
346
- }
347
- function handleFileSelect(input, labelId) { if (input.files[0]) { document.getElementById(labelId).innerText = input.files[0].name; document.getElementById(labelId).classList.add("text-primary", "font-bold"); } }
348
- function resetUI() {
349
- document.getElementById("analysis-panel").classList.remove("hidden");
350
- document.getElementById("review-panel").classList.add("hidden");
351
- document.getElementById("prompt-box").value = "";
352
- document.getElementById("merged-download-container").classList.add("hidden");
353
- currentVideoAPath = ""; currentVideoCPath = "";
354
- const b = document.getElementById("bridge-content");
355
- b.innerHTML = `<div class="absolute inset-0 bg-cover bg-center opacity-20" style="background-image:url('https://images.unsplash.com/photo-1614850523060-8da1d56ae167')"></div><div class="absolute inset-0 flex flex-col items-center justify-center"><span class="material-symbols-outlined text-3xl text-primary mb-2">auto_awesome</span><p class="text-xs text-gray-400">Ready</p></div>`;
356
- document.getElementById("bridge-card-outer").classList.replace("border-primary", "border-primary/20");
357
- document.getElementById("bridge-border").classList.replace("border-primary/50", "border-transparent");
358
- }
359
- document.getElementById("analyze-btn").addEventListener("click", async () => {
360
- const fA = document.getElementById("video-upload-a").files[0]; const fC = document.getElementById("video-upload-c").files[0];
361
- if (!fA || !fC) return alert("Upload both scenes.");
362
- const btn = document.getElementById("analyze-btn"); btn.disabled = true; btn.innerHTML = `Analyzing...`;
363
- const fd = new FormData(); fd.append("video_a", fA); fd.append("video_c", fC);
364
- try {
365
- const res = await fetch("/analyze", { method: "POST", body: fd });
366
- const data = await res.json();
367
- document.getElementById("prompt-box").value = data.prompt;
368
- currentVideoAPath = data.video_a_path; currentVideoCPath = data.video_c_path;
369
- document.getElementById("analysis-panel").classList.add("hidden");
370
- document.getElementById("review-panel").classList.remove("hidden");
371
- } catch (e) { alert(e.message); } finally { btn.disabled = false; btn.innerHTML = `<span class="material-symbols-outlined text-lg">analytics</span> Analyze Scenes`; }
372
- });
373
- document.getElementById("generate-btn").addEventListener("click", async () => {
374
- const btn = document.getElementById("generate-btn");
375
- const prompt = document.getElementById("prompt-box").value;
376
- const negPrompt = document.getElementById("negative-prompt").value;
377
- const guidance = document.getElementById("guidance-scale").value;
378
- const style = document.getElementById("style-select").value;
379
- const audio = document.getElementById("audio-input").value;
380
- const motion = document.getElementById("motion-strength").value;
381
- btn.disabled = true; btn.innerHTML = `Generating...`;
382
-
383
- // Hide download button if visible
384
- document.getElementById("merged-download-container").classList.add("hidden");
385
-
386
- try {
387
- const res = await fetch("/generate", {
388
- method: "POST", headers: { "Content-Type": "application/json" },
389
- body: JSON.stringify({
390
- prompt: prompt,
391
- style: style,
392
- audio_prompt: audio,
393
- negative_prompt: negPrompt,
394
- guidance_scale: parseFloat(guidance),
395
- motion_strength: parseInt(motion),
396
- video_a_path: currentVideoAPath,
397
- video_c_path: currentVideoCPath
398
- })
399
- });
400
- const data = await res.json();
401
- const poll = setInterval(async () => {
402
- const sRes = await fetch(`/status/${data.job_id}?t=${Date.now()}`);
403
- if (sRes.ok) {
404
- const s = await sRes.json();
405
- if (s.status === "completed") {
406
- clearInterval(poll);
407
-
408
- // Show BRIDGE video in player
409
- 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>`;
410
- document.getElementById("bridge-card-outer").classList.replace("border-primary/20", "border-primary");
411
- document.getElementById("bridge-border").classList.replace("border-transparent", "border-primary/50");
412
-
413
- // Show SEPARATE download button for Merged Video if available
414
- if (s.merged_video_url) {
415
- const dlBtn = document.getElementById("merged-download-btn");
416
- dlBtn.href = s.merged_video_url;
417
- // Force download filename
418
- dlBtn.download = `continuity_merged_${data.job_id}.mp4`;
419
- document.getElementById("merged-download-container").classList.remove("hidden");
420
- }
421
-
422
- btn.innerHTML = "Done!"; setTimeout(() => { btn.disabled = false; btn.innerHTML = `<span class="material-symbols-outlined">auto_fix_high</span> Generate Video`; }, 3000);
423
- } else if (s.status === "error") {
424
- clearInterval(poll); alert(s.log); btn.disabled = false; btn.innerHTML = "Try Again";
425
- } else {
426
- btn.innerHTML = `${s.log} (${s.progress}%)`;
427
- }
428
- }
429
- }, 1500);
430
- } catch (e) { alert(e.message); btn.disabled = false; }
431
- });
432
- </script>
433
  </body>
434
 
435
  </html>
 
48
 
49
  #gallery-drawer {
50
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 
51
  }
52
 
53
  .drawer-open {
 
81
  background: #362445;
82
  border-radius: 2px;
83
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  </style>
85
  </head>
86
 
87
  <body
88
  class="bg-background-dark font-body text-white h-screen w-screen overflow-hidden flex flex-col selection:bg-primary selection:text-white">
89
+ <div class="flex items-center gap-3">
90
+ <div
91
+ class="size-8 flex items-center justify-center bg-primary/20 rounded-lg border border-primary/30 text-primary">
92
+ <span class="material-symbols-outlined">movie_filter</span>
 
 
 
 
 
 
 
 
 
 
 
 
93
  </div>
94
+ <h1 class="text-xl font-display font-bold tracking-tight">Continuity</h1>
95
  </div>
96
+ <div class="flex items-center gap-3">
97
+ <button onclick="toggleDrawer(true)"
98
+ class="flex items-center gap-2 px-4 py-2 bg-primary hover:bg-[#6b0bc9] text-white rounded-lg transition-colors text-xs font-bold uppercase tracking-wider shadow-neon">
99
+ <span class="material-symbols-outlined text-lg">history</span> Gallery
100
+ </button>
 
 
 
 
 
 
 
 
 
 
 
101
  </div>
102
+ <div class="w-full max-w-6xl mx-auto flex items-center justify-center gap-4 md:gap-8 lg:gap-12 mt-12">
103
+ <div class="flex flex-col gap-3 flex-1 max-w-[320px] group">
104
+ <div class="flex justify-between px-1"><span
105
+ class="text-[10px] font-bold tracking-widest text-gray-500 uppercase">Scene A (Start)</span></div>
106
+ <div class="relative aspect-[9/16] md:aspect-[3/4] bg-surface-dark border-2 border-dashed border-border-dark rounded-2xl flex flex-col items-center justify-center gap-4 hover:border-primary/50 hover:bg-surface-dark/80 transition-all cursor-pointer shadow-lg overflow-hidden"
107
+ onclick="document.getElementById('video-upload-a').click()">
108
+ <div
109
+ class="size-12 rounded-full bg-white/5 flex items-center justify-center group-hover:scale-110 transition-transform relative z-10">
110
+ <span class="material-symbols-outlined text-2xl text-gray-400 group-hover:text-white">upload</span>
 
 
 
 
 
 
 
 
 
 
 
 
111
  </div>
112
+ <p id="label-a" class="text-xs font-medium text-gray-400 text-center px-4 relative z-10">Upload Start
113
+ Clip</p>
114
+ <input type="file" id="video-upload-a" accept="video/*" class="hidden"
115
+ onchange="handleFileSelect(this, 'label-a')">
116
  </div>
117
+ </div>
118
+ <div class="flex flex-col gap-3 flex-[1.5] max-w-[500px] relative z-20">
119
+ <div class="flex justify-center px-1"><span
120
+ class="text-[10px] font-bold tracking-[0.2em] text-primary uppercase animate-pulse">Generated
121
+ Bridge</span></div>
122
+ <div id="bridge-card-outer"
123
+ class="relative aspect-video rounded-2xl shadow-neon transition-all duration-500 border border-primary/20">
124
+ <div id="bridge-card-inner" class="force-clip w-full h-full bg-black relative">
125
+ <div id="bridge-content" class="w-full h-full">
126
+ <div
127
+ class="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1614850523060-8da1d56ae167?q=80&w=1000&auto=format&fit=crop')] bg-cover bg-center opacity-20 mix-blend-overlay">
 
 
 
 
 
 
 
 
 
128
  </div>
129
+ <div class="absolute inset-0 flex flex-col items-center justify-center text-center p-6">
130
+ <div
131
+ class="size-16 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center mb-3">
132
+ <span class="material-symbols-outlined text-3xl text-primary">auto_awesome</span></div>
133
+ <p class="text-sm text-gray-300">Ready to bridge the gap</p>
134
  </div>
135
  </div>
136
+ <div id="bridge-border"
137
+ class="absolute inset-0 rounded-2xl border-2 border-transparent pointer-events-none z-30"></div>
 
 
 
 
 
 
 
138
  </div>
139
  </div>
140
+ <div id="merged-download-container"
141
+ class="hidden flex justify-center mt-4 animate-in fade-in slide-in-from-top-2">
142
+ <a id="merged-download-btn" href="#" download
143
+ class="flex items-center gap-2 px-6 py-3 bg-surface-dark hover:bg-white/5 border border-primary/30 hover:border-primary text-primary hover:text-white rounded-xl font-bold text-xs uppercase tracking-widest transition-all shadow-lg group">
144
+ <span class="material-symbols-outlined group-hover:animate-bounce">download</span> Download merged
145
+ video
146
+ </a>
147
+ </div>
148
+ </div>
149
+ <div class="flex flex-col gap-3 flex-1 max-w-[320px] group">
150
+ <div class="flex justify-end px-1"><span
151
+ class="text-[10px] font-bold tracking-widest text-gray-500 uppercase">Scene C (End)</span></div>
152
+ <div class="relative aspect-[9/16] md:aspect-[3/4] bg-surface-dark border-2 border-dashed border-border-dark rounded-2xl flex flex-col items-center justify-center gap-4 hover:border-primary/50 hover:bg-surface-dark/80 transition-all cursor-pointer shadow-lg overflow-hidden"
153
+ onclick="document.getElementById('video-upload-c').click()">
154
+ <div
155
+ class="size-12 rounded-full bg-white/5 flex items-center justify-center group-hover:scale-110 transition-transform relative z-10">
156
+ <span class="material-symbols-outlined text-2xl text-gray-400 group-hover:text-white">upload</span>
157
  </div>
158
+ <p id="label-c" class="text-xs font-medium text-gray-400 text-center px-4 relative z-10">Upload End Clip
159
+ </p>
160
+ <input type="file" id="video-upload-c" accept="video/*" class="hidden"
161
+ onchange="handleFileSelect(this, 'label-c')">
162
  </div>
163
  </div>
164
  </div>
165
+ <div class="p-6 border-b border-white/5 flex items-center justify-between bg-surface-dark">
166
+ <h2 class="text-lg font-bold text-white flex items-center gap-2"><span
167
+ class="material-symbols-outlined text-primary">history</span> History</h2>
168
+ <button onclick="toggleDrawer(false)" class="text-gray-400 hover:text-white transition-colors"><span
169
+ class="material-symbols-outlined">close</span></button>
170
+ </div>
171
+ <div id="gallery-content" class="flex-1 overflow-y-auto p-4 space-y-4">
172
+ <div class="text-center text-gray-500 mt-10">Loading...</div>
173
+ </div>
174
+ <div id="analysis-panel"
175
+ class="glass-panel p-2 rounded-full shadow-neon flex items-center justify-between pl-6 pr-2">
176
+ <div class="flex flex-col"><span class="text-sm font-bold text-white">Continuity Engine</span><span
177
+ class="text-[10px] text-gray-400 uppercase tracking-wide">Ready for analysis</span></div>
178
+ <div class="flex gap-2">
179
+ <button onclick="toggleDrawer(true)"
180
+ class="bg-surface-dark hover:bg-white/10 text-white p-3 rounded-full transition-all flex items-center justify-center border border-white/10"><span
181
+ class="material-symbols-outlined text-lg">history</span></button>
182
+ <button id="analyze-btn"
183
+ class="bg-primary hover:bg-[#6b0bc9] text-white px-6 py-3 rounded-full font-bold text-sm transition-all flex items-center gap-2 shadow-lg"><span
184
+ class="material-symbols-outlined text-lg">analytics</span> Analyze Scenes</button>
185
+ </div>
186
+ </div>
187
+ <div id="review-panel"
188
+ class="hidden glass-panel rounded-2xl p-5 shadow-2xl flex flex-col gap-4 animate-in slide-in-from-bottom-4 duration-300 max-h-[80vh] overflow-y-auto custom-scrollbar">
189
+ <div class="flex items-center justify-between border-b border-white/10 pb-3">
190
+ <h3 class="text-sm font-bold text-white flex items-center gap-2"><span
191
+ class="material-symbols-outlined text-primary">movie_edit</span> Director's Configuration</h3>
192
  <div class="flex gap-2">
193
  <button onclick="toggleDrawer(true)"
194
+ class="text-xs text-gray-400 hover:text-white uppercase tracking-wider flex items-center gap-1"><span
195
+ class="material-symbols-outlined text-sm">history</span> History</button>
196
+ <span class="text-white/10">|</span>
197
+ <button onclick="resetUI()"
198
+ class="text-xs text-gray-500 hover:text-white uppercase tracking-wider">Reset</button>
199
  </div>
200
  </div>
201
+ <div><label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Visual
202
+ Direction</label><textarea id="prompt-box" rows="2"
203
+ 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>
204
+ </div>
205
+ <div class="grid grid-cols-2 gap-4">
206
+ <div><label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Visual
207
+ Style</label><select id="style-select" onchange="savePreference('style', this.value)"
208
+ class="w-full bg-black/20 border border-white/10 rounded-lg p-2.5 text-sm text-white focus:border-primary outline-none cursor-pointer">
209
+ <option value="Cinematic">Cinematic</option>
210
+ <option value="Anime">Anime</option>
211
+ <option value="Cyberpunk">Cyberpunk</option>
212
+ <option value="VHS">VHS Glitch</option>
213
+ <option value="Noir">Noir</option>
214
+ </select></div>
215
+ <div><label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Audio
216
+ Mood</label><select id="audio-input" onchange="savePreference('audio', this.value)"
217
+ class="w-full bg-black/20 border border-white/10 rounded-lg p-2.5 text-sm text-white focus:border-primary outline-none cursor-pointer">
218
+ <option value="Cinematic orchestral score">Cinematic</option>
219
+ <option value="Industrial synthwave">Cyberpunk</option>
220
+ <option value="Nature sounds">Nature</option>
221
+ <option value="Tense atmosphere">Horror</option>
222
+ <option value="High energy rock">Action</option>
223
+ </select></div>
224
+ </div>
225
+ <div class="border-t border-white/10 pt-3">
226
+ <button onclick="document.getElementById('advanced-settings').classList.toggle('hidden')"
227
+ class="flex items-center gap-2 text-xs font-bold text-gray-400 uppercase tracking-widest hover:text-white transition-colors w-full"><span
228
+ class="material-symbols-outlined text-sm">tune</span> Advanced Physics & Controls <span
229
+ class="material-symbols-outlined text-sm ml-auto">expand_more</span></button>
230
+ <div id="advanced-settings"
231
+ class="hidden pt-3 grid grid-cols-1 md:grid-cols-2 gap-4 animate-in fade-in slide-in-from-top-2">
232
+ <div class="col-span-1 md:col-span-2"><label
233
+ class="text-[10px] text-gray-500 uppercase font-bold">Negative Prompt</label><input
234
+ id="negative-prompt" type="text" placeholder="text, blurry, watermark"
235
+ class="w-full bg-black/20 border border-white/10 rounded-lg p-2 text-xs text-white focus:border-red-500/50 outline-none mt-1">
236
  </div>
 
 
 
 
 
 
 
 
237
  <div>
238
+ <div class="flex justify-between"><label
239
+ class="text-[10px] text-gray-500 uppercase font-bold">Guidance Scale</label><span
240
+ id="guidance-val" class="text-[10px] text-primary">5.0</span></div><input
241
+ id="guidance-scale" type="range" min="1" max="20" value="5" step="0.5" class="w-full mt-2"
242
+ oninput="document.getElementById('guidance-val').innerText = this.value">
 
 
 
 
 
243
  </div>
244
  <div>
245
+ <div class="flex justify-between"><label
246
+ class="text-[10px] text-gray-500 uppercase font-bold">Motion Strength</label><span
247
+ id="motion-val" class="text-[10px] text-primary">5</span></div><input id="motion-strength"
248
+ type="range" min="1" max="10" value="5" class="w-full mt-2"
249
+ oninput="document.getElementById('motion-val').innerText = this.value">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  </div>
251
  </div>
 
 
 
 
252
  </div>
253
+ <button id="generate-btn"
254
+ class="w-full bg-gradient-to-r from-primary to-[#9d4edd] hover:brightness-110 text-white py-3.5 rounded-xl font-bold text-sm tracking-wide shadow-lg flex items-center justify-center gap-2 mt-1"><span
255
+ class="material-symbols-outlined">auto_fix_high</span> Generate Video</button>
256
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  </body>
258
 
259
  </html>