EmmyHenz001 commited on
Commit
d592711
Β·
verified Β·
1 Parent(s): dfdef09

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +844 -0
app.py ADDED
@@ -0,0 +1,844 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_file, render_template_string
2
+ from flask_cors import CORS
3
+ from huggingface_hub import InferenceClient
4
+ import tempfile
5
+ import os
6
+ import base64
7
+ from io import BytesIO
8
+ from PIL import Image
9
+ import uuid
10
+ from pathlib import Path
11
+
12
+ app = Flask(__name__)
13
+ CORS(app)
14
+
15
+ # Configuration
16
+ HF_TOKEN = os.environ.get("HF_TOKEN", "your_huggingface_token_here")
17
+ TEMP_DIR = Path(tempfile.gettempdir()) / "veo_videos"
18
+ TEMP_DIR.mkdir(exist_ok=True)
19
+
20
+ # Initialize the client
21
+ client = InferenceClient(
22
+ provider="fal-ai",
23
+ api_key=HF_TOKEN,
24
+ bill_to="huggingface",
25
+ )
26
+
27
+ def cleanup_old_files():
28
+ """Clean up files older than 1 hour"""
29
+ import time
30
+ current_time = time.time()
31
+ for file_path in TEMP_DIR.glob("*.mp4"):
32
+ if current_time - file_path.stat().st_mtime > 3600:
33
+ try:
34
+ file_path.unlink()
35
+ except:
36
+ pass
37
+
38
+ # HTML Template for the website
39
+ HTML_TEMPLATE = """
40
+ <!DOCTYPE html>
41
+ <html lang="en">
42
+ <head>
43
+ <meta charset="UTF-8">
44
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
45
+ <title>Veo 3.1 Video Generator</title>
46
+ <style>
47
+ * {
48
+ margin: 0;
49
+ padding: 0;
50
+ box-sizing: border-box;
51
+ }
52
+
53
+ body {
54
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
55
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
56
+ min-height: 100vh;
57
+ padding: 20px;
58
+ }
59
+
60
+ .container {
61
+ max-width: 1200px;
62
+ margin: 0 auto;
63
+ background: white;
64
+ border-radius: 20px;
65
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
66
+ overflow: hidden;
67
+ }
68
+
69
+ .header {
70
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
71
+ color: white;
72
+ padding: 40px;
73
+ text-align: center;
74
+ }
75
+
76
+ .header h1 {
77
+ font-size: 2.5em;
78
+ margin-bottom: 10px;
79
+ }
80
+
81
+ .header p {
82
+ font-size: 1.1em;
83
+ opacity: 0.9;
84
+ }
85
+
86
+ .tabs {
87
+ display: flex;
88
+ background: #f5f5f5;
89
+ border-bottom: 2px solid #ddd;
90
+ }
91
+
92
+ .tab {
93
+ flex: 1;
94
+ padding: 20px;
95
+ text-align: center;
96
+ cursor: pointer;
97
+ font-size: 1.1em;
98
+ font-weight: 600;
99
+ transition: all 0.3s;
100
+ border-bottom: 3px solid transparent;
101
+ }
102
+
103
+ .tab:hover {
104
+ background: #e0e0e0;
105
+ }
106
+
107
+ .tab.active {
108
+ background: white;
109
+ color: #667eea;
110
+ border-bottom: 3px solid #667eea;
111
+ }
112
+
113
+ .tab-content {
114
+ display: none;
115
+ padding: 40px;
116
+ }
117
+
118
+ .tab-content.active {
119
+ display: block;
120
+ }
121
+
122
+ .form-group {
123
+ margin-bottom: 25px;
124
+ }
125
+
126
+ label {
127
+ display: block;
128
+ margin-bottom: 8px;
129
+ font-weight: 600;
130
+ color: #333;
131
+ font-size: 1em;
132
+ }
133
+
134
+ input[type="text"],
135
+ textarea {
136
+ width: 100%;
137
+ padding: 15px;
138
+ border: 2px solid #ddd;
139
+ border-radius: 10px;
140
+ font-size: 1em;
141
+ transition: border 0.3s;
142
+ font-family: inherit;
143
+ }
144
+
145
+ input[type="text"]:focus,
146
+ textarea:focus {
147
+ outline: none;
148
+ border-color: #667eea;
149
+ }
150
+
151
+ textarea {
152
+ resize: vertical;
153
+ min-height: 120px;
154
+ }
155
+
156
+ .file-upload {
157
+ position: relative;
158
+ display: inline-block;
159
+ width: 100%;
160
+ }
161
+
162
+ .file-upload input[type="file"] {
163
+ display: none;
164
+ }
165
+
166
+ .file-upload-btn {
167
+ display: block;
168
+ padding: 15px;
169
+ background: #f5f5f5;
170
+ border: 2px dashed #ddd;
171
+ border-radius: 10px;
172
+ cursor: pointer;
173
+ text-align: center;
174
+ transition: all 0.3s;
175
+ }
176
+
177
+ .file-upload-btn:hover {
178
+ background: #e0e0e0;
179
+ border-color: #667eea;
180
+ }
181
+
182
+ .image-preview {
183
+ margin-top: 15px;
184
+ max-width: 100%;
185
+ border-radius: 10px;
186
+ display: none;
187
+ }
188
+
189
+ .image-preview img {
190
+ max-width: 100%;
191
+ border-radius: 10px;
192
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
193
+ }
194
+
195
+ button {
196
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
197
+ color: white;
198
+ padding: 15px 40px;
199
+ border: none;
200
+ border-radius: 10px;
201
+ font-size: 1.1em;
202
+ font-weight: 600;
203
+ cursor: pointer;
204
+ transition: transform 0.2s, box-shadow 0.2s;
205
+ width: 100%;
206
+ }
207
+
208
+ button:hover {
209
+ transform: translateY(-2px);
210
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
211
+ }
212
+
213
+ button:active {
214
+ transform: translateY(0);
215
+ }
216
+
217
+ button:disabled {
218
+ opacity: 0.6;
219
+ cursor: not-allowed;
220
+ transform: none;
221
+ }
222
+
223
+ .loading {
224
+ display: none;
225
+ text-align: center;
226
+ padding: 30px;
227
+ }
228
+
229
+ .loading.active {
230
+ display: block;
231
+ }
232
+
233
+ .spinner {
234
+ border: 4px solid #f3f3f3;
235
+ border-top: 4px solid #667eea;
236
+ border-radius: 50%;
237
+ width: 50px;
238
+ height: 50px;
239
+ animation: spin 1s linear infinite;
240
+ margin: 0 auto 20px;
241
+ }
242
+
243
+ @keyframes spin {
244
+ 0% { transform: rotate(0deg); }
245
+ 100% { transform: rotate(360deg); }
246
+ }
247
+
248
+ .result {
249
+ display: none;
250
+ margin-top: 30px;
251
+ padding: 20px;
252
+ background: #f9f9f9;
253
+ border-radius: 10px;
254
+ }
255
+
256
+ .result.active {
257
+ display: block;
258
+ }
259
+
260
+ .result video {
261
+ width: 100%;
262
+ border-radius: 10px;
263
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
264
+ }
265
+
266
+ .download-btn {
267
+ margin-top: 15px;
268
+ background: #4CAF50;
269
+ }
270
+
271
+ .error {
272
+ background: #f44336;
273
+ color: white;
274
+ padding: 15px;
275
+ border-radius: 10px;
276
+ margin-top: 20px;
277
+ display: none;
278
+ }
279
+
280
+ .error.active {
281
+ display: block;
282
+ }
283
+
284
+ .api-docs {
285
+ padding: 40px;
286
+ background: #f9f9f9;
287
+ }
288
+
289
+ .api-docs h3 {
290
+ color: #667eea;
291
+ margin-bottom: 15px;
292
+ }
293
+
294
+ .code-block {
295
+ background: #2d2d2d;
296
+ color: #f8f8f2;
297
+ padding: 20px;
298
+ border-radius: 10px;
299
+ overflow-x: auto;
300
+ margin: 15px 0;
301
+ font-family: 'Courier New', monospace;
302
+ }
303
+
304
+ .examples {
305
+ margin-top: 30px;
306
+ display: grid;
307
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
308
+ gap: 15px;
309
+ }
310
+
311
+ .example-card {
312
+ background: white;
313
+ padding: 15px;
314
+ border-radius: 10px;
315
+ cursor: pointer;
316
+ transition: transform 0.2s;
317
+ border: 2px solid #ddd;
318
+ }
319
+
320
+ .example-card:hover {
321
+ transform: translateY(-3px);
322
+ border-color: #667eea;
323
+ }
324
+ </style>
325
+ </head>
326
+ <body>
327
+ <div class="container">
328
+ <div class="header">
329
+ <h1>🎬 AI Video Generator</h1>
330
+ <p>Powered by Veo 3.1 Fast Model</p>
331
+ </div>
332
+
333
+ <div class="tabs">
334
+ <div class="tab active" onclick="switchTab('text-to-video')">
335
+ πŸ“ Text to Video
336
+ </div>
337
+ <div class="tab" onclick="switchTab('image-to-video')">
338
+ πŸ–ΌοΈ Image to Video
339
+ </div>
340
+ <div class="tab" onclick="switchTab('api-docs')">
341
+ πŸ“š API Docs
342
+ </div>
343
+ </div>
344
+
345
+ <!-- Text to Video Tab -->
346
+ <div id="text-to-video" class="tab-content active">
347
+ <h2>Generate Video from Text</h2>
348
+ <form id="text-form" onsubmit="generateTextVideo(event)">
349
+ <div class="form-group">
350
+ <label for="text-prompt">Enter Your Prompt</label>
351
+ <textarea id="text-prompt" placeholder="Describe the video you want to create... (e.g., 'A young man walking on the street during sunset')" required></textarea>
352
+ </div>
353
+
354
+ <button type="submit" id="text-btn">🎬 Generate Video</button>
355
+ </form>
356
+
357
+ <div class="examples">
358
+ <div class="example-card" onclick="setTextPrompt('A serene beach at sunset with gentle waves')">
359
+ πŸ–οΈ Beach Sunset
360
+ </div>
361
+ <div class="example-card" onclick="setTextPrompt('A bustling city street with neon lights at night')">
362
+ πŸŒƒ City Night
363
+ </div>
364
+ <div class="example-card" onclick="setTextPrompt('A majestic eagle soaring through mountain peaks')">
365
+ πŸ¦… Eagle Flight
366
+ </div>
367
+ <div class="example-card" onclick="setTextPrompt('Cherry blossoms falling in slow motion in a Japanese garden')">
368
+ 🌸 Cherry Blossoms
369
+ </div>
370
+ </div>
371
+
372
+ <div id="text-loading" class="loading">
373
+ <div class="spinner"></div>
374
+ <p>Generating your video... This may take a minute.</p>
375
+ </div>
376
+
377
+ <div id="text-error" class="error"></div>
378
+
379
+ <div id="text-result" class="result">
380
+ <h3>Your Generated Video</h3>
381
+ <video id="text-video" controls autoplay></video>
382
+ <button class="download-btn" onclick="downloadVideo('text')">⬇️ Download Video</button>
383
+ </div>
384
+ </div>
385
+
386
+ <!-- Image to Video Tab -->
387
+ <div id="image-to-video" class="tab-content">
388
+ <h2>Animate Your Image</h2>
389
+ <form id="image-form" onsubmit="generateImageVideo(event)">
390
+ <div class="form-group">
391
+ <label>Upload Image</label>
392
+ <div class="file-upload">
393
+ <input type="file" id="image-input" accept="image/*" onchange="previewImage()" required>
394
+ <label for="image-input" class="file-upload-btn">
395
+ πŸ“ Click to upload image
396
+ </label>
397
+ </div>
398
+ <div id="image-preview" class="image-preview">
399
+ <img id="preview-img" src="" alt="Preview">
400
+ </div>
401
+ </div>
402
+
403
+ <div class="form-group">
404
+ <label for="image-prompt">Motion Prompt</label>
405
+ <textarea id="image-prompt" placeholder="Describe how the image should move... (e.g., 'The cat starts to dance')" required></textarea>
406
+ </div>
407
+
408
+ <button type="submit" id="image-btn">🎬 Animate Image</button>
409
+ </form>
410
+
411
+ <div id="image-loading" class="loading">
412
+ <div class="spinner"></div>
413
+ <p>Animating your image... This may take a minute.</p>
414
+ </div>
415
+
416
+ <div id="image-error" class="error"></div>
417
+
418
+ <div id="image-result" class="result">
419
+ <h3>Your Animated Video</h3>
420
+ <video id="image-video" controls autoplay></video>
421
+ <button class="download-btn" onclick="downloadVideo('image')">⬇️ Download Video</button>
422
+ </div>
423
+ </div>
424
+
425
+ <!-- API Documentation Tab -->
426
+ <div id="api-docs" class="tab-content">
427
+ <div class="api-docs">
428
+ <h2>API Documentation</h2>
429
+ <p>Use these endpoints to integrate video generation into your applications.</p>
430
+
431
+ <h3>1. Text to Video</h3>
432
+ <p><strong>Endpoint:</strong> POST /api/text-to-video</p>
433
+ <div class="code-block">
434
+ curl -X POST http://localhost:5000/api/text-to-video \\
435
+ -H "Content-Type: application/json" \\
436
+ -d '{"prompt": "A young man walking on the street during sunset"}'
437
+ </div>
438
+
439
+ <h3>2. Image to Video</h3>
440
+ <p><strong>Endpoint:</strong> POST /api/image-to-video</p>
441
+ <div class="code-block">
442
+ curl -X POST http://localhost:5000/api/image-to-video \\
443
+ -F "image=@photo.jpg" \\
444
+ -F "prompt=The person starts walking forward"
445
+ </div>
446
+
447
+ <h3>Python Example</h3>
448
+ <div class="code-block">
449
+ import requests
450
+
451
+ # Text to Video
452
+ response = requests.post('http://localhost:5000/api/text-to-video',
453
+ json={'prompt': 'A sunset over the ocean'})
454
+ data = response.json()
455
+ print(data['message'])
456
+
457
+ # Image to Video
458
+ with open('image.jpg', 'rb') as f:
459
+ response = requests.post('http://localhost:5000/api/image-to-video',
460
+ files={'image': f},
461
+ data={'prompt': 'Camera zooms in slowly'})
462
+ print(response.json())
463
+ </div>
464
+
465
+ <h3>Response Format</h3>
466
+ <div class="code-block">
467
+ {
468
+ "success": true,
469
+ "video_id": "uuid-here",
470
+ "video_base64": "base64_encoded_video",
471
+ "prompt": "your prompt",
472
+ "message": "Video generated successfully"
473
+ }
474
+ </div>
475
+ </div>
476
+ </div>
477
+ </div>
478
+
479
+ <script>
480
+ let currentVideoBlob = null;
481
+ let currentVideoType = null;
482
+
483
+ function switchTab(tabName) {
484
+ // Hide all tabs
485
+ document.querySelectorAll('.tab-content').forEach(content => {
486
+ content.classList.remove('active');
487
+ });
488
+ document.querySelectorAll('.tab').forEach(tab => {
489
+ tab.classList.remove('active');
490
+ });
491
+
492
+ // Show selected tab
493
+ document.getElementById(tabName).classList.add('active');
494
+ event.target.classList.add('active');
495
+ }
496
+
497
+ function setTextPrompt(prompt) {
498
+ document.getElementById('text-prompt').value = prompt;
499
+ }
500
+
501
+ function previewImage() {
502
+ const input = document.getElementById('image-input');
503
+ const preview = document.getElementById('image-preview');
504
+ const img = document.getElementById('preview-img');
505
+
506
+ if (input.files && input.files[0]) {
507
+ const reader = new FileReader();
508
+ reader.onload = function(e) {
509
+ img.src = e.target.result;
510
+ preview.style.display = 'block';
511
+ };
512
+ reader.readAsDataURL(input.files[0]);
513
+ }
514
+ }
515
+
516
+ async function generateTextVideo(event) {
517
+ event.preventDefault();
518
+
519
+ const prompt = document.getElementById('text-prompt').value;
520
+ const btn = document.getElementById('text-btn');
521
+ const loading = document.getElementById('text-loading');
522
+ const error = document.getElementById('text-error');
523
+ const result = document.getElementById('text-result');
524
+
525
+ // Reset states
526
+ loading.classList.add('active');
527
+ error.classList.remove('active');
528
+ result.classList.remove('active');
529
+ btn.disabled = true;
530
+
531
+ try {
532
+ const response = await fetch('/api/text-to-video', {
533
+ method: 'POST',
534
+ headers: {
535
+ 'Content-Type': 'application/json'
536
+ },
537
+ body: JSON.stringify({ prompt })
538
+ });
539
+
540
+ const data = await response.json();
541
+
542
+ if (!response.ok) {
543
+ throw new Error(data.error || 'Failed to generate video');
544
+ }
545
+
546
+ // Convert base64 to blob
547
+ const videoData = atob(data.video_base64);
548
+ const videoArray = new Uint8Array(videoData.length);
549
+ for (let i = 0; i < videoData.length; i++) {
550
+ videoArray[i] = videoData.charCodeAt(i);
551
+ }
552
+ const blob = new Blob([videoArray], { type: 'video/mp4' });
553
+ currentVideoBlob = blob;
554
+ currentVideoType = 'text';
555
+
556
+ // Display video
557
+ const videoElement = document.getElementById('text-video');
558
+ videoElement.src = URL.createObjectURL(blob);
559
+
560
+ result.classList.add('active');
561
+
562
+ } catch (err) {
563
+ error.textContent = err.message;
564
+ error.classList.add('active');
565
+ } finally {
566
+ loading.classList.remove('active');
567
+ btn.disabled = false;
568
+ }
569
+ }
570
+
571
+ async function generateImageVideo(event) {
572
+ event.preventDefault();
573
+
574
+ const imageInput = document.getElementById('image-input');
575
+ const prompt = document.getElementById('image-prompt').value;
576
+ const btn = document.getElementById('image-btn');
577
+ const loading = document.getElementById('image-loading');
578
+ const error = document.getElementById('image-error');
579
+ const result = document.getElementById('image-result');
580
+
581
+ // Reset states
582
+ loading.classList.add('active');
583
+ error.classList.remove('active');
584
+ result.classList.remove('active');
585
+ btn.disabled = true;
586
+
587
+ try {
588
+ const formData = new FormData();
589
+ formData.append('image', imageInput.files[0]);
590
+ formData.append('prompt', prompt);
591
+
592
+ const response = await fetch('/api/image-to-video', {
593
+ method: 'POST',
594
+ body: formData
595
+ });
596
+
597
+ const data = await response.json();
598
+
599
+ if (!response.ok) {
600
+ throw new Error(data.error || 'Failed to generate video');
601
+ }
602
+
603
+ // Convert base64 to blob
604
+ const videoData = atob(data.video_base64);
605
+ const videoArray = new Uint8Array(videoData.length);
606
+ for (let i = 0; i < videoData.length; i++) {
607
+ videoArray[i] = videoData.charCodeAt(i);
608
+ }
609
+ const blob = new Blob([videoArray], { type: 'video/mp4' });
610
+ currentVideoBlob = blob;
611
+ currentVideoType = 'image';
612
+
613
+ // Display video
614
+ const videoElement = document.getElementById('image-video');
615
+ videoElement.src = URL.createObjectURL(blob);
616
+
617
+ result.classList.add('active');
618
+
619
+ } catch (err) {
620
+ error.textContent = err.message;
621
+ error.classList.add('active');
622
+ } finally {
623
+ loading.classList.remove('active');
624
+ btn.disabled = false;
625
+ }
626
+ }
627
+
628
+ function downloadVideo(type) {
629
+ if (!currentVideoBlob) return;
630
+
631
+ const url = URL.createObjectURL(currentVideoBlob);
632
+ const a = document.createElement('a');
633
+ a.href = url;
634
+ a.download = `generated_video_${Date.now()}.mp4`;
635
+ document.body.appendChild(a);
636
+ a.click();
637
+ document.body.removeChild(a);
638
+ URL.revokeObjectURL(url);
639
+ }
640
+ </script>
641
+ </body>
642
+ </html>
643
+ """
644
+
645
+ @app.route('/')
646
+ def index():
647
+ """Render the main website"""
648
+ return render_template_string(HTML_TEMPLATE)
649
+
650
+ @app.route('/health', methods=['GET'])
651
+ def health_check():
652
+ """Health check endpoint"""
653
+ return jsonify({
654
+ "status": "healthy",
655
+ "service": "Veo 3.1 Video Generation API",
656
+ "version": "1.0"
657
+ })
658
+
659
+ @app.route('/api/text-to-video', methods=['POST'])
660
+ def text_to_video():
661
+ """Generate video from text prompt"""
662
+ try:
663
+ data = request.get_json()
664
+
665
+ if not data or 'prompt' not in data:
666
+ return jsonify({
667
+ "error": "Missing 'prompt' in request body"
668
+ }), 400
669
+
670
+ prompt = data.get('prompt', '').strip()
671
+
672
+ if not prompt:
673
+ return jsonify({
674
+ "error": "Prompt cannot be empty"
675
+ }), 400
676
+
677
+ print(f"Generating video from prompt: {prompt[:50]}...")
678
+ video_bytes = client.text_to_video(
679
+ prompt,
680
+ model="akhaliq/veo3.1-fast",
681
+ )
682
+
683
+ video_id = str(uuid.uuid4())
684
+ video_path = TEMP_DIR / f"{video_id}.mp4"
685
+
686
+ with open(video_path, "wb") as f:
687
+ f.write(video_bytes)
688
+
689
+ cleanup_old_files()
690
+
691
+ return_type = data.get('return_type', 'base64')
692
+
693
+ if return_type == 'file':
694
+ return send_file(
695
+ video_path,
696
+ mimetype='video/mp4',
697
+ as_attachment=True,
698
+ download_name=f"generated_{video_id}.mp4"
699
+ )
700
+ else:
701
+ video_base64 = base64.b64encode(video_bytes).decode('utf-8')
702
+ return jsonify({
703
+ "success": True,
704
+ "video_id": video_id,
705
+ "video_base64": video_base64,
706
+ "prompt": prompt,
707
+ "message": "Video generated successfully"
708
+ })
709
+
710
+ except Exception as e:
711
+ print(f"Error in text_to_video: {str(e)}")
712
+ return jsonify({
713
+ "error": f"Failed to generate video: {str(e)}"
714
+ }), 500
715
+
716
+ @app.route('/api/image-to-video', methods=['POST'])
717
+ def image_to_video():
718
+ """Generate video from image and motion prompt"""
719
+ try:
720
+ if request.is_json:
721
+ data = request.get_json()
722
+
723
+ if not data or 'image_base64' not in data or 'prompt' not in data:
724
+ return jsonify({
725
+ "error": "Missing 'image_base64' or 'prompt' in request body"
726
+ }), 400
727
+
728
+ image_data = base64.b64decode(data['image_base64'])
729
+ prompt = data.get('prompt', '').strip()
730
+
731
+ else:
732
+ if 'image' not in request.files:
733
+ return jsonify({
734
+ "error": "Missing 'image' file in request"
735
+ }), 400
736
+
737
+ image_file = request.files['image']
738
+ image_data = image_file.read()
739
+ prompt = request.form.get('prompt', '').strip()
740
+
741
+ if not prompt:
742
+ return jsonify({
743
+ "error": "Prompt cannot be empty"
744
+ }), 400
745
+
746
+ try:
747
+ img = Image.open(BytesIO(image_data))
748
+ if img.mode != 'RGB':
749
+ img = img.convert('RGB')
750
+
751
+ img_buffer = BytesIO()
752
+ img.save(img_buffer, format='PNG')
753
+ image_data = img_buffer.getvalue()
754
+ except Exception as e:
755
+ return jsonify({
756
+ "error": f"Invalid image format: {str(e)}"
757
+ }), 400
758
+
759
+ print(f"Generating video from image with prompt: {prompt[:50]}...")
760
+ video_bytes = client.image_to_video(
761
+ image_data,
762
+ prompt=prompt,
763
+ model="akhaliq/veo3.1-fast-image-to-video",
764
+ )
765
+
766
+ video_id = str(uuid.uuid4())
767
+ video_path = TEMP_DIR / f"{video_id}.mp4"
768
+
769
+ with open(video_path, "wb") as f:
770
+ f.write(video_bytes)
771
+
772
+ cleanup_old_files()
773
+
774
+ return_type = request.form.get('return_type') if not request.is_json else request.get_json().get('return_type', 'base64')
775
+
776
+ if return_type == 'file':
777
+ return send_file(
778
+ video_path,
779
+ mimetype='video/mp4',
780
+ as_attachment=True,
781
+ download_name=f"animated_{video_id}.mp4"
782
+ )
783
+ else:
784
+ video_base64 = base64.b64encode(video_bytes).decode('utf-8')
785
+ return jsonify({
786
+ "success": True,
787
+ "video_id": video_id,
788
+ "video_base64": video_base64,
789
+ "prompt": prompt,
790
+ "message": "Video generated successfully"
791
+ })
792
+
793
+ except Exception as e:
794
+ print(f"Error in image_to_video: {str(e)}")
795
+ return jsonify({
796
+ "error": f"Failed to generate video: {str(e)}"
797
+ }), 500
798
+
799
+ @app.route('/api/download/<video_id>', methods=['GET'])
800
+ def download_video(video_id):
801
+ """Download a previously generated video by ID"""
802
+ try:
803
+ video_path = TEMP_DIR / f"{video_id}.mp4"
804
+
805
+ if not video_path.exists():
806
+ return jsonify({
807
+ "error": "Video not found or expired"
808
+ }), 404
809
+
810
+ return send_file(
811
+ video_path,
812
+ mimetype='video/mp4',
813
+ as_attachment=True,
814
+ download_name=f"video_{video_id}.mp4"
815
+ )
816
+
817
+ except Exception as e:
818
+ return jsonify({
819
+ "error": f"Failed to download video: {str(e)}"
820
+ }), 500
821
+
822
+ if __name__ == '__main__':
823
+ print("""
824
+ ╔═══════════════════════════════════════════════════╗
825
+ β•‘ Veo 3.1 Video Generation - Website + API β•‘
826
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
827
+
828
+ πŸ“ Set your HF_TOKEN environment variable:
829
+ export HF_TOKEN=your_huggingface_token_here
830
+
831
+ 🌐 Website: http://localhost:5000
832
+ πŸ”Œ API Endpoints:
833
+ - POST /api/text-to-video
834
+ - POST /api/image-to-video
835
+ - GET /api/download/<video_id>
836
+
837
+ πŸš€ Server starting...
838
+ """)
839
+
840
+ app.run(
841
+ host='0.0.0.0',
842
+ port=7860,
843
+ debug=True
844
+ )