diff --git a/.gitignore b/.gitignore
index a820009..4b24cc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
\ No newline at end of file
+.cache/
+
+
+# Test files
+veo3.1.txt
+test_veo31_images.py
diff --git a/backend/.env.development b/backend/.env.development
index bc5f287..7c0b270 100644
--- a/backend/.env.development
+++ b/backend/.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=development
diff --git a/backend/.env.production b/backend/.env.production
index a7de1d3..2d28ce0 100644
--- a/backend/.env.production
+++ b/backend/.env.production
@@ -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
diff --git a/backend/config.py b/backend/config.py
index 1993757..d93fc61 100644
--- a/backend/config.py
+++ b/backend/config.py
@@ -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
}
}
diff --git a/backend/requirements.txt b/backend/requirements.txt
index a3a47c9..4ff40f6 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -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
diff --git a/backend/routes/api.py b/backend/routes/api.py
index be61ca3..ec7dae2 100644
--- a/backend/routes/api.py
+++ b/backend/routes/api.py
@@ -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)}")
diff --git a/backend/temp_downloads/job_10167a5d-8ce0-4403-91f8-a6c746c80b46/generated_video_1.mp4 b/backend/temp_downloads/job_10167a5d-8ce0-4403-91f8-a6c746c80b46/generated_video_1.mp4
new file mode 100644
index 0000000..b1d2b71
Binary files /dev/null and b/backend/temp_downloads/job_10167a5d-8ce0-4403-91f8-a6c746c80b46/generated_video_1.mp4 differ
diff --git a/backend/temp_downloads/job_2fbd536e-6909-4b5d-8baf-1acbeee8ae91/generated_video_1.mp4 b/backend/temp_downloads/job_2fbd536e-6909-4b5d-8baf-1acbeee8ae91/generated_video_1.mp4
new file mode 100644
index 0000000..9cb5aaf
Binary files /dev/null and b/backend/temp_downloads/job_2fbd536e-6909-4b5d-8baf-1acbeee8ae91/generated_video_1.mp4 differ
diff --git a/backend/temp_downloads/job_322a0809-59d7-48a8-b11e-45781839049b/stickman_running.jpg b/backend/temp_downloads/job_322a0809-59d7-48a8-b11e-45781839049b/stickman_running.jpg
new file mode 100644
index 0000000..3e08c52
Binary files /dev/null and b/backend/temp_downloads/job_322a0809-59d7-48a8-b11e-45781839049b/stickman_running.jpg differ
diff --git a/backend/temp_downloads/job_3a76d267-b814-4740-9a36-24cc7774211d/stickman_running.jpg b/backend/temp_downloads/job_3a76d267-b814-4740-9a36-24cc7774211d/stickman_running.jpg
new file mode 100644
index 0000000..3e08c52
Binary files /dev/null and b/backend/temp_downloads/job_3a76d267-b814-4740-9a36-24cc7774211d/stickman_running.jpg differ
diff --git a/backend/temp_downloads/job_53062d5e-69eb-4772-ab60-d727c858288d/generated_video_1.mp4 b/backend/temp_downloads/job_53062d5e-69eb-4772-ab60-d727c858288d/generated_video_1.mp4
new file mode 100644
index 0000000..bd1d1ad
Binary files /dev/null and b/backend/temp_downloads/job_53062d5e-69eb-4772-ab60-d727c858288d/generated_video_1.mp4 differ
diff --git a/backend/temp_downloads/job_5bc3436a-f729-4976-8415-84f945ead761/stickman_running.jpg b/backend/temp_downloads/job_5bc3436a-f729-4976-8415-84f945ead761/stickman_running.jpg
new file mode 100644
index 0000000..3e08c52
Binary files /dev/null and b/backend/temp_downloads/job_5bc3436a-f729-4976-8415-84f945ead761/stickman_running.jpg differ
diff --git a/backend/temp_downloads/job_c94bcd60-db65-456e-8093-1f1ded9cc10f/stickman_running.jpg b/backend/temp_downloads/job_c94bcd60-db65-456e-8093-1f1ded9cc10f/stickman_running.jpg
new file mode 100644
index 0000000..3e08c52
Binary files /dev/null and b/backend/temp_downloads/job_c94bcd60-db65-456e-8093-1f1ded9cc10f/stickman_running.jpg differ
diff --git a/backend/temp_downloads/job_d2574dad-353b-40c9-9565-f95b7d19859e/stickman_running.jpg b/backend/temp_downloads/job_d2574dad-353b-40c9-9565-f95b7d19859e/stickman_running.jpg
new file mode 100644
index 0000000..3e08c52
Binary files /dev/null and b/backend/temp_downloads/job_d2574dad-353b-40c9-9565-f95b7d19859e/stickman_running.jpg differ
diff --git a/backend/temp_downloads/job_e7e57f00-aa8c-4d42-a9c4-d2df0ac84dec/stickman_running.jpg b/backend/temp_downloads/job_e7e57f00-aa8c-4d42-a9c4-d2df0ac84dec/stickman_running.jpg
new file mode 100644
index 0000000..3e08c52
Binary files /dev/null and b/backend/temp_downloads/job_e7e57f00-aa8c-4d42-a9c4-d2df0ac84dec/stickman_running.jpg differ
diff --git a/backend/temp_downloads/veo_img_1762178595_9530.jpg b/backend/temp_downloads/veo_img_1762178595_9530.jpg
new file mode 100644
index 0000000..a560393
Binary files /dev/null and b/backend/temp_downloads/veo_img_1762178595_9530.jpg differ
diff --git a/backend/temp_downloads/veo_img_1762178710_9530.jpg b/backend/temp_downloads/veo_img_1762178710_9530.jpg
new file mode 100644
index 0000000..a560393
Binary files /dev/null and b/backend/temp_downloads/veo_img_1762178710_9530.jpg differ
diff --git a/backend/temp_downloads/veo_img_1762180768_13778.jpg b/backend/temp_downloads/veo_img_1762180768_13778.jpg
new file mode 100644
index 0000000..082d949
Binary files /dev/null and b/backend/temp_downloads/veo_img_1762180768_13778.jpg differ
diff --git a/backend/temp_downloads/veo_img_1762197238_15165.jpg b/backend/temp_downloads/veo_img_1762197238_15165.jpg
new file mode 100644
index 0000000..082d949
Binary files /dev/null and b/backend/temp_downloads/veo_img_1762197238_15165.jpg differ
diff --git a/backend/utils/storage.py b/backend/utils/storage.py
index 411d006..e73e669 100644
--- a/backend/utils/storage.py
+++ b/backend/utils/storage.py
@@ -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
\ No newline at end of file
diff --git a/backend/video_generator.py b/backend/video_generator.py
index 6c96f7a..a7d8f9d 100644
--- a/backend/video_generator.py
+++ b/backend/video_generator.py
@@ -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)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b4f31c6..9944806 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -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": {
diff --git a/frontend/package.json b/frontend/package.json
index 790a2d3..783258a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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"
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/VideoForm.jsx b/frontend/src/components/VideoForm.jsx
index 5311bdd..4deae4f 100644
--- a/frontend/src/components/VideoForm.jsx
+++ b/frontend/src/components/VideoForm.jsx
@@ -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 = [] }) => {
+ {/* Last Frame Upload - Veo 3.1 Only */}
+ {modelCapabilities.supportsLastFrame && (
+
+
+
+
+ Last Frame Image (Veo 3.1 - Optional)
+
+
+ Upload a last frame for interpolation between first and last frames
+
+
+ Frame Interpolation: When you provide both a first frame and last frame, Veo 3.1 will generate video content that smoothly transitions between them.
+
+
+ {!lastFramePreview ? (
+
+
+
+
+ Supported: JPG, PNG • Max: 10MB
+
+
+ ) : (
+
+
+
+ }
+ onClick={handleLastFrameRemove}
+ sx={{
+ position: 'absolute',
+ top: 8,
+ right: 8,
+ minWidth: 'auto'
+ }}
+ >
+ Remove
+
+
+
+ {selectedLastFrame?.name}
+
+
+ )}
+
+ {errors.lastFrame && (
+
+ {errors.lastFrame}
+
+ )}
+
+
+ )}
+
+ {/* Reference Images - Veo 3.1 Only */}
+ {modelCapabilities.supportsReferenceImages && (
+
+
+
+
+ Reference Images (Veo 3.1 - Optional)
+
+
+ Upload up to 3 reference images to guide video content and preserve subject appearance
+
+
+ Reference Images: Use these to maintain consistency of subjects, styles, or specific visual elements throughout the generated video.
+
+
+
+ {referenceImagePreviews.map((preview, index) => (
+
+
+
+
+ Ref {index + 1}
+
+
+ ))}
+
+
+ {selectedReferenceImages.length < REFERENCE_IMAGE_CONFIG.maxReferenceImages && (
+
+
+
+
+ {selectedReferenceImages.length}/{REFERENCE_IMAGE_CONFIG.maxReferenceImages} images • Supported: JPG, PNG • Max: 10MB each
+
+
+ )}
+
+ {errors.referenceImages && (
+
+ {errors.referenceImages}
+
+ )}
+
+
+ )}
+
}>
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index 5a4e8d0..23cd460 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -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',
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
index e52f08d..7319f81 100644
--- a/frontend/src/utils/constants.js
+++ b/frontend/src/utils/constants.js
@@ -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']
};
\ No newline at end of file
diff --git a/test-images/test_1_puppies.jpg b/test-images/test_1_puppies.jpg
new file mode 100644
index 0000000..53767be
Binary files /dev/null and b/test-images/test_1_puppies.jpg differ
diff --git a/test-images/test_2_dogs.jpg b/test-images/test_2_dogs.jpg
new file mode 100644
index 0000000..cca165b
Binary files /dev/null and b/test-images/test_2_dogs.jpg differ