Gaurav vashistha commited on
Commit
80ca058
·
1 Parent(s): 6955891

fix(ui) & feat(phase2): layout stability and auteur controls

Browse files
Files changed (3) hide show
  1. agent.py +11 -22
  2. server.py +3 -1
  3. stitch_continuity_dashboard/code.html +203 -154
agent.py CHANGED
@@ -21,6 +21,7 @@ class ContinuityState(TypedDict):
21
  audio_prompt: Optional[str]
22
  negative_prompt: Optional[str]
23
  guidance_scale: Optional[float]
 
24
  scene_analysis: Optional[str]
25
  veo_prompt: Optional[str]
26
  generated_video_url: Optional[str]
@@ -53,7 +54,7 @@ def analyze_videos(state: ContinuityState) -> dict:
53
  try:
54
  if attempt > 0:
55
  update_job_status(job_id, "analyzing", 20, f"Retrying analysis (Attempt {attempt+1})...")
56
-
57
  file_a = client.files.upload(file=path_a)
58
  file_c = client.files.upload(file=path_c)
59
 
@@ -67,9 +68,8 @@ def analyze_videos(state: ContinuityState) -> dict:
67
  prompt_text = f"You are a film director. Analyze the motion, lighting, and subject of the first video (Video A) and the second video (Video C). Write a detailed visual prompt for a 2-second video (Video B) that smoothly transitions from the end of A to the start of C. STYLE: {style}. Output only the prompt."
68
 
69
  update_job_status(job_id, "analyzing", 30, "Director writing scene transition...")
70
-
71
  response = client.models.generate_content(
72
- model="gemini-2.0-flash-exp",
73
  contents=[prompt_text, file_a, file_c]
74
  )
75
  transition_prompt = response.text
@@ -88,37 +88,25 @@ def generate_video(state: ContinuityState) -> dict:
88
  visual_prompt = state.get('veo_prompt', "")
89
  audio_context = state.get('audio_prompt', "Realistic ambient sound")
90
  negative = state.get('negative_prompt', "")
91
-
92
- # Construct Enhanced Prompt
93
  full_prompt = f"{visual_prompt} Soundtrack: {audio_context}"
94
  if negative:
95
- full_prompt += f" --no {negative}" # Common pattern for Veo/Imagen prompting
96
 
97
  update_job_status(job_id, "generating", 50, "Veo initializing...")
98
  local_path = None
99
-
100
  try:
101
  if Settings.GCP_PROJECT_ID:
102
  client = genai.Client(vertexai=True, project=Settings.GCP_PROJECT_ID, location=Settings.GCP_LOCATION)
103
  update_job_status(job_id, "generating", 60, f"Veo 3.1 generating...")
104
-
105
- # Note: Guidance scale is not directly supported in the unified SDK's simplest form usually,
106
- # or requires specific config. The user requested guidance_scale handling but the provided
107
- # code snippet in the prompt mostly used it to pass to generate_only.
108
- # In the provided generate_video snippet, guidance_scale isn't explicitly used in the config.
109
- # I will follow the user's snippet which didn't use guidance_scale in generate_videos call,
110
- # except implicitly or maybe they forgot it.
111
- # Wait, the user said "Updated generate_video to incorporate these parameters".
112
- # But the provided code for `generate_video` ONLY used `negative` in the prompt string construction.
113
- # It did NOT use guidance_scale in `types.GenerateVideosConfig`.
114
- # I must follow the provided code.
115
-
116
  operation = client.models.generate_videos(
117
  model='veo-3.1-generate-preview',
118
  prompt=full_prompt,
119
  config=types.GenerateVideosConfig(number_of_videos=1)
120
  )
121
-
122
  while not operation.done:
123
  time.sleep(5)
124
  operation = client.operations.get(operation)
@@ -162,7 +150,7 @@ def analyze_only(state_or_path_a, path_c=None, job_id=None):
162
  result = analyze_videos(state)
163
  return {"prompt": result.get("scene_analysis"), "status": "success"}
164
 
