Spaces:
Paused
Paused
| import gradio as gr | |
| import replicate | |
| import os | |
| from PIL import Image | |
| import requests | |
| from io import BytesIO | |
| import tempfile | |
| import base64 | |
| import spaces | |
| import torch | |
| import numpy as np | |
| import random | |
| import gc | |
| import time | |
| # =========================== | |
| # Configuration | |
| # =========================== | |
| # Set up Replicate API key | |
| os.environ['REPLICATE_API_TOKEN'] = os.getenv('REPLICATE_API_TOKEN') | |
| # Video Model Configuration | |
| MAX_SEED = np.iinfo(np.int32).max | |
| FIXED_FPS = 16 | |
| default_prompt_i2v = "make this image come alive, smooth animation, cinematic motion" | |
| default_negative_prompt = "static, still, blurry, low quality, distorted" | |
| # =========================== | |
| # Helper Functions | |
| # =========================== | |
| def check_api_token(): | |
| """Check if Replicate API token is set""" | |
| token = os.getenv('REPLICATE_API_TOKEN') | |
| return token is not None and token.strip() != "" | |
| def upload_image_to_hosting(image): | |
| """Upload image to hosting service - exact same as example""" | |
| # Method 1: Try imgbb.com (most reliable) | |
| try: | |
| buffered = BytesIO() | |
| image.save(buffered, format="PNG") | |
| buffered.seek(0) | |
| img_base64 = base64.b64encode(buffered.getvalue()).decode() | |
| response = requests.post( | |
| "https://api.imgbb.com/1/upload", | |
| data={ | |
| 'key': '6d207e02198a847aa98d0a2a901485a5', | |
| 'image': img_base64, | |
| } | |
| ) | |
| if response.status_code == 200: | |
| data = response.json() | |
| if data.get('success'): | |
| return data['data']['url'] | |
| except: | |
| pass | |
| # Method 2: Try 0x0.st (simple and reliable) | |
| try: | |
| buffered = BytesIO() | |
| image.save(buffered, format="PNG") | |
| buffered.seek(0) | |
| files = {'file': ('image.png', buffered, 'image/png')} | |
| response = requests.post("https://0x0.st", files=files) | |
| if response.status_code == 200: | |
| return response.text.strip() | |
| except: | |
| pass | |
| # Method 3: Fallback to base64 | |
| buffered = BytesIO() | |
| image.save(buffered, format="PNG") | |
| buffered.seek(0) | |
| img_base64 = base64.b64encode(buffered.getvalue()).decode() | |
| return f"data:image/png;base64,{img_base64}" | |
| # =========================== | |
| # Image Generation with google/nano-banana | |
| # =========================== | |
| def process_images(prompt, image1, image2=None): | |
| """Process images using google/nano-banana model - exact same logic as example""" | |
| if not image1: | |
| return None, "Please upload at least one image", None | |
| if not check_api_token(): | |
| return None, "⚠️ Please set REPLICATE_API_TOKEN in Space settings", None | |
| try: | |
| image_urls = [] | |
| # Upload images | |
| url1 = upload_image_to_hosting(image1) | |
| image_urls.append(url1) | |
| if image2: | |
| url2 = upload_image_to_hosting(image2) | |
| image_urls.append(url2) | |
| print(f"Running google/nano-banana with prompt: {prompt}") | |
| print(f"Image URLs: {image_urls}") | |
| # Run the model - exactly as in example | |
| output = replicate.run( | |
| "google/nano-banana", | |
| input={ | |
| "prompt": prompt, | |
| "image_input": image_urls | |
| } | |
| ) | |
| if output is None: | |
| return None, "No output received", None | |
| # Get the generated image - exact same handling as example | |
| img = None | |
| # Try method 1 | |
| try: | |
| if hasattr(output, 'read'): | |
| img_data = output.read() | |
| img = Image.open(BytesIO(img_data)) | |
| except: | |
| pass | |
| # Try method 2 | |
| if img is None: | |
| try: | |
| if hasattr(output, 'url'): | |
| output_url = output.url() | |
| response = requests.get(output_url, timeout=30) | |
| if response.status_code == 200: | |
| img = Image.open(BytesIO(response.content)) | |
| except: | |
| pass | |
| # Try method 3 | |
| if img is None: | |
| output_url = None | |
| if isinstance(output, str): | |
| output_url = output | |
| elif isinstance(output, list) and len(output) > 0: | |
| output_url = output[0] | |
| if output_url: | |
| response = requests.get(output_url, timeout=30) | |
| if response.status_code == 200: | |
| img = Image.open(BytesIO(response.content)) | |
| if img: | |
| return img, "✨ Image generated successfully! You can now create a video.", img | |
| else: | |
| return None, "Could not process output", None | |
| except Exception as e: | |
| error_msg = str(e) | |
| print(f"Error in process_images: {error_msg}") | |
| if "authentication" in error_msg.lower(): | |
| return None, "❌ Invalid API token. Please check your REPLICATE_API_TOKEN.", None | |
| elif "rate limit" in error_msg.lower(): | |
| return None, "⏳ Rate limit reached. Please try again later.", None | |
| else: | |
| return None, f"Error: {str(e)[:100]}", None | |
| # =========================== | |
| # Video Generation Functions | |
| # =========================== | |
| def resize_image_for_video(image: Image.Image, target_width=None, target_height=None): | |
| """Resize image for video generation while maintaining aspect ratio""" | |
| # Convert RGBA to RGB | |
| if image.mode == 'RGBA': | |
| background = Image.new('RGB', image.size, (255, 255, 255)) | |
| background.paste(image, mask=image.split()[3]) | |
| image = background | |
| elif image.mode != 'RGB': | |
| image = image.convert('RGB') | |
| # Get original dimensions | |
| orig_width, orig_height = image.size | |
| aspect_ratio = orig_width / orig_height | |
| # If no target dimensions specified, use original aspect ratio with constraints | |
| if target_width is None or target_height is None: | |
| # Determine if landscape or portrait | |
| if aspect_ratio > 1: # Landscape | |
| target_width = min(1024, orig_width) | |
| target_height = int(target_width / aspect_ratio) | |
| else: # Portrait or square | |
| target_height = min(1024, orig_height) | |
| target_width = int(target_height * aspect_ratio) | |
| # Ensure dimensions are divisible by 8 (required by many models) | |
| target_width = (target_width // 8) * 8 | |
| target_height = (target_height // 8) * 8 | |
| # Minimum size constraints | |
| target_width = max(256, target_width) | |
| target_height = max(256, target_height) | |
| # Resize image | |
| resized = image.resize((target_width, target_height), Image.LANCZOS) | |
| return resized, target_width, target_height | |
| def generate_video_gpu( | |
| input_image, | |
| prompt, | |
| steps, | |
| negative_prompt, | |
| duration_seconds, | |
| seed, | |
| randomize_seed, | |
| maintain_aspect_ratio | |
| ): | |
| """GPU-accelerated video generation""" | |
| try: | |
| # Clear GPU memory | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| gc.collect() | |
| # Simulate processing | |
| time.sleep(2) | |
| return None, seed, "🎬 GPU test completed successfully" | |
| except Exception as e: | |
| return None, seed, f"GPU Error: {str(e)[:200]}" | |
| def generate_video_replicate( | |
| input_image, | |
| prompt, | |
| steps=30, | |
| negative_prompt="", | |
| duration_seconds=2.0, | |
| seed=42, | |
| randomize_seed=False, | |
| maintain_aspect_ratio=True | |
| ): | |
| """Generate video using Replicate API with aspect ratio preservation""" | |
| if not check_api_token(): | |
| return None, seed, "⚠️ Please set REPLICATE_API_TOKEN" | |
| if input_image is None: | |
| return None, seed, "Please provide an input image" | |
| try: | |
| # Get image dimensions while maintaining aspect ratio | |
| if maintain_aspect_ratio: | |
| resized_image, video_width, video_height = resize_image_for_video(input_image) | |
| print(f"Video dimensions: {video_width}x{video_height} (maintaining aspect ratio)") | |
| else: | |
| # Default landscape dimensions | |
| resized_image, video_width, video_height = resize_image_for_video(input_image, 768, 512) | |
| print(f"Video dimensions: {video_width}x{video_height} (fixed landscape)") | |
| # Upload image | |
| img_url = upload_image_to_hosting(resized_image) | |
| current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed) | |
| print("Generating video with Stable Video Diffusion...") | |
| # Use Stable Video Diffusion | |
| output = replicate.run( | |
| "stability-ai/stable-video-diffusion:3f0457e4619daac51203dedb472816fd4af51f3149fa7a9e0b5ffcf1b8172438", | |
| input={ | |
| "input_image": img_url, | |
| "frames_per_second": FIXED_FPS, | |
| "motion_bucket_id": 127, # Controls motion amount (0-255) | |
| "cond_aug": 0.02, # Conditioning augmentation | |
| "decoding_t": min(14, int(duration_seconds * 7)), # Number of frames | |
| "seed": current_seed, | |
| "sizing_strategy": "maintain_aspect_ratio" # Preserve aspect ratio | |
| } | |
| ) | |
| if output: | |
| # Download video | |
| video_url = output if isinstance(output, str) else str(output) | |
| response = requests.get(video_url, timeout=60) | |
| if response.status_code == 200: | |
| with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp_video: | |
| tmp_video.write(response.content) | |
| return tmp_video.name, current_seed, f"🎬 Video generated! ({video_width}x{video_height})" | |
| return None, seed, "Failed to generate video" | |
| except Exception as e: | |
| error_msg = str(e) | |
| if "authentication" in error_msg.lower(): | |
| return None, seed, "❌ Invalid API token" | |
| else: | |
| return None, seed, f"Error: {error_msg[:200]}" | |
| # =========================== | |
| # Enhanced CSS (same as example) | |
| # =========================== | |
| css = """ | |
| .gradio-container { | |
| background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| min-height: 100vh; | |
| } | |
| .header-container { | |
| background: linear-gradient(135deg, #ffd93d 0%, #ffb347 100%); | |
| padding: 2.5rem; | |
| border-radius: 24px; | |
| margin-bottom: 2.5rem; | |
| box-shadow: 0 20px 60px rgba(255, 179, 71, 0.25); | |
| } | |
| .logo-text { | |
| font-size: 3.5rem; | |
| font-weight: 900; | |
| color: #2d3436; | |
| text-align: center; | |
| margin: 0; | |
| letter-spacing: -2px; | |
| } | |
| .subtitle { | |
| color: #2d3436; | |
| text-align: center; | |
| font-size: 1.2rem; | |
| margin-top: 0.5rem; | |
| opacity: 0.9; | |
| font-weight: 600; | |
| } | |
| .main-content { | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(20px); | |
| border-radius: 24px; | |
| padding: 2.5rem; | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08); | |
| margin-bottom: 2rem; | |
| } | |
| .gr-button-primary { | |
| background: linear-gradient(135deg, #ffd93d 0%, #ffb347 100%) !important; | |
| border: none !important; | |
| color: #2d3436 !important; | |
| font-weight: 700 !important; | |
| font-size: 1.1rem !important; | |
| padding: 1.2rem 2rem !important; | |
| border-radius: 14px !important; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| width: 100%; | |
| margin-top: 1rem !important; | |
| } | |
| .gr-button-primary:hover { | |
| transform: translateY(-3px) !important; | |
| box-shadow: 0 15px 40px rgba(255, 179, 71, 0.35) !important; | |
| } | |
| .gr-button-secondary { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| border: none !important; | |
| color: white !important; | |
| font-weight: 700 !important; | |
| font-size: 1.1rem !important; | |
| padding: 1.2rem 2rem !important; | |
| border-radius: 14px !important; | |
| } | |
| .section-title { | |
| font-size: 1.8rem; | |
| font-weight: 800; | |
| color: #2d3436; | |
| margin-bottom: 1rem; | |
| padding-bottom: 0.5rem; | |
| border-bottom: 3px solid #ffd93d; | |
| } | |
| .status-text { | |
| font-family: 'SF Mono', 'Monaco', monospace; | |
| color: #00b894; | |
| font-size: 0.9rem; | |
| } | |
| .image-container { | |
| border-radius: 14px !important; | |
| overflow: hidden; | |
| border: 2px solid #e1e8ed !important; | |
| background: #fafbfc !important; | |
| } | |
| footer { | |
| display: none !important; | |
| } | |
| """ | |
| # =========================== | |
| # Gradio Interface | |
| # =========================== | |
| def create_interface(): | |
| with gr.Blocks(css=css, theme=gr.themes.Base()) as demo: | |
| # Shared state | |
| generated_image_state = gr.State(None) | |
| # Header | |
| with gr.Column(elem_classes="header-container"): | |
| gr.HTML(""" | |
| <h1 class="logo-text">🍌 Nano Banana VIDEO</h1> | |
| <p class="subtitle">AI-Powered Image Style Transfer</p> | |
| <div style="display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 20px;"> | |
| <a href="https://huggingface.co/spaces/ginigen/Nano-Banana-PRO" target="_blank"> | |
| <img src="https://img.shields.io/static/v1?label=NANO%20BANANA&message=PRO&color=%230000ff&labelColor=%23800080&logo=HUGGINGFACE&logoColor=white&style=for-the-badge" alt="badge"> | |
| </a> | |
| <a href="https://huggingface.co/spaces/openfree/Nano-Banana-Upscale" target="_blank"> | |
| <img src="https://img.shields.io/static/v1?label=NANO%20BANANA&message=UPSCALE&color=%230000ff&labelColor=%23800080&logo=GOOGLE&logoColor=white&style=for-the-badge" alt="Nano Banana Upscale"> | |
| </a> | |
| <a href="https://huggingface.co/spaces/openfree/Free-Nano-Banana" target="_blank"> | |
| <img src="https://img.shields.io/static/v1?label=NANO%20BANANA&message=FREE&color=%230000ff&labelColor=%23800080&logo=GOOGLE&logoColor=white&style=for-the-badge" alt="Free Nano Banana"> | |
| </a> | |
| <a href="https://huggingface.co/spaces/aiqtech/Nano-Banana-API" target="_blank"> | |
| <img src="https://img.shields.io/static/v1?label=NANO%20BANANA&message=API&color=%230000ff&labelColor=%23800080&logo=GOOGLE&logoColor=white&style=for-the-badge" alt="Nano Banana API"> | |
| </a> | |
| <a href="https://discord.gg/openfreeai" target="_blank"> | |
| <img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge" alt="Discord Openfree AI"> | |
| </a> | |
| </div> | |
| """) | |
| # API Token Status | |
| with gr.Row(): | |
| gr.HTML(f""" | |
| <div class="status-box" style="background: {'#d4edda' if check_api_token() else '#f8d7da'}; | |
| color: {'#155724' if check_api_token() else '#721c24'}; | |
| padding: 12px; border-radius: 10px; margin: 15px 0;"> | |
| <b>API Status:</b> {'✅ Token configured' if check_api_token() else '❌ Token missing - Add REPLICATE_API_TOKEN in Settings > Repository secrets'} | |
| </div> | |
| """) | |
| # Tabs | |
| with gr.Tabs(): | |
| # Tab 1: Image Generation | |
| with gr.TabItem("🎨 Step 1: Generate Image"): | |
| with gr.Column(elem_classes="main-content"): | |
| gr.HTML('<h2 class="section-title">🎨 Image Style Transfer</h2>') | |
| with gr.Row(equal_height=True): | |
| with gr.Column(scale=1): | |
| style_prompt = gr.Textbox( | |
| label="Style Description", | |
| placeholder="Describe your style...", | |
| lines=3, | |
| value="Make the sheets in the style of the logo. Make the scene natural.", | |
| ) | |
| with gr.Row(equal_height=True): | |
| image1 = gr.Image( | |
| label="Primary Image", | |
| type="pil", | |
| height=200, | |
| elem_classes="image-container" | |
| ) | |
| image2 = gr.Image( | |
| label="Secondary Image (Optional)", | |
| type="pil", | |
| height=200, | |
| elem_classes="image-container" | |
| ) | |
| generate_img_btn = gr.Button( | |
| "Generate Magic ✨", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| with gr.Column(scale=1): | |
| output_image = gr.Image( | |
| label="Generated Result", | |
| type="pil", | |
| height=420, | |
| elem_classes="image-container" | |
| ) | |
| img_status = gr.Textbox( | |
| label="Status", | |
| interactive=False, | |
| lines=1, | |
| elem_classes="status-text", | |
| value="Ready to generate..." | |
| ) | |
| send_to_video_btn = gr.Button( | |
| "Send to Video Generation →", | |
| variant="secondary", | |
| size="lg", | |
| visible=False | |
| ) | |
| # Tab 2: Video Generation | |
| with gr.TabItem("🎬 Step 2: Generate Video"): | |
| with gr.Column(elem_classes="main-content"): | |
| gr.HTML('<h2 class="section-title">🎬 Video Generation from Image</h2>') | |
| with gr.Row(): | |
| with gr.Column(): | |
| video_input_image = gr.Image( | |
| type="pil", | |
| label="Input Image (from Step 1 or upload new)", | |
| elem_classes="image-container" | |
| ) | |
| video_prompt = gr.Textbox( | |
| label="Animation Prompt", | |
| value=default_prompt_i2v, | |
| lines=3 | |
| ) | |
| with gr.Row(): | |
| duration_input = gr.Slider( | |
| minimum=1.0, | |
| maximum=4.0, | |
| step=0.5, | |
| value=2.0, | |
| label="Duration (seconds)" | |
| ) | |
| maintain_aspect = gr.Checkbox( | |
| label="Maintain Original Aspect Ratio", | |
| value=True | |
| ) | |
| with gr.Accordion("Advanced Settings", open=False): | |
| video_negative_prompt = gr.Textbox( | |
| label="Negative Prompt", | |
| value=default_negative_prompt, | |
| lines=3 | |
| ) | |
| video_seed = gr.Slider( | |
| label="Seed", | |
| minimum=0, | |
| maximum=MAX_SEED, | |
| step=1, | |
| value=42 | |
| ) | |
| randomize_seed = gr.Checkbox( | |
| label="Randomize seed", | |
| value=True | |
| ) | |
| steps_slider = gr.Slider( | |
| minimum=10, | |
| maximum=50, | |
| step=5, | |
| value=30, | |
| label="Quality Steps" | |
| ) | |
| generate_video_btn = gr.Button( | |
| "Generate Video 🎬", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| with gr.Column(): | |
| video_output = gr.Video( | |
| label="Generated Video", | |
| autoplay=True | |
| ) | |
| video_status = gr.Textbox( | |
| label="Status", | |
| interactive=False, | |
| lines=1, | |
| elem_classes="status-text", | |
| value="Ready to generate video..." | |
| ) | |
| # Event Handlers | |
| def on_image_generated(prompt, img1, img2): | |
| img, status, state_img = process_images(prompt, img1, img2) | |
| if img: | |
| return img, status, state_img, gr.update(visible=True) | |
| return img, status, state_img, gr.update(visible=False) | |
| def send_image_to_video(img): | |
| if img: | |
| return img, "Image loaded! Ready to generate video." | |
| return None, "No image to send." | |
| # Image generation events | |
| generate_img_btn.click( | |
| fn=on_image_generated, | |
| inputs=[style_prompt, image1, image2], | |
| outputs=[output_image, img_status, generated_image_state, send_to_video_btn] | |
| ) | |
| # Send to video tab | |
| send_to_video_btn.click( | |
| fn=send_image_to_video, | |
| inputs=[generated_image_state], | |
| outputs=[video_input_image, video_status] | |
| ) | |
| # Video generation events | |
| generate_video_btn.click( | |
| fn=generate_video_replicate, | |
| inputs=[ | |
| video_input_image, | |
| video_prompt, | |
| steps_slider, | |
| video_negative_prompt, | |
| duration_input, | |
| video_seed, | |
| randomize_seed, | |
| maintain_aspect | |
| ], | |
| outputs=[video_output, video_seed, video_status] | |
| ) | |
| return demo | |
| # Launch | |
| if __name__ == "__main__": | |
| print("=" * 50) | |
| print("Starting Nano Banana + Video Application") | |
| print("=" * 50) | |
| if check_api_token(): | |
| print("✅ Replicate API token found") | |
| else: | |
| print("⚠️ REPLICATE_API_TOKEN not found") | |
| print("Please add it in Settings > Repository secrets") | |
| print("=" * 50) | |
| # Create and launch the interface | |
| demo = create_interface() | |
| demo.launch( | |
| show_error=True, | |
| share=False | |
| ) |