veo3 / app.py
EmmyHenz001's picture
Create app.py
d592711 verified
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>
"""
@app.route('/')
def index():
"""Render the main website"""
return render_template_string(HTML_TEMPLATE)
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
"status": "healthy",
"service": "Veo 3.1 Video Generation API",
"version": "1.0"
})
@app.route('/api/text-to-video', methods=['POST'])
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
@app.route('/api/image-to-video', methods=['POST'])
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
@app.route('/api/download/<video_id>', methods=['GET'])
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
)