165
- def generate_only(prompt, path_a, path_c, job_id=None, style="Cinematic", audio_prompt="Cinematic", negative_prompt="", guidance_scale=5.0):
166
  state = {
167
  "job_id": job_id,
168
  "video_a_url": "local",
@@ -173,6 +161,7 @@ def generate_only(prompt, path_a, path_c, job_id=None, style="Cinematic", audio_
173
  "style": style,
174
  "audio_prompt": audio_prompt,
175
  "negative_prompt": negative_prompt,
176
- "guidance_scale": guidance_scale
 
177
  }
178
  return generate_video(state)
 
21
  audio_prompt: Optional[str]
22
  negative_prompt: Optional[str]
23
  guidance_scale: Optional[float]
24
+ motion_strength: Optional[int]
25
  scene_analysis: Optional[str]
26
  veo_prompt: Optional[str]
27
  generated_video_url: Optional[str]
 
54
  try:
55
  if attempt > 0:
56
  update_job_status(job_id, "analyzing", 20, f"Retrying analysis (Attempt {attempt+1})...")
57
+
58
  file_a = client.files.upload(file=path_a)
59
  file_c = client.files.upload(file=path_c)
60
 
 
68
  prompt_text = f"You are a film director. Analyze the motion, lighting, and subject of the first video (Video A) and the second video (Video C). Write a detailed visual prompt for a 2-second video (Video B) that smoothly transitions from the end of A to the start of C. STYLE: {style}. Output only the prompt."
69
 
70
  update_job_status(job_id, "analyzing", 30, "Director writing scene transition...")
 
71
  response = client.models.generate_content(
72
+ model="gemini-2.0-flash-exp",
73
  contents=[prompt_text, file_a, file_c]
74
  )
75
  transition_prompt = response.text
 
88
  visual_prompt = state.get('veo_prompt', "")
89
  audio_context = state.get('audio_prompt', "Realistic ambient sound")
90
  negative = state.get('negative_prompt', "")
91
+
92
+ # Construct Enhanced Prompt (Veo 3.1 Prompt Engineering)
93
  full_prompt = f"{visual_prompt} Soundtrack: {audio_context}"
94
  if negative:
95
+ full_prompt += f" --no {negative}"
96
 
97
  update_job_status(job_id, "generating", 50, "Veo initializing...")
98
  local_path = None
99
+
100
  try:
101
  if Settings.GCP_PROJECT_ID:
102
  client = genai.Client(vertexai=True, project=Settings.GCP_PROJECT_ID, location=Settings.GCP_LOCATION)
103
  update_job_status(job_id, "generating", 60, f"Veo 3.1 generating...")
104
+ # Note: Unified SDK usually handles configs like this
 
 
 
 
 
 
 
 
 
 
 
105
  operation = client.models.generate_videos(
106
  model='veo-3.1-generate-preview',
107
  prompt=full_prompt,
108
  config=types.GenerateVideosConfig(number_of_videos=1)
109
  )
 
110
  while not operation.done:
111
  time.sleep(5)
112
  operation = client.operations.get(operation)
 
150
  result = analyze_videos(state)
151
  return {"prompt": result.get("scene_analysis"), "status": "success"}
152
 
153
+ def generate_only(prompt, path_a, path_c, job_id=None, style="Cinematic", audio_prompt="Cinematic", negative_prompt="", guidance_scale=5.0, motion_strength=5):
154
  state = {
155
  "job_id": job_id,
156
  "video_a_url": "local",
 
161
  "style": style,
162
  "audio_prompt": audio_prompt,
163
  "negative_prompt": negative_prompt,
164
+ "guidance_scale": guidance_scale,
165
+ "motion_strength": motion_strength
166
  }
167
  return generate_video(state)
server.py CHANGED
@@ -67,6 +67,7 @@ def generate_endpoint(
67
  audio_prompt: str = Body("Cinematic ambient sound"),
68
  negative_prompt: str = Body(""),
69
  guidance_scale: float = Body(5.0),
 
70
  video_a_path: str = Body(...),
71
  video_c_path: str = Body(...)
72
  ):
@@ -80,7 +81,8 @@ def generate_endpoint(
80
  with open(status_file, "w") as f:
81
  json.dump({"status": "queued", "progress": 0, "log": "Job queued..."}, f)
82
 
83
- background_tasks.add_task(generate_only, prompt, video_a_path, video_c_path, job_id, style, audio_prompt, negative_prompt, guidance_scale)
 
84
 
85
  return {"job_id": job_id}
86
  except Exception as e:
 
67
  audio_prompt: str = Body("Cinematic ambient sound"),
68
  negative_prompt: str = Body(""),
69
  guidance_scale: float = Body(5.0),
70
+ motion_strength: int = Body(5),
71
  video_a_path: str = Body(...),
72
  video_c_path: str = Body(...)
73
  ):
 
81
  with open(status_file, "w") as f:
82
  json.dump({"status": "queued", "progress": 0, "log": "Job queued..."}, f)
83
 
84
+ # Pass new params to agent
85
+ background_tasks.add_task(generate_only, prompt, video_a_path, video_c_path, job_id, style, audio_prompt, negative_prompt, guidance_scale, motion_strength)
86
 
87
  return {"job_id": job_id}
88
  except Exception as e:
stitch_continuity_dashboard/code.html CHANGED
@@ -38,11 +38,6 @@
38
  border: 1px solid rgba(255, 255, 255, 0.08);
39
  }
