veo3.1 features

This commit is contained in:
Manish Tanwar 2025-11-04 02:31:40 +05:30
parent 1231b2e9c9
commit 146f032f1d
28 changed files with 1065 additions and 213 deletions

11
.gitignore vendored
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}
}

View file

@ -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

View file

@ -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)}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -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

View file

@ -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)

View file

@ -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": {

View file

@ -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"
}
}
}

View file

@ -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 />}>

View file

@ -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',

View file

@ -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']
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

BIN
test-images/test_2_dogs.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 MiB