multimodalart HF Staff commited on
Commit
06ec34e
·
verified ·
1 Parent(s): 2842cd8

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +734 -99
index.html CHANGED
@@ -20,29 +20,10 @@
20
  }
21
 
22
  .container {
23
- max-width: 1200px;
24
  margin: 0 auto;
25
  }
26
 
27
- header {
28
- text-align: center;
29
- margin-bottom: 30px;
30
- }
31
-
32
- h1 {
33
- font-size: 2rem;
34
- margin-bottom: 10px;
35
- background: linear-gradient(to right, #22d3ee, #3b82f6, #a855f7);
36
- -webkit-background-clip: text;
37
- background-clip: text;
38
- color: transparent;
39
- }
40
-
41
- .subtitle {
42
- color: #94a3b8;
43
- font-size: 0.9rem;
44
- }
45
-
46
  /* Login Screen */
47
  .login-container {
48
  max-width: 500px;
@@ -60,11 +41,6 @@
60
  color: #e2e8f0;
61
  }
62
 
63
- .login-container p {
64
- color: #94a3b8;
65
- margin-bottom: 30px;
66
- }
67
-
68
  .btn {
69
  padding: 12px 24px;
70
  border: none;
@@ -87,27 +63,46 @@
87
  background: #059669;
88
  }
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  .info-box {
91
  background: rgba(59, 130, 246, 0.1);
92
  border: 1px solid rgba(59, 130, 246, 0.3);
93
- border-radius: 6px;
94
- padding: 15px;
95
- margin-top: 30px;
96
- font-size: 0.85rem;
97
  color: #93c5fd;
98
- text-align: left;
99
  }
100
 
101
  .error-box {
102
  background: rgba(239, 68, 68, 0.1);
103
  border: 1px solid rgba(239, 68, 68, 0.3);
104
- border-radius: 6px;
105
- padding: 12px;
106
- margin-top: 15px;
107
- font-size: 0.85rem;
108
  color: #fca5a5;
109
  }
110
 
 
 
 
 
 
 
111
  /* User Bar */
112
  .user-bar {
113
  background: rgba(15, 23, 42, 0.6);
@@ -134,11 +129,6 @@
134
  border: 2px solid #3b82f6;
135
  }
136
 
137
- .user-details h3 {
138
- font-size: 1rem;
139
- margin-bottom: 3px;
140
- }
141
-
142
  .user-badge {
143
  display: inline-block;
144
  padding: 2px 8px;
@@ -158,11 +148,6 @@
158
  color: #94a3b8;
159
  }
160
 
161
- .usage-info {
162
- font-size: 0.85rem;
163
- color: #94a3b8;
164
- }
165
-
166
  .btn-logout {
167
  padding: 8px 16px;
168
  background: #475569;
@@ -177,6 +162,204 @@
177
  background: #64748b;
178
  }
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  .limit-warning {
181
  background: rgba(239, 68, 68, 0.1);
182
  border: 1px solid rgba(239, 68, 68, 0.3);
@@ -186,26 +369,24 @@
186
  text-align: center;
187
  }
188
 
189
- .limit-warning h3 {
190
- color: #fca5a5;
191
- margin-bottom: 10px;
192
- }
193
-
194
  .upgrade-link {
195
  color: #3b82f6;
196
  text-decoration: none;
197
  font-weight: 500;
198
  }
199
 
200
- .upgrade-link:hover {
201
- text-decoration: underline;
202
- }
203
-
204
- .hidden {
205
- display: none;
206
  }
207
 
208
  @media (max-width: 768px) {
 
 
 
 
209
  .user-bar {
210
  flex-direction: column;
211
  gap: 15px;
@@ -219,7 +400,7 @@
219
  <!-- Login Screen -->
220
  <div class="login-container">
221
  <h2>🤗 Sign in with Hugging Face</h2>
222
- <p>Authenticate with your Hugging Face account to start generating videos</p>
223
 
224
  <a href="https://huggingface.co/oauth/authorize?response_type=code&client_id={{ oauth_client_id }}&redirect_uri=https://{{ space_host }}/oauth/callback&scope=openid%20profile&state={{ range(10000, 99999) | random }}"
225
  class="btn btn-primary"
@@ -231,7 +412,7 @@
231
  <div class="error-box">{{ error }}</div>
232
  {% endif %}
233
 
234
- <div class="info-box">
235
  <strong>📊 Usage Limits:</strong><br>
236
  • <strong>Free Users:</strong> 1 session per day<br>
237
  • <strong>PRO Users:</strong> 15 sessions per day<br><br>
@@ -247,24 +428,24 @@
247
  <img src="{{ user.avatar }}" alt="Avatar" class="user-avatar">
248
  {% endif %}
249
  <div class="user-details">
250
- <h3>{{ user.fullname }}</h3>
251
  <span class="user-badge {% if user.is_pro %}badge-pro{% else %}badge-free{% endif %}">
252
  {% if user.is_pro %}PRO{% else %}FREE{% endif %}
253
  </span>
254
  </div>
255
  </div>
256
  <div style="text-align: right;">
257
- <div class="usage-info">Sessions: {{ sessions_used }}/{{ sessions_limit }} today</div>
258
  <button class="btn-logout" onclick="logout()">Logout</button>
259
  </div>
260
  </div>
261
 
262
  {% if not can_start %}
263
  <div class="limit-warning">
264
- <h3>⚠️ Daily Limit Reached</h3>
265
- <p>You've used all {{ sessions_limit }} sessions for today. Come back tomorrow!</p>
266
  {% if not user.is_pro %}
267
- <p style="margin-top: 10px;">
268
  Upgrade to <a href="https://huggingface.co/pricing" target="_blank" class="upgrade-link">Hugging Face PRO</a> for 15 sessions per day!
269
  </p>
270
  {% endif %}
@@ -273,58 +454,512 @@
273
 
274
  <header>
275
  <h1>Real-time Video Generation</h1>
276
- <p class="subtitle">Self-Forcing Diffusion • 832×480 Resolution</p>
277
  </header>
278
 
279
- <div style="text-align: center; padding: 40px;">
280
- <h2>🚧 Video Generation Interface Coming Soon</h2>
281
- <p style="color: #94a3b8; margin-top: 10px;">
282
- The full video generation interface will be integrated here.
283
- </p>
284
-
285
- {% if can_start %}
286
- <button class="btn btn-primary" onclick="startSession()" id="testBtn" style="margin-top: 20px;">
287
- Test Start Session
288
- </button>
289
- {% endif %}
290
-
291
- <div id="statusMessage" style="margin-top: 20px;"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  </div>
293
  {% endif %}
294
  </div>
295
 
 
 
 
 
296
  <script>
297
- {% if authenticated %}
 
 
 
298
  async function logout() {
299
  await fetch('/api/logout', {method: 'POST'});
300
  window.location.reload();
301
  }
302
 
303
- async function startSession() {
304
- const btn = document.getElementById('testBtn');
305
- const msg = document.getElementById('statusMessage');
306
-
307
- btn.disabled = true;
308
- btn.textContent = 'Starting...';
309
-
310
- try {
311
- const response = await fetch('/api/start-session', {method: 'POST'});
312
- const data = await response.json();
313
-
314
- if (response.ok) {
315
- msg.innerHTML = `<div class="info-box">✓ Session started! (${data.sessions_used}/${data.sessions_limit} used today)</div>`;
316
- btn.textContent = 'Session Active';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  } else {
318
- msg.innerHTML = `<div class="error-box">${data.detail || 'Failed to start session'}</div>`;
319
- btn.disabled = false;
320
- btn.textContent = 'Test Start Session';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  }
322
- } catch (error) {
323
- msg.innerHTML = `<div class="error-box">Network error. Please try again.</div>`;
324
- btn.disabled = false;
325
- btn.textContent = 'Test Start Session';
326
  }
327
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  {% endif %}
329
  </script>
330
  </body>
 
20
  }
21
 
22
  .container {
23
+ max-width: 1400px;
24
  margin: 0 auto;
25
  }
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  /* Login Screen */
28
  .login-container {
29
  max-width: 500px;
 
41
  color: #e2e8f0;
42
  }
43
 
 
 
 
 
 
44
  .btn {
45
  padding: 12px 24px;
46
  border: none;
 
63
  background: #059669;
64
  }
65
 
66
+ .btn-primary:disabled {
67
+ opacity: 0.5;
68
+ cursor: not-allowed;
69
+ }
70
+
71
+ .btn-danger {
72
+ background: #ef4444;
73
+ color: white;
74
+ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
75
+ }
76
+
77
+ .btn-danger:hover {
78
+ background: #dc2626;
79
+ }
80
+
81
+ .info-box, .error-box, .warning-box {
82
+ border-radius: 6px;
83
+ padding: 12px;
84
+ margin-top: 15px;
85
+ font-size: 0.85rem;
86
+ }
87
+
88
  .info-box {
89
  background: rgba(59, 130, 246, 0.1);
90
  border: 1px solid rgba(59, 130, 246, 0.3);
 
 
 
 
91
  color: #93c5fd;
 
92
  }
93
 
94
  .error-box {
95
  background: rgba(239, 68, 68, 0.1);
96
  border: 1px solid rgba(239, 68, 68, 0.3);
 
 
 
 
97
  color: #fca5a5;
98
  }
99
 
100
+ .warning-box {
101
+ background: rgba(251, 191, 36, 0.1);
102
+ border: 1px solid rgba(251, 191, 36, 0.3);
103
+ color: #fcd34d;
104
+ }
105
+
106
  /* User Bar */
107
  .user-bar {
108
  background: rgba(15, 23, 42, 0.6);
 
129
  border: 2px solid #3b82f6;
130
  }
131
 
 
 
 
 
 
132
  .user-badge {
133
  display: inline-block;
134
  padding: 2px 8px;
 
148
  color: #94a3b8;
149
  }
150
 
 
 
 
 
 
151
  .btn-logout {
152
  padding: 8px 16px;
153
  background: #475569;
 
162
  background: #64748b;
163
  }
164
 
165
+ /* Main App Layout */
166
+ header {
167
+ text-align: center;
168
+ margin-bottom: 30px;
169
+ }
170
+
171
+ h1 {
172
+ font-size: 2rem;
173
+ margin-bottom: 10px;
174
+ background: linear-gradient(to right, #22d3ee, #3b82f6, #a855f7);
175
+ -webkit-background-clip: text;
176
+ background-clip: text;
177
+ color: transparent;
178
+ }
179
+
180
+ .subtitle {
181
+ color: #94a3b8;
182
+ font-size: 0.9rem;
183
+ }
184
+
185
+ .grid {
186
+ display: grid;
187
+ grid-template-columns: 2fr 1fr;
188
+ gap: 20px;
189
+ }
190
+
191
+ .panel {
192
+ background: rgba(15, 23, 42, 0.6);
193
+ backdrop-filter: blur(10px);
194
+ border-radius: 12px;
195
+ border: 1px solid rgba(148, 163, 184, 0.2);
196
+ padding: 20px;
197
+ }
198
+
199
+ .video-container {
200
+ background: #000;
201
+ border-radius: 8px;
202
+ aspect-ratio: 832/480;
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: center;
206
+ position: relative;
207
+ overflow: hidden;
208
+ }
209
+
210
+ #outputCanvas {
211
+ max-width: 100%;
212
+ max-height: 100%;
213
+ object-fit: contain;
214
+ }
215
+
216
+ .placeholder {
217
+ text-align: center;
218
+ color: #475569;
219
+ }
220
+
221
+ .status-bar {
222
+ display: flex;
223
+ justify-content: space-between;
224
+ align-items: center;
225
+ margin-top: 15px;
226
+ padding-top: 15px;
227
+ border-top: 1px solid rgba(148, 163, 184, 0.2);
228
+ font-size: 0.85rem;
229
+ }
230
+
231
+ .status-pill {
232
+ padding: 4px 12px;
233
+ border-radius: 12px;
234
+ font-size: 0.75rem;
235
+ font-weight: 500;
236
+ }
237
+
238
+ .status-connected {
239
+ background: rgba(16, 185, 129, 0.2);
240
+ color: #10b981;
241
+ }
242
+
243
+ .status-disconnected {
244
+ background: rgba(239, 68, 68, 0.2);
245
+ color: #ef4444;
246
+ }
247
+
248
+ .mode-toggle {
249
+ display: flex;
250
+ gap: 10px;
251
+ margin-bottom: 20px;
252
+ }
253
+
254
+ .mode-btn {
255
+ flex: 1;
256
+ padding: 10px;
257
+ border: none;
258
+ border-radius: 6px;
259
+ background: #334155;
260
+ color: #e2e8f0;
261
+ cursor: pointer;
262
+ transition: all 0.2s;
263
+ font-size: 0.85rem;
264
+ }
265
+
266
+ .mode-btn:hover:not(:disabled) {
267
+ background: #475569;
268
+ }
269
+
270
+ .mode-btn.active {
271
+ background: #3b82f6;
272
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
273
+ }
274
+
275
+ .mode-btn:disabled {
276
+ opacity: 0.5;
277
+ cursor: not-allowed;
278
+ }
279
+
280
+ .form-group {
281
+ margin-bottom: 15px;
282
+ }
283
+
284
+ label {
285
+ display: block;
286
+ font-size: 0.85rem;
287
+ color: #94a3b8;
288
+ margin-bottom: 5px;
289
+ }
290
+
291
+ input[type="text"],
292
+ input[type="number"],
293
+ input[type="password"],
294
+ textarea,
295
+ select {
296
+ width: 100%;
297
+ padding: 8px 12px;
298
+ background: rgba(51, 65, 85, 0.5);
299
+ border: 1px solid #475569;
300
+ border-radius: 6px;
301
+ color: #e2e8f0;
302
+ font-size: 0.9rem;
303
+ }
304
+
305
+ input:focus, textarea:focus {
306
+ outline: none;
307
+ border-color: #3b82f6;
308
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
309
+ }
310
+
311
+ textarea {
312
+ min-height: 80px;
313
+ resize: vertical;
314
+ font-family: inherit;
315
+ }
316
+
317
+ input[type="range"] {
318
+ width: 100%;
319
+ height: 4px;
320
+ background: #334155;
321
+ border-radius: 2px;
322
+ outline: none;
323
+ }
324
+
325
+ .range-value {
326
+ display: inline-block;
327
+ margin-left: 10px;
328
+ font-weight: 500;
329
+ }
330
+
331
+ .hint {
332
+ font-size: 0.75rem;
333
+ color: #64748b;
334
+ margin-top: 4px;
335
+ }
336
+
337
+ .progress-bar {
338
+ width: 100%;
339
+ height: 6px;
340
+ background: #334155;
341
+ border-radius: 3px;
342
+ margin-top: 10px;
343
+ overflow: hidden;
344
+ }
345
+
346
+ .progress-fill {
347
+ height: 100%;
348
+ background: linear-gradient(to right, #22d3ee, #3b82f6);
349
+ transition: width 0.3s ease;
350
+ width: 0%;
351
+ }
352
+
353
+ .action-buttons {
354
+ display: flex;
355
+ gap: 10px;
356
+ margin-top: 20px;
357
+ }
358
+
359
+ .hidden {
360
+ display: none;
361
+ }
362
+
363
  .limit-warning {
364
  background: rgba(239, 68, 68, 0.1);
365
  border: 1px solid rgba(239, 68, 68, 0.3);
 
369
  text-align: center;
370
  }
371
 
 
 
 
 
 
372
  .upgrade-link {
373
  color: #3b82f6;
374
  text-decoration: none;
375
  font-weight: 500;
376
  }
377
 
378
+ #webcamVideo {
379
+ width: 100%;
380
+ border: 1px solid #475569;
381
+ border-radius: 6px;
382
+ background: #000;
 
383
  }
384
 
385
  @media (max-width: 768px) {
386
+ .grid {
387
+ grid-template-columns: 1fr;
388
+ }
389
+
390
  .user-bar {
391
  flex-direction: column;
392
  gap: 15px;
 
400
  <!-- Login Screen -->
401
  <div class="login-container">
402
  <h2>🤗 Sign in with Hugging Face</h2>
403
+ <p style="color: #94a3b8; margin-bottom: 30px;">Authenticate with your Hugging Face account to start generating videos</p>
404
 
405
  <a href="https://huggingface.co/oauth/authorize?response_type=code&client_id={{ oauth_client_id }}&redirect_uri=https://{{ space_host }}/oauth/callback&scope=openid%20profile&state={{ range(10000, 99999) | random }}"
406
  class="btn btn-primary"
 
412
  <div class="error-box">{{ error }}</div>
413
  {% endif %}
414
 
415
+ <div class="info-box" style="text-align: left;">
416
  <strong>📊 Usage Limits:</strong><br>
417
  • <strong>Free Users:</strong> 1 session per day<br>
418
  • <strong>PRO Users:</strong> 15 sessions per day<br><br>
 
428
  <img src="{{ user.avatar }}" alt="Avatar" class="user-avatar">
429
  {% endif %}
430
  <div class="user-details">
431
+ <h3 style="font-size: 1rem; margin-bottom: 3px;">{{ user.fullname }}</h3>
432
  <span class="user-badge {% if user.is_pro %}badge-pro{% else %}badge-free{% endif %}">
433
  {% if user.is_pro %}PRO{% else %}FREE{% endif %}
434
  </span>
435
  </div>
436
  </div>
437
  <div style="text-align: right;">
438
+ <div style="font-size: 0.85rem; color: #94a3b8;">Sessions: {{ sessions_used }}/{{ sessions_limit }} today</div>
439
  <button class="btn-logout" onclick="logout()">Logout</button>
440
  </div>
441
  </div>
442
 
443
  {% if not can_start %}
444
  <div class="limit-warning">
445
+ <h3 style="color: #fca5a5; margin-bottom: 10px;">⚠️ Daily Limit Reached</h3>
446
+ <p style="color: #94a3b8; font-size: 0.9rem;">You've used all {{ sessions_limit }} sessions for today. Come back tomorrow!</p>
447
  {% if not user.is_pro %}
448
+ <p style="margin-top: 10px; color: #94a3b8; font-size: 0.9rem;">
449
  Upgrade to <a href="https://huggingface.co/pricing" target="_blank" class="upgrade-link">Hugging Face PRO</a> for 15 sessions per day!
450
  </p>
451
  {% endif %}
 
454
 
455
  <header>
456
  <h1>Real-time Video Generation</h1>
457
+ <p class="subtitle">Self-Forcing Diffusion with Dynamic Prompt Rewriting • 832×480 Fixed Resolution</p>
458
  </header>
459
 
460
+ <div class="grid">
461
+ <!-- Video Output Panel -->
462
+ <div class="panel">
463
+ <div class="video-container">
464
+ <canvas id="outputCanvas"></canvas>
465
+ <div id="placeholder" class="placeholder">
466
+ <p>{% if can_start %}Configure settings and click Start{% else %}Daily limit reached{% endif %}</p>
467
+ </div>
468
+ </div>
469
+
470
+ <div class="status-bar">
471
+ <div>
472
+ <span id="statusPill" class="status-pill status-disconnected">Disconnected</span>
473
+ <span style="margin-left: 15px;">Frames: <strong id="frameCount">0</strong> / <strong id="maxFrames">234</strong></span>
474
+ </div>
475
+ <div>
476
+ <label style="display: inline; margin: 0;">Playback:</label>
477
+ <input type="range" id="playbackFps" min="8" max="20" value="12" style="width: 100px;">
478
+ <span class="range-value" id="playbackFpsValue">12 fps</span>
479
+ </div>
480
+ </div>
481
+
482
+ <div class="progress-bar">
483
+ <div id="progressFill" class="progress-fill"></div>
484
+ </div>
485
+ </div>
486
+
487
+ <!-- Controls Panel -->
488
+ <div class="panel">
489
+ <h2 style="margin-bottom: 20px;">Controls</h2>
490
+
491
+ <!-- Mode Selection -->
492
+ <div class="mode-toggle">
493
+ <button id="textModeBtn" class="mode-btn active" {% if not can_start %}disabled{% endif %}>Text-to-Video</button>
494
+ <button id="videoModeBtn" class="mode-btn" {% if not can_start %}disabled{% endif %}>Video-to-Video</button>
495
+ <button id="webcamModeBtn" class="mode-btn" {% if not can_start %}disabled{% endif %}>Webcam-to-Video</button>
496
+ </div>
497
+
498
+ <!-- Prompt -->
499
+ <div class="form-group">
500
+ <label>Prompt <span style="color: #10b981; font-size: 0.75rem;">(updates sent every 2 seconds)</span></label>
501
+ <textarea id="prompt" {% if not can_start %}disabled{% endif %}>A cat riding a skateboard through a neon city at night</textarea>
502
+ <div class="hint">💡 Detailed prompts work better</div>
503
+ </div>
504
+
505
+ <!-- Video Upload (V2V mode) -->
506
+ <div id="videoControls" class="hidden">
507
+ <div class="form-group">
508
+ <label>Upload Video</label>
509
+ <input type="file" id="videoFile" accept="video/*" class="hidden">
510
+ <button class="btn btn-primary" style="width: 100%;" onclick="document.getElementById('videoFile').click()">
511
+ Select Video File
512
+ </button>
513
+ <div id="videoInfo" style="margin-top: 10px; font-size: 0.85rem; color: #94a3b8;"></div>
514
+ </div>
515
+ </div>
516
+
517
+ <!-- Webcam Preview -->
518
+ <div id="webcamControls" class="hidden">
519
+ <div class="form-group">
520
+ <label>Webcam Preview (832×480 center crop)</label>
521
+ <video id="webcamVideo" autoplay playsinline muted></video>
522
+ </div>
523
+ </div>
524
+
525
+ <!-- Input Controls (V2V and Webcam modes) -->
526
+ <div id="inputControls" class="hidden">
527
+ <div class="form-group">
528
+ <label>Input Frame Rate: <span class="range-value" id="inputFpsValue">12</span> fps</label>
529
+ <input type="range" id="inputFps" min="10" max="14" value="12">
530
+ <div class="hint">Recommended: 10-14 fps</div>
531
+ </div>
532
+
533
+ <div class="form-group">
534
+ <label>Transformation Strength: <span class="range-value" id="strengthValue">0.45</span></label>
535
+ <input type="range" id="strength" min="0.1" max="0.6" step="0.05" value="0.45">
536
+ <div class="hint">Keep low (&lt;0.5-0.6) to avoid artifacts</div>
537
+ </div>
538
+ </div>
539
+
540
+ <div class="form-group">
541
+ <label>Max Frames (Blocks): <span class="range-value" id="numBlocksValue">20</span> → ~<span id="estimatedFrames">234</span> frames</label>
542
+ <input type="range" id="numBlocks" min="10" max="50" step="5" value="20" {% if not can_start %}disabled{% endif %}>
543
+ <div class="hint">Each block generates ~12 frames</div>
544
+ </div>
545
+
546
+ <div class="form-group">
547
+ <label>Seed (optional)</label>
548
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
549
+ <input type="number" id="seed" placeholder="random" {% if not can_start %}disabled{% endif %}>
550
+ <button class="btn" onclick="app.randomizeSeed()" {% if not can_start %}disabled{% endif %}>Random</button>
551
+ </div>
552
+ </div>
553
+
554
+ <div class="action-buttons">
555
+ <button id="startStopBtn" class="btn btn-primary" onclick="app.toggleGeneration()" {% if not can_start %}disabled{% endif %}>
556
+ {% if can_start %}Start Generation{% else %}Limit Reached{% endif %}
557
+ </button>
558
+ <button class="btn" onclick="app.downloadVideo()">Download</button>
559
+ </div>
560
+
561
+ <div id="errorBox" class="error-box hidden"></div>
562
+ <div id="infoBox" class="info-box hidden"></div>
563
+ </div>
564
  </div>
565
  {% endif %}
566
  </div>
567
 
568
+ <!-- Hidden elements for video processing -->
569
+ <video id="hiddenVideo" style="display: none;" muted playsinline></video>
570
+ <canvas id="extractionCanvas" style="display: none;"></canvas>
571
+
572
  <script>
573
+ {% if not authenticated %}
574
+ // Not authenticated - no app logic needed
575
+ {% else %}
576
+
577
  async function logout() {
578
  await fetch('/api/logout', {method: 'POST'});
579
  window.location.reload();
580
  }
581
 
582
+ // MsgPack Encoder (abbreviated for space - full version from original)
583
+ const createMsgpackEncoder = (() => {
584
+ const textEncoder = new TextEncoder();
585
+ const ensureUint8Array = (value) => (value instanceof Uint8Array ? value : new Uint8Array(value));
586
+ const writeUInt8 = (buffer, value) => buffer.push(value & 0xff);
587
+ const writeUInt16 = (buffer, value) => {
588
+ buffer.push((value >>> 8) & 0xff, value & 0xff);
589
+ };
590
+ const writeUInt32 = (buffer, value) => {
591
+ buffer.push((value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff);
592
+ };
593
+ const writeFloat64 = (buffer, value) => {
594
+ const array = new ArrayBuffer(8);
595
+ new DataView(array).setFloat64(0, value);
596
+ buffer.push(...new Uint8Array(array));
597
+ };
598
+ const encodeString = (buffer, value) => {
599
+ const utf8 = ensureUint8Array(textEncoder.encode(value));
600
+ const length = utf8.length;
601
+ if (length <= 31) {
602
+ writeUInt8(buffer, 0xa0 | length);
603
+ } else if (length <= 0xff) {
604
+ writeUInt8(buffer, 0xd9);
605
+ writeUInt8(buffer, length);
606
+ } else if (length <= 0xffff) {
607
+ writeUInt8(buffer, 0xda);
608
+ writeUInt16(buffer, length);
609
+ } else {
610
+ writeUInt8(buffer, 0xdb);
611
+ writeUInt32(buffer, length);
612
+ }
613
+ buffer.push(...utf8);
614
+ };
615
+ const encodeNumber = (buffer, value) => {
616
+ if (Number.isInteger(value)) {
617
+ if (value >= 0 && value <= 0x7f) {
618
+ writeUInt8(buffer, value);
619
+ } else if (value < 0 && value >= -32) {
620
+ buffer.push(value & 0xff);
621
+ } else if (value >= 0 && value <= 0xff) {
622
+ writeUInt8(buffer, 0xcc);
623
+ writeUInt8(buffer, value);
624
+ } else if (value >= 0 && value <= 0xffff) {
625
+ writeUInt8(buffer, 0xcd);
626
+ writeUInt16(buffer, value);
627
+ } else if (value >= 0 && value <= 0xffffffff) {
628
+ writeUInt8(buffer, 0xce);
629
+ writeUInt32(buffer, value);
630
+ } else {
631
+ writeUInt8(buffer, 0xcb);
632
+ writeFloat64(buffer, value);
633
+ }
634
+ } else {
635
+ writeUInt8(buffer, 0xcb);
636
+ writeFloat64(buffer, value);
637
+ }
638
+ };
639
+ const encodeArray = (buffer, value) => {
640
+ const length = value.length;
641
+ if (length < 16) {
642
+ writeUInt8(buffer, 0x90 | length);
643
+ } else if (length <= 0xffff) {
644
+ writeUInt8(buffer, 0xdc);
645
+ writeUInt16(buffer, length);
646
  } else {
647
+ writeUInt8(buffer, 0xdd);
648
+ writeUInt32(buffer, length);
649
+ }
650
+ value.forEach((item) => encodeValue(buffer, item));
651
+ };
652
+ const encodeObject = (buffer, value) => {
653
+ const entries = Object.entries(value).filter(([, v]) => v !== undefined);
654
+ const length = entries.length;
655
+ if (length < 16) {
656
+ writeUInt8(buffer, 0x80 | length);
657
+ } else if (length <= 0xffff) {
658
+ writeUInt8(buffer, 0xde);
659
+ writeUInt16(buffer, length);
660
+ } else {
661
+ writeUInt8(buffer, 0xdf);
662
+ writeUInt32(buffer, length);
663
+ }
664
+ for (const [key, entryValue] of entries) {
665
+ encodeString(buffer, key);
666
+ encodeValue(buffer, entryValue);
667
+ }
668
+ };
669
+ function encodeValue(buffer, value) {
670
+ if (value === null) {
671
+ writeUInt8(buffer, 0xc0);
672
+ } else if (value === false) {
673
+ writeUInt8(buffer, 0xc2);
674
+ } else if (value === true) {
675
+ writeUInt8(buffer, 0xc3);
676
+ } else if (typeof value === "number") {
677
+ encodeNumber(buffer, value);
678
+ } else if (typeof value === "string") {
679
+ encodeString(buffer, value);
680
+ } else if (Array.isArray(value)) {
681
+ encodeArray(buffer, value);
682
+ } else if (value instanceof Uint8Array) {
683
+ const length = value.length;
684
+ if (length <= 0xff) {
685
+ writeUInt8(buffer, 0xc4);
686
+ writeUInt8(buffer, length);
687
+ } else if (length <= 0xffff) {
688
+ writeUInt8(buffer, 0xc5);
689
+ writeUInt16(buffer, length);
690
+ } else {
691
+ writeUInt8(buffer, 0xc6);
692
+ writeUInt32(buffer, length);
693
+ }
694
+ buffer.push(...value);
695
+ } else if (value && typeof value === "object") {
696
+ encodeObject(buffer, value);
697
+ } else {
698
+ throw new Error("Unsupported type for MsgPack encoding");
699
  }
 
 
 
 
700
  }
701
+ return (input) => {
702
+ const buffer = [];
703
+ encodeValue(buffer, input);
704
+ return new Uint8Array(buffer);
705
+ };
706
+ })();
707
+
708
+ // Main App (simplified - add full video generation logic here)
709
+ const app = {
710
+ mode: 'text',
711
+ isGenerating: false,
712
+ frameCount: 0,
713
+ maxFrames: 234,
714
+ ws: null,
715
+ frameBuffer: [],
716
+ playbackInterval: null,
717
+ sessionStarted: false,
718
+
719
+ init() {
720
+ this.setupEventListeners();
721
+ this.updateEstimatedFrames();
722
+ },
723
+
724
+ setupEventListeners() {
725
+ document.getElementById('textModeBtn').addEventListener('click', () => this.setMode('text'));
726
+ document.getElementById('videoModeBtn').addEventListener('click', () => this.setMode('video'));
727
+ document.getElementById('webcamModeBtn').addEventListener('click', () => this.setMode('webcam'));
728
+
729
+ document.getElementById('playbackFps').addEventListener('input', (e) => {
730
+ document.getElementById('playbackFpsValue').textContent = e.target.value + ' fps';
731
+ });
732
+
733
+ document.getElementById('inputFps').addEventListener('input', (e) => {
734
+ document.getElementById('inputFpsValue').textContent = e.target.value;
735
+ });
736
+
737
+ document.getElementById('strength').addEventListener('input', (e) => {
738
+ document.getElementById('strengthValue').textContent = parseFloat(e.target.value).toFixed(2);
739
+ });
740
+
741
+ document.getElementById('numBlocks').addEventListener('input', (e) => {
742
+ document.getElementById('numBlocksValue').textContent = e.target.value;
743
+ this.updateEstimatedFrames();
744
+ });
745
+ },
746
+
747
+ setMode(mode) {
748
+ if (this.isGenerating) return;
749
+ this.mode = mode;
750
+ document.getElementById('textModeBtn').classList.toggle('active', mode === 'text');
751
+ document.getElementById('videoModeBtn').classList.toggle('active', mode === 'video');
752
+ document.getElementById('webcamModeBtn').classList.toggle('active', mode === 'webcam');
753
+ document.getElementById('videoControls').classList.toggle('hidden', mode !== 'video');
754
+ document.getElementById('webcamControls').classList.toggle('hidden', mode !== 'webcam');
755
+ document.getElementById('inputControls').classList.toggle('hidden', !(mode === 'video' || mode === 'webcam'));
756
+ },
757
+
758
+ updateEstimatedFrames() {
759
+ const numBlocks = parseInt(document.getElementById('numBlocks').value);
760
+ const estimatedFrames = (12 * numBlocks) - 6;
761
+ this.maxFrames = estimatedFrames;
762
+ document.getElementById('estimatedFrames').textContent = estimatedFrames;
763
+ document.getElementById('maxFrames').textContent = estimatedFrames;
764
+ },
765
+
766
+ randomizeSeed() {
767
+ document.getElementById('seed').value = Math.floor(Math.random() * (1 << 24));
768
+ },
769
+
770
+ async toggleGeneration() {
771
+ if (this.isGenerating) {
772
+ this.disconnect();
773
+ } else {
774
+ // Record session start
775
+ if (!this.sessionStarted) {
776
+ try {
777
+ const response = await fetch('/api/start-session', {method: 'POST'});
778
+ if (!response.ok) {
779
+ const data = await response.json();
780
+ this.showError(data.detail || 'Failed to start session');
781
+ return;
782
+ }
783
+ this.sessionStarted = true;
784
+ } catch (error) {
785
+ this.showError('Failed to start session');
786
+ return;
787
+ }
788
+ }
789
+
790
+ const prompt = document.getElementById('prompt').value.trim();
791
+ if (!prompt) {
792
+ this.showError('Please enter a prompt');
793
+ return;
794
+ }
795
+
796
+ this.isGenerating = true;
797
+ this.updateUI();
798
+
799
+ // Connect to backend WebSocket proxy (keeps API key secure)
800
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
801
+ const wsUrl = `${protocol}//${window.location.host}/ws/video-gen`;
802
+
803
+ try {
804
+ this.ws = new WebSocket(wsUrl);
805
+ this.ws.binaryType = 'arraybuffer';
806
+
807
+ this.ws.onopen = () => {
808
+ this.showInfo('Connected! Waiting for ready signal...');
809
+ };
810
+
811
+ this.ws.onmessage = async (event) => {
812
+ if (typeof event.data === 'string') {
813
+ try {
814
+ const data = JSON.parse(event.data);
815
+ if (data.status === 'ready') {
816
+ this.showInfo('Ready - sending parameters...');
817
+ await this.sendInitialParams();
818
+ } else if (data.error) {
819
+ this.showError(`Server error: ${JSON.stringify(data.error)}`);
820
+ this.disconnect();
821
+ }
822
+ } catch (e) {
823
+ console.log('Text message:', event.data);
824
+ }
825
+ } else if (event.data instanceof ArrayBuffer) {
826
+ await this.displayFrame(event.data);
827
+ }
828
+ };
829
+
830
+ this.ws.onerror = (error) => {
831
+ this.showError('WebSocket connection error');
832
+ console.error('WebSocket error:', error);
833
+ };
834
+
835
+ this.ws.onclose = (event) => {
836
+ this.showInfo(`Disconnected: ${event.reason || 'Connection closed'}`);
837
+ this.isGenerating = false;
838
+ this.updateUI();
839
+ };
840
+
841
+ } catch (error) {
842
+ this.showError('Failed to connect: ' + error.message);
843
+ this.isGenerating = false;
844
+ this.updateUI();
845
+ }
846
+ }
847
+ },
848
+
849
+ async sendInitialParams() {
850
+ const payload = {
851
+ prompt: document.getElementById('prompt').value,
852
+ num_blocks: parseInt(document.getElementById('numBlocks').value),
853
+ num_denoising_steps: 4,
854
+ strength: parseFloat(document.getElementById('strength').value) || 0.45,
855
+ width: 832,
856
+ height: 480
857
+ };
858
+
859
+ const seedValue = document.getElementById('seed').value;
860
+ if (seedValue && seedValue.trim() !== '') {
861
+ payload.seed = parseInt(seedValue);
862
+ } else {
863
+ payload.seed = Math.floor(Math.random() * (1 << 24));
864
+ }
865
+
866
+ try {
867
+ const encoded = createMsgpackEncoder(payload);
868
+ this.ws.send(encoded);
869
+ this.showInfo('Generation started!');
870
+ } catch (error) {
871
+ this.showError('Failed to send parameters: ' + error.message);
872
+ }
873
+ },
874
+
875
+ async displayFrame(imageData) {
876
+ const blob = new Blob([imageData], { type: 'image/jpeg' });
877
+ const bitmap = await createImageBitmap(blob);
878
+
879
+ this.frameBuffer.push(bitmap);
880
+ this.frameCount++;
881
+ document.getElementById('frameCount').textContent = this.frameCount;
882
+
883
+ if (this.frameCount === 1) {
884
+ document.getElementById('placeholder').style.display = 'none';
885
+ this.startPlaybackLoop();
886
+ }
887
+
888
+ // Update progress
889
+ const progress = Math.min(100, (this.frameCount / this.maxFrames) * 100);
890
+ document.getElementById('progressFill').style.width = progress + '%';
891
+ },
892
+
893
+ drawNextFrame() {
894
+ if (this.frameBuffer.length === 0) return;
895
+
896
+ const canvas = document.getElementById('outputCanvas');
897
+ const bitmap = this.frameBuffer.shift();
898
+
899
+ canvas.width = bitmap.width;
900
+ canvas.height = bitmap.height;
901
+
902
+ const ctx = canvas.getContext('2d');
903
+ ctx.drawImage(bitmap, 0, 0);
904
+
905
+ if (typeof bitmap.close === 'function') {
906
+ bitmap.close();
907
+ }
908
+ },
909
+
910
+ startPlaybackLoop() {
911
+ if (this.playbackInterval) {
912
+ clearInterval(this.playbackInterval);
913
+ }
914
+
915
+ const fps = parseInt(document.getElementById('playbackFps').value);
916
+ const intervalMs = Math.max(10, Math.floor(1000 / fps));
917
+ this.playbackInterval = setInterval(() => this.drawNextFrame(), intervalMs);
918
+ },
919
+
920
+ disconnect() {
921
+ if (this.ws) {
922
+ this.ws.close();
923
+ this.ws = null;
924
+ }
925
+ if (this.playbackInterval) {
926
+ clearInterval(this.playbackInterval);
927
+ this.playbackInterval = null;
928
+ }
929
+ this.isGenerating = false;
930
+ this.updateUI();
931
+ },
932
+
933
+ downloadVideo() {
934
+ this.showInfo('Download functionality coming soon');
935
+ },
936
+
937
+ updateUI() {
938
+ const btn = document.getElementById('startStopBtn');
939
+ btn.textContent = this.isGenerating ? 'Stop Generation' : 'Start Generation';
940
+ btn.className = this.isGenerating ? 'btn btn-danger' : 'btn btn-primary';
941
+ },
942
+
943
+ showError(message) {
944
+ const box = document.getElementById('errorBox');
945
+ box.textContent = '❌ ' + message;
946
+ box.classList.remove('hidden');
947
+ setTimeout(() => box.classList.add('hidden'), 5000);
948
+ },
949
+
950
+ showInfo(message) {
951
+ const box = document.getElementById('infoBox');
952
+ box.textContent = '✓ ' + message;
953
+ box.classList.remove('hidden');
954
+ setTimeout(() => box.classList.add('hidden'), 3000);
955
+ }
956
+ };
957
+
958
+ document.addEventListener('DOMContentLoaded', () => {
959
+ {% if can_start %}
960
+ app.init();
961
+ {% endif %}
962
+ });
963
  {% endif %}
964
  </script>
965
  </body>