Spaces:
Runtime error
Runtime error
| from flask import Flask, request, jsonify, send_file, render_template_string | |
| from flask_cors import CORS | |
| from huggingface_hub import InferenceClient | |
| import tempfile | |
| import os | |
| import base64 | |
| from io import BytesIO | |
| from PIL import Image | |
| import uuid | |
| from pathlib import Path | |
| app = Flask(__name__) | |
| CORS(app) | |
| # Configuration | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "your_huggingface_token_here") | |
| TEMP_DIR = Path(tempfile.gettempdir()) / "veo_videos" | |
| TEMP_DIR.mkdir(exist_ok=True) | |
| # Initialize the client | |
| client = InferenceClient( | |
| provider="fal-ai", | |
| api_key=HF_TOKEN, | |
| bill_to="huggingface", | |
| ) | |
| def cleanup_old_files(): | |
| """Clean up files older than 1 hour""" | |
| import time | |
| current_time = time.time() | |
| for file_path in TEMP_DIR.glob("*.mp4"): | |
| if current_time - file_path.stat().st_mtime > 3600: | |
| try: | |
| file_path.unlink() | |
| except: | |
| pass | |
| # HTML Template for the website | |
| HTML_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Veo 3.1 Video Generator</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 20px; | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.3); | |
| overflow: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 40px; | |
| text-align: center; | |
| } | |
| .header h1 { | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| } | |
| .header p { | |
| font-size: 1.1em; | |
| opacity: 0.9; | |
| } | |
| .tabs { | |
| display: flex; | |
| background: #f5f5f5; | |
| border-bottom: 2px solid #ddd; | |
| } | |
| .tab { | |
| flex: 1; | |
| padding: 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| font-size: 1.1em; | |
| font-weight: 600; | |
| transition: all 0.3s; | |
| border-bottom: 3px solid transparent; | |
| } | |
| .tab:hover { | |
| background: #e0e0e0; | |
| } | |
| .tab.active { | |
| background: white; | |
| color: #667eea; | |
| border-bottom: 3px solid #667eea; | |
| } | |
| .tab-content { | |
| display: none; | |
| padding: 40px; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| .form-group { | |
| margin-bottom: 25px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 600; | |
| color: #333; | |
| font-size: 1em; | |
| } | |
| input[type="text"], | |
| textarea { | |
| width: 100%; | |
| padding: 15px; | |
| border: 2px solid #ddd; | |
| border-radius: 10px; | |
| font-size: 1em; | |
| transition: border 0.3s; | |
| font-family: inherit; | |
| } | |
| input[type="text"]:focus, | |
| textarea:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| } | |
| textarea { | |
| resize: vertical; | |
| min-height: 120px; | |
| } | |
| .file-upload { | |
| position: relative; | |
| display: inline-block; | |
| width: 100%; | |
| } | |
| .file-upload input[type="file"] { | |
| display: none; | |
| } | |
| .file-upload-btn { | |
| display: block; | |
| padding: 15px; | |
| background: #f5f5f5; | |
| border: 2px dashed #ddd; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| text-align: center; | |
| transition: all 0.3s; | |
| } | |
| .file-upload-btn:hover { | |
| background: #e0e0e0; | |
| border-color: #667eea; | |
| } | |
| .image-preview { | |
| margin-top: 15px; | |
| max-width: 100%; | |
| border-radius: 10px; | |
| display: none; | |
| } | |
| .image-preview img { | |
| max-width: 100%; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
| } | |
| button { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 15px 40px; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 1.1em; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| width: 100%; | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| button:active { | |
| transform: translateY(0); | |
| } | |
| button:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .loading { | |
| display: none; | |
| text-align: center; | |
| padding: 30px; | |
| } | |
| .loading.active { | |
| display: block; | |
| } | |
| .spinner { | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #667eea; | |
| border-radius: 50%; | |
| width: 50px; | |
| height: 50px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 20px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .result { | |
| display: none; | |
| margin-top: 30px; | |
| padding: 20px; | |
| background: #f9f9f9; | |
| border-radius: 10px; | |
| } | |
| .result.active { | |
| display: block; | |
| } | |
| .result video { | |
| width: 100%; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
| } | |
| .download-btn { | |
| margin-top: 15px; | |
| background: #4CAF50; | |
| } | |
| .error { | |
| background: #f44336; | |
| color: white; | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin-top: 20px; | |
| display: none; | |
| } | |
| .error.active { | |
| display: block; | |
| } | |
| .api-docs { | |
| padding: 40px; | |
| background: #f9f9f9; | |
| } | |
| .api-docs h3 { | |
| color: #667eea; | |
| margin-bottom: 15px; | |
| } | |
| .code-block { | |
| background: #2d2d2d; | |
| color: #f8f8f2; | |
| padding: 20px; | |
| border-radius: 10px; | |
| overflow-x: auto; | |
| margin: 15px 0; | |
| font-family: 'Courier New', monospace; | |
| } | |
| .examples { | |
| margin-top: 30px; | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 15px; | |
| } | |
| .example-card { | |
| background: white; | |
| padding: 15px; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: transform 0.2s; | |
| border: 2px solid #ddd; | |
| } | |
| .example-card:hover { | |
| transform: translateY(-3px); | |
| border-color: #667eea; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>π¬ AI Video Generator</h1> | |
| <p>Powered by Veo 3.1 Fast Model</p> | |
| </div> | |
| <div class="tabs"> | |
| <div class="tab active" onclick="switchTab('text-to-video')"> | |
| π Text to Video | |
| </div> | |
| <div class="tab" onclick="switchTab('image-to-video')"> | |
| πΌοΈ Image to Video | |
| </div> | |
| <div class="tab" onclick="switchTab('api-docs')"> | |
| π API Docs | |
| </div> | |
| </div> | |
| <!-- Text to Video Tab --> | |
| <div id="text-to-video" class="tab-content active"> | |
| <h2>Generate Video from Text</h2> | |
| <form id="text-form" onsubmit="generateTextVideo(event)"> | |
| <div class="form-group"> | |
| <label for="text-prompt">Enter Your Prompt</label> | |
| <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> | |
| </div> | |
| <button type="submit" id="text-btn">π¬ Generate Video</button> | |
| </form> | |
| <div class="examples"> | |
| <div class="example-card" onclick="setTextPrompt('A serene beach at sunset with gentle waves')"> | |
| ποΈ Beach Sunset | |
| </div> | |
| <div class="example-card" onclick="setTextPrompt('A bustling city street with neon lights at night')"> | |
| π City Night | |
| </div> | |
| <div class="example-card" onclick="setTextPrompt('A majestic eagle soaring through mountain peaks')"> | |
| π¦ Eagle Flight | |
| </div> | |
| <div class="example-card" onclick="setTextPrompt('Cherry blossoms falling in slow motion in a Japanese garden')"> | |
| πΈ Cherry Blossoms | |
| </div> | |
| </div> | |
| <div id="text-loading" class="loading"> | |
| <div class="spinner"></div> | |
| <p>Generating your video... This may take a minute.</p> | |
| </div> | |
| <div id="text-error" class="error"></div> | |
| <div id="text-result" class="result"> | |
| <h3>Your Generated Video</h3> | |
| <video id="text-video" controls autoplay></video> | |
| <button class="download-btn" onclick="downloadVideo('text')">β¬οΈ Download Video</button> | |
| </div> | |
| </div> | |
| <!-- Image to Video Tab --> | |
| <div id="image-to-video" class="tab-content"> | |
| <h2>Animate Your Image</h2> | |
| <form id="image-form" onsubmit="generateImageVideo(event)"> | |
| <div class="form-group"> | |
| <label>Upload Image</label> | |
| <div class="file-upload"> | |
| <input type="file" id="image-input" accept="image/*" onchange="previewImage()" required> | |
| <label for="image-input" class="file-upload-btn"> | |
| π Click to upload image | |
| </label> | |
| </div> | |
| <div id="image-preview" class="image-preview"> | |
| <img id="preview-img" src="" alt="Preview"> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="image-prompt">Motion Prompt</label> | |
| <textarea id="image-prompt" placeholder="Describe how the image should move... (e.g., 'The cat starts to dance')" required></textarea> | |
| </div> | |
| <button type="submit" id="image-btn">π¬ Animate Image</button> | |
| </form> | |
| <div id="image-loading" class="loading"> | |
| <div class="spinner"></div> | |
| <p>Animating your image... This may take a minute.</p> | |
| </div> | |
| <div id="image-error" class="error"></div> | |
| <div id="image-result" class="result"> | |
| <h3>Your Animated Video</h3> | |
| <video id="image-video" controls autoplay></video> | |
| <button class="download-btn" onclick="downloadVideo('image')">β¬οΈ Download Video</button> | |
| </div> | |
| </div> | |
| <!-- API Documentation Tab --> | |
| <div id="api-docs" class="tab-content"> | |
| <div class="api-docs"> | |
| <h2>API Documentation</h2> | |
| <p>Use these endpoints to integrate video generation into your applications.</p> | |
| <h3>1. Text to Video</h3> | |
| <p><strong>Endpoint:</strong> POST /api/text-to-video</p> | |
| <div class="code-block"> | |
| curl -X POST http://localhost:5000/api/text-to-video \\ | |
| -H "Content-Type: application/json" \\ | |
| -d '{"prompt": "A young man walking on the street during sunset"}' | |
| </div> | |
| <h3>2. Image to Video</h3> | |
| <p><strong>Endpoint:</strong> POST /api/image-to-video</p> | |
| <div class="code-block"> | |
| curl -X POST http://localhost:5000/api/image-to-video \\ | |
| -F "image=@photo.jpg" \\ | |
| -F "prompt=The person starts walking forward" | |
| </div> | |
| <h3>Python Example</h3> | |
| <div class="code-block"> | |
| import requests | |
| # Text to Video | |
| response = requests.post('http://localhost:5000/api/text-to-video', | |
| json={'prompt': 'A sunset over the ocean'}) | |
| data = response.json() | |
| print(data['message']) | |
| # Image to Video | |
| with open('image.jpg', 'rb') as f: | |
| response = requests.post('http://localhost:5000/api/image-to-video', | |
| files={'image': f}, | |
| data={'prompt': 'Camera zooms in slowly'}) | |
| print(response.json()) | |
| </div> | |
| <h3>Response Format</h3> | |
| <div class="code-block"> | |
| { | |
| "success": true, | |
| "video_id": "uuid-here", | |
| "video_base64": "base64_encoded_video", | |
| "prompt": "your prompt", | |
| "message": "Video generated successfully" | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let currentVideoBlob = null; | |
| let currentVideoType = null; | |
| function switchTab(tabName) { | |
| // Hide all tabs | |
| document.querySelectorAll('.tab-content').forEach(content => { | |
| content.classList.remove('active'); | |
| }); | |
| document.querySelectorAll('.tab').forEach(tab => { | |
| tab.classList.remove('active'); | |
| }); | |
| // Show selected tab | |
| document.getElementById(tabName).classList.add('active'); | |
| event.target.classList.add('active'); | |
| } | |
| function setTextPrompt(prompt) { | |
| document.getElementById('text-prompt').value = prompt; | |
| } | |
| function previewImage() { | |
| const input = document.getElementById('image-input'); | |
| const preview = document.getElementById('image-preview'); | |
| const img = document.getElementById('preview-img'); | |
| if (input.files && input.files[0]) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| img.src = e.target.result; | |
| preview.style.display = 'block'; | |
| }; | |
| reader.readAsDataURL(input.files[0]); | |
| } | |
| } | |
| async function generateTextVideo(event) { | |
| event.preventDefault(); | |
| const prompt = document.getElementById('text-prompt').value; | |
| const btn = document.getElementById('text-btn'); | |
| const loading = document.getElementById('text-loading'); | |
| const error = document.getElementById('text-error'); | |
| const result = document.getElementById('text-result'); | |
| // Reset states | |
| loading.classList.add('active'); | |
| error.classList.remove('active'); | |
| result.classList.remove('active'); | |
| btn.disabled = true; | |
| try { | |
| const response = await fetch('/api/text-to-video', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ prompt }) | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| throw new Error(data.error || 'Failed to generate video'); | |
| } | |
| // Convert base64 to blob | |
| const videoData = atob(data.video_base64); | |
| const videoArray = new Uint8Array(videoData.length); | |
| for (let i = 0; i < videoData.length; i++) { | |
| videoArray[i] = videoData.charCodeAt(i); | |
| } | |
| const blob = new Blob([videoArray], { type: 'video/mp4' }); | |
| currentVideoBlob = blob; | |
| currentVideoType = 'text'; | |
| // Display video | |
| const videoElement = document.getElementById('text-video'); | |
| videoElement.src = URL.createObjectURL(blob); | |
| result.classList.add('active'); | |
| } catch (err) { | |
| error.textContent = err.message; | |
| error.classList.add('active'); | |
| } finally { | |
| loading.classList.remove('active'); | |
| btn.disabled = false; | |
| } | |
| } | |
| async function generateImageVideo(event) { | |
| event.preventDefault(); | |
| const imageInput = document.getElementById('image-input'); | |
| const prompt = document.getElementById('image-prompt').value; | |
| const btn = document.getElementById('image-btn'); | |
| const loading = document.getElementById('image-loading'); | |
| const error = document.getElementById('image-error'); | |
| const result = document.getElementById('image-result'); | |
| // Reset states | |
| loading.classList.add('active'); | |
| error.classList.remove('active'); | |
| result.classList.remove('active'); | |
| btn.disabled = true; | |
| try { | |
| const formData = new FormData(); | |
| formData.append('image', imageInput.files[0]); | |
| formData.append('prompt', prompt); | |
| const response = await fetch('/api/image-to-video', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| throw new Error(data.error || 'Failed to generate video'); | |
| } | |
| // Convert base64 to blob | |
| const videoData = atob(data.video_base64); | |
| const videoArray = new Uint8Array(videoData.length); | |
| for (let i = 0; i < videoData.length; i++) { | |
| videoArray[i] = videoData.charCodeAt(i); | |
| } | |
| const blob = new Blob([videoArray], { type: 'video/mp4' }); | |
| currentVideoBlob = blob; | |
| currentVideoType = 'image'; | |
| // Display video | |
| const videoElement = document.getElementById('image-video'); | |
| videoElement.src = URL.createObjectURL(blob); | |
| result.classList.add('active'); | |
| } catch (err) { | |
| error.textContent = err.message; | |
| error.classList.add('active'); | |
| } finally { | |
| loading.classList.remove('active'); | |
| btn.disabled = false; | |
| } | |
| } | |
| function downloadVideo(type) { | |
| if (!currentVideoBlob) return; | |
| const url = URL.createObjectURL(currentVideoBlob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `generated_video_${Date.now()}.mp4`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def index(): | |
| """Render the main website""" | |
| return render_template_string(HTML_TEMPLATE) | |
| def health_check(): | |
| """Health check endpoint""" | |
| return jsonify({ | |
| "status": "healthy", | |
| "service": "Veo 3.1 Video Generation API", | |
| "version": "1.0" | |
| }) | |
| def text_to_video(): | |
| """Generate video from text prompt""" | |
| try: | |
| data = request.get_json() | |
| if not data or 'prompt' not in data: | |
| return jsonify({ | |
| "error": "Missing 'prompt' in request body" | |
| }), 400 | |
| prompt = data.get('prompt', '').strip() | |
| if not prompt: | |
| return jsonify({ | |
| "error": "Prompt cannot be empty" | |
| }), 400 | |
| print(f"Generating video from prompt: {prompt[:50]}...") | |
| video_bytes = client.text_to_video( | |
| prompt, | |
| model="akhaliq/veo3.1-fast", | |
| ) | |
| video_id = str(uuid.uuid4()) | |
| video_path = TEMP_DIR / f"{video_id}.mp4" | |
| with open(video_path, "wb") as f: | |
| f.write(video_bytes) | |
| cleanup_old_files() | |
| return_type = data.get('return_type', 'base64') | |
| if return_type == 'file': | |
| return send_file( | |
| video_path, | |
| mimetype='video/mp4', | |
| as_attachment=True, | |
| download_name=f"generated_{video_id}.mp4" | |
| ) | |
| else: | |
| video_base64 = base64.b64encode(video_bytes).decode('utf-8') | |
| return jsonify({ | |
| "success": True, | |
| "video_id": video_id, | |
| "video_base64": video_base64, | |
| "prompt": prompt, | |
| "message": "Video generated successfully" | |
| }) | |
| except Exception as e: | |
| print(f"Error in text_to_video: {str(e)}") | |
| return jsonify({ | |
| "error": f"Failed to generate video: {str(e)}" | |
| }), 500 | |
| def image_to_video(): | |
| """Generate video from image and motion prompt""" | |
| try: | |
| if request.is_json: | |
| data = request.get_json() | |
| if not data or 'image_base64' not in data or 'prompt' not in data: | |
| return jsonify({ | |
| "error": "Missing 'image_base64' or 'prompt' in request body" | |
| }), 400 | |
| image_data = base64.b64decode(data['image_base64']) | |
| prompt = data.get('prompt', '').strip() | |
| else: | |
| if 'image' not in request.files: | |
| return jsonify({ | |
| "error": "Missing 'image' file in request" | |
| }), 400 | |
| image_file = request.files['image'] | |
| image_data = image_file.read() | |
| prompt = request.form.get('prompt', '').strip() | |
| if not prompt: | |
| return jsonify({ | |
| "error": "Prompt cannot be empty" | |
| }), 400 | |
| try: | |
| img = Image.open(BytesIO(image_data)) | |
| if img.mode != 'RGB': | |
| img = img.convert('RGB') | |
| img_buffer = BytesIO() | |
| img.save(img_buffer, format='PNG') | |
| image_data = img_buffer.getvalue() | |
| except Exception as e: | |
| return jsonify({ | |
| "error": f"Invalid image format: {str(e)}" | |
| }), 400 | |
| print(f"Generating video from image with prompt: {prompt[:50]}...") | |
| video_bytes = client.image_to_video( | |
| image_data, | |
| prompt=prompt, | |
| model="akhaliq/veo3.1-fast-image-to-video", | |
| ) | |
| video_id = str(uuid.uuid4()) | |
| video_path = TEMP_DIR / f"{video_id}.mp4" | |
| with open(video_path, "wb") as f: | |
| f.write(video_bytes) | |
| cleanup_old_files() | |
| return_type = request.form.get('return_type') if not request.is_json else request.get_json().get('return_type', 'base64') | |
| if return_type == 'file': | |
| return send_file( | |
| video_path, | |
| mimetype='video/mp4', | |
| as_attachment=True, | |
| download_name=f"animated_{video_id}.mp4" | |
| ) | |
| else: | |
| video_base64 = base64.b64encode(video_bytes).decode('utf-8') | |
| return jsonify({ | |
| "success": True, | |
| "video_id": video_id, | |
| "video_base64": video_base64, | |
| "prompt": prompt, | |
| "message": "Video generated successfully" | |
| }) | |
| except Exception as e: | |
| print(f"Error in image_to_video: {str(e)}") | |
| return jsonify({ | |
| "error": f"Failed to generate video: {str(e)}" | |
| }), 500 | |
| def download_video(video_id): | |
| """Download a previously generated video by ID""" | |
| try: | |
| video_path = TEMP_DIR / f"{video_id}.mp4" | |
| if not video_path.exists(): | |
| return jsonify({ | |
| "error": "Video not found or expired" | |
| }), 404 | |
| return send_file( | |
| video_path, | |
| mimetype='video/mp4', | |
| as_attachment=True, | |
| download_name=f"video_{video_id}.mp4" | |
| ) | |
| except Exception as e: | |
| return jsonify({ | |
| "error": f"Failed to download video: {str(e)}" | |
| }), 500 | |
| if __name__ == '__main__': | |
| print(""" | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β Veo 3.1 Video Generation - Website + API β | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| π Set your HF_TOKEN environment variable: | |
| export HF_TOKEN=your_huggingface_token_here | |
| π Website: http://localhost:5000 | |
| π API Endpoints: | |
| - POST /api/text-to-video | |
| - POST /api/image-to-video | |
| - GET /api/download/<video_id> | |
| π Server starting... | |
| """) | |
| app.run( | |
| host='0.0.0.0', | |
| port=7860, | |
| debug=True | |
| ) |