apolinario commited on
Commit
79389ec
·
1 Parent(s): 34f845e
Files changed (1) hide show
  1. index.html +450 -264
index.html CHANGED
@@ -3,7 +3,8 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Real-time Video Generation</title>
 
7
  <style>
8
  * {
9
  margin: 0;
@@ -12,55 +13,75 @@
12
  }
13
 
14
  body {
15
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
- background: linear-gradient(135deg, #0f172a, #1e3a8a, #312e81);
17
- color: #e2e8f0;
18
  min-height: 100vh;
19
- padding: 20px;
20
  }
21
 
22
  .container {
23
- max-width: 1400px;
24
  margin: 0 auto;
 
25
  }
26
 
27
- /* Login Screen */
28
- .login-container {
29
- max-width: 500px;
30
- margin: 100px auto;
31
- background: rgba(15, 23, 42, 0.95);
32
- backdrop-filter: blur(10px);
33
- border-radius: 12px;
34
- border: 1px solid rgba(148, 163, 184, 0.2);
35
- padding: 40px;
36
- text-align: center;
37
  }
38
 
39
- .login-container h2 {
40
- margin-bottom: 20px;
41
- color: #e2e8f0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
 
44
  .btn {
45
- padding: 12px 24px;
46
  border: none;
47
  border-radius: 6px;
48
  font-weight: 500;
49
  cursor: pointer;
50
- transition: all 0.2s;
51
  text-decoration: none;
52
  display: inline-block;
 
 
53
  }
54
 
55
  .btn-primary {
56
- background: #10b981;
57
- color: white;
58
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
59
- font-size: 1rem;
60
  }
61
 
62
  .btn-primary:hover {
63
- background: #059669;
 
 
 
 
 
 
64
  }
65
 
66
  .btn-primary:disabled {
@@ -68,14 +89,36 @@
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 {
@@ -83,37 +126,39 @@
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);
109
- backdrop-filter: blur(10px);
110
- border-radius: 12px;
111
- border: 1px solid rgba(148, 163, 184, 0.2);
112
  padding: 15px 20px;
113
  margin-bottom: 20px;
114
  display: flex;
115
  justify-content: space-between;
116
  align-items: center;
 
117
  }
118
 
119
  .user-info {
@@ -126,7 +171,7 @@
126
  width: 40px;
127
  height: 40px;
128
  border-radius: 50%;
129
- border: 2px solid #3b82f6;
130
  }
131
 
132
  .user-badge {
@@ -139,19 +184,19 @@
139
  }
140
 
141
  .badge-pro {
142
- background: linear-gradient(135deg, #f59e0b, #d97706);
143
  color: white;
144
  }
145
 
146
  .badge-free {
147
- background: #475569;
148
- color: #94a3b8;
149
  }
150
 
151
  .btn-logout {
152
  padding: 8px 16px;
153
- background: #475569;
154
- color: #e2e8f0;
155
  border: none;
156
  border-radius: 6px;
157
  cursor: pointer;
@@ -159,7 +204,7 @@
159
  }
160
 
161
  .btn-logout:hover {
162
- background: #64748b;
163
  }
164
 
165
  /* Main App Layout */
@@ -169,31 +214,56 @@
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 {
@@ -205,17 +275,19 @@
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 {
@@ -224,7 +296,7 @@
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
 
@@ -236,13 +308,13 @@
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 {
@@ -254,22 +326,25 @@
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 {
@@ -284,8 +359,9 @@
284
  label {
285
  display: block;
286
  font-size: 0.85rem;
287
- color: #94a3b8;
288
  margin-bottom: 5px;
 
289
  }
290
 
291
  input[type="text"],
@@ -295,49 +371,73 @@
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;
@@ -345,7 +445,7 @@
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
  }
@@ -361,32 +461,74 @@
361
  }
362
 
363
  .limit-warning {
364
- background: rgba(239, 68, 68, 0.1);
365
- border: 1px solid rgba(239, 68, 68, 0.3);
366
- border-radius: 12px;
367
  padding: 20px;
368
  margin-bottom: 20px;
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;
@@ -397,31 +539,26 @@
397
  <body>
398
  <div class="container">
399
  {% if not authenticated %}
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"
407
- target="_blank">
408
- Sign in with Hugging Face
409
- </a>
410
-
411
- {% if error %}
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 generation per day<br>
418
- • <strong>PRO Users:</strong> 15 generations per day<br><br>
419
- <small>Limits reset daily at midnight UTC</small>
420
  </div>
421
  </div>
422
- {% else %}
423
-
 
 
 
424
  <!-- Authenticated App -->
 
425
  <div class="user-bar">
426
  <div class="user-info">
427
  {% if user.avatar %}
@@ -435,38 +572,57 @@
435
  </div>
436
  </div>
437
  <div style="text-align: right;">
438
- <div style="font-size: 0.85rem; color: #94a3b8;">Generations: {{ 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 }} generations 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 generations per day!
450
  </p>
451
  {% endif %}
452
  </div>
453
  {% 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>
@@ -478,89 +634,96 @@
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>
@@ -573,7 +736,7 @@
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();
@@ -707,7 +870,7 @@
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,
@@ -726,23 +889,32 @@
726
  init() {
727
  this.setupEventListeners();
728
  this.updateEstimatedFrames();
 
 
 
 
 
 
 
 
 
729
  },
730
 
731
  setupEventListeners() {
732
  document.getElementById('textModeBtn').addEventListener('click', () => this.setMode('text'));
733
  document.getElementById('videoModeBtn').addEventListener('click', () => this.setMode('video'));
734
  document.getElementById('webcamModeBtn').addEventListener('click', () => this.setMode('webcam'));
735
-
736
  // Video file upload
737
  document.getElementById('videoFile').addEventListener('change', (e) => this.handleVideoUpload(e));
738
-
739
  document.getElementById('playbackFps').addEventListener('input', (e) => {
740
  document.getElementById('playbackFpsValue').textContent = e.target.value + ' fps';
741
  if (this.playbackInterval) {
742
  this.startPlaybackLoop();
743
  }
744
  });
745
-
746
  document.getElementById('inputFps').addEventListener('input', (e) => {
747
  document.getElementById('inputFpsValue').textContent = e.target.value;
748
  if (this.isGenerating && (this.mode === 'video' || this.mode === 'webcam')) {
@@ -750,11 +922,11 @@
750
  this.startFrameExtraction();
751
  }
752
  });
753
-
754
  document.getElementById('strength').addEventListener('input', (e) => {
755
  document.getElementById('strengthValue').textContent = parseFloat(e.target.value).toFixed(2);
756
  });
757
-
758
  document.getElementById('numBlocks').addEventListener('input', (e) => {
759
  document.getElementById('numBlocksValue').textContent = e.target.value;
760
  this.updateEstimatedFrames();
@@ -762,7 +934,7 @@
762
  this.queuePromptUpdate();
763
  }
764
  });
765
-
766
  // Prompt changes for live updates
767
  document.getElementById('prompt').addEventListener('input', () => {
768
  if (this.isGenerating && (this.mode === 'text' || this.mode === 'webcam')) {
@@ -778,9 +950,15 @@
778
  document.getElementById('videoModeBtn').classList.toggle('active', mode === 'video');
779
  document.getElementById('webcamModeBtn').classList.toggle('active', mode === 'webcam');
780
  document.getElementById('videoControls').classList.toggle('hidden', mode !== 'video');
781
- document.getElementById('webcamControls').classList.toggle('hidden', mode !== 'webcam');
782
- document.getElementById('inputControls').classList.toggle('hidden', !(mode === 'video' || mode === 'webcam'));
783
-
 
 
 
 
 
 
784
  // Start/stop webcam based on mode
785
  if (mode === 'webcam') {
786
  this.startWebcam();
@@ -798,9 +976,10 @@
798
  },
799
  audio: false
800
  });
801
-
802
  const video = document.getElementById('webcamVideo');
803
  video.srcObject = this.webcamStream;
 
804
  this.showInfo('Webcam started successfully');
805
  } catch (error) {
806
  this.showError(`Failed to access webcam: ${error.message}`);
@@ -816,6 +995,7 @@
816
  this.webcamStream = null;
817
  const video = document.getElementById('webcamVideo');
818
  video.srcObject = null;
 
819
  }
820
  },
821
 
@@ -853,44 +1033,44 @@
853
  this.showError('Failed to start session');
854
  return;
855
  }
856
-
857
  const prompt = document.getElementById('prompt').value.trim();
858
  if (!prompt) {
859
  this.showError('Please enter a prompt');
860
  return;
861
  }
862
-
863
  if (this.mode === 'video' && !this.currentVideoFile) {
864
  this.showError('Please upload a video file');
865
  return;
866
  }
867
-
868
  if (this.mode === 'webcam' && !this.webcamStream) {
869
  this.showError('Webcam not started');
870
  return;
871
  }
872
-
873
  this.isGenerating = true;
874
  this.frameCount = 0;
875
  document.getElementById('frameCount').textContent = '0';
876
  this.frameBuffer = [];
877
  this.updateUI();
878
-
879
  // Start recording
880
  this.startRecording();
881
-
882
  // Connect to backend WebSocket proxy
883
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
884
  const wsUrl = `${protocol}//${window.location.host}/ws/video-gen`;
885
-
886
  try {
887
  this.ws = new WebSocket(wsUrl);
888
  this.ws.binaryType = 'arraybuffer';
889
-
890
  this.ws.onopen = () => {
891
  this.showInfo('Connected! Waiting for ready signal...');
892
  };
893
-
894
  this.ws.onmessage = async (event) => {
895
  if (typeof event.data === 'string') {
896
  try {
@@ -909,24 +1089,24 @@
909
  await this.displayFrame(event.data);
910
  }
911
  };
912
-
913
  this.ws.onerror = (error) => {
914
  this.showError('WebSocket connection error');
915
  console.error('WebSocket error:', error);
916
  };
917
-
918
  this.ws.onclose = (event) => {
919
  console.log('WebSocket closed:', event.reason);
920
-
921
  // Force state reset
922
  this.isGenerating = false;
923
  this.ws = null;
924
-
925
  // Stop all processes
926
  this.stopFrameExtraction();
927
  this.stopRecording();
928
  this.stopPlaybackLoop();
929
-
930
  // Update UI immediately
931
  const btn = document.getElementById('startStopBtn');
932
  if (btn) {
@@ -934,23 +1114,23 @@
934
  btn.className = 'btn btn-primary';
935
  btn.disabled = false;
936
  }
937
-
938
  // Update status
939
  const statusPill = document.getElementById('statusPill');
940
  if (statusPill) {
941
  statusPill.className = 'status-pill status-disconnected';
942
  statusPill.textContent = 'Disconnected';
943
  }
944
-
945
  // Re-enable mode buttons
946
  ['textModeBtn', 'videoModeBtn', 'webcamModeBtn'].forEach(id => {
947
  const btn = document.getElementById(id);
948
  if (btn) btn.disabled = false;
949
  });
950
-
951
  this.showInfo(`Disconnected: ${event.reason || 'Generation complete'}`);
952
  };
953
-
954
  } catch (error) {
955
  this.showError('Failed to connect: ' + error.message);
956
  this.isGenerating = false;
@@ -968,7 +1148,7 @@
968
  width: 832,
969
  height: 480
970
  };
971
-
972
  // Add start_frame for video and webcam modes
973
  if (this.mode === 'video' && this.currentVideoFile) {
974
  try {
@@ -1000,19 +1180,19 @@
1000
  return;
1001
  }
1002
  }
1003
-
1004
  const seedValue = document.getElementById('seed').value;
1005
  if (seedValue && seedValue.trim() !== '') {
1006
  payload.seed = parseInt(seedValue);
1007
  } else {
1008
  payload.seed = Math.floor(Math.random() * (1 << 24));
1009
  }
1010
-
1011
  try {
1012
  const encoded = createMsgpackEncoder(payload);
1013
  this.ws.send(encoded);
1014
  this.showInfo('Generation started!');
1015
-
1016
  // Start frame extraction for v2v and webcam modes
1017
  if (this.mode === 'video' || this.mode === 'webcam') {
1018
  setTimeout(() => this.startFrameExtraction(), 500);
@@ -1025,16 +1205,16 @@
1025
  async displayFrame(imageData) {
1026
  const blob = new Blob([imageData], { type: 'image/jpeg' });
1027
  const bitmap = await createImageBitmap(blob);
1028
-
1029
  this.frameBuffer.push(bitmap);
1030
  this.frameCount++;
1031
  document.getElementById('frameCount').textContent = this.frameCount;
1032
-
1033
  if (this.frameCount === 1) {
1034
  document.getElementById('placeholder').style.display = 'none';
1035
  this.startPlaybackLoop();
1036
  }
1037
-
1038
  // Update progress
1039
  const progress = Math.min(100, (this.frameCount / this.maxFrames) * 100);
1040
  document.getElementById('progressFill').style.width = progress + '%';
@@ -1042,16 +1222,16 @@
1042
 
1043
  drawNextFrame() {
1044
  if (this.frameBuffer.length === 0) return;
1045
-
1046
  const canvas = document.getElementById('outputCanvas');
1047
  const bitmap = this.frameBuffer.shift();
1048
-
1049
  canvas.width = bitmap.width;
1050
  canvas.height = bitmap.height;
1051
-
1052
  const ctx = canvas.getContext('2d');
1053
  ctx.drawImage(bitmap, 0, 0);
1054
-
1055
  if (typeof bitmap.close === 'function') {
1056
  bitmap.close();
1057
  }
@@ -1061,12 +1241,19 @@
1061
  if (this.playbackInterval) {
1062
  clearInterval(this.playbackInterval);
1063
  }
1064
-
1065
  const fps = parseInt(document.getElementById('playbackFps').value);
1066
  const intervalMs = Math.max(10, Math.floor(1000 / fps));
1067
  this.playbackInterval = setInterval(() => this.drawNextFrame(), intervalMs);
1068
  },
1069
 
 
 
 
 
 
 
 
1070
  async handleVideoUpload(event) {
1071
  const file = event.target.files?.[0];
1072
  if (!file) return;
@@ -1082,12 +1269,12 @@
1082
  width: video.videoWidth,
1083
  height: video.videoHeight
1084
  };
1085
-
1086
  document.getElementById('videoInfo').innerHTML = `
1087
  ${file.name}<br>
1088
  ${this.videoMetadata.width}×${this.videoMetadata.height} • ${this.videoMetadata.duration.toFixed(1)}s
1089
  `;
1090
-
1091
  this.showInfo('Video loaded successfully');
1092
  resolve();
1093
  };
@@ -1105,20 +1292,20 @@
1105
  async extractVideoFrameBytes() {
1106
  const video = document.getElementById('hiddenVideo');
1107
  const canvas = document.getElementById('extractionCanvas');
1108
-
1109
  if (!video || !canvas) return null;
1110
-
1111
  const ctx = canvas.getContext('2d');
1112
  canvas.width = 832;
1113
  canvas.height = 480;
1114
-
1115
  // Scale and crop to 832x480
1116
  const scale = Math.max(832 / video.videoWidth, 480 / video.videoHeight);
1117
  const scaledWidth = video.videoWidth * scale;
1118
  const scaledHeight = video.videoHeight * scale;
1119
  const offsetX = (832 - scaledWidth) / 2;
1120
  const offsetY = (480 - scaledHeight) / 2;
1121
-
1122
  ctx.clearRect(0, 0, 832, 480);
1123
  ctx.drawImage(video, offsetX, offsetY, scaledWidth, scaledHeight);
1124
 
@@ -1143,22 +1330,22 @@
1143
  async extractWebcamBytes() {
1144
  const video = document.getElementById('webcamVideo');
1145
  const canvas = document.getElementById('extractionCanvas');
1146
-
1147
  if (!video || !canvas || !video.videoWidth) return null;
1148
-
1149
  const ctx = canvas.getContext('2d');
1150
  const targetWidth = 832;
1151
  const targetHeight = 480;
1152
  canvas.width = targetWidth;
1153
  canvas.height = targetHeight;
1154
-
1155
  const videoWidth = video.videoWidth;
1156
  const videoHeight = video.videoHeight;
1157
  const sourceAspect = videoWidth / videoHeight;
1158
  const targetAspect = targetWidth / targetHeight;
1159
-
1160
  let sx, sy, sWidth, sHeight;
1161
-
1162
  if (sourceAspect > targetAspect) {
1163
  sHeight = videoHeight;
1164
  sWidth = videoHeight * targetAspect;
@@ -1170,7 +1357,7 @@
1170
  sx = 0;
1171
  sy = (videoHeight - sHeight) / 2;
1172
  }
1173
-
1174
  ctx.drawImage(video, sx, sy, sWidth, sHeight, 0, 0, targetWidth, targetHeight);
1175
 
1176
  return new Promise((resolve) => {
@@ -1195,11 +1382,11 @@
1195
  const inputFps = parseInt(document.getElementById('inputFps').value);
1196
  const intervalMs = Math.floor(1000 / inputFps);
1197
  const video = document.getElementById('hiddenVideo');
1198
-
1199
  if (this.mode === 'video') {
1200
  video.play();
1201
  }
1202
-
1203
  this.frameExtractionInterval = setInterval(async () => {
1204
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1205
  this.stopFrameExtraction();
@@ -1221,20 +1408,20 @@
1221
  console.warn('Empty frame, skipping');
1222
  return;
1223
  }
1224
-
1225
  const strengthValue = parseFloat(document.getElementById('strength').value);
1226
  const message = {
1227
  image: frameBytes,
1228
  strength: strengthValue,
1229
  timestamp: Date.now()
1230
  };
1231
-
1232
  // For webcam mode, include prompt and num_blocks
1233
  if (this.mode === 'webcam') {
1234
  message.prompt = document.getElementById('prompt').value;
1235
  message.num_blocks = parseInt(document.getElementById('numBlocks').value);
1236
  }
1237
-
1238
  const encoded = createMsgpackEncoder(message);
1239
  this.ws.send(encoded);
1240
  }, intervalMs);
@@ -1245,7 +1432,7 @@
1245
  clearInterval(this.frameExtractionInterval);
1246
  this.frameExtractionInterval = null;
1247
  }
1248
-
1249
  const video = document.getElementById('hiddenVideo');
1250
  video.pause();
1251
  video.currentTime = 0;
@@ -1276,20 +1463,20 @@
1276
  const canvas = document.getElementById('outputCanvas');
1277
  const fps = parseInt(document.getElementById('playbackFps').value);
1278
  const stream = canvas.captureStream(fps);
1279
-
1280
  this.recordedChunks = [];
1281
  this.mediaRecorder = new MediaRecorder(stream, {
1282
- mimeType: MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
1283
- ? 'video/webm;codecs=vp9'
1284
  : 'video/webm'
1285
  });
1286
-
1287
  this.mediaRecorder.ondataavailable = (event) => {
1288
  if (event.data && event.data.size > 0) {
1289
  this.recordedChunks.push(event.data);
1290
  }
1291
  };
1292
-
1293
  this.mediaRecorder.start();
1294
  },
1295
 
@@ -1314,7 +1501,6 @@
1314
  }
1315
  this.pendingPromptUpdate = null;
1316
  this.stopFrameExtraction();
1317
- this.stopWebcam();
1318
  this.stopRecording();
1319
  this.isGenerating = false;
1320
  this.updateUI();
@@ -1325,7 +1511,7 @@
1325
  this.showError('No video data to download');
1326
  return;
1327
  }
1328
-
1329
  const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
1330
  const url = URL.createObjectURL(blob);
1331
  const a = document.createElement('a');
@@ -1340,12 +1526,12 @@
1340
  const btn = document.getElementById('startStopBtn');
1341
  btn.textContent = this.isGenerating ? 'Stop Generation' : 'Start Generation';
1342
  btn.className = this.isGenerating ? 'btn btn-danger' : 'btn btn-primary';
1343
-
1344
  // Update mode buttons
1345
  const textBtn = document.getElementById('textModeBtn');
1346
  const videoBtn = document.getElementById('videoModeBtn');
1347
  const webcamBtn = document.getElementById('webcamModeBtn');
1348
-
1349
  if (textBtn && videoBtn && webcamBtn) {
1350
  textBtn.disabled = this.isGenerating;
1351
  videoBtn.disabled = this.isGenerating;
@@ -1355,14 +1541,14 @@
1355
 
1356
  showError(message) {
1357
  const box = document.getElementById('errorBox');
1358
- box.textContent = '❌ ' + message;
1359
  box.classList.remove('hidden');
1360
  setTimeout(() => box.classList.add('hidden'), 5000);
1361
  },
1362
 
1363
  showInfo(message) {
1364
  const box = document.getElementById('infoBox');
1365
- box.textContent = '✓ ' + message;
1366
  box.classList.remove('hidden');
1367
  setTimeout(() => box.classList.add('hidden'), 3000);
1368
  }
@@ -1376,4 +1562,4 @@
1376
  {% endif %}
1377
  </script>
1378
  </body>
1379
- </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>KREA Realtime Video</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;500;700&family=Roboto+Mono&display=swap" rel="stylesheet">
8
  <style>
9
  * {
10
  margin: 0;
 
13
  }
14
 
15
  body {
16
+ font-family: 'Ubuntu', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
17
+ background: #fafaf9;
18
+ color: #1c1917;
19
  min-height: 100vh;
 
20
  }
21
 
22
  .container {
23
+ max-width: 1600px;
24
  margin: 0 auto;
25
+ padding: 20px;
26
  }
27
 
28
+ /* Login Strip */
29
+ .login-strip {
30
+ background: #fef3c7;
31
+ border-bottom: 2px solid #f59e0b;
32
+ padding: 15px 20px;
33
+ box-shadow: 0 3px 0 0 #d97706;
34
+ position: sticky;
35
+ top: 0;
36
+ z-index: 100;
 
37
  }
38
 
39
+ .login-strip-content {
40
+ max-width: 1600px;
41
+ margin: 0 auto;
42
+ display: flex;
43
+ justify-content: space-between;
44
+ align-items: center;
45
+ }
46
+
47
+ .login-message {
48
+ color: #78350f;
49
+ font-weight: 500;
50
+ }
51
+
52
+ .app-grayed {
53
+ opacity: 0.4;
54
+ pointer-events: none;
55
+ filter: grayscale(50%);
56
  }
57
 
58
  .btn {
59
+ padding: 10px 20px;
60
  border: none;
61
  border-radius: 6px;
62
  font-weight: 500;
63
  cursor: pointer;
64
+ transition: all 0.1s;
65
  text-decoration: none;
66
  display: inline-block;
67
+ font-family: 'Ubuntu', sans-serif;
68
+ border-width: 0px;
69
  }
70
 
71
  .btn-primary {
72
+ background: #f59e0b;
73
+ color: #78350f;
74
+ box-shadow: 0px 3px 0px 0px #d97706;
 
75
  }
76
 
77
  .btn-primary:hover {
78
+ box-shadow: 0px 5px 0px 0px #d97706;
79
+ transform: translateY(-2px);
80
+ }
81
+
82
+ .btn-primary:active {
83
+ box-shadow: 0px 2px 0px 0px #d97706;
84
+ transform: translateY(1px);
85
  }
86
 
87
  .btn-primary:disabled {
 
89
  cursor: not-allowed;
90
  }
91
 
92
+ .btn-secondary {
93
+ background: #fbbf24;
94
+ color: #78350f;
95
+ box-shadow: 0px 3px 0px 0px #f59e0b;
96
+ }
97
+
98
+ .btn-secondary:hover {
99
+ box-shadow: 0px 5px 0px 0px #f59e0b;
100
+ transform: translateY(-2px);
101
+ }
102
+
103
+ .btn-secondary:active {
104
+ box-shadow: 0px 2px 0px 0px #f59e0b;
105
+ transform: translateY(1px);
106
+ }
107
+
108
  .btn-danger {
109
  background: #ef4444;
110
  color: white;
111
+ box-shadow: 0px 3px 0px 0px #dc2626;
112
  }
113
 
114
  .btn-danger:hover {
115
+ box-shadow: 0px 5px 0px 0px #dc2626;
116
+ transform: translateY(-2px);
117
+ }
118
+
119
+ .btn-danger:active {
120
+ box-shadow: 0px 2px 0px 0px #dc2626;
121
+ transform: translateY(1px);
122
  }
123
 
124
  .info-box, .error-box, .warning-box {
 
126
  padding: 12px;
127
  margin-top: 15px;
128
  font-size: 0.85rem;
129
+ border-width: 1px;
130
+ border-style: solid;
131
  }
132
 
133
  .info-box {
134
+ background: #fef3c7;
135
+ border-color: #fbbf24;
136
+ color: #78350f;
137
  }
138
 
139
  .error-box {
140
+ background: #fee2e2;
141
+ border-color: #fca5a5;
142
+ color: #7f1d1d;
143
  }
144
 
145
  .warning-box {
146
+ background: #fef3c7;
147
+ border-color: #fbbf24;
148
+ color: #78350f;
149
  }
150
 
151
  /* User Bar */
152
  .user-bar {
153
+ background: #fafaf9;
154
+ border-radius: 8px;
155
+ border: 1px solid #d6d3d1;
 
156
  padding: 15px 20px;
157
  margin-bottom: 20px;
158
  display: flex;
159
  justify-content: space-between;
160
  align-items: center;
161
+ box-shadow: 0px 3px 0px 0px #d6d3d1;
162
  }
163
 
164
  .user-info {
 
171
  width: 40px;
172
  height: 40px;
173
  border-radius: 50%;
174
+ border: 2px solid #f59e0b;
175
  }
176
 
177
  .user-badge {
 
184
  }
185
 
186
  .badge-pro {
187
+ background: #f59e0b;
188
  color: white;
189
  }
190
 
191
  .badge-free {
192
+ background: #78716c;
193
+ color: #e7e5e4;
194
  }
195
 
196
  .btn-logout {
197
  padding: 8px 16px;
198
+ background: #78716c;
199
+ color: #fafaf9;
200
  border: none;
201
  border-radius: 6px;
202
  cursor: pointer;
 
204
  }
205
 
206
  .btn-logout:hover {
207
+ background: #57534e;
208
  }
209
 
210
  /* Main App Layout */
 
214
  }
215
 
216
  h1 {
217
+ font-size: 2.5rem;
218
  margin-bottom: 10px;
219
+ color: #1c1917;
220
+ font-weight: 700;
221
+ }
222
+
223
+ .header-links {
224
+ display: flex;
225
+ gap: 15px;
226
+ justify-content: center;
227
+ margin-top: 10px;
228
+ }
229
+
230
+ .header-link {
231
+ color: #f59e0b;
232
+ text-decoration: none;
233
+ font-size: 0.9rem;
234
+ font-weight: 500;
235
+ }
236
+
237
+ .header-link:hover {
238
+ text-decoration: underline;
239
  }
240
 
241
  .subtitle {
242
+ color: #78716c;
243
  font-size: 0.9rem;
244
+ margin-top: 5px;
245
  }
246
 
247
+ /* Video Grid */
248
+ .video-grid {
249
  display: grid;
250
+ grid-template-columns: 1fr 1fr;
251
  gap: 20px;
252
+ margin-bottom: 20px;
253
  }
254
 
255
  .panel {
256
+ background: #fafaf9;
257
+ border-radius: 8px;
258
+ border: 1px solid #d6d3d1;
 
259
  padding: 20px;
260
+ box-shadow: 0px 3px 0px 0px #d6d3d1;
261
+ }
262
+
263
+ .panel h2 {
264
+ font-size: 1.1rem;
265
+ margin-bottom: 15px;
266
+ color: #1c1917;
267
  }
268
 
269
  .video-container {
 
275
  justify-content: center;
276
  position: relative;
277
  overflow: hidden;
278
+ box-shadow: 0px -1px 0px 0px #d6d3d1;
279
  }
280
 
281
+ #outputCanvas, #webcamVideo {
282
  max-width: 100%;
283
  max-height: 100%;
284
  object-fit: contain;
285
+ border-radius: 6px;
286
  }
287
 
288
  .placeholder {
289
  text-align: center;
290
+ color: #78716c;
291
  }
292
 
293
  .status-bar {
 
296
  align-items: center;
297
  margin-top: 15px;
298
  padding-top: 15px;
299
+ border-top: 1px solid #d6d3d1;
300
  font-size: 0.85rem;
301
  }
302
 
 
308
  }
309
 
310
  .status-connected {
311
+ background: #dcfce7;
312
+ color: #15803d;
313
  }
314
 
315
  .status-disconnected {
316
+ background: #fee2e2;
317
+ color: #991b1b;
318
  }
319
 
320
  .mode-toggle {
 
326
  .mode-btn {
327
  flex: 1;
328
  padding: 10px;
329
+ border: 1px solid #d6d3d1;
330
  border-radius: 6px;
331
+ background: #fafaf9;
332
+ color: #57534e;
333
  cursor: pointer;
334
  transition: all 0.2s;
335
  font-size: 0.85rem;
336
+ font-weight: 500;
337
  }
338
 
339
  .mode-btn:hover:not(:disabled) {
340
+ background: #fef3c7;
341
  }
342
 
343
  .mode-btn.active {
344
+ background: #fbbf24;
345
+ color: #78350f;
346
+ box-shadow: 0px 3px 0px 0px #f59e0b;
347
+ border-color: #f59e0b;
348
  }
349
 
350
  .mode-btn:disabled {
 
359
  label {
360
  display: block;
361
  font-size: 0.85rem;
362
+ color: #57534e;
363
  margin-bottom: 5px;
364
+ font-weight: 500;
365
  }
366
 
367
  input[type="text"],
 
371
  select {
372
  width: 100%;
373
  padding: 8px 12px;
374
+ background: #fafaf9;
375
+ border: 1px solid #d6d3d1;
376
  border-radius: 6px;
377
+ color: #1c1917;
378
  font-size: 0.9rem;
379
+ box-shadow: 0px -1px 0px 0px #d6d3d1;
380
+ font-family: 'Ubuntu', sans-serif;
381
  }
382
 
383
+ input:focus, textarea:focus, select:focus {
384
  outline: none;
385
+ border-color: #f59e0b;
386
+ box-shadow: 0px -1px 0px 0px #f59e0b;
387
+ background: #fef3c7;
388
  }
389
 
390
  textarea {
391
  min-height: 80px;
392
  resize: vertical;
393
+ font-family: 'Ubuntu', sans-serif;
394
  }
395
 
396
  input[type="range"] {
397
  width: 100%;
398
  height: 4px;
399
+ background: #d6d3d1;
400
  border-radius: 2px;
401
  outline: none;
402
+ -webkit-appearance: none;
403
+ }
404
+
405
+ input[type="range"]::-webkit-slider-thumb {
406
+ -webkit-appearance: none;
407
+ appearance: none;
408
+ width: 16px;
409
+ height: 16px;
410
+ background: #f59e0b;
411
+ cursor: pointer;
412
+ border-radius: 50%;
413
+ }
414
+
415
+ input[type="range"]::-moz-range-thumb {
416
+ width: 16px;
417
+ height: 16px;
418
+ background: #f59e0b;
419
+ cursor: pointer;
420
+ border-radius: 50%;
421
+ border: none;
422
  }
423
 
424
  .range-value {
425
  display: inline-block;
426
  margin-left: 10px;
427
  font-weight: 500;
428
+ color: #f59e0b;
429
  }
430
 
431
  .hint {
432
  font-size: 0.75rem;
433
+ color: #78716c;
434
  margin-top: 4px;
435
  }
436
 
437
  .progress-bar {
438
  width: 100%;
439
  height: 6px;
440
+ background: #e7e5e4;
441
  border-radius: 3px;
442
  margin-top: 10px;
443
  overflow: hidden;
 
445
 
446
  .progress-fill {
447
  height: 100%;
448
+ background: #f59e0b;
449
  transition: width 0.3s ease;
450
  width: 0%;
451
  }
 
461
  }
462
 
463
  .limit-warning {
464
+ background: #fee2e2;
465
+ border: 1px solid #fca5a5;
466
+ border-radius: 8px;
467
  padding: 20px;
468
  margin-bottom: 20px;
469
  text-align: center;
470
  }
471
 
472
  .upgrade-link {
473
+ color: #f59e0b;
474
  text-decoration: none;
475
  font-weight: 500;
476
  }
477
 
478
+ .upgrade-link:hover {
479
+ text-decoration: underline;
480
+ }
481
+
482
+ /* Accordion */
483
+ .accordion {
484
+ margin-top: 15px;
485
+ }
486
+
487
+ .accordion-header {
488
+ background: #e7e5e4;
489
+ padding: 12px 15px;
490
  border-radius: 6px;
491
+ cursor: pointer;
492
+ font-weight: 500;
493
+ display: flex;
494
+ justify-content: space-between;
495
+ align-items: center;
496
+ user-select: none;
497
+ color: #1c1917;
498
+ border: 1px solid #d6d3d1;
499
  }
500
 
501
+ .accordion-header:hover {
502
+ background: #d6d3d1;
503
+ }
504
+
505
+ .accordion-icon {
506
+ transition: transform 0.2s;
507
+ }
508
+
509
+ .accordion-icon.open {
510
+ transform: rotate(180deg);
511
+ }
512
+
513
+ .accordion-content {
514
+ max-height: 0;
515
+ overflow: hidden;
516
+ transition: max-height 0.3s ease;
517
+ }
518
+
519
+ .accordion-content.open {
520
+ max-height: 1000px;
521
+ }
522
+
523
+ .accordion-body {
524
+ padding: 15px 0;
525
+ }
526
+
527
+ @media (max-width: 1024px) {
528
+ .video-grid {
529
  grid-template-columns: 1fr;
530
  }
531
+
532
  .user-bar {
533
  flex-direction: column;
534
  gap: 15px;
 
539
  <body>
540
  <div class="container">
541
  {% if not authenticated %}
542
+ <!-- Login Strip -->
543
+ <div class="login-strip">
544
+ <div class="login-strip-content">
545
+ <div class="login-message">
546
+ Sign in with Hugging Face to start generating videos
547
+ </div>
548
+ <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 }}"
549
+ class="btn btn-primary"
550
+ target="_blank">
551
+ Sign in with Hugging Face
552
+ </a>
 
 
 
 
 
 
 
 
 
553
  </div>
554
  </div>
555
+
556
+ <!-- Grayed Out App -->
557
+ <div class="app-grayed">
558
+ {% endif %}
559
+
560
  <!-- Authenticated App -->
561
+ {% if authenticated %}
562
  <div class="user-bar">
563
  <div class="user-info">
564
  {% if user.avatar %}
 
572
  </div>
573
  </div>
574
  <div style="text-align: right;">
575
+ <div class="usage-info" style="font-size: 0.85rem; color: #78716c;">Generations: {{ sessions_used }}/{{ sessions_limit }} today</div>
576
  <button class="btn-logout" onclick="logout()">Logout</button>
577
  </div>
578
  </div>
579
 
580
  {% if not can_start %}
581
  <div class="limit-warning">
582
+ <h3 style="color: #991b1b; margin-bottom: 10px;">Daily Limit Reached</h3>
583
+ <p style="color: #78716c; font-size: 0.9rem;">You've used all {{ sessions_limit }} generations for today. Come back tomorrow!</p>
584
  {% if not user.is_pro %}
585
+ <p style="margin-top: 10px; color: #78716c; font-size: 0.9rem;">
586
  Upgrade to <a href="https://huggingface.co/pricing" target="_blank" class="upgrade-link">Hugging Face PRO</a> for 15 generations per day!
587
  </p>
588
  {% endif %}
589
  </div>
590
  {% endif %}
591
+ {% endif %}
592
 
593
  <header>
594
+ <h1>KREA Realtime Video</h1>
595
+ <div class="header-links">
596
+ <a href="https://huggingface.co/krea/krea-realtime-video" target="_blank" class="header-link">Model on Hugging Face</a>
597
+ <span style="color: #d6d3d1;">•</span>
598
+ <a href="https://github.com/krea-ai/realtime-video" target="_blank" class="header-link">GitHub Repository</a>
599
+ </div>
600
  <p class="subtitle">Self-Forcing Diffusion with Dynamic Prompt Rewriting • 832×480 Fixed Resolution</p>
601
  </header>
602
 
603
+ <!-- Video Grid: Webcam Left, Output Right -->
604
+ <div class="video-grid">
605
+ <!-- Webcam Panel -->
606
+ <div class="panel">
607
+ <h2>Webcam Input</h2>
608
+ <div class="video-container">
609
+ <video id="webcamVideo" autoplay playsinline muted></video>
610
+ <div id="webcamPlaceholder" class="placeholder">
611
+ <p>{% if can_start %}Webcam will appear here{% else %}Sign in to use webcam{% endif %}</p>
612
+ </div>
613
+ </div>
614
+ </div>
615
+
616
  <!-- Video Output Panel -->
617
  <div class="panel">
618
+ <h2>Generated Video</h2>
619
  <div class="video-container">
620
  <canvas id="outputCanvas"></canvas>
621
  <div id="placeholder" class="placeholder">
622
  <p>{% if can_start %}Configure settings and click Start{% else %}Daily limit reached{% endif %}</p>
623
  </div>
624
  </div>
625
+
626
  <div class="status-bar">
627
  <div>
628
  <span id="statusPill" class="status-pill status-disconnected">Disconnected</span>
 
634
  <span class="range-value" id="playbackFpsValue">12 fps</span>
635
  </div>
636
  </div>
637
+
638
  <div class="progress-bar">
639
  <div id="progressFill" class="progress-fill"></div>
640
  </div>
641
  </div>
642
+ </div>
643
 
644
+ <!-- Controls Panel -->
645
+ <div class="panel">
646
+ <h2 style="margin-bottom: 20px;">Controls</h2>
 
 
 
 
 
 
 
647
 
648
+ <!-- Mode Selection -->
649
+ <div class="mode-toggle">
650
+ <button id="textModeBtn" class="mode-btn" {% if not can_start %}disabled{% endif %}>Text-to-Video</button>
651
+ <button id="videoModeBtn" class="mode-btn" {% if not can_start %}disabled{% endif %}>Video-to-Video</button>
652
+ <button id="webcamModeBtn" class="mode-btn active" {% if not can_start %}disabled{% endif %}>Webcam-to-Video</button>
653
+ </div>
654
 
655
+ <!-- Prompt (Main Setting) -->
656
+ <div class="form-group">
657
+ <label>Prompt <span style="color: #15803d; font-size: 0.75rem;">(updates sent every 2 seconds)</span></label>
658
+ <textarea id="prompt" {% if not can_start %}disabled{% endif %}>A cat riding a skateboard through a neon city at night</textarea>
659
+ <div class="hint">Detailed prompts work better</div>
660
+ </div>
 
 
 
 
 
661
 
662
+ <!-- Video Upload (V2V mode) -->
663
+ <div id="videoControls" class="hidden">
664
+ <div class="form-group">
665
+ <label>Upload Video</label>
666
+ <input type="file" id="videoFile" accept="video/*" class="hidden">
667
+ <button class="btn btn-secondary" style="width: 100%;" onclick="document.getElementById('videoFile').click()">
668
+ Select Video File
669
+ </button>
670
+ <div id="videoInfo" style="margin-top: 10px; font-size: 0.85rem; color: #78716c;"></div>
671
  </div>
672
+ </div>
673
 
674
+ <!-- Action Buttons (Outside Accordion) -->
675
+ <div class="action-buttons">
676
+ <button id="startStopBtn" class="btn btn-primary" onclick="app.toggleGeneration()" style="flex: 1;" {% if not can_start %}disabled{% endif %}>
677
+ {% if can_start %}Start Generation{% else %}Limit Reached{% endif %}
678
+ </button>
679
+ <button class="btn btn-secondary" onclick="app.downloadVideo()">Download</button>
680
+ </div>
681
 
682
+ <!-- Advanced Settings Accordion -->
683
+ <div class="accordion">
684
+ <div class="accordion-header" onclick="app.toggleAccordion()">
685
+ <span>Advanced Settings</span>
686
+ <span class="accordion-icon" id="accordionIcon">▼</span>
687
  </div>
688
+ <div class="accordion-content" id="accordionContent">
689
+ <div class="accordion-body">
690
+ <!-- Input Controls (V2V and Webcam modes) -->
691
+ <div id="inputControls">
692
+ <div class="form-group">
693
+ <label>Input Frame Rate: <span class="range-value" id="inputFpsValue">12</span> fps</label>
694
+ <input type="range" id="inputFps" min="10" max="14" value="12" {% if not can_start %}disabled{% endif %}>
695
+ <div class="hint">Recommended: 10-14 fps</div>
696
+ </div>
697
+
698
+ <div class="form-group">
699
+ <label>Transformation Strength: <span class="range-value" id="strengthValue">0.45</span></label>
700
+ <input type="range" id="strength" min="0.1" max="0.6" step="0.05" value="0.45" {% if not can_start %}disabled{% endif %}>
701
+ <div class="hint">Keep low (&lt;0.5-0.6) to avoid artifacts</div>
702
+ </div>
703
+ </div>
704
+
705
+ <div class="form-group">
706
+ <label>Max Frames (Blocks): <span class="range-value" id="numBlocksValue">20</span> → ~<span id="estimatedFrames">234</span> frames</label>
707
+ <input type="range" id="numBlocks" min="10" max="50" step="5" value="20" {% if not can_start %}disabled{% endif %}>
708
+ <div class="hint">Each block generates ~12 frames</div>
709
+ </div>
710
+
711
+ <div class="form-group">
712
+ <label>Seed (optional)</label>
713
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
714
+ <input type="number" id="seed" placeholder="random" {% if not can_start %}disabled{% endif %}>
715
+ <button class="btn btn-secondary" onclick="app.randomizeSeed()" {% if not can_start %}disabled{% endif %}>Random</button>
716
+ </div>
717
+ </div>
718
  </div>
719
  </div>
720
+ </div>
721
 
722
+ <div id="errorBox" class="error-box hidden"></div>
723
+ <div id="infoBox" class="info-box hidden"></div>
724
+ </div>
 
 
 
725
 
726
+ {% if not authenticated %}
 
 
727
  </div>
728
  {% endif %}
729
  </div>
 
736
  {% if not authenticated %}
737
  // Not authenticated - no app logic needed
738
  {% else %}
739
+
740
  async function logout() {
741
  await fetch('/api/logout', {method: 'POST'});
742
  window.location.reload();
 
870
 
871
  // Main App (simplified - add full video generation logic here)
872
  const app = {
873
+ mode: 'webcam',
874
  isGenerating: false,
875
  frameCount: 0,
876
  maxFrames: 234,
 
889
  init() {
890
  this.setupEventListeners();
891
  this.updateEstimatedFrames();
892
+ // Auto-start webcam since it's the default mode
893
+ this.setMode('webcam');
894
+ },
895
+
896
+ toggleAccordion() {
897
+ const content = document.getElementById('accordionContent');
898
+ const icon = document.getElementById('accordionIcon');
899
+ content.classList.toggle('open');
900
+ icon.classList.toggle('open');
901
  },
902
 
903
  setupEventListeners() {
904
  document.getElementById('textModeBtn').addEventListener('click', () => this.setMode('text'));
905
  document.getElementById('videoModeBtn').addEventListener('click', () => this.setMode('video'));
906
  document.getElementById('webcamModeBtn').addEventListener('click', () => this.setMode('webcam'));
907
+
908
  // Video file upload
909
  document.getElementById('videoFile').addEventListener('change', (e) => this.handleVideoUpload(e));
910
+
911
  document.getElementById('playbackFps').addEventListener('input', (e) => {
912
  document.getElementById('playbackFpsValue').textContent = e.target.value + ' fps';
913
  if (this.playbackInterval) {
914
  this.startPlaybackLoop();
915
  }
916
  });
917
+
918
  document.getElementById('inputFps').addEventListener('input', (e) => {
919
  document.getElementById('inputFpsValue').textContent = e.target.value;
920
  if (this.isGenerating && (this.mode === 'video' || this.mode === 'webcam')) {
 
922
  this.startFrameExtraction();
923
  }
924
  });
925
+
926
  document.getElementById('strength').addEventListener('input', (e) => {
927
  document.getElementById('strengthValue').textContent = parseFloat(e.target.value).toFixed(2);
928
  });
929
+
930
  document.getElementById('numBlocks').addEventListener('input', (e) => {
931
  document.getElementById('numBlocksValue').textContent = e.target.value;
932
  this.updateEstimatedFrames();
 
934
  this.queuePromptUpdate();
935
  }
936
  });
937
+
938
  // Prompt changes for live updates
939
  document.getElementById('prompt').addEventListener('input', () => {
940
  if (this.isGenerating && (this.mode === 'text' || this.mode === 'webcam')) {
 
950
  document.getElementById('videoModeBtn').classList.toggle('active', mode === 'video');
951
  document.getElementById('webcamModeBtn').classList.toggle('active', mode === 'webcam');
952
  document.getElementById('videoControls').classList.toggle('hidden', mode !== 'video');
953
+
954
+ // Input controls always visible in accordion for webcam/video modes
955
+ const inputControls = document.getElementById('inputControls');
956
+ if (mode === 'text') {
957
+ inputControls.style.display = 'none';
958
+ } else {
959
+ inputControls.style.display = 'block';
960
+ }
961
+
962
  // Start/stop webcam based on mode
963
  if (mode === 'webcam') {
964
  this.startWebcam();
 
976
  },
977
  audio: false
978
  });
979
+
980
  const video = document.getElementById('webcamVideo');
981
  video.srcObject = this.webcamStream;
982
+ document.getElementById('webcamPlaceholder').style.display = 'none';
983
  this.showInfo('Webcam started successfully');
984
  } catch (error) {
985
  this.showError(`Failed to access webcam: ${error.message}`);
 
995
  this.webcamStream = null;
996
  const video = document.getElementById('webcamVideo');
997
  video.srcObject = null;
998
+ document.getElementById('webcamPlaceholder').style.display = 'flex';
999
  }
1000
  },
1001
 
 
1033
  this.showError('Failed to start session');
1034
  return;
1035
  }
1036
+
1037
  const prompt = document.getElementById('prompt').value.trim();
1038
  if (!prompt) {
1039
  this.showError('Please enter a prompt');
1040
  return;
1041
  }
1042
+
1043
  if (this.mode === 'video' && !this.currentVideoFile) {
1044
  this.showError('Please upload a video file');
1045
  return;
1046
  }
1047
+
1048
  if (this.mode === 'webcam' && !this.webcamStream) {
1049
  this.showError('Webcam not started');
1050
  return;
1051
  }
1052
+
1053
  this.isGenerating = true;
1054
  this.frameCount = 0;
1055
  document.getElementById('frameCount').textContent = '0';
1056
  this.frameBuffer = [];
1057
  this.updateUI();
1058
+
1059
  // Start recording
1060
  this.startRecording();
1061
+
1062
  // Connect to backend WebSocket proxy
1063
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1064
  const wsUrl = `${protocol}//${window.location.host}/ws/video-gen`;
1065
+
1066
  try {
1067
  this.ws = new WebSocket(wsUrl);
1068
  this.ws.binaryType = 'arraybuffer';
1069
+
1070
  this.ws.onopen = () => {
1071
  this.showInfo('Connected! Waiting for ready signal...');
1072
  };
1073
+
1074
  this.ws.onmessage = async (event) => {
1075
  if (typeof event.data === 'string') {
1076
  try {
 
1089
  await this.displayFrame(event.data);
1090
  }
1091
  };
1092
+
1093
  this.ws.onerror = (error) => {
1094
  this.showError('WebSocket connection error');
1095
  console.error('WebSocket error:', error);
1096
  };
1097
+
1098
  this.ws.onclose = (event) => {
1099
  console.log('WebSocket closed:', event.reason);
1100
+
1101
  // Force state reset
1102
  this.isGenerating = false;
1103
  this.ws = null;
1104
+
1105
  // Stop all processes
1106
  this.stopFrameExtraction();
1107
  this.stopRecording();
1108
  this.stopPlaybackLoop();
1109
+
1110
  // Update UI immediately
1111
  const btn = document.getElementById('startStopBtn');
1112
  if (btn) {
 
1114
  btn.className = 'btn btn-primary';
1115
  btn.disabled = false;
1116
  }
1117
+
1118
  // Update status
1119
  const statusPill = document.getElementById('statusPill');
1120
  if (statusPill) {
1121
  statusPill.className = 'status-pill status-disconnected';
1122
  statusPill.textContent = 'Disconnected';
1123
  }
1124
+
1125
  // Re-enable mode buttons
1126
  ['textModeBtn', 'videoModeBtn', 'webcamModeBtn'].forEach(id => {
1127
  const btn = document.getElementById(id);
1128
  if (btn) btn.disabled = false;
1129
  });
1130
+
1131
  this.showInfo(`Disconnected: ${event.reason || 'Generation complete'}`);
1132
  };
1133
+
1134
  } catch (error) {
1135
  this.showError('Failed to connect: ' + error.message);
1136
  this.isGenerating = false;
 
1148
  width: 832,
1149
  height: 480
1150
  };
1151
+
1152
  // Add start_frame for video and webcam modes
1153
  if (this.mode === 'video' && this.currentVideoFile) {
1154
  try {
 
1180
  return;
1181
  }
1182
  }
1183
+
1184
  const seedValue = document.getElementById('seed').value;
1185
  if (seedValue && seedValue.trim() !== '') {
1186
  payload.seed = parseInt(seedValue);
1187
  } else {
1188
  payload.seed = Math.floor(Math.random() * (1 << 24));
1189
  }
1190
+
1191
  try {
1192
  const encoded = createMsgpackEncoder(payload);
1193
  this.ws.send(encoded);
1194
  this.showInfo('Generation started!');
1195
+
1196
  // Start frame extraction for v2v and webcam modes
1197
  if (this.mode === 'video' || this.mode === 'webcam') {
1198
  setTimeout(() => this.startFrameExtraction(), 500);
 
1205
  async displayFrame(imageData) {
1206
  const blob = new Blob([imageData], { type: 'image/jpeg' });
1207
  const bitmap = await createImageBitmap(blob);
1208
+
1209
  this.frameBuffer.push(bitmap);
1210
  this.frameCount++;
1211
  document.getElementById('frameCount').textContent = this.frameCount;
1212
+
1213
  if (this.frameCount === 1) {
1214
  document.getElementById('placeholder').style.display = 'none';
1215
  this.startPlaybackLoop();
1216
  }
1217
+
1218
  // Update progress
1219
  const progress = Math.min(100, (this.frameCount / this.maxFrames) * 100);
1220
  document.getElementById('progressFill').style.width = progress + '%';
 
1222
 
1223
  drawNextFrame() {
1224
  if (this.frameBuffer.length === 0) return;
1225
+
1226
  const canvas = document.getElementById('outputCanvas');
1227
  const bitmap = this.frameBuffer.shift();
1228
+
1229
  canvas.width = bitmap.width;
1230
  canvas.height = bitmap.height;
1231
+
1232
  const ctx = canvas.getContext('2d');
1233
  ctx.drawImage(bitmap, 0, 0);
1234
+
1235
  if (typeof bitmap.close === 'function') {
1236
  bitmap.close();
1237
  }
 
1241
  if (this.playbackInterval) {
1242
  clearInterval(this.playbackInterval);
1243
  }
1244
+
1245
  const fps = parseInt(document.getElementById('playbackFps').value);
1246
  const intervalMs = Math.max(10, Math.floor(1000 / fps));
1247
  this.playbackInterval = setInterval(() => this.drawNextFrame(), intervalMs);
1248
  },
1249
 
1250
+ stopPlaybackLoop() {
1251
+ if (this.playbackInterval) {
1252
+ clearInterval(this.playbackInterval);
1253
+ this.playbackInterval = null;
1254
+ }
1255
+ },
1256
+
1257
  async handleVideoUpload(event) {
1258
  const file = event.target.files?.[0];
1259
  if (!file) return;
 
1269
  width: video.videoWidth,
1270
  height: video.videoHeight
1271
  };
1272
+
1273
  document.getElementById('videoInfo').innerHTML = `
1274
  ${file.name}<br>
1275
  ${this.videoMetadata.width}×${this.videoMetadata.height} • ${this.videoMetadata.duration.toFixed(1)}s
1276
  `;
1277
+
1278
  this.showInfo('Video loaded successfully');
1279
  resolve();
1280
  };
 
1292
  async extractVideoFrameBytes() {
1293
  const video = document.getElementById('hiddenVideo');
1294
  const canvas = document.getElementById('extractionCanvas');
1295
+
1296
  if (!video || !canvas) return null;
1297
+
1298
  const ctx = canvas.getContext('2d');
1299
  canvas.width = 832;
1300
  canvas.height = 480;
1301
+
1302
  // Scale and crop to 832x480
1303
  const scale = Math.max(832 / video.videoWidth, 480 / video.videoHeight);
1304
  const scaledWidth = video.videoWidth * scale;
1305
  const scaledHeight = video.videoHeight * scale;
1306
  const offsetX = (832 - scaledWidth) / 2;
1307
  const offsetY = (480 - scaledHeight) / 2;
1308
+
1309
  ctx.clearRect(0, 0, 832, 480);
1310
  ctx.drawImage(video, offsetX, offsetY, scaledWidth, scaledHeight);
1311
 
 
1330
  async extractWebcamBytes() {
1331
  const video = document.getElementById('webcamVideo');
1332
  const canvas = document.getElementById('extractionCanvas');
1333
+
1334
  if (!video || !canvas || !video.videoWidth) return null;
1335
+
1336
  const ctx = canvas.getContext('2d');
1337
  const targetWidth = 832;
1338
  const targetHeight = 480;
1339
  canvas.width = targetWidth;
1340
  canvas.height = targetHeight;
1341
+
1342
  const videoWidth = video.videoWidth;
1343
  const videoHeight = video.videoHeight;
1344
  const sourceAspect = videoWidth / videoHeight;
1345
  const targetAspect = targetWidth / targetHeight;
1346
+
1347
  let sx, sy, sWidth, sHeight;
1348
+
1349
  if (sourceAspect > targetAspect) {
1350
  sHeight = videoHeight;
1351
  sWidth = videoHeight * targetAspect;
 
1357
  sx = 0;
1358
  sy = (videoHeight - sHeight) / 2;
1359
  }
1360
+
1361
  ctx.drawImage(video, sx, sy, sWidth, sHeight, 0, 0, targetWidth, targetHeight);
1362
 
1363
  return new Promise((resolve) => {
 
1382
  const inputFps = parseInt(document.getElementById('inputFps').value);
1383
  const intervalMs = Math.floor(1000 / inputFps);
1384
  const video = document.getElementById('hiddenVideo');
1385
+
1386
  if (this.mode === 'video') {
1387
  video.play();
1388
  }
1389
+
1390
  this.frameExtractionInterval = setInterval(async () => {
1391
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1392
  this.stopFrameExtraction();
 
1408
  console.warn('Empty frame, skipping');
1409
  return;
1410
  }
1411
+
1412
  const strengthValue = parseFloat(document.getElementById('strength').value);
1413
  const message = {
1414
  image: frameBytes,
1415
  strength: strengthValue,
1416
  timestamp: Date.now()
1417
  };
1418
+
1419
  // For webcam mode, include prompt and num_blocks
1420
  if (this.mode === 'webcam') {
1421
  message.prompt = document.getElementById('prompt').value;
1422
  message.num_blocks = parseInt(document.getElementById('numBlocks').value);
1423
  }
1424
+
1425
  const encoded = createMsgpackEncoder(message);
1426
  this.ws.send(encoded);
1427
  }, intervalMs);
 
1432
  clearInterval(this.frameExtractionInterval);
1433
  this.frameExtractionInterval = null;
1434
  }
1435
+
1436
  const video = document.getElementById('hiddenVideo');
1437
  video.pause();
1438
  video.currentTime = 0;
 
1463
  const canvas = document.getElementById('outputCanvas');
1464
  const fps = parseInt(document.getElementById('playbackFps').value);
1465
  const stream = canvas.captureStream(fps);
1466
+
1467
  this.recordedChunks = [];
1468
  this.mediaRecorder = new MediaRecorder(stream, {
1469
+ mimeType: MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
1470
+ ? 'video/webm;codecs=vp9'
1471
  : 'video/webm'
1472
  });
1473
+
1474
  this.mediaRecorder.ondataavailable = (event) => {
1475
  if (event.data && event.data.size > 0) {
1476
  this.recordedChunks.push(event.data);
1477
  }
1478
  };
1479
+
1480
  this.mediaRecorder.start();
1481
  },
1482
 
 
1501
  }
1502
  this.pendingPromptUpdate = null;
1503
  this.stopFrameExtraction();
 
1504
  this.stopRecording();
1505
  this.isGenerating = false;
1506
  this.updateUI();
 
1511
  this.showError('No video data to download');
1512
  return;
1513
  }
1514
+
1515
  const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
1516
  const url = URL.createObjectURL(blob);
1517
  const a = document.createElement('a');
 
1526
  const btn = document.getElementById('startStopBtn');
1527
  btn.textContent = this.isGenerating ? 'Stop Generation' : 'Start Generation';
1528
  btn.className = this.isGenerating ? 'btn btn-danger' : 'btn btn-primary';
1529
+
1530
  // Update mode buttons
1531
  const textBtn = document.getElementById('textModeBtn');
1532
  const videoBtn = document.getElementById('videoModeBtn');
1533
  const webcamBtn = document.getElementById('webcamModeBtn');
1534
+
1535
  if (textBtn && videoBtn && webcamBtn) {
1536
  textBtn.disabled = this.isGenerating;
1537
  videoBtn.disabled = this.isGenerating;
 
1541
 
1542
  showError(message) {
1543
  const box = document.getElementById('errorBox');
1544
+ box.textContent = message;
1545
  box.classList.remove('hidden');
1546
  setTimeout(() => box.classList.add('hidden'), 5000);
1547
  },
1548
 
1549
  showInfo(message) {
1550
  const box = document.getElementById('infoBox');
1551
+ box.textContent = message;
1552
  box.classList.remove('hidden');
1553
  setTimeout(() => box.classList.add('hidden'), 3000);
1554
  }
 
1562
  {% endif %}
1563
  </script>
1564
  </body>
1565
+ </html>