40
 
41
- .footer-mask {
42
- background: linear-gradient(to top, #0a060f 20%, transparent 100%);
43
- pointer-events: none;
44
- }
45
-
46
  .force-clip {
47
  -webkit-mask-image: -webkit-radial-gradient(white, black);
48
  mask-image: radial-gradient(white, black);
@@ -53,6 +48,7 @@
53
 
54
  #gallery-drawer {
55
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 
56
  }
57
 
58
  .drawer-open {
@@ -63,7 +59,6 @@
63
  transform: translateX(100%);
64
  }
65
 
66
- /* Custom Range Slider */
67
  input[type=range] {
68
  -webkit-appearance: none;
69
  background: transparent;
@@ -87,186 +82,238 @@
87
  background: #362445;
88
  border-radius: 2px;
89
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  </style>
91
  </head>
92
 
93
  <body
94
- class="relative flex h-screen w-full flex-col bg-background-dark font-body text-white overflow-hidden selection:bg-primary selection:text-white">
95
- <div class="flex items-center gap-3">
96
- <div
97
- class="size-8 flex items-center justify-center bg-primary/20 rounded-lg border border-primary/30 text-primary">
98
- <span class="material-symbols-outlined">movie_filter</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  </div>
100
- <h1 class="text-xl font-display font-bold tracking-tight">Continuity <span
101
- class="opacity-50 font-normal text-sm ml-2">v2.5 (Auteur)</span></h1>
102
  </div>
103
- <div class="flex items-center gap-3">
104
- <button onclick="toggleDrawer(true)"
105
- 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">
106
- <span class="material-symbols-outlined text-lg">history</span> Gallery
107
- </button>
108
- <div class="flex items-center gap-2 px-3 py-1 bg-green-500/10 border border-green-500/20 rounded-full">
109
- <div class="size-2 bg-green-500 rounded-full animate-pulse"></div>
110
- <span class="text-xs font-bold text-green-400 tracking-wide uppercase">System Online</span>
 
 
 
 
 
 
 
111
  </div>
112
  </div>
113
- <div class="w-full max-w-6xl flex items-center justify-center gap-4 md:gap-8 lg:gap-12 mt-20">
114
- <div class="flex flex-col gap-3 flex-1 max-w-[320px] group">
115
- <div class="flex justify-between px-1"><span
116
- class="text-[10px] font-bold tracking-widest text-gray-500 uppercase">Scene A (Start)</span></div>
117
- <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"
118
- onclick="document.getElementById('video-upload-a').click()">
119
- <div
120
- class="size-12 rounded-full bg-white/5 flex items-center justify-center group-hover:scale-110 transition-transform relative z-10">
121
- <span class="material-symbols-outlined text-2xl text-gray-400 group-hover:text-white">upload</span>
 
 
 
 
 
 
 
 
 
 
 
 
122
  </div>
123
- <p id="label-a" class="text-xs font-medium text-gray-400 text-center px-4 relative z-10">Upload Start
124
- Clip</p>
125
- <input type="file" id="video-upload-a" accept="video/*" class="hidden"
126
- onchange="handleFileSelect(this, 'label-a')">
127
  </div>
128
- </div>
129
- <div class="flex flex-col gap-3 flex-[1.5] max-w-[500px] relative z-20">
130
- <div class="flex justify-center px-1"><span
131
- class="text-[10px] font-bold tracking-[0.2em] text-primary uppercase animate-pulse">Generated
132
- Bridge</span></div>
133
- <div id="bridge-card-outer"
134
- class="relative aspect-video rounded-2xl shadow-neon transition-all duration-500 border border-primary/20">
135
- <div id="bridge-card-inner" class="force-clip w-full h-full bg-black relative">
136
- <div id="bridge-content" class="w-full h-full">
137
- <div
138
- 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">
139
- </div>
140
- <div class="absolute inset-0 flex flex-col items-center justify-center text-center p-6">
141
  <div
142
- class="size-16 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center mb-3">
143
- <span class="material-symbols-outlined text-3xl text-primary">auto_awesome</span></div>
144
- <p class="text-sm text-gray-300">Ready to bridge the gap</p>
 
 
 
 
 
 
 
 
 
145
  </div>
146
  </div>
147
- <div id="bridge-border"
148
- class="absolute inset-0 rounded-2xl border-2 border-transparent pointer-events-none z-30"></div>
149
  </div>
150
  </div>
151
- </div>
152
- <div class="flex flex-col gap-3 flex-1 max-w-[320px] group">
153
- <div class="flex justify-end px-1"><span
154
- class="text-[10px] font-bold tracking-widest text-gray-500 uppercase">Scene C (End)</span></div>
155
- <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"
156
- onclick="document.getElementById('video-upload-c').click()">
157
- <div
158
- class="size-12 rounded-full bg-white/5 flex items-center justify-center group-hover:scale-110 transition-transform relative z-10">
159
- <span class="material-symbols-outlined text-2xl text-gray-400 group-hover:text-white">upload</span>
 
 
 
 
 
 
 
160
  </div>
161
- <p id="label-c" class="text-xs font-medium text-gray-400 text-center px-4 relative z-10">Upload End Clip
162
- </p>
163
- <input type="file" id="video-upload-c" accept="video/*" class="hidden"
164
- onchange="handleFileSelect(this, 'label-c')">
165
  </div>
166
  </div>
167
  </div>
168
- <div class="p-6 border-b border-white/5 flex items-center justify-between">
169
- <h2 class="text-lg font-bold text-white flex items-center gap-2"><span
170
- class="material-symbols-outlined text-primary">history</span> Creation History</h2>
171
- <button onclick="toggleDrawer(false)" class="text-gray-400 hover:text-white transition-colors"><span
172
- class="material-symbols-outlined">close</span></button>
173
- </div>
174
- <div id="gallery-content" class="flex-1 overflow-y-auto p-4 space-y-4">
175
- <div class="text-center text-gray-500 mt-10">Loading history...</div>
176
- </div>
177
- <div id="analysis-panel"
178
- class="glass-panel p-2 rounded-full shadow-neon flex items-center justify-between pl-6 pr-2">
179
- <div class="flex flex-col"><span class="text-sm font-bold text-white">Continuity Engine</span><span
180
- class="text-[10px] text-gray-400 uppercase tracking-wide">Ready for analysis</span></div>
181
- <div class="flex gap-2">
182
- <button onclick="toggleDrawer(true)"
183
- 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
184
- class="material-symbols-outlined text-lg">history</span></button>
185
- <button id="analyze-btn"
186
- 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
187
- class="material-symbols-outlined text-lg">analytics</span> Analyze Scenes</button>
188
- </div>
189
- </div>
190
- <div id="review-panel"
191
- class="hidden glass-panel rounded-2xl p-5 shadow-2xl flex flex-col gap-4 animate-in slide-in-from-bottom-4 duration-300">
192
- <div class="flex items-center justify-between border-b border-white/10 pb-3">
193
- <h3 class="text-sm font-bold text-white flex items-center gap-2"><span
194
- class="material-symbols-outlined text-primary">movie_edit</span> Director's Configuration</h3>
195
  <div class="flex gap-2">
196
  <button onclick="toggleDrawer(true)"
197
- class="text-xs text-gray-400 hover:text-white uppercase tracking-wider flex items-center gap-1"><span
198
- class="material-symbols-outlined text-sm">history</span> History</button>
199
- <span class="text-white/10">|</span>
200
- <button onclick="resetUI()"
201
- class="text-xs text-gray-500 hover:text-white uppercase tracking-wider">Reset</button>
202
  </div>
203
  </div>
204
- <div>
205
- <label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Visual
206
- Direction</label>
207
- <textarea id="prompt-box" rows="2"
208
- 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>
209
- </div>
210
- <div class="grid grid-cols-2 gap-4">
211
- <div>
212
- <label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Visual
213
- Style</label>
214
- <select id="style-select" onchange="savePreference('style', this.value)"
215
- 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">
216
- <option value="Cinematic">Cinematic</option>
217
- <option value="Anime">Anime</option>
218
- <option value="Cyberpunk">Cyberpunk</option>
219
- <option value="VHS">VHS Glitch</option>
220
- <option value="Noir">Noir</option>
221
- </select>
222
  </div>
223
  <div>
224
- <label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Audio
225
- Mood</label>
226
- <select id="audio-input" onchange="savePreference('audio', this.value)"
227
- 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">
228
- <option value="Cinematic orchestral score">Cinematic</option>
229
- <option value="Industrial synthwave">Cyberpunk</option>
230
- <option value="Nature sounds">Nature</option>
231
- <option value="Tense atmosphere">Horror</option>
232
- <option value="High energy rock">Action</option>
233
- </select>
234
  </div>
235
- </div>
236
- <div class="border-t border-white/10 pt-3">
237
- <button onclick="document.getElementById('advanced-settings').classList.toggle('hidden')"
238
- class="flex items-center gap-2 text-xs font-bold text-gray-400 uppercase tracking-widest hover:text-white transition-colors w-full">
239
- <span class="material-symbols-outlined text-sm">tune</span> Advanced Physics & Controls <span
240
- class="material-symbols-outlined text-sm ml-auto">expand_more</span>
241
- </button>
242
- <div id="advanced-settings"
243
- class="hidden pt-3 grid grid-cols-1 md:grid-cols-2 gap-4 animate-in fade-in slide-in-from-top-2">
244
- <div class="col-span-1 md:col-span-2">
245
- <label class="text-[10px] text-gray-500 uppercase font-bold">Negative Prompt (Exclude)</label>
246
- <input id="negative-prompt" type="text" placeholder="text, blurry, watermark, distorted"
247
- 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">
248
- </div>
249
  <div>
250
- <div class="flex justify-between"><label
251
- class="text-[10px] text-gray-500 uppercase font-bold">Guidance Scale</label><span
252
- id="guidance-val" class="text-[10px] text-primary">5.0</span></div>
253
- <input id="guidance-scale" type="range" min="1" max="20" value="5" step="0.5" class="w-full mt-2"
254
- oninput="document.getElementById('guidance-val').innerText = this.value">
 
 
 
 
 
255
  </div>
256
  <div>
257
- <div class="flex justify-between"><label
258
- class="text-[10px] text-gray-500 uppercase font-bold">Motion Strength</label><span
259
- id="motion-val" class="text-[10px] text-primary">5</span></div>
260
- <input id="motion-strength" type="range" min="1" max="10" value="5" class="w-full mt-2"
261
- oninput="document.getElementById('motion-val').innerText = this.value">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  </div>
263
  </div>
 
 
 
 
264
  </div>
265
- <button id="generate-btn"
266
- 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">
267
- <span class="material-symbols-outlined">auto_fix_high</span> Generate Video
268
- </button>
269
  </div>
 
270
  <script>
271
  function savePreference(key, value) { localStorage.setItem('continuity_' + key, value); }
272
  function loadPreferences() {
@@ -325,6 +372,7 @@
325
  const guidance = document.getElementById("guidance-scale").value;
326
  const style = document.getElementById("style-select").value;
327
  const audio = document.getElementById("audio-input").value;
 
328
  btn.disabled = true; btn.innerHTML = `Generating...`;
329
  try {
330
  const res = await fetch("/generate", {
@@ -334,7 +382,8 @@
334
  style: style,
335
  audio_prompt: audio,
336
  negative_prompt: negPrompt,
337
- guidance_scale: guidance,
 
338
  video_a_path: currentVideoAPath,
339
  video_c_path: currentVideoCPath
340
  })
 
38
  border: 1px solid rgba(255, 255, 255, 0.08);
39
  }
40
 
 
 
 
 
 
41
  .force-clip {
42
  -webkit-mask-image: -webkit-radial-gradient(white, black);
43
  mask-image: radial-gradient(white, black);
 
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 {
 
59
  transform: translateX(100%);
60
  }
61
 
 
62
  input[type=range] {
63
  -webkit-appearance: none;
64
  background: transparent;
 
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 <span
114
+ class="opacity-50 font-normal text-sm ml-2">v2.5 (Auteur)</span></h1>
115
+ </div>
116
+ <div class="flex items-center gap-3">
117
+ <button onclick="toggleDrawer(true)"
118
+ 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">
119
+ <span class="material-symbols-outlined text-lg">history</span> Gallery
120
+ </button>
121
+ <div class="flex items-center gap-2 px-3 py-1 bg-green-500/10 border border-green-500/20 rounded-full">
122
+ <div class="size-2 bg-green-500 rounded-full animate-pulse"></div>
123
+ <span class="text-xs font-bold text-green-400 tracking-wide uppercase">System Online</span>
124
+ </div>
125
  </div>
 
 
126
  </div>
127
+
128
+ <!-- GALLERY DRAWER (Fixed absolute overlay) -->
129
+ <div id="drawer-overlay"
130
+ class="fixed inset-0 bg-black/80 backdrop-blur-sm z-40 hidden transition-opacity duration-300 pointer-events-auto"
131
+ onclick="toggleDrawer(false)"></div>
132
+ <div id="gallery-drawer"
133
+ 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">
134
+ <div class="p-6 border-b border-white/5 flex items-center justify-between bg-surface-dark">
135
+ <h2 class="text-lg font-bold text-white flex items-center gap-2"><span
136
+ class="material-symbols-outlined text-primary">history</span> History</h2>
137
+ <button onclick="toggleDrawer(false)" class="text-gray-400 hover:text-white transition-colors"><span
138
+ class="material-symbols-outlined">close</span></button>
139
+ </div>
140
+ <div id="gallery-content" class="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar">
141
+ <div class="text-center text-gray-500 mt-10">Loading...</div>
142
  </div>
143
  </div>
144
+
145
+ <!-- MAIN CONTENT (Flex grow, scrollable) -->
146
+ <div class="flex-1 overflow-y-auto w-full relative z-0 flex flex-col items-center pt-8 pb-32 px-4">
147
+ <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">
148
+
149
+ <!-- Scene A -->
150
+ <div class="flex flex-col gap-3 flex-1 w-full max-w-[320px] group">
151
+ <div class="flex justify-between px-1"><span
152
+ class="text-[10px] font-bold tracking-widest text-gray-500 uppercase">Scene A (Start)</span>
153
+ </div>
154
+ <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"
155
+ onclick="document.getElementById('video-upload-a').click()">
156
+ <div
157
+ class="size-12 rounded-full bg-white/5 flex items-center justify-center group-hover:scale-110 transition-transform relative z-10">
158
+ <span
159
+ class="material-symbols-outlined text-2xl text-gray-400 group-hover:text-white">upload</span>
160
+ </div>
161
+ <p id="label-a" class="text-xs font-medium text-gray-400 text-center px-4 relative z-10">Upload
162
+ Start Clip</p>
163
+ <input type="file" id="video-upload-a" accept="video/*" class="hidden"
164
+ onchange="handleFileSelect(this, 'label-a')">
165
  </div>
 
 
 
 
166
  </div>
167
+
168
+ <!-- Bridge -->
169
+ <div class="flex flex-col gap-3 flex-[1.5] w-full max-w-[500px] relative z-20">
170
+ <div class="flex justify-center px-1"><span
171
+ class="text-[10px] font-bold tracking-[0.2em] text-primary uppercase animate-pulse">Generated
172
+ Bridge</span></div>
173
+ <div id="bridge-card-outer"
174
+ class="relative aspect-video rounded-2xl shadow-neon transition-all duration-500 border border-primary/20">
175
+ <div id="bridge-card-inner" class="force-clip w-full h-full bg-black relative">
176
+ <div id="bridge-content" class="w-full h-full">
 
 
 
177
  <div
178
+ 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">
179
+ </div>
180
+ <div class="absolute inset-0 flex flex-col items-center justify-center text-center p-6">
181
+ <div
182
+ class="size-16 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center mb-3">
183
+ <span class="material-symbols-outlined text-3xl text-primary">auto_awesome</span>
184
+ </div>
185
+ <p class="text-sm text-gray-300">Ready to bridge the gap</p>
186
+ </div>
187
+ </div>
188
+ <div id="bridge-border"
189
+ class="absolute inset-0 rounded-2xl border-2 border-transparent pointer-events-none z-30">
190
  </div>
191
  </div>
 
 
192
  </div>
193
  </div>
194
+
195
+ <!-- Scene C -->
196
+ <div class="flex flex-col gap-3 flex-1 w-full max-w-[320px] group">
197
+ <div class="flex justify-end px-1"><span
198
+ class="text-[10px] font-bold tracking-widest text-gray-500 uppercase">Scene C (End)</span></div>
199
+ <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"
200
+ onclick="document.getElementById('video-upload-c').click()">
201
+ <div
202
+ class="size-12 rounded-full bg-white/5 flex items-center justify-center group-hover:scale-110 transition-transform relative z-10">
203
+ <span
204
+ class="material-symbols-outlined text-2xl text-gray-400 group-hover:text-white">upload</span>
205
+ </div>
206
+ <p id="label-c" class="text-xs font-medium text-gray-400 text-center px-4 relative z-10">Upload End
207
+ Clip</p>
208
+ <input type="file" id="video-upload-c" accept="video/*" class="hidden"
209
+ onchange="handleFileSelect(this, 'label-c')">
210
  </div>
 
 
 
 
211
  </div>
212
  </div>
213
  </div>
214
+
215
+ <!-- FLOATING PANELS (Fixed Bottom) -->
216
+ <div class="fixed bottom-6 left-1/2 -translate-x-1/2 w-full max-w-2xl px-4 z-30">
217
+
218
+ <!-- ANALYSIS PANEL -->
219
+ <div id="analysis-panel"
220
+ class="glass-panel p-2 rounded-full shadow-neon flex items-center justify-between pl-6 pr-2 mb-4">
221
+ <div class="flex flex-col"><span class="text-sm font-bold text-white">Continuity Engine</span><span
222
+ class="text-[10px] text-gray-400 uppercase tracking-wide">Ready for analysis</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  <div class="flex gap-2">
224
  <button onclick="toggleDrawer(true)"
225
+ 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
226
+ class="material-symbols-outlined text-lg">history</span></button>
227
+ <button id="analyze-btn"
228
+ 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
229
+ class="material-symbols-outlined text-lg">analytics</span> Analyze Scenes</button>
230
  </div>
231
  </div>
232
+
233
+ <!-- REVIEW PANEL -->
234
+ <div id="review-panel"
235
+ 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">
236
+ <div class="flex items-center justify-between border-b border-white/10 pb-3">
237
+ <h3 class="text-sm font-bold text-white flex items-center gap-2"><span
238
+ class="material-symbols-outlined text-primary">movie_edit</span> Director's Configuration</h3>
239
+ <div class="flex gap-2">
240
+ <button onclick="toggleDrawer(true)"
241
+ class="text-xs text-gray-400 hover:text-white uppercase tracking-wider flex items-center gap-1"><span
242
+ class="material-symbols-outlined text-sm">history</span> History</button>
243
+ <span class="text-white/10">|</span>
244
+ <button onclick="resetUI()"
245
+ class="text-xs text-gray-500 hover:text-white uppercase tracking-wider">Reset</button>
246
+ </div>
 
 
 
247
  </div>
248
  <div>
249
+ <label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Visual
250
+ Direction</label>
251
+ <textarea id="prompt-box" rows="2"
252
+ 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>
 
 
 
 
 
 
253
  </div>
254
+ <div class="grid grid-cols-2 gap-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  <div>
256
+ <label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Visual
257
+ Style</label>
258
+ <select id="style-select" onchange="savePreference('style', this.value)"
259
+ 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">
260
+ <option value="Cinematic">Cinematic</option>
261
+ <option value="Anime">Anime</option>
262
+ <option value="Cyberpunk">Cyberpunk</option>
263
+ <option value="VHS">VHS Glitch</option>
264
+ <option value="Noir">Noir</option>
265
+ </select>
266
  </div>
267
  <div>
268
+ <label class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1 block">Audio
269
+ Mood</label>
270
+ <select id="audio-input" onchange="savePreference('audio', this.value)"
271
+ 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">
272
+ <option value="Cinematic orchestral score">Cinematic</option>
273
+ <option value="Industrial synthwave">Cyberpunk</option>
274
+ <option value="Nature sounds">Nature</option>
275
+ <option value="Tense atmosphere">Horror</option>
276
+ <option value="High energy rock">Action</option>
277
+ </select>
278
+ </div>
279
+ </div>
280
+ <div class="border-t border-white/10 pt-3">
281
+ <button onclick="document.getElementById('advanced-settings').classList.toggle('hidden')"
282
+ class="flex items-center gap-2 text-xs font-bold text-gray-400 uppercase tracking-widest hover:text-white transition-colors w-full">
283
+ <span class="material-symbols-outlined text-sm">tune</span> Advanced Physics & Controls <span
284
+ class="material-symbols-outlined text-sm ml-auto">expand_more</span>
285
+ </button>
286
+ <div id="advanced-settings"
287
+ class="hidden pt-3 grid grid-cols-1 md:grid-cols-2 gap-4 animate-in fade-in slide-in-from-top-2">
288
+ <div class="col-span-1 md:col-span-2">
289
+ <label class="text-[10px] text-gray-500 uppercase font-bold">Negative Prompt</label>
290
+ <input id="negative-prompt" type="text" placeholder="text, blurry, watermark, distorted"
291
+ 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">
292
+ </div>
293
+ <div>
294
+ <div class="flex justify-between"><label
295
+ class="text-[10px] text-gray-500 uppercase font-bold">Guidance Scale</label><span
296
+ id="guidance-val" class="text-[10px] text-primary">5.0</span></div>
297
+ <input id="guidance-scale" type="range" min="1" max="20" value="5" step="0.5"
298
+ class="w-full mt-2"
299
+ oninput="document.getElementById('guidance-val').innerText = this.value">
300
+ </div>
301
+ <div>
302
+ <div class="flex justify-between"><label
303
+ class="text-[10px] text-gray-500 uppercase font-bold">Motion Strength</label><span
304
+ id="motion-val" class="text-[10px] text-primary">5</span></div>
305
+ <input id="motion-strength" type="range" min="1" max="10" value="5" class="w-full mt-2"
306
+ oninput="document.getElementById('motion-val').innerText = this.value">
307
+ </div>
308
  </div>
309
  </div>
310
+ <button id="generate-btn"
311
+ 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">
312
+ <span class="material-symbols-outlined">auto_fix_high</span> Generate Video
313
+ </button>
314
  </div>
 
 
 
 
315
  </div>
316
+
317
  <script>
318
  function savePreference(key, value) { localStorage.setItem('continuity_' + key, value); }
319
  function loadPreferences() {
 
372
  const guidance = document.getElementById("guidance-scale").value;
373
  const style = document.getElementById("style-select").value;
374
  const audio = document.getElementById("audio-input").value;
375
+ const motion = document.getElementById("motion-strength").value;
376
  btn.disabled = true; btn.innerHTML = `Generating...`;
377
  try {
378
  const res = await fetch("/generate", {
 
382
  style: style,
383
  audio_prompt: audio,
384
  negative_prompt: negPrompt,
385
+ guidance_scale: parseFloat(guidance),
386
+ motion_strength: parseInt(motion),
387
  video_a_path: currentVideoAPath,
388
  video_c_path: currentVideoCPath
389
  })