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 + + + ) : ( + + + Last Frame Preview + + + + {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) => ( + + {`Reference + + + 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