factorstudios commited on
Commit
a6270eb
·
verified ·
1 Parent(s): b251505

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +3 -0
  2. server.py +106 -3
  3. static/index.html +525 -0
Dockerfile CHANGED
@@ -31,6 +31,9 @@ COPY composer_v2.py .
31
  # Copy assets (fonts, etc.)
32
  COPY asset/ asset/
33
 
 
 
 
34
  # Create necessary directories
35
  RUN mkdir -p selected candidates renders upload_candidates
36
 
 
31
  # Copy assets (fonts, etc.)
32
  COPY asset/ asset/
33
 
34
+ # Copy static files (UI)
35
+ COPY static/ static/
36
+
37
  # Create necessary directories
38
  RUN mkdir -p selected candidates renders upload_candidates
39
 
server.py CHANGED
@@ -5,6 +5,7 @@ Dynamically generates video from manifest without hardcoded labels
5
 
6
  from fastapi import FastAPI, HTTPException, File, UploadFile, Form
7
  from fastapi.responses import FileResponse
 
8
  from pydantic import BaseModel
9
  from typing import List, Dict, Any, Optional
10
  import os
@@ -16,6 +17,7 @@ import sys
16
  import requests
17
  from PIL import Image
18
  from io import BytesIO
 
19
 
20
  # ─────────────────────────────────────────────────────────────────────────
21
  # Pydantic Models
@@ -68,6 +70,11 @@ CANDIDATES_DIR.mkdir(exist_ok=True)
68
  SELECTED_DIR.mkdir(exist_ok=True)
69
  RENDERS_DIR.mkdir(exist_ok=True)
70
 
 
 
 
 
 
71
 
72
  # ─────────────────────────────────────────────────────────────────────────
73
  # Scene Configuration Generation
@@ -172,6 +179,72 @@ def generate_scene_config(manifest: ManifestRequest) -> list:
172
  config.append(scene_cfg)
173
 
174
  return config
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  # ─────────────────────────────────────────────────────────────────────────
176
  # Endpoints
177
  # ─────────────────────────────────────────────────────────────────────────
@@ -666,25 +739,55 @@ async def generate_from_prompt(request: PromptRequest):
666
  print(f"[OK] Video composed ({compose_result['size_mb']:.1f}MB)")
667
 
668
  # ─────────────────────────────────────────────────────────────────
669
- # Step 5: Return Video File
670
  # ─────────────────────────────────────────────────────────────────
671
  output_file = Path(compose_result["output_path"])
672
  if not output_file.exists():
673
  raise Exception("Output video file not found")
674
 
 
 
 
 
 
 
 
 
 
675
  print(f"[SUCCESS] Video ready: {output_file.name}")
676
 
677
- return FileResponse(
 
 
 
 
 
678
  path=output_file,
679
  media_type="video/mp4",
680
- filename="video.mp4"
 
 
 
 
 
681
  )
 
 
682
 
683
  except Exception as e:
684
  print(f"[ERROR] {str(e)}")
685
  raise HTTPException(status_code=500, detail=str(e))
686
 
687
 
 
 
 
 
 
 
 
 
 
688
  @app.get("/health")
689
  async def health_check():
690
  """Health check endpoint"""
 
5
 
6
  from fastapi import FastAPI, HTTPException, File, UploadFile, Form
7
  from fastapi.responses import FileResponse
8
+ from fastapi.staticfiles import StaticFiles
9
  from pydantic import BaseModel
10
  from typing import List, Dict, Any, Optional
11
  import os
 
17
  import requests
18
  from PIL import Image
19
  from io import BytesIO
20
+ from huggingface_hub import HfApi, login
21
 
22
  # ─────────────────────────────────────────────────────────────────────────
23
  # Pydantic Models
 
70
  SELECTED_DIR.mkdir(exist_ok=True)
71
  RENDERS_DIR.mkdir(exist_ok=True)
72
 
73
+ # Mount static files (UI)
74
+ STATIC_DIR = BASE_DIR / "static"
75
+ if STATIC_DIR.exists():
76
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
77
+
78
 
