veo3.1 features
11
.gitignore
vendored
|
|
@ -44,6 +44,10 @@ service-account.json
|
|||
# Generated videos
|
||||
generated_videos/
|
||||
|
||||
# Test Images
|
||||
test_images/
|
||||
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
|
|
@ -80,4 +84,9 @@ node_modules/
|
|||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
.cache/
|
||||
|
||||
|
||||
# Test files
|
||||
veo3.1.txt
|
||||
test_veo31_images.py
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ PROJECT_ID=optical-414516
|
|||
REGION=us-central1
|
||||
MODEL_ID=veo-3.0-generate-preview
|
||||
OUTPUT_GCS_BUCKET_NAME=optical-veo3-test
|
||||
SERVICE_ACCOUNT_KEY_PATH=../service-account.json
|
||||
SERVICE_ACCOUNT_KEY_PATH=./service-account.json
|
||||
|
||||
# Flask Configuration
|
||||
FLASK_ENV=development
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ PROJECT_ID=optical-414516
|
|||
REGION=us-central1
|
||||
MODEL_ID=veo-3.0-generate-preview
|
||||
OUTPUT_GCS_BUCKET_NAME=optical-veo3-test
|
||||
SERVICE_ACCOUNT_KEY_PATH=../service-account.json
|
||||
SERVICE_ACCOUNT_KEY_PATH=./service-account.json
|
||||
|
||||
# Flask Configuration
|
||||
FLASK_ENV=production
|
||||
|
|
|
|||
|
|
@ -14,17 +14,43 @@ class Config:
|
|||
|
||||
# Model configurations
|
||||
SUPPORTED_MODELS = {
|
||||
# Veo 3.0 Models
|
||||
'veo-3.0-generate-preview': {
|
||||
'name': 'Veo 3.0',
|
||||
'description': 'High-quality video generation',
|
||||
'price_per_second': 0.75,
|
||||
'speed': 'Standard'
|
||||
'price_per_second': 0.40,
|
||||
'speed': 'Standard',
|
||||
'supports_reference_images': False,
|
||||
'supports_last_frame': False,
|
||||
'supports_video_extension': False
|
||||
},
|
||||
'veo-3.0-fast-generate-preview': {
|
||||
'name': 'Veo 3.0 Fast',
|
||||
'description': 'Optimized for speed and cost',
|
||||
'price_per_second': 0.15,
|
||||
'speed': 'Fast',
|
||||
'supports_reference_images': False,
|
||||
'supports_last_frame': False,
|
||||
'supports_video_extension': False
|
||||
},
|
||||
# Veo 3.1 Models
|
||||
'veo-3.1-generate-preview': {
|
||||
'name': 'Veo 3.1',
|
||||
'description': 'Next-gen with reference images & frame interpolation',
|
||||
'price_per_second': 0.40,
|
||||
'speed': 'Fast'
|
||||
'speed': 'Standard',
|
||||
'supports_reference_images': True,
|
||||
'supports_last_frame': True,
|
||||
'supports_video_extension': True
|
||||
},
|
||||
'veo-3.1-fast-generate-preview': {
|
||||
'name': 'Veo 3.1 Fast',
|
||||
'description': 'Optimized Veo 3.1 with last frame interpolation (no reference images)',
|
||||
'price_per_second': 0.15,
|
||||
'speed': 'Fast',
|
||||
'supports_reference_images': False, # Reference images NOT supported in Fast model
|
||||
'supports_last_frame': True,
|
||||
'supports_video_extension': True
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
flask==3.0.0
|
||||
flask-cors==4.0.0
|
||||
google-genai==1.17.0
|
||||
google-genai==1.47.0
|
||||
google-cloud-storage==2.12.0
|
||||
google-auth==2.24.0
|
||||
google-cloud-aiplatform==1.38.0
|
||||
|
|
|
|||
|
|
@ -27,33 +27,90 @@ def generate_video():
|
|||
|
||||
# Handle both multipart and JSON requests
|
||||
image_path = None
|
||||
|
||||
last_frame_path = None
|
||||
reference_image_paths = []
|
||||
|
||||
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())}")
|
||||
|
||||
|
||||
# Use job_id for consistent folder naming
|
||||
job_folder = os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}")
|
||||
|
||||
# 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
|
||||
# Save uploaded first frame 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}")
|
||||
|
||||
print(f"DEBUG: First frame 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
|
||||
if os.path.exists(job_folder) and not os.listdir(job_folder):
|
||||
os.rmdir(job_folder)
|
||||
return jsonify({'error': f'First frame 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
|
||||
return jsonify({'error': 'Invalid first frame image format. Supported formats: ' + ', '.join(Config.SUPPORTED_IMAGE_EXTENSIONS)}), 400
|
||||
|
||||
# Handle last frame upload (Veo 3.1 feature)
|
||||
if request.files and 'lastFrame' in request.files:
|
||||
last_frame_file = request.files['lastFrame']
|
||||
|
||||
if last_frame_file and last_frame_file.filename and allowed_file(last_frame_file.filename):
|
||||
filename = secure_filename(last_frame_file.filename)
|
||||
os.makedirs(job_folder, exist_ok=True)
|
||||
last_frame_path = os.path.join(job_folder, f"last_frame_{filename}")
|
||||
last_frame_file.save(last_frame_path)
|
||||
print(f"DEBUG: Last frame image saved to: {last_frame_path}")
|
||||
|
||||
# Validate file size
|
||||
if os.path.getsize(last_frame_path) > Config.MAX_IMAGE_SIZE:
|
||||
# Clean up
|
||||
os.remove(last_frame_path)
|
||||
if image_path and os.path.exists(image_path):
|
||||
os.remove(image_path)
|
||||
if os.path.exists(job_folder) and not os.listdir(job_folder):
|
||||
os.rmdir(job_folder)
|
||||
return jsonify({'error': f'Last frame image too large. Maximum size: {Config.MAX_IMAGE_SIZE} bytes'}), 400
|
||||
elif last_frame_file and last_frame_file.filename:
|
||||
return jsonify({'error': 'Invalid last frame image format. Supported formats: ' + ', '.join(Config.SUPPORTED_IMAGE_EXTENSIONS)}), 400
|
||||
|
||||
# Handle reference images upload (Veo 3.1 feature - up to 3 images)
|
||||
for i in range(1, 4):
|
||||
ref_key = f'referenceImage{i}'
|
||||
if request.files and ref_key in request.files:
|
||||
ref_file = request.files[ref_key]
|
||||
|
||||
if ref_file and ref_file.filename and allowed_file(ref_file.filename):
|
||||
filename = secure_filename(ref_file.filename)
|
||||
os.makedirs(job_folder, exist_ok=True)
|
||||
ref_path = os.path.join(job_folder, f"ref{i}_{filename}")
|
||||
ref_file.save(ref_path)
|
||||
reference_image_paths.append(ref_path)
|
||||
print(f"DEBUG: Reference image {i} saved to: {ref_path}")
|
||||
|
||||
# Validate file size
|
||||
if os.path.getsize(ref_path) > Config.MAX_IMAGE_SIZE:
|
||||
# Clean up all uploaded files
|
||||
for path in reference_image_paths:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
if last_frame_path and os.path.exists(last_frame_path):
|
||||
os.remove(last_frame_path)
|
||||
if image_path and os.path.exists(image_path):
|
||||
os.remove(image_path)
|
||||
if os.path.exists(job_folder) and not os.listdir(job_folder):
|
||||
os.rmdir(job_folder)
|
||||
return jsonify({'error': f'Reference image {i} too large. Maximum size: {Config.MAX_IMAGE_SIZE} bytes'}), 400
|
||||
elif ref_file and ref_file.filename:
|
||||
return jsonify({'error': f'Invalid reference image {i} 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'):
|
||||
|
|
@ -196,7 +253,7 @@ def generate_video():
|
|||
# Queue limit check removed - unlimited queue
|
||||
|
||||
# Start video generation
|
||||
print(f"DEBUG: About to call generate_video_async with job_id: {job_id} and image_path: {image_path}")
|
||||
print(f"DEBUG: About to call generate_video_async with job_id: {job_id}, image_path: {image_path}, last_frame_path: {last_frame_path}, reference_image_paths: {reference_image_paths}")
|
||||
result_job_id = generate_video_async(
|
||||
job_id=job_id,
|
||||
prompt=prompt,
|
||||
|
|
@ -206,6 +263,8 @@ def generate_video():
|
|||
sample_count=sample_count,
|
||||
person_generation=person_generation,
|
||||
image_path=image_path,
|
||||
last_frame_path=last_frame_path,
|
||||
reference_image_paths=reference_image_paths if reference_image_paths else None,
|
||||
user_email=user_email,
|
||||
seed=seed,
|
||||
generate_audio=generate_audio
|
||||
|
|
@ -215,14 +274,35 @@ def generate_video():
|
|||
return jsonify({'job_id': result_job_id, 'status': 'started'}), 202
|
||||
|
||||
except Exception as e:
|
||||
# Clean up temp image file on error
|
||||
# Clean up temp image files 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: Error during first frame cleanup: {cleanup_error}")
|
||||
|
||||
if 'last_frame_path' in locals() and last_frame_path and os.path.exists(last_frame_path):
|
||||
try:
|
||||
os.remove(last_frame_path)
|
||||
except Exception as cleanup_error:
|
||||
print(f"DEBUG: Error during last frame cleanup: {cleanup_error}")
|
||||
|
||||
if 'reference_image_paths' in locals() and reference_image_paths:
|
||||
for ref_path in reference_image_paths:
|
||||
if os.path.exists(ref_path):
|
||||
try:
|
||||
os.remove(ref_path)
|
||||
except Exception as cleanup_error:
|
||||
print(f"DEBUG: Error during reference image cleanup: {cleanup_error}")
|
||||
|
||||
# Remove job folder if empty
|
||||
if 'job_id' in locals():
|
||||
try:
|
||||
job_folder = os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}")
|
||||
if os.path.exists(job_folder) and not os.listdir(job_folder):
|
||||
os.rmdir(job_folder)
|
||||
except Exception as cleanup_error:
|
||||
print(f"DEBUG: Error during folder cleanup: {cleanup_error}")
|
||||
|
||||
print(f"DEBUG: Exception in generate_video route: {str(e)}")
|
||||
print(f"DEBUG: Exception type: {type(e)}")
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
BIN
backend/temp_downloads/veo_img_1762178595_9530.jpg
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
backend/temp_downloads/veo_img_1762178710_9530.jpg
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
backend/temp_downloads/veo_img_1762180768_13778.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
backend/temp_downloads/veo_img_1762197238_15165.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
|
|
@ -243,12 +243,83 @@ def upload_image_to_gcs(image_path: str, job_id: str) -> str:
|
|||
except Exception as cleanup_error:
|
||||
print(f"Warning: Could not clean up temporary file {converted_image_path}: {cleanup_error}")
|
||||
|
||||
def upload_reference_images_to_gcs(image_paths: list, job_id: str) -> list:
|
||||
"""
|
||||
Upload multiple reference images to GCS after validating and converting to JPEG format.
|
||||
Returns a list of GCS URIs for the uploaded images.
|
||||
|
||||
Args:
|
||||
image_paths: List of local image file paths (max 3)
|
||||
job_id: Job ID for unique naming
|
||||
|
||||
Returns:
|
||||
List of GCS URIs
|
||||
"""
|
||||
if not image_paths:
|
||||
return []
|
||||
|
||||
if len(image_paths) > 3:
|
||||
raise ValueError("Maximum 3 reference images allowed")
|
||||
|
||||
gcs_uris = []
|
||||
converted_files = []
|
||||
|
||||
try:
|
||||
credentials = get_google_credentials()
|
||||
|
||||
if credentials:
|
||||
storage_client = storage.Client(project=Config.PROJECT_ID, credentials=credentials)
|
||||
else:
|
||||
storage_client = storage.Client(project=Config.PROJECT_ID)
|
||||
|
||||
bucket = storage_client.bucket(Config.OUTPUT_GCS_BUCKET_NAME)
|
||||
|
||||
for idx, image_path in enumerate(image_paths):
|
||||
# Validate and convert image
|
||||
validation_result = validate_and_convert_image(image_path)
|
||||
if not validation_result['valid']:
|
||||
raise ValueError(f"Reference image {idx+1} validation failed: {validation_result['error']}")
|
||||
|
||||
converted_image_path = validation_result['converted_path']
|
||||
converted_files.append(converted_image_path)
|
||||
|
||||
# Create unique blob name with index
|
||||
timestamp = int(time.time())
|
||||
blob_name = f"{Config.TEMP_IMAGE_GCS_PATH}/{job_id}_ref{idx+1}_{timestamp}.jpg"
|
||||
|
||||
blob = bucket.blob(blob_name)
|
||||
|
||||
# Upload converted image with timeout
|
||||
blob.upload_from_filename(converted_image_path, timeout=300) # 5 minute timeout
|
||||
|
||||
# Set content type to JPEG
|
||||
blob.content_type = 'image/jpeg'
|
||||
blob.patch()
|
||||
|
||||
gcs_uri = f"gs://{Config.OUTPUT_GCS_BUCKET_NAME}/{blob_name}"
|
||||
gcs_uris.append(gcs_uri)
|
||||
print(f"Reference image {idx+1} converted from {validation_result['original_format']} and uploaded to: {gcs_uri}")
|
||||
|
||||
return gcs_uris
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error uploading reference images to GCS: {e}")
|
||||
raise
|
||||
finally:
|
||||
# Clean up temporary converted files
|
||||
for converted_path in converted_files:
|
||||
try:
|
||||
if os.path.exists(converted_path):
|
||||
os.remove(converted_path)
|
||||
except Exception as cleanup_error:
|
||||
print(f"Warning: Could not clean up temporary file {converted_path}: {cleanup_error}")
|
||||
|
||||
def cleanup_image_files(job_id: str, local_path: str = None, gcs_blob_name: str = None) -> bool:
|
||||
"""
|
||||
Clean up image files from local storage and GCS.
|
||||
"""
|
||||
success = True
|
||||
|
||||
|
||||
# Delete local file
|
||||
if local_path and os.path.exists(local_path):
|
||||
try:
|
||||
|
|
@ -257,7 +328,7 @@ def cleanup_image_files(job_id: str, local_path: str = None, gcs_blob_name: str
|
|||
except Exception as e:
|
||||
print(f"Error deleting local image: {e}")
|
||||
success = False
|
||||
|
||||
|
||||
# Delete GCS file
|
||||
if gcs_blob_name:
|
||||
try:
|
||||
|
|
@ -265,5 +336,42 @@ def cleanup_image_files(job_id: str, local_path: str = None, gcs_blob_name: str
|
|||
except Exception as e:
|
||||
print(f"Error deleting GCS image: {e}")
|
||||
success = False
|
||||
|
||||
|
||||
return success
|
||||
|
||||
def cleanup_multiple_images(job_id: str, local_paths: list = None, gcs_blob_names: list = None) -> bool:
|
||||
"""
|
||||
Clean up multiple image files from local storage and GCS.
|
||||
|
||||
Args:
|
||||
job_id: Job ID for logging
|
||||
local_paths: List of local file paths to delete
|
||||
gcs_blob_names: List of GCS blob names to delete
|
||||
|
||||
Returns:
|
||||
True if all cleanups succeeded, False otherwise
|
||||
"""
|
||||
success = True
|
||||
|
||||
# Delete local files
|
||||
if local_paths:
|
||||
for local_path in local_paths:
|
||||
if local_path and os.path.exists(local_path):
|
||||
try:
|
||||
os.remove(local_path)
|
||||
print(f"Deleted local image: {local_path}")
|
||||
except Exception as e:
|
||||
print(f"Error deleting local image: {e}")
|
||||
success = False
|
||||
|
||||
# Delete GCS files
|
||||
if gcs_blob_names:
|
||||
for gcs_blob_name in gcs_blob_names:
|
||||
if gcs_blob_name:
|
||||
try:
|
||||
delete_blob(Config.OUTPUT_GCS_BUCKET_NAME, gcs_blob_name)
|
||||
except Exception as e:
|
||||
print(f"Error deleting GCS image: {e}")
|
||||
success = False
|
||||
|
||||
return success
|
||||
|
|
@ -290,6 +290,8 @@ def generate_video_async(
|
|||
sample_count: int = 1,
|
||||
person_generation: str = "dont_allow",
|
||||
image_path: str = None,
|
||||
last_frame_path: str = None,
|
||||
reference_image_paths: list = None,
|
||||
user_email: str = "anonymous",
|
||||
seed: int = None,
|
||||
generate_audio: bool = True
|
||||
|
|
@ -326,6 +328,12 @@ def generate_video_async(
|
|||
'image_gcs_uri': None,
|
||||
'image_blob_name': None,
|
||||
'local_image_path': image_path,
|
||||
'last_frame_gcs_uri': None,
|
||||
'last_frame_blob_name': None,
|
||||
'local_last_frame_path': last_frame_path,
|
||||
'reference_image_gcs_uris': None,
|
||||
'reference_image_blob_names': None,
|
||||
'local_reference_image_paths': reference_image_paths,
|
||||
'model_name': model_name,
|
||||
'video_length_sec': video_length_sec,
|
||||
'aspect_ratio': aspect_ratio,
|
||||
|
|
@ -361,9 +369,37 @@ def process_video_generation(job_id: str) -> None:
|
|||
sample_count = job['videos_requested']
|
||||
person_generation = job['person_generation']
|
||||
image_path = job['local_image_path']
|
||||
last_frame_path = job['local_last_frame_path']
|
||||
reference_image_paths = job['local_reference_image_paths']
|
||||
seed = job['seed']
|
||||
generate_audio = job['generate_audio']
|
||||
|
||||
|
||||
# Validate Veo 3.1 feature constraints
|
||||
if last_frame_path or (reference_image_paths and len(reference_image_paths) > 0):
|
||||
if video_length_sec != 8:
|
||||
error_message = "Veo 3.1 features (last frame interpolation and reference images) require 8-second duration"
|
||||
job_status[job_id].update({
|
||||
'status': 'failed',
|
||||
'progress': 0,
|
||||
'message': error_message,
|
||||
'error': error_message
|
||||
})
|
||||
complete_job(job_id)
|
||||
remove_from_queue(job_id, user_email)
|
||||
return
|
||||
|
||||
if reference_image_paths and len(reference_image_paths) > 0 and aspect_ratio != "16:9":
|
||||
error_message = "Reference images feature requires 16:9 aspect ratio"
|
||||
job_status[job_id].update({
|
||||
'status': 'failed',
|
||||
'progress': 0,
|
||||
'message': error_message,
|
||||
'error': error_message
|
||||
})
|
||||
complete_job(job_id)
|
||||
remove_from_queue(job_id, user_email)
|
||||
return
|
||||
|
||||
# Update status to starting
|
||||
job_status[job_id].update({
|
||||
'status': 'starting',
|
||||
|
|
@ -375,7 +411,15 @@ def process_video_generation(job_id: str) -> None:
|
|||
# Validate and upload image if provided
|
||||
image_gcs_uri = None
|
||||
image_blob_name = None
|
||||
last_frame_gcs_uri = None
|
||||
last_frame_blob_name = None
|
||||
reference_image_gcs_uris = []
|
||||
reference_image_blob_names = []
|
||||
|
||||
print(f"DEBUG: generate_video_async received image_path: {image_path}")
|
||||
print(f"DEBUG: generate_video_async received last_frame_path: {last_frame_path}")
|
||||
print(f"DEBUG: generate_video_async received reference_image_paths: {reference_image_paths}")
|
||||
|
||||
if image_path:
|
||||
try:
|
||||
# Validate and convert image to JPEG
|
||||
|
|
@ -384,13 +428,13 @@ def process_video_generation(job_id: str) -> None:
|
|||
job_status[job_id].update({
|
||||
'status': 'failed',
|
||||
'progress': 0,
|
||||
'message': f'Image validation failed: {validation_result["error"]}',
|
||||
'message': f'First frame validation failed: {validation_result["error"]}',
|
||||
'error': validation_result['error']
|
||||
})
|
||||
complete_job(job_id)
|
||||
remove_from_queue(job_id, user_email)
|
||||
return
|
||||
|
||||
|
||||
# Auto-select aspect ratio based on the converted image
|
||||
detected_aspect_ratio = validation_result['aspect_ratio']
|
||||
if detected_aspect_ratio in ['16:9', '9:16']:
|
||||
|
|
@ -398,17 +442,17 @@ def process_video_generation(job_id: str) -> None:
|
|||
print(f"Using detected aspect ratio: {aspect_ratio}")
|
||||
if validation_result.get('was_cropped'):
|
||||
print(f"Image was automatically cropped from {validation_result['original_size']} to {validation_result['size']} to fit {aspect_ratio} aspect ratio")
|
||||
|
||||
|
||||
# Upload image to GCS
|
||||
job_status[job_id].update({
|
||||
'status': 'uploading_image',
|
||||
'progress': 5,
|
||||
'message': 'Uploading reference image...'
|
||||
'message': 'Uploading first frame image...'
|
||||
})
|
||||
|
||||
|
||||
image_gcs_uri = upload_image_to_gcs(image_path, job_id)
|
||||
image_blob_name = image_gcs_uri.replace(f"gs://{Config.OUTPUT_GCS_BUCKET_NAME}/", "")
|
||||
|
||||
|
||||
job_status[job_id].update({
|
||||
'image_gcs_uri': image_gcs_uri,
|
||||
'image_blob_name': image_blob_name,
|
||||
|
|
@ -416,9 +460,74 @@ def process_video_generation(job_id: str) -> None:
|
|||
'detected_aspect_ratio': detected_aspect_ratio,
|
||||
'image_was_cropped': validation_result.get('was_cropped', False)
|
||||
})
|
||||
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Failed to process image: {str(e)}"
|
||||
error_message = f"Failed to process first frame image: {str(e)}"
|
||||
job_status[job_id].update({
|
||||
'status': 'failed',
|
||||
'progress': 0,
|
||||
'message': error_message,
|
||||
'error': error_message
|
||||
})
|
||||
complete_job(job_id)
|
||||
remove_from_queue(job_id, user_email)
|
||||
return
|
||||
|
||||
# Upload last frame if provided (Veo 3.1 feature)
|
||||
if last_frame_path:
|
||||
try:
|
||||
job_status[job_id].update({
|
||||
'status': 'uploading_image',
|
||||
'progress': 7,
|
||||
'message': 'Uploading last frame image...'
|
||||
})
|
||||
|
||||
last_frame_gcs_uri = upload_image_to_gcs(last_frame_path, f"{job_id}_last")
|
||||
last_frame_blob_name = last_frame_gcs_uri.replace(f"gs://{Config.OUTPUT_GCS_BUCKET_NAME}/", "")
|
||||
|
||||
job_status[job_id].update({
|
||||
'last_frame_gcs_uri': last_frame_gcs_uri,
|
||||
'last_frame_blob_name': last_frame_blob_name,
|
||||
'local_last_frame_path': last_frame_path
|
||||
})
|
||||
|
||||
print(f"Uploaded last frame: {last_frame_gcs_uri}")
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Failed to process last frame image: {str(e)}"
|
||||
job_status[job_id].update({
|
||||
'status': 'failed',
|
||||
'progress': 0,
|
||||
'message': error_message,
|
||||
'error': error_message
|
||||
})
|
||||
complete_job(job_id)
|
||||
remove_from_queue(job_id, user_email)
|
||||
return
|
||||
|
||||
# Upload reference images if provided (Veo 3.1 feature)
|
||||
if reference_image_paths and len(reference_image_paths) > 0:
|
||||
try:
|
||||
job_status[job_id].update({
|
||||
'status': 'uploading_image',
|
||||
'progress': 8,
|
||||
'message': f'Uploading {len(reference_image_paths)} reference image(s)...'
|
||||
})
|
||||
|
||||
from utils.storage import upload_reference_images_to_gcs
|
||||
reference_image_gcs_uris = upload_reference_images_to_gcs(reference_image_paths, job_id)
|
||||
reference_image_blob_names = [uri.replace(f"gs://{Config.OUTPUT_GCS_BUCKET_NAME}/", "") for uri in reference_image_gcs_uris]
|
||||
|
||||
job_status[job_id].update({
|
||||
'reference_image_gcs_uris': reference_image_gcs_uris,
|
||||
'reference_image_blob_names': reference_image_blob_names,
|
||||
'local_reference_image_paths': reference_image_paths
|
||||
})
|
||||
|
||||
print(f"Uploaded {len(reference_image_gcs_uris)} reference images")
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Failed to process reference images: {str(e)}"
|
||||
job_status[job_id].update({
|
||||
'status': 'failed',
|
||||
'progress': 0,
|
||||
|
|
@ -526,20 +635,37 @@ def process_video_generation(job_id: str) -> None:
|
|||
|
||||
# Add seed to config
|
||||
config_kwargs['seed'] = seed
|
||||
|
||||
|
||||
# Veo 3.1 features - Add to config (not request_kwargs)
|
||||
if last_frame_gcs_uri:
|
||||
config_kwargs['last_frame'] = types.Image(
|
||||
gcs_uri=last_frame_gcs_uri,
|
||||
mime_type='image/jpeg'
|
||||
)
|
||||
print(f"✓ Using last frame for interpolation: {last_frame_gcs_uri}")
|
||||
|
||||
if reference_image_gcs_uris and len(reference_image_gcs_uris) > 0:
|
||||
config_kwargs['reference_images'] = [
|
||||
types.VideoGenerationReferenceImage(
|
||||
image=types.Image(gcs_uri=uri, mime_type='image/jpeg'),
|
||||
reference_type='asset'
|
||||
) for uri in reference_image_gcs_uris
|
||||
]
|
||||
print(f"✓ Using {len(reference_image_gcs_uris)} reference image(s) for content guidance")
|
||||
|
||||
request_kwargs = {
|
||||
'model': selected_model,
|
||||
'prompt': prompt,
|
||||
'config': types.GenerateVideosConfig(**config_kwargs)
|
||||
}
|
||||
|
||||
# Add image if provided
|
||||
|
||||
# Add first frame image if provided
|
||||
if image_gcs_uri:
|
||||
request_kwargs['image'] = types.Image(
|
||||
gcs_uri=image_gcs_uri,
|
||||
mime_type='image/jpeg' # Always JPEG after conversion
|
||||
)
|
||||
print(f"Using reference image: {image_gcs_uri}")
|
||||
print(f"✓ Using first frame image: {image_gcs_uri}")
|
||||
|
||||
# Make multiple API calls to ensure we get the requested number of videos
|
||||
# Each call typically generates 1 video, especially when using an image
|
||||
|
|
@ -824,6 +950,15 @@ def process_video_generation(job_id: str) -> None:
|
|||
# Clean up temporary image files
|
||||
if image_path and image_blob_name:
|
||||
cleanup_image_files(job_id, image_path, image_blob_name)
|
||||
|
||||
# Clean up last frame if provided
|
||||
if last_frame_path and last_frame_blob_name:
|
||||
cleanup_image_files(job_id, last_frame_path, last_frame_blob_name)
|
||||
|
||||
# Clean up reference images if provided
|
||||
if reference_image_paths and reference_image_blob_names:
|
||||
from utils.storage import cleanup_multiple_images
|
||||
cleanup_multiple_images(job_id, reference_image_paths, reference_image_blob_names)
|
||||
|
||||
# Mark job as completed in queue
|
||||
complete_job(job_id)
|
||||
|
|
@ -906,6 +1041,15 @@ def process_video_generation(job_id: str) -> None:
|
|||
# Clean up image files on failure
|
||||
if image_path and image_blob_name:
|
||||
cleanup_image_files(job_id, image_path, image_blob_name)
|
||||
|
||||
# Clean up last frame on failure
|
||||
if last_frame_path and last_frame_blob_name:
|
||||
cleanup_image_files(job_id, last_frame_path, last_frame_blob_name)
|
||||
|
||||
# Clean up reference images on failure
|
||||
if reference_image_paths and reference_image_blob_names:
|
||||
from utils.storage import cleanup_multiple_images
|
||||
cleanup_multiple_images(job_id, reference_image_paths, reference_image_blob_names)
|
||||
|
||||
# Mark job as completed in queue (even if failed)
|
||||
complete_job(job_id)
|
||||
|
|
|
|||
442
frontend/package-lock.json
generated
|
|
@ -27,7 +27,7 @@
|
|||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"vite": "^5.0.8"
|
||||
"vite": "^7.1.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-browser": {
|
||||
|
|
@ -462,371 +462,445 @@
|
|||
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
||||
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
|
|
@ -2096,6 +2170,15 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig/node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -2444,41 +2527,45 @@
|
|||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
"@esbuild/aix-ppc64": "0.25.12",
|
||||
"@esbuild/android-arm": "0.25.12",
|
||||
"@esbuild/android-arm64": "0.25.12",
|
||||
"@esbuild/android-x64": "0.25.12",
|
||||
"@esbuild/darwin-arm64": "0.25.12",
|
||||
"@esbuild/darwin-x64": "0.25.12",
|
||||
"@esbuild/freebsd-arm64": "0.25.12",
|
||||
"@esbuild/freebsd-x64": "0.25.12",
|
||||
"@esbuild/linux-arm": "0.25.12",
|
||||
"@esbuild/linux-arm64": "0.25.12",
|
||||
"@esbuild/linux-ia32": "0.25.12",
|
||||
"@esbuild/linux-loong64": "0.25.12",
|
||||
"@esbuild/linux-mips64el": "0.25.12",
|
||||
"@esbuild/linux-ppc64": "0.25.12",
|
||||
"@esbuild/linux-riscv64": "0.25.12",
|
||||
"@esbuild/linux-s390x": "0.25.12",
|
||||
"@esbuild/linux-x64": "0.25.12",
|
||||
"@esbuild/netbsd-arm64": "0.25.12",
|
||||
"@esbuild/netbsd-x64": "0.25.12",
|
||||
"@esbuild/openbsd-arm64": "0.25.12",
|
||||
"@esbuild/openbsd-x64": "0.25.12",
|
||||
"@esbuild/openharmony-arm64": "0.25.12",
|
||||
"@esbuild/sunos-x64": "0.25.12",
|
||||
"@esbuild/win32-arm64": "0.25.12",
|
||||
"@esbuild/win32-ia32": "0.25.12",
|
||||
"@esbuild/win32-x64": "0.25.12"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
|
|
@ -2753,6 +2840,24 @@
|
|||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
|
|
@ -4090,6 +4195,19 @@
|
|||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
|
@ -4785,6 +4903,23 @@
|
|||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
|
@ -4941,20 +5076,24 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.20",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz",
|
||||
"integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
|
||||
"version": "7.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
|
|
@ -4963,19 +5102,25 @@
|
|||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"less": "*",
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
|
|
@ -4996,6 +5141,12 @@
|
|||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -5121,11 +5272,18 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
"node": ">= 14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,6 @@
|
|||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"vite": "^5.0.8"
|
||||
"vite": "^7.1.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
ImageRounded,
|
||||
DeleteRounded
|
||||
} from '@mui/icons-material';
|
||||
import { VIDEO_GENERATION_OPTIONS, IMAGE_UPLOAD_CONFIG } from '../utils/constants';
|
||||
import { VIDEO_GENERATION_OPTIONS, IMAGE_UPLOAD_CONFIG, REFERENCE_IMAGE_CONFIG } from '../utils/constants';
|
||||
|
||||
const VideoForm = ({ onSubmit, isGenerating, userJobs = [] }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
|
|
@ -33,7 +33,7 @@ const VideoForm = ({ onSubmit, isGenerating, userJobs = [] }) => {
|
|||
video_length_sec: 8,
|
||||
aspect_ratio: '16:9',
|
||||
person_generation: 'allow_adult',
|
||||
model_name: 'veo-3.0-generate-preview',
|
||||
model_name: 'veo-3.1-generate-preview',
|
||||
seed: '',
|
||||
generate_audio: true,
|
||||
sampleCount: 1
|
||||
|
|
@ -42,6 +42,18 @@ const VideoForm = ({ onSubmit, isGenerating, userJobs = [] }) => {
|
|||
const [errors, setErrors] = useState({});
|
||||
const [selectedImage, setSelectedImage] = useState(null);
|
||||
const [imagePreview, setImagePreview] = useState(null);
|
||||
const [selectedLastFrame, setSelectedLastFrame] = useState(null);
|
||||
const [lastFramePreview, setLastFramePreview] = useState(null);
|
||||
const [selectedReferenceImages, setSelectedReferenceImages] = useState([]);
|
||||
const [referenceImagePreviews, setReferenceImagePreviews] = useState([]);
|
||||
|
||||
// Get model capabilities based on selected model
|
||||
const selectedModelConfig = VIDEO_GENERATION_OPTIONS.models.find(m => m.value === formData.model_name) || {};
|
||||
const modelCapabilities = {
|
||||
supportsReferenceImages: selectedModelConfig.supportsReferenceImages || false,
|
||||
supportsLastFrame: selectedModelConfig.supportsLastFrame || false,
|
||||
supportsVideoExtension: selectedModelConfig.supportsVideoExtension || false
|
||||
};
|
||||
|
||||
const handleChange = (field) => (event) => {
|
||||
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
||||
|
|
@ -114,9 +126,95 @@ const VideoForm = ({ onSubmit, isGenerating, userJobs = [] }) => {
|
|||
if (fileInput) fileInput.value = '';
|
||||
};
|
||||
|
||||
const handleLastFrameSelect = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const imageErrors = validateImage(file);
|
||||
if (imageErrors.length > 0) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
lastFrame: imageErrors[0]
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedLastFrame(file);
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
lastFrame: ''
|
||||
}));
|
||||
|
||||
// Create preview
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
setLastFramePreview(e.target.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleLastFrameRemove = () => {
|
||||
setSelectedLastFrame(null);
|
||||
setLastFramePreview(null);
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
lastFrame: ''
|
||||
}));
|
||||
const fileInput = document.getElementById('last-frame-upload');
|
||||
if (fileInput) fileInput.value = '';
|
||||
};
|
||||
|
||||
const handleReferenceImageSelect = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Check if max reference images reached
|
||||
if (selectedReferenceImages.length >= REFERENCE_IMAGE_CONFIG.maxReferenceImages) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
referenceImages: `Maximum ${REFERENCE_IMAGE_CONFIG.maxReferenceImages} reference images allowed`
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const imageErrors = validateImage(file);
|
||||
if (imageErrors.length > 0) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
referenceImages: imageErrors[0]
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedReferenceImages(prev => [...prev, file]);
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
referenceImages: ''
|
||||
}));
|
||||
|
||||
// Create preview
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
setReferenceImagePreviews(prev => [...prev, e.target.result]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// Reset file input for next selection
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
const handleReferenceImageRemove = (index) => {
|
||||
setSelectedReferenceImages(prev => prev.filter((_, i) => i !== index));
|
||||
setReferenceImagePreviews(prev => prev.filter((_, i) => i !== index));
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
referenceImages: ''
|
||||
}));
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
|
||||
if (!formData.prompt.trim()) {
|
||||
newErrors.prompt = 'Prompt is required';
|
||||
} else if (formData.prompt.trim().length < 10) {
|
||||
|
|
@ -135,8 +233,14 @@ const VideoForm = ({ onSubmit, isGenerating, userJobs = [] }) => {
|
|||
newErrors.sampleCount = 'Sample count must be between 1 and 4';
|
||||
}
|
||||
|
||||
// No queue limit check - unlimited queue
|
||||
// Veo 3.1 specific validations
|
||||
if (selectedLastFrame && !selectedImage) {
|
||||
newErrors.lastFrame = 'Last frame requires a first frame image';
|
||||
}
|
||||
|
||||
if (selectedReferenceImages.length > REFERENCE_IMAGE_CONFIG.maxReferenceImages) {
|
||||
newErrors.referenceImages = `Maximum ${REFERENCE_IMAGE_CONFIG.maxReferenceImages} reference images allowed`;
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
|
|
@ -144,11 +248,13 @@ const VideoForm = ({ onSubmit, isGenerating, userJobs = [] }) => {
|
|||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
||||
if (validateForm()) {
|
||||
onSubmit({
|
||||
...formData,
|
||||
image: selectedImage
|
||||
image: selectedImage,
|
||||
lastFrame: selectedLastFrame,
|
||||
referenceImages: selectedReferenceImages
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -257,6 +363,174 @@ const VideoForm = ({ onSubmit, isGenerating, userJobs = [] }) => {
|
|||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Last Frame Upload - Veo 3.1 Only */}
|
||||
{modelCapabilities.supportsLastFrame && (
|
||||
<Grid item xs={12}>
|
||||
<Box sx={{ border: '2px dashed #9c27b0', borderRadius: 2, p: 3, textAlign: 'center', bgcolor: '#f3e5f5' }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, color: '#7b1fa2' }}>
|
||||
<ImageRounded />
|
||||
Last Frame Image (Veo 3.1 - Optional)
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Upload a last frame for interpolation between first and last frames
|
||||
</Typography>
|
||||
<Alert severity="info" sx={{ mt: 1, mb: 2, textAlign: 'left' }}>
|
||||
<strong>Frame Interpolation:</strong> When you provide both a first frame and last frame, Veo 3.1 will generate video content that smoothly transitions between them.
|
||||
</Alert>
|
||||
|
||||
{!lastFramePreview ? (
|
||||
<Box>
|
||||
<input
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
id="last-frame-upload"
|
||||
type="file"
|
||||
onChange={handleLastFrameSelect}
|
||||
/>
|
||||
<label htmlFor="last-frame-upload">
|
||||
<Button
|
||||
variant="outlined"
|
||||
component="span"
|
||||
startIcon={<CloudUploadRounded />}
|
||||
sx={{ mt: 1 }}
|
||||
color="secondary"
|
||||
>
|
||||
Upload Last Frame
|
||||
</Button>
|
||||
</label>
|
||||
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
||||
Supported: JPG, PNG • Max: 10MB
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
<Box sx={{ position: 'relative', display: 'inline-block' }}>
|
||||
<img
|
||||
src={lastFramePreview}
|
||||
alt="Last Frame Preview"
|
||||
style={{
|
||||
maxWidth: '300px',
|
||||
maxHeight: '200px',
|
||||
borderRadius: '8px',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
size="small"
|
||||
startIcon={<DeleteRounded />}
|
||||
onClick={handleLastFrameRemove}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
minWidth: 'auto'
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
{selectedLastFrame?.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{errors.lastFrame && (
|
||||
<Typography variant="body2" color="error" sx={{ mt: 1 }}>
|
||||
{errors.lastFrame}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Reference Images - Veo 3.1 Only */}
|
||||
{modelCapabilities.supportsReferenceImages && (
|
||||
<Grid item xs={12}>
|
||||
<Box sx={{ border: '2px dashed #1976d2', borderRadius: 2, p: 3, textAlign: 'center', bgcolor: '#e3f2fd' }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, color: '#1565c0' }}>
|
||||
<ImageRounded />
|
||||
Reference Images (Veo 3.1 - Optional)
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Upload up to 3 reference images to guide video content and preserve subject appearance
|
||||
</Typography>
|
||||
<Alert severity="info" sx={{ mt: 1, mb: 2, textAlign: 'left' }}>
|
||||
<strong>Reference Images:</strong> Use these to maintain consistency of subjects, styles, or specific visual elements throughout the generated video.
|
||||
</Alert>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', justifyContent: 'center', mt: 2 }}>
|
||||
{referenceImagePreviews.map((preview, index) => (
|
||||
<Box key={index} sx={{ position: 'relative', display: 'inline-block' }}>
|
||||
<img
|
||||
src={preview}
|
||||
alt={`Reference ${index + 1}`}
|
||||
style={{
|
||||
width: '150px',
|
||||
height: '150px',
|
||||
borderRadius: '8px',
|
||||
objectFit: 'cover',
|
||||
border: '2px solid #1976d2'
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() => handleReferenceImageRemove(index)}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
right: 4,
|
||||
minWidth: 'auto',
|
||||
padding: '4px 8px'
|
||||
}}
|
||||
>
|
||||
<DeleteRounded fontSize="small" />
|
||||
</Button>
|
||||
<Typography variant="caption" display="block" sx={{ mt: 0.5, color: '#1565c0' }}>
|
||||
Ref {index + 1}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{selectedReferenceImages.length < REFERENCE_IMAGE_CONFIG.maxReferenceImages && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<input
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
id="reference-image-upload"
|
||||
type="file"
|
||||
onChange={handleReferenceImageSelect}
|
||||
/>
|
||||
<label htmlFor="reference-image-upload">
|
||||
<Button
|
||||
variant="outlined"
|
||||
component="span"
|
||||
startIcon={<CloudUploadRounded />}
|
||||
color="primary"
|
||||
>
|
||||
Add Reference Image
|
||||
</Button>
|
||||
</label>
|
||||
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
||||
{selectedReferenceImages.length}/{REFERENCE_IMAGE_CONFIG.maxReferenceImages} images • Supported: JPG, PNG • Max: 10MB each
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{errors.referenceImages && (
|
||||
<Typography variant="body2" color="error" sx={{ mt: 1 }}>
|
||||
{errors.referenceImages}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary expandIcon={<ExpandMoreRounded />}>
|
||||
|
|
|
|||
|
|
@ -18,31 +18,47 @@ const getUserEmail = () => {
|
|||
};
|
||||
|
||||
export const generateVideo = async (videoConfig, userEmail = null) => {
|
||||
// Check if image is included
|
||||
if (videoConfig.image) {
|
||||
// Check if any images are included (image, lastFrame, or referenceImages)
|
||||
const hasImages = videoConfig.image || videoConfig.lastFrame || (videoConfig.referenceImages && videoConfig.referenceImages.length > 0);
|
||||
|
||||
if (hasImages) {
|
||||
// Use FormData for multipart upload
|
||||
const formData = new FormData();
|
||||
|
||||
// Add all form fields
|
||||
|
||||
// Add all form fields (excluding image files)
|
||||
Object.keys(videoConfig).forEach(key => {
|
||||
if (key !== 'image') {
|
||||
if (key !== 'image' && key !== 'lastFrame' && key !== 'referenceImages') {
|
||||
formData.append(key, videoConfig[key]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Ensure video_length_sec is always included (in case it's missing from disabled field)
|
||||
if (!videoConfig.video_length_sec) {
|
||||
formData.append('video_length_sec', '8');
|
||||
}
|
||||
|
||||
|
||||
// Add user email if provided
|
||||
if (userEmail) {
|
||||
formData.append('user_email', userEmail);
|
||||
}
|
||||
|
||||
// Add image file
|
||||
formData.append('image', videoConfig.image);
|
||||
|
||||
|
||||
// Add first frame image file
|
||||
if (videoConfig.image) {
|
||||
formData.append('image', videoConfig.image);
|
||||
}
|
||||
|
||||
// Add last frame image file (Veo 3.1 feature)
|
||||
if (videoConfig.lastFrame) {
|
||||
formData.append('lastFrame', videoConfig.lastFrame);
|
||||
}
|
||||
|
||||
// Add reference images (Veo 3.1 feature - up to 3)
|
||||
if (videoConfig.referenceImages && videoConfig.referenceImages.length > 0) {
|
||||
videoConfig.referenceImages.forEach((refImage, index) => {
|
||||
formData.append(`referenceImage${index + 1}`, refImage);
|
||||
});
|
||||
}
|
||||
|
||||
// Send multipart request - don't set Content-Type manually, let axios handle it
|
||||
const response = await apiClient.post('/api/generate', formData);
|
||||
return response.data;
|
||||
|
|
@ -52,7 +68,7 @@ export const generateVideo = async (videoConfig, userEmail = null) => {
|
|||
if (userEmail) {
|
||||
videoConfigWithUser.user_email = userEmail;
|
||||
}
|
||||
|
||||
|
||||
const response = await apiClient.post('/api/generate', videoConfigWithUser, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
|
|||
|
|
@ -7,21 +7,51 @@ export const VIDEO_GENERATION_OPTIONS = {
|
|||
{ value: '9:16', label: '9:16 (Portrait)' }
|
||||
],
|
||||
models: [
|
||||
{
|
||||
value: 'veo-3.0-generate-preview',
|
||||
label: 'Veo 3.0 (Standard)',
|
||||
// Veo 3.0 Models
|
||||
{
|
||||
value: 'veo-3.0-generate-preview',
|
||||
label: 'Veo 3.0',
|
||||
description: 'High-quality video generation',
|
||||
pricePerSecond: 0.75,
|
||||
speed: 'Standard',
|
||||
recommended: true
|
||||
},
|
||||
{
|
||||
value: 'veo-3.0-fast-generate-preview',
|
||||
label: 'Veo 3.0 Fast',
|
||||
description: 'Optimized for speed and cost',
|
||||
pricePerSecond: 0.40,
|
||||
speed: 'Standard',
|
||||
recommended: false,
|
||||
supportsReferenceImages: false,
|
||||
supportsLastFrame: false,
|
||||
supportsVideoExtension: false
|
||||
},
|
||||
{
|
||||
value: 'veo-3.0-fast-generate-preview',
|
||||
label: 'Veo 3.0 Fast',
|
||||
description: 'Optimized for speed and cost',
|
||||
pricePerSecond: 0.15,
|
||||
speed: 'Fast',
|
||||
recommended: false
|
||||
recommended: false,
|
||||
supportsReferenceImages: false,
|
||||
supportsLastFrame: false,
|
||||
supportsVideoExtension: false
|
||||
},
|
||||
// Veo 3.1 Models
|
||||
{
|
||||
value: 'veo-3.1-generate-preview',
|
||||
label: 'Veo 3.1',
|
||||
description: 'Next-gen with reference images & frame interpolation',
|
||||
pricePerSecond: 0.40,
|
||||
speed: 'Standard',
|
||||
recommended: true,
|
||||
supportsReferenceImages: true,
|
||||
supportsLastFrame: true,
|
||||
supportsVideoExtension: true
|
||||
},
|
||||
{
|
||||
value: 'veo-3.1-fast-generate-preview',
|
||||
label: 'Veo 3.1 Fast',
|
||||
description: 'Optimized Veo 3.1 with last frame interpolation (no reference images)',
|
||||
pricePerSecond: 0.15,
|
||||
speed: 'Fast',
|
||||
recommended: false,
|
||||
supportsReferenceImages: false, // Reference images NOT supported in Fast model
|
||||
supportsLastFrame: true,
|
||||
supportsVideoExtension: true
|
||||
}
|
||||
],
|
||||
personGeneration: [
|
||||
|
|
@ -63,4 +93,11 @@ export const IMAGE_UPLOAD_CONFIG = {
|
|||
supportedExtensions: ['.jpg', '.jpeg', '.png'],
|
||||
minResolution: { width: 720, height: 720 },
|
||||
supportedAspectRatios: ['16:9', '9:16']
|
||||
};
|
||||
|
||||
export const REFERENCE_IMAGE_CONFIG = {
|
||||
maxReferenceImages: 3,
|
||||
maxSizePerImage: 10 * 1024 * 1024, // 10MB
|
||||
supportedFormats: ['image/jpeg', 'image/png', 'image/jpg'],
|
||||
supportedExtensions: ['.jpg', '.jpeg', '.png']
|
||||
};
|
||||
BIN
test-images/test_1_puppies.jpg
Normal file
|
After Width: | Height: | Size: 986 KiB |
BIN
test-images/test_2_dogs.jpg
Normal file
|
After Width: | Height: | Size: 3 MiB |