490 lines
No EOL
22 KiB
Python
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 |