veo3/backend/routes/api.py
2025-10-08 19:03:04 +05:30

490 lines
No EOL
22 KiB
Python

import os
import time
import threading
import tempfile
import datetime
from werkzeug.utils import secure_filename
from flask import Blueprint, request, jsonify, send_file
from video_generator import generate_video_async, get_job_status, cleanup_job_files, get_user_jobs, get_queue_status, cancel_job, can_add_to_queue
from config import Config
api_bp = Blueprint('api', __name__)
def allowed_file(filename):
"""Check if uploaded file has allowed extension."""
if not filename:
return False
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in [ext[1:] for ext in Config.SUPPORTED_IMAGE_EXTENSIONS]
@api_bp.route('/generate', methods=['POST'])
def generate_video():
"""Start video generation."""
try:
# Generate job_id early so we can use it for folder naming
import uuid
job_id = str(uuid.uuid4())
# Handle both multipart and JSON requests
image_path = None
print(f"DEBUG: Request content type: {request.content_type}")
print(f"DEBUG: Request has files: {bool(request.files)}")
if request.files:
print(f"DEBUG: Files in request: {list(request.files.keys())}")
# Check if request has files (multipart)
if request.files and 'image' in request.files:
image_file = request.files['image']
if image_file and image_file.filename and allowed_file(image_file.filename):
# Save uploaded image to job-specific folder
filename = secure_filename(image_file.filename)
# Use job_id for consistent folder naming
job_folder = os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}")
os.makedirs(job_folder, exist_ok=True)
image_path = os.path.join(job_folder, filename)
image_file.save(image_path)
print(f"DEBUG: Image saved to: {image_path}")
# Validate file size
if os.path.getsize(image_path) > Config.MAX_IMAGE_SIZE:
os.remove(image_path)
os.rmdir(job_folder)
return jsonify({'error': f'Image too large. Maximum size: {Config.MAX_IMAGE_SIZE} bytes'}), 400
elif image_file and image_file.filename:
return jsonify({'error': 'Invalid image format. Supported formats: ' + ', '.join(Config.SUPPORTED_IMAGE_EXTENSIONS)}), 400
# Get form data or JSON data
if request.content_type and request.content_type.startswith('multipart/form-data'):
# Parse form data
data = request.form.to_dict()
print(f"DEBUG: Raw form data received: {data}")
# Convert numeric fields
try:
if 'video_length_sec' in data:
data['video_length_sec'] = int(data['video_length_sec'])
print(f"DEBUG: Converted video_length_sec to {data['video_length_sec']}")
else:
print("DEBUG: video_length_sec not found in form data")
if 'seed' in data and data['seed'] and str(data['seed']).strip():
data['seed'] = int(data['seed'])
print(f"DEBUG: Converted seed to {data['seed']}")
else:
data['seed'] = None
print("DEBUG: No seed provided, will generate random seed")
if 'generate_audio' in data:
data['generate_audio'] = data['generate_audio'].lower() in ['true', '1', 'yes', 'on']
print(f"DEBUG: Converted generate_audio to {data['generate_audio']}")
if 'sampleCount' in data:
data['sampleCount'] = int(data['sampleCount'])
print(f"DEBUG: Converted sampleCount to {data['sampleCount']}")
except ValueError as e:
print(f"DEBUG: Failed to convert numeric values: {e}")
return jsonify({'error': 'Invalid numeric value provided'}), 400
else:
# Parse JSON data
data = request.get_json()
print(f"DEBUG: Raw JSON data received: {data}")
if not data or not data.get('prompt'):
if image_path and os.path.exists(image_path):
try:
os.remove(image_path)
os.rmdir(os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}"))
except Exception as e:
print(f"DEBUG: Error cleaning up image: {e}")
return jsonify({'error': 'Prompt is required'}), 400
prompt = data['prompt']
model_name = data.get('model_name')
user_email = data.get('user_email', 'anonymous')
# Get video_length_sec and ensure it's an integer
video_length_sec = data.get('video_length_sec', 8)
if isinstance(video_length_sec, str):
try:
video_length_sec = int(video_length_sec)
print(f"DEBUG: Converted video_length_sec from string to int: {video_length_sec}")
except ValueError:
if image_path:
os.remove(image_path)
os.rmdir(os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}"))
return jsonify({'error': 'Invalid video_length_sec value - must be a number'}), 400
aspect_ratio = data.get('aspect_ratio', '16:9')
person_generation = data.get('person_generation', 'dont_allow')
seed = data.get('seed')
# Handle empty seed values by setting to None
if seed == '' or seed is None:
seed = None
generate_audio = data.get('generate_audio', True)
sample_count = data.get('sampleCount', 1)
print(f"DEBUG: Final video_length_sec value: {video_length_sec} (type: {type(video_length_sec)})")
print(f"DEBUG: aspect_ratio: {aspect_ratio}")
print(f"DEBUG: person_generation: {person_generation}")
print(f"DEBUG: seed: {seed}")
print(f"DEBUG: generate_audio: {generate_audio}")
print(f"DEBUG: sample_count: {sample_count}")
print(f"DEBUG: image_path provided: {image_path is not None}")
# Validate inputs
if aspect_ratio not in ['16:9', '9:16']:
if image_path and os.path.exists(image_path):
try:
os.remove(image_path)
os.rmdir(os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}"))
except Exception as e:
print(f"DEBUG: Error cleaning up image: {e}")
return jsonify({'error': 'Invalid aspect ratio. Must be "16:9" or "9:16"'}), 400
# Validate model selection
if model_name and model_name not in Config.SUPPORTED_MODELS:
if image_path and os.path.exists(image_path):
try:
os.remove(image_path)
os.rmdir(os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}"))
except Exception as e:
print(f"DEBUG: Error cleaning up image: {e}")
return jsonify({'error': f'Invalid model. Supported models: {", ".join(Config.SUPPORTED_MODELS.keys())}'}), 400
if person_generation not in ['dont_allow', 'allow_adult']:
if image_path and os.path.exists(image_path):
try:
os.remove(image_path)
os.rmdir(os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}"))
except Exception as e:
print(f"DEBUG: Error cleaning up image: {e}")
return jsonify({'error': 'Invalid person generation setting. Must be "dont_allow" or "allow_adult"'}), 400
print(f"DEBUG: Validating video_length_sec - isinstance check: {isinstance(video_length_sec, int)}, value: {video_length_sec}, type: {type(video_length_sec)}")
if not isinstance(video_length_sec, int) or video_length_sec not in [4, 6, 8]:
print(f"DEBUG: Video length validation FAILED - value: {video_length_sec}, type: {type(video_length_sec)}")
if image_path and os.path.exists(image_path):
try:
os.remove(image_path)
os.rmdir(os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}"))
except Exception as e:
print(f"DEBUG: Error cleaning up image: {e}")
return jsonify({'error': 'Video length must be 4, 6, or 8 seconds'}), 400
# Validate seed if provided (None is allowed for random generation)
if seed is not None:
if not isinstance(seed, int) or seed < 0 or seed > 4294967295:
if image_path and os.path.exists(image_path):
try:
os.remove(image_path)
os.rmdir(os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}"))
except Exception as e:
print(f"DEBUG: Error cleaning up image: {e}")
return jsonify({'error': 'Seed must be a number between 0 and 4294967295'}), 400
# Validate sample count (increased limit to 10)
if not isinstance(sample_count, int) or sample_count < 1 or sample_count > 10:
if image_path and os.path.exists(image_path):
try:
os.remove(image_path)
os.rmdir(os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}"))
except Exception as e:
print(f"DEBUG: Error cleaning up image: {e}")
return jsonify({'error': 'Sample count must be between 1 and 10'}), 400
# Check queue limit for user
if not can_add_to_queue(user_email):
if image_path and os.path.exists(image_path):
try:
os.remove(image_path)
os.rmdir(os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}"))
except Exception as e:
print(f"DEBUG: Error cleaning up image: {e}")
return jsonify({'error': 'Queue limit exceeded. Maximum 4 jobs per user allowed.'}), 400
# Start video generation
print(f"DEBUG: About to call generate_video_async with job_id: {job_id} and image_path: {image_path}")
result_job_id = generate_video_async(
job_id=job_id,
prompt=prompt,
model_name=model_name,
video_length_sec=video_length_sec,
aspect_ratio=aspect_ratio,
sample_count=sample_count,
person_generation=person_generation,
image_path=image_path,
user_email=user_email,
seed=seed,
generate_audio=generate_audio
)
print(f"DEBUG: Video generation started with job_id: {result_job_id}")
return jsonify({'job_id': result_job_id, 'status': 'started'}), 202
except Exception as e:
# Clean up temp image file on error
if 'image_path' in locals() and image_path and os.path.exists(image_path):
try:
os.remove(image_path)
if 'job_id' in locals():
os.rmdir(os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}"))
except Exception as cleanup_error:
print(f"DEBUG: Error during cleanup: {cleanup_error}")
print(f"DEBUG: Exception in generate_video route: {str(e)}")
print(f"DEBUG: Exception type: {type(e)}")
import traceback
traceback.print_exc()
return jsonify({'error': f'Failed to start video generation: {str(e)}'}), 500
@api_bp.route('/status/<job_id>', methods=['GET'])
def check_status(job_id):
"""Check the status of a video generation job."""
try:
status = get_job_status(job_id)
# No need for individual download links anymore - using zip download
return jsonify(status), 200
except Exception as e:
return jsonify({'error': f'Failed to get job status: {str(e)}'}), 500
@api_bp.route('/download/<job_id>', methods=['GET'])
def download_video(job_id):
"""Download the generated video."""
try:
job = get_job_status(job_id)
if job['status'] == 'not_found':
return jsonify({'error': 'Job not found'}), 404
if job['status'] != 'completed':
return jsonify({'error': 'Video not ready for download'}), 400
video_path = job.get('video_path')
download_folder = job.get('download_folder')
is_zip = job.get('is_zip', False)
print(f"DEBUG: video_path from job: {video_path}")
print(f"DEBUG: download_folder: {download_folder}")
print(f"DEBUG: is_zip: {is_zip}")
print(f"DEBUG: file exists: {os.path.exists(video_path) if video_path else False}")
if video_path and os.path.exists(video_path):
print(f"DEBUG: file size: {os.path.getsize(video_path)} bytes")
elif download_folder and is_zip:
# Check if zip exists in download folder
zip_in_folder = os.path.join(download_folder, "all_videos.zip")
print(f"DEBUG: checking for zip in folder: {zip_in_folder}")
print(f"DEBUG: zip in folder exists: {os.path.exists(zip_in_folder)}")
if os.path.exists(zip_in_folder):
video_path = zip_in_folder
print(f"DEBUG: using zip from folder: {video_path}")
print(f"DEBUG: zip file size: {os.path.getsize(video_path)} bytes")
if not video_path or not os.path.exists(video_path):
return jsonify({'error': f'Video file not found at path: {video_path}'}), 404
# Re-enable cleanup with longer delay for zip files to ensure download completes
def cleanup_after_send():
time.sleep(5) # Additional delay to ensure send_file completes
cleanup_job_files(job_id)
cleanup_thread = threading.Timer(60.0, cleanup_after_send) # 60 second delay
cleanup_thread.start()
# Determine download type and filename
is_zip = job.get('is_zip', False)
download_type = job.get('download_type', 'zip')
video_count = job.get('video_count', 1)
has_image = job.get('has_image', False)
print(f"=== DOWNLOAD ENDPOINT DEBUG ===")
print(f"is_zip: {is_zip}")
print(f"download_type: {download_type}")
print(f"video_count: {video_count}")
print(f"has_image: {has_image}")
print(f"video_path: {video_path}")
print("===============================")
if is_zip:
download_name = f'generated_content_{job_id}.zip'
mimetype = 'application/zip'
content_desc = f"{video_count} video(s)"
if has_image:
content_desc += " + original image"
print(f"Downloading zip file containing: {content_desc}")
else:
download_name = f'generated_video_{job_id}.mp4'
mimetype = 'video/mp4'
print(f"Downloading single video file (no zip needed)")
print(f"DEBUG: Starting file download: {download_name}")
print(f"DEBUG: File path: {video_path}")
print(f"DEBUG: Mimetype: {mimetype}")
try:
response = send_file(
video_path,
as_attachment=True,
download_name=download_name,
mimetype=mimetype
)
print(f"DEBUG: File download response created successfully")
return response
except Exception as send_error:
print(f"DEBUG: Error creating send_file response: {send_error}")
raise
except Exception as e:
return jsonify({'error': f'Failed to download video: {str(e)}'}), 500
@api_bp.route('/download_all/<job_id>', methods=['GET'])
def download_all_videos(job_id):
"""Alternative download endpoint that creates a fresh zip of all videos."""
try:
job = get_job_status(job_id)
if job['status'] == 'not_found':
return jsonify({'error': 'Job not found'}), 404
if job['status'] != 'completed':
return jsonify({'error': 'Videos not ready for download'}), 400
video_paths = job.get('video_paths', [])
download_folder = job.get('download_folder')
if not video_paths:
return jsonify({'error': 'No videos found for this job'}), 404
print(f"Creating fresh zip for job {job_id}")
print(f"Video paths: {video_paths}")
print(f"Download folder: {download_folder}")
# Create a fresh zip file
import tempfile
import tarfile
zip_path = os.path.join(Config.TEMP_DOWNLOAD_PATH, f"{job_id}_all_videos.tar.gz")
with tarfile.open(zip_path, "w:gz") as tar:
for i, video_path in enumerate(video_paths):
if os.path.exists(video_path):
tar.add(video_path, arcname=f"generated_video_{i+1}.mp4")
print(f"Added video {i+1} to tar: {video_path}")
# Also add the original image if it exists in the download folder
if download_folder:
for file in os.listdir(download_folder):
if file.lower().endswith(('.jpg', '.jpeg', '.png')):
image_path = os.path.join(download_folder, file)
tar.add(image_path, arcname=file)
print(f"Added image to tar: {file}")
print(f"Created tar file: {zip_path}")
if not os.path.exists(zip_path):
return jsonify({'error': 'Failed to create download file'}), 500
return send_file(
zip_path,
as_attachment=True,
download_name=f'generated_content_{job_id}.tar.gz',
mimetype='application/gzip'
)
except Exception as e:
print(f"Error in download_all_videos: {str(e)}")
return jsonify({'error': f'Failed to download videos: {str(e)}'}), 500
@api_bp.route('/cleanup/<job_id>', methods=['DELETE'])
def cleanup_job(job_id):
"""Clean up job files manually."""
try:
success = cleanup_job_files(job_id)
if success:
return jsonify({'message': 'Files cleaned up successfully'}), 200
else:
return jsonify({'error': 'Failed to clean up some files'}), 500
except Exception as e:
return jsonify({'error': f'Failed to cleanup: {str(e)}'}), 500
@api_bp.route('/user-jobs', methods=['GET'])
def get_user_job_list():
"""Get all jobs for the current user."""
try:
# Get user email from request (you may need to adjust this based on your auth system)
user_email = request.args.get('user_email', 'anonymous')
jobs = get_user_jobs(user_email)
return jsonify({'jobs': jobs}), 200
except Exception as e:
return jsonify({'error': f'Failed to get user jobs: {str(e)}'}), 500
@api_bp.route('/queue-status', methods=['GET'])
def get_queue_status_endpoint():
"""Get overall queue status."""
try:
status = get_queue_status()
return jsonify(status), 200
except Exception as e:
return jsonify({'error': f'Failed to get queue status: {str(e)}'}), 500
@api_bp.route('/cancel/<job_id>', methods=['DELETE'])
def cancel_job_endpoint(job_id):
"""Cancel a queued job."""
try:
success = cancel_job(job_id)
if success:
return jsonify({'message': 'Job cancelled successfully'}), 200
else:
return jsonify({'error': 'Job could not be cancelled (not found or already processing)'}), 400
except Exception as e:
return jsonify({'error': f'Failed to cancel job: {str(e)}'}), 500
@api_bp.route('/test-queue', methods=['GET'])
def test_queue_endpoints():
"""Test endpoint to verify queue routes are working."""
return jsonify({
'message': 'Queue endpoints are working',
'timestamp': datetime.datetime.now().isoformat(),
'available_endpoints': [
'/api/user-jobs',
'/api/queue-status',
'/api/cancel/<job_id>',
'/api/download/<job_id>/video/<index>'
]
}), 200
@api_bp.route('/download/<job_id>/video/<int:video_index>', methods=['GET'])
def download_individual_video(job_id, video_index):
"""Download an individual video from a multi-video job."""
try:
job = get_job_status(job_id)
if job['status'] == 'not_found':
return jsonify({'error': 'Job not found'}), 404
if job['status'] != 'completed':
return jsonify({'error': 'Video not ready for download'}), 400
individual_videos = job.get('individual_video_paths', [])
if video_index < 1 or video_index > len(individual_videos):
return jsonify({'error': f'Video index {video_index} out of range. Available: 1-{len(individual_videos)}'}), 400
video_path = individual_videos[video_index - 1] # Convert to 0-based index
if not os.path.exists(video_path):
return jsonify({'error': f'Video file not found: {video_path}'}), 404
download_name = f'video_{video_index}_{job_id}.mp4'
return send_file(
video_path,
as_attachment=True,
download_name=download_name,
mimetype='video/mp4'
)
except Exception as e:
return jsonify({'error': f'Failed to download video: {str(e)}'}), 500