79
  # ─────────────────────────────────────────────────────────────────────────
80
  # Scene Configuration Generation
 
179
  config.append(scene_cfg)
180
 
181
  return config
182
+
183
+
184
+ # ─────────────────────────────────────────────────────────────────────────
185
+ # Upload to HuggingFace Dataset
186
+ # ─────────────────────────────────────────────────────────────────────────
187
+
188
+ async def upload_video_to_hf(video_path: Path, video_name: str) -> dict:
189
+ """
190
+ Upload generated video to HuggingFace dataset.
191
+
192
+ Args:
193
+ video_path: Path to the MP4 file
194
+ video_name: Name for the video in the dataset
195
+
196
+ Returns:
197
+ dict with status and message
198
+ """
199
+ try:
200
+ # Get HF token from environment
201
+ hf_token = os.getenv("HF_TOKEN", "")
202
+ if not hf_token:
203
+ return {
204
+ "status": "warning",
205
+ "message": "HF_TOKEN not set, skipping upload",
206
+ "uploaded": False
207
+ }
208
+
209
+ # Initialize HF API
210
+ api = HfApi(token=hf_token)
211
+
212
+ # Upload file to dataset
213
+ repo_id = "factorstudios/AA"
214
+
215
+ # Create a unique filename with timestamp
216
+ timestamp = __import__('time').strftime("%Y%m%d_%H%M%S")
217
+ filename = f"{timestamp}_{video_name}.mp4"
218
+
219
+ # Upload to dataset
220
+ print(f"[HF UPLOAD] Uploading {video_name} to {repo_id}...")
221
+
222
+ api.upload_file(
223
+ path_or_fileobj=str(video_path),
224
+ path_in_repo=filename,
225
+ repo_id=repo_id,
226
+ repo_type="dataset",
227
+ commit_message=f"Add generated video: {video_name}"
228
+ )
229
+
230
+ print(f"[HF UPLOAD] Successfully uploaded to {repo_id}/{filename}")
231
+
232
+ return {
233
+ "status": "success",
234
+ "message": f"Video uploaded to HuggingFace dataset: {repo_id}/{filename}",
235
+ "uploaded": True,
236
+ "dataset_path": f"{repo_id}/{filename}"
237
+ }
238
+
239
+ except Exception as e:
240
+ print(f"[HF UPLOAD ERROR] {str(e)}")
241
+ return {
242
+ "status": "error",
243
+ "message": f"Failed to upload to HuggingFace: {str(e)}",
244
+ "uploaded": False
245
+ }
246
+
247
+
248
  # ─────────────────────────────────────────────────────────────────────────
249
  # Endpoints
250
  # ─────────────────────────────────────────────────────────────────────────
 
739
  print(f"[OK] Video composed ({compose_result['size_mb']:.1f}MB)")
740
 
741
  # ─────────────────────────────────────────────────────────────────
742
+ # Step 5: Upload to HuggingFace Dataset
743
  # ─────────────────────────────────────────────────────────────────
744
  output_file = Path(compose_result["output_path"])
745
  if not output_file.exists():
746
  raise Exception("Output video file not found")
747
 
748
+ print("[STEP 5] Uploading to HuggingFace...")
749
+ upload_result = await upload_video_to_hf(
750
+ output_file,
751
+ f"trendclip_{manifest_req.title[:30]}"
752
+ )
753
+
754
+ # ─────────────────────────────────────────────────────────────────
755
+ # Step 6: Return Video File with Upload Status
756
+ # ─────────────────────────────────────────────────────────────────
757
  print(f"[SUCCESS] Video ready: {output_file.name}")
758
 
759
+ # Read video file
760
+ with open(output_file, "rb") as f:
761
+ video_data = f.read()
762
+
763
+ # Return as FileResponse with headers for upload status
764
+ response = FileResponse(
765
  path=output_file,
766
  media_type="video/mp4",
767
+ filename="video.mp4",
768
+ headers={
769
+ "X-Upload-Status": upload_result.get("status", "unknown"),
770
+ "X-Upload-Message": upload_result.get("message", ""),
771
+ "X-Dataset-Path": upload_result.get("dataset_path", "")
772
+ }
773
  )
774
+
775
+ return response
776
 
777
  except Exception as e:
778
  print(f"[ERROR] {str(e)}")
779
  raise HTTPException(status_code=500, detail=str(e))
780
 
781
 
782
+ @app.get("/")
783
+ async def root():
784
+ """Serve the UI"""
785
+ index_path = BASE_DIR / "static" / "index.html"
786
+ if index_path.exists():
787
+ return FileResponse(index_path, media_type="text/html")
788
+ return {"message": "TrendClip Video Composer API"}
789
+
790
+
791
  @app.get("/health")
792
  async def health_check():
793
  """Health check endpoint"""
static/index.html ADDED
@@ -0,0 +1,525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>TrendClip - Video Composer</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ padding: 20px;
22
+ }
23
+
24
+ .container {
25
+ width: 100%;
26
+ max-width: 900px;
27
+ background: white;
28
+ border-radius: 16px;
29
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
30
+ overflow: hidden;
31
+ }
32
+
33
+ .header {
34
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
35
+ color: white;
36
+ padding: 40px;
37
+ text-align: center;
38
+ }
39
+
40
+ .header h1 {
41
+ font-size: 2.5em;
42
+ margin-bottom: 10px;
43
+ font-weight: 700;
44
+ }
45
+
46
+ .header p {
47
+ font-size: 1.1em;
48
+ opacity: 0.9;
49
+ }
50
+
51
+ .content {
52
+ padding: 40px;
53
+ }
54
+
55
+ .form-group {
56
+ margin-bottom: 30px;
57
+ }
58
+
59
+ .form-group label {
60
+ display: block;
61
+ margin-bottom: 12px;
62
+ font-weight: 600;
63
+ color: #333;
64
+ font-size: 1.1em;
65
+ }
66
+
67
+ .form-group input,
68
+ .form-group textarea {
69
+ width: 100%;
70
+ padding: 14px 16px;
71
+ border: 2px solid #e0e0e0;
72
+ border-radius: 8px;
73
+ font-size: 1em;
74
+ font-family: inherit;
75
+ transition: all 0.3s ease;
76
+ }
77
+
78
+ .form-group textarea {
79
+ min-height: 120px;
80
+ resize: vertical;
81
+ }
82
+
83
+ .form-group input:focus,
84
+ .form-group textarea:focus {
85
+ outline: none;
86
+ border-color: #667eea;
87
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
88
+ }
89
+
90
+ .button-group {
91
+ display: flex;
92
+ gap: 12px;
93
+ margin-bottom: 30px;
94
+ }
95
+
96
+ button {
97
+ flex: 1;
98
+ padding: 14px 24px;
99
+ border: none;
100
+ border-radius: 8px;
101
+ font-size: 1em;
102
+ font-weight: 600;
103
+ cursor: pointer;
104
+ transition: all 0.3s ease;
105
+ }
106
+
107
+ .btn-generate {
108
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
109
+ color: white;
110
+ }
111
+
112
+ .btn-generate:hover:not(:disabled) {
113
+ transform: translateY(-2px);
114
+ box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
115
+ }
116
+
117
+ .btn-generate:disabled {
118
+ opacity: 0.6;
119
+ cursor: not-allowed;
120
+ }
121
+
122
+ .btn-download {
123
+ background: #4caf50;
124
+ color: white;
125
+ display: none;
126
+ }
127
+
128
+ .btn-download:hover:not(:disabled) {
129
+ background: #45a049;
130
+ transform: translateY(-2px);
131
+ }
132
+
133
+ .btn-download:disabled {
134
+ opacity: 0.6;
135
+ cursor: not-allowed;
136
+ }
137
+
138
+ .status {
139
+ padding: 16px;
140
+ border-radius: 8px;
141
+ margin-bottom: 20px;
142
+ display: none;
143
+ font-weight: 500;
144
+ }
145
+
146
+ .status.loading {
147
+ background: #e3f2fd;
148
+ color: #1976d2;
149
+ display: block;
150
+ }
151
+
152
+ .status.success {
153
+ background: #e8f5e9;
154
+ color: #388e3c;
155
+ display: block;
156
+ }
157
+
158
+ .status.error {
159
+ background: #ffebee;
160
+ color: #c62828;
161
+ display: block;
162
+ }
163
+
164
+ .progress-bar {
165
+ width: 100%;
166
+ height: 4px;
167
+ background: #e0e0e0;
168
+ border-radius: 2px;
169
+ overflow: hidden;
170
+ margin-top: 8px;
171
+ }
172
+
173
+ .progress-fill {
174
+ height: 100%;
175
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
176
+ width: 0%;
177
+ animation: loading 2s infinite;
178
+ }
179
+
180
+ @keyframes loading {
181
+ 0% { width: 0%; }
182
+ 50% { width: 70%; }
183
+ 100% { width: 100%; }
184
+ }
185
+
186
+ .video-container {
187
+ display: none;
188
+ margin: 30px 0;
189
+ }
190
+
191
+ .video-container.show {
192
+ display: block;
193
+ }
194
+
195
+ .video-wrapper {
196
+ position: relative;
197
+ width: 100%;
198
+ background: #000;
199
+ border-radius: 8px;
200
+ overflow: hidden;
201
+ margin-bottom: 20px;
202
+ }
203
+
204
+ video {
205
+ width: 100%;
206
+ height: auto;
207
+ display: block;
208
+ }
209
+
210
+ .video-info {
211
+ background: #f5f5f5;
212
+ padding: 16px;
213
+ border-radius: 8px;
214
+ margin-bottom: 16px;
215
+ }
216
+
217
+ .video-info p {
218
+ margin: 8px 0;
219
+ color: #666;
220
+ font-size: 0.95em;
221
+ }
222
+
223
+ .video-info strong {
224
+ color: #333;
225
+ }
226
+
227
+ .spinner {
228
+ display: inline-block;
229
+ width: 20px;
230
+ height: 20px;
231
+ border: 3px solid rgba(102, 126, 234, 0.3);
232
+ border-top-color: #667eea;
233
+ border-radius: 50%;
234
+ animation: spin 0.8s linear infinite;
235
+ vertical-align: middle;
236
+ margin-right: 8px;
237
+ }
238
+
239
+ @keyframes spin {
240
+ to { transform: rotate(360deg); }
241
+ }
242
+
243
+ .steps {
244
+ display: none;
245
+ margin: 20px 0;
246
+ padding: 20px;
247
+ background: #f9f9f9;
248
+ border-radius: 8px;
249
+ border-left: 4px solid #667eea;
250
+ }
251
+
252
+ .steps.show {
253
+ display: block;
254
+ }
255
+
256
+ .step {
257
+ display: flex;
258
+ align-items: center;
259
+ margin: 10px 0;
260
+ font-size: 0.9em;
261
+ }
262
+
263
+ .step-number {
264
+ display: inline-block;
265
+ width: 24px;
266
+ height: 24px;
267
+ background: #667eea;
268
+ color: white;
269
+ border-radius: 50%;
270
+ text-align: center;
271
+ line-height: 24px;
272
+ margin-right: 12px;
273
+ flex-shrink: 0;
274
+ font-weight: 600;
275
+ }
276
+
277
+ .step.completed .step-number {
278
+ background: #4caf50;
279
+ }
280
+
281
+ .step-text {
282
+ color: #666;
283
+ }
284
+
285
+ .step.completed .step-text {
286
+ color: #333;
287
+ font-weight: 500;
288
+ }
289
+
290
+ @media (max-width: 600px) {
291
+ .header h1 {
292
+ font-size: 1.8em;
293
+ }
294
+
295
+ .content {
296
+ padding: 20px;
297
+ }
298
+
299
+ button {
300
+ padding: 12px 20px;
301
+ }
302
+ }
303
+ </style>
304
+ </head>
305
+ <body>
306
+ <div class="container">
307
+ <div class="header">
308
+ <h1>🎬 TrendClip</h1>
309
+ <p>AI-Powered Video Composer</p>
310
+ </div>
311
+
312
+ <div class="content">
313
+ <form id="promptForm">
314
+ <div class="form-group">
315
+ <label for="prompt">📝 Describe Your Video</label>
316
+ <textarea
317
+ id="prompt"
318
+ placeholder="e.g., signs you're highly ambitious, morning routine aesthetic, productivity tips for entrepreneurs..."
319
+ required
320
+ ></textarea>
321
+ </div>
322
+
323
+ <div class="button-group">
324
+ <button type="submit" class="btn-generate" id="generateBtn">
325
+ Generate Video
326
+ </button>
327
+ <button type="button" class="btn-download" id="downloadBtn" style="display: none;">
328
+ ⬇️ Download Video
329
+ </button>
330
+ </div>
331
+ </form>
332
+
333
+ <div class="status loading" id="status"></div>
334
+
335
+ <div class="steps" id="steps">
336
+ <div class="step" id="step1">
337
+ <span class="step-number">1</span>
338
+ <span class="step-text">Generating manifest...</span>
339
+ </div>
340
+ <div class="step" id="step2">
341
+ <span class="step-number">2</span>
342
+ <span class="step-text">Downloading images...</span>
343
+ </div>
344
+ <div class="step" id="step3">
345
+ <span class="step-number">3</span>
346
+ <span class="step-text">Selecting best images...</span>
347
+ </div>
348
+ <div class="step" id="step4">
349
+ <span class="step-number">4</span>
350
+ <span class="step-text">Composing video...</span>
351
+ </div>
352
+ <div class="step" id="step5">
353
+ <span class="step-number">5</span>
354
+ <span class="step-text">Uploading to dataset...</span>
355
+ </div>
356
+ </div>
357
+
358
+ <div class="video-container" id="videoContainer">
359
+ <div class="video-wrapper">
360
+ <video id="videoPlayer" controls></video>
361
+ </div>
362
+ <div class="video-info" id="videoInfo">
363
+ <p><strong>Status:</strong> <span id="videoStatus">Ready</span></p>
364
+ <p><strong>Size:</strong> <span id="videoSize">-</span></p>
365
+ <p><strong>Duration:</strong> <span id="videoDuration">-</span></p>
366
+ <div id="uploadStatus" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid #ddd;"></div>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ </div>
371
+
372
+ <script>
373
+ const promptForm = document.getElementById('promptForm');
374
+ const promptInput = document.getElementById('prompt');
375
+ const generateBtn = document.getElementById('generateBtn');
376
+ const downloadBtn = document.getElementById('downloadBtn');
377
+ const statusDiv = document.getElementById('status');
378
+ const stepsDiv = document.getElementById('steps');
379
+ const videoContainer = document.getElementById('videoContainer');
380
+ const videoPlayer = document.getElementById('videoPlayer');
381
+ const videoStatus = document.getElementById('videoStatus');
382
+ const videoSize = document.getElementById('videoSize');
383
+ const videoDuration = document.getElementById('videoDuration');
384
+
385
+ let videoBlob = null;
386
+
387
+ promptForm.addEventListener('submit', async (e) => {
388
+ e.preventDefault();
389
+ await generateVideo();
390
+ });
391
+
392
+ async function generateVideo() {
393
+ const prompt = promptInput.value.trim();
394
+ if (!prompt) {
395
+ showStatus('Please enter a prompt', 'error');
396
+ return;
397
+ }
398
+
399
+ // Reset UI
400
+ videoContainer.classList.remove('show');
401
+ downloadBtn.style.display = 'none';
402
+ generateBtn.disabled = true;
403
+ stepsDiv.classList.add('show');
404
+ updateAllSteps('');
405
+
406
+ showStatus(
407
+ `<span class="spinner"></span>Generating your video...`,
408
+ 'loading'
409
+ );
410
+
411
+ try {
412
+ const response = await fetch('/generate-from-prompt', {
413
+ method: 'POST',
414
+ headers: {
415
+ 'Content-Type': 'application/json',
416
+ },
417
+ body: JSON.stringify({ prompt })
418
+ });
419
+
420
+ if (!response.ok) {
421
+ const error = await response.json();
422
+ throw new Error(error.detail || 'Video generation failed');
423
+ }
424
+
425
+ // Mark steps as completed
426
+ updateSteps([1, 2, 3, 4, 5]);
427
+
428
+ // Get video blob
429
+ videoBlob = await response.blob();
430
+
431
+ // Read upload status from response headers
432
+ const uploadStatus = response.headers.get('X-Upload-Status') || 'unknown';
433
+ const uploadMessage = response.headers.get('X-Upload-Message') || '';
434
+ const datasetPath = response.headers.get('X-Dataset-Path') || '';
435
+
436
+ // Display video
437
+ const videoUrl = URL.createObjectURL(videoBlob);
438
+ videoPlayer.src = videoUrl;
439
+ videoContainer.classList.add('show');
440
+ downloadBtn.style.display = 'block';
441
+
442
+ // Update video info
443
+ const sizeMB = (videoBlob.size / (1024 * 1024)).toFixed(2);
444
+ videoSize.textContent = `${sizeMB} MB`;
445
+ videoStatus.textContent = 'Ready to download';
446
+
447
+ // Display upload status
448
+ const uploadStatusDiv = document.getElementById('uploadStatus');
449
+ if (uploadStatus === 'success') {
450
+ uploadStatusDiv.innerHTML = `
451
+ <p style="color: #388e3c; font-weight: 500;">
452
+ ✅ Uploaded to Dataset Successfully!
453
+ </p>
454
+ <p style="color: #666; font-size: 0.9em;">
455
+ 📍 ${datasetPath}
456
+ </p>
457
+ `;
458
+ } else if (uploadStatus === 'warning') {
459
+ uploadStatusDiv.innerHTML = `
460
+ <p style="color: #f57f17; font-weight: 500;">
461
+ ⚠️ ${uploadMessage}
462
+ </p>
463
+ `;
464
+ } else if (uploadStatus === 'error') {
465
+ uploadStatusDiv.innerHTML = `
466
+ <p style="color: #c62828; font-weight: 500;">
467
+ ❌ Upload Error
468
+ </p>
469
+ <p style="color: #666; font-size: 0.9em;">
470
+ ${uploadMessage}
471
+ </p>
472
+ `;
473
+ }
474
+
475
+ showStatus('✅ Video generated successfully!', 'success');
476
+
477
+ } catch (error) {
478
+ showStatus(`❌ Error: ${error.message}`, 'error');
479
+ updateAllSteps('');
480
+ } finally {
481
+ generateBtn.disabled = false;
482
+ }
483
+ }
484
+
485
+ function showStatus(message, type) {
486
+ statusDiv.innerHTML = message;
487
+ statusDiv.className = `status ${type}`;
488
+ }
489
+
490
+ function updateSteps(completedSteps) {
491
+ for (let i = 1; i <= 5; i++) {
492
+ const step = document.getElementById(`step${i}`);
493
+ if (completedSteps.includes(i)) {
494
+ step.classList.add('completed');
495
+ } else {
496
+ step.classList.remove('completed');
497
+ }
498
+ }
499
+ }
500
+
501
+ function updateAllSteps(type) {
502
+ for (let i = 1; i <= 4; i++) {
503
+ const step = document.getElementById(`step${i}`);
504
+ step.classList.remove('completed');
505
+ }
506
+ }
507
+
508
+ downloadBtn.addEventListener('click', () => {
509
+ if (!videoBlob) return;
510
+
511
+ const url = URL.createObjectURL(videoBlob);
512
+ const a = document.createElement('a');
513
+ a.href = url;
514
+ a.download = `trendclip_${Date.now()}.mp4`;
515
+ document.body.appendChild(a);
516
+ a.click();
517
+ document.body.removeChild(a);
518
+ URL.revokeObjectURL(url);
519
+ });
520
+
521
+ // Set initial focus
522
+ promptInput.focus();
523
+ </script>
524
+ </body>
525
+ </html>