Spaces:
Paused
Paused
| import gradio as gr | |
| import replicate | |
| import os | |
| from typing import Optional, List | |
| from huggingface_hub import whoami | |
| from PIL import Image | |
| import requests | |
| from io import BytesIO | |
| import tempfile | |
| import base64 | |
| # --- Replicate API Configuration --- | |
| REPLICATE_API_TOKEN = os.getenv("REPLICATE_API_TOKEN") | |
| if not REPLICATE_API_TOKEN: | |
| raise ValueError("REPLICATE_API_TOKEN environment variable is not set.") | |
| # Initialize Replicate client | |
| os.environ["REPLICATE_API_TOKEN"] = REPLICATE_API_TOKEN | |
| def verify_login_status(token: Optional[gr.OAuthToken]) -> bool: | |
| """Verifies if the user is logged in to Hugging Face.""" | |
| if not token: | |
| return False | |
| try: | |
| user_info = whoami(token=token.token) | |
| return True if user_info else False | |
| except Exception as e: | |
| print(f"Could not verify user's login status: {e}") | |
| return False | |
| def upload_image_to_hosting(image_path: str) -> str: | |
| """ | |
| Upload image to hosting service and return URL. | |
| Using multiple fallback methods for reliability. | |
| """ | |
| # Open the image | |
| img = Image.open(image_path) | |
| # Method 1: Try imgbb.com (most reliable) | |
| try: | |
| buffered = BytesIO() | |
| img.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', # Free API key | |
| 'image': img_base64, | |
| } | |
| ) | |
| if response.status_code == 200: | |
| data = response.json() | |
| if data.get('success'): | |
| return data['data']['url'] | |
| except Exception as e: | |
| print(f"imgbb upload failed: {e}") | |
| # Method 2: Try 0x0.st (simple and reliable) | |
| try: | |
| buffered = BytesIO() | |
| img.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: | |
| url = response.text.strip() | |
| if url.startswith('http'): | |
| return url | |
| except Exception as e: | |
| print(f"0x0.st upload failed: {e}") | |
| # Method 3: Fallback to data URI (last resort) | |
| buffered = BytesIO() | |
| img.save(buffered, format="PNG") | |
| buffered.seek(0) | |
| img_base64 = base64.b64encode(buffered.getvalue()).decode() | |
| return f"data:image/png;base64,{img_base64}" | |
| def image_to_data_uri(image_path: str) -> str: | |
| """Convert local image file to data URI format (kept for backwards compatibility).""" | |
| with open(image_path, "rb") as img_file: | |
| img_data = img_file.read() | |
| img_base64 = base64.b64encode(img_data).decode('utf-8') | |
| # Get the image format | |
| img = Image.open(image_path) | |
| img_format = img.format.lower() if img.format else 'png' | |
| # Create data URI | |
| data_uri = f"data:image/{img_format};base64,{img_base64}" | |
| return data_uri | |
| def run_single_image_logic(prompt: str, image_path: Optional[str] = None, progress=gr.Progress()) -> str: | |
| """Handles text-to-image or single image-to-image using Replicate's Nano Banana.""" | |
| try: | |
| progress(0.2, desc="π¨ Preparing...") | |
| # Prepare input for Replicate API | |
| input_data = { | |
| "prompt": prompt | |
| } | |
| # If there's an input image, upload it to get a proper URL | |
| if image_path: | |
| progress(0.3, desc="π€ Uploading image...") | |
| # Upload to hosting service for proper URL | |
| image_url = upload_image_to_hosting(image_path) | |
| if image_url.startswith('http'): | |
| print(f"Image uploaded successfully: {image_url[:50]}...") | |
| else: | |
| print("Using data URI fallback") | |
| input_data["image_input"] = [image_url] | |
| progress(0.5, desc="β¨ Generating...") | |
| # Run the model on Replicate | |
| output = replicate.run( | |
| "google/nano-banana", | |
| input=input_data | |
| ) | |
| progress(0.8, desc="πΌοΈ Finalizing...") | |
| # Handle the output - output is already a URL string or FileObject | |
| if output: | |
| return process_output(output, progress) | |
| else: | |
| raise ValueError("No output received from Replicate API") | |
| except Exception as e: | |
| print(f"Error details: {e}") | |
| print(f"Error type: {type(e)}") | |
| if 'output' in locals(): | |
| print(f"Output value: {output}") | |
| print(f"Output type: {type(output)}") | |
| raise gr.Error(f"Image generation failed: {str(e)[:200]}") | |
| def run_multi_image_logic(prompt: str, images: List[str], progress=gr.Progress()) -> str: | |
| """ | |
| Handles multi-image editing by sending a list of images and a prompt. | |
| """ | |
| if not images: | |
| raise gr.Error("Please upload at least one image in the 'Multiple Images' tab.") | |
| try: | |
| progress(0.2, desc="π¨ Preparing images...") | |
| # Convert all images to data URI format | |
| data_uris = [] | |
| for image_path in images: | |
| if isinstance(image_path, (list, tuple)): | |
| image_path = image_path[0] | |
| data_uri = image_to_data_uri(image_path) | |
| data_uris.append(data_uri) | |
| # Prepare input for Replicate API with multiple images | |
| input_data = { | |
| "prompt": prompt, | |
| "image_input": data_uris | |
| } | |
| progress(0.5, desc="β¨ Generating...") | |
| # Run the model on Replicate | |
| output = replicate.run( | |
| "google/nano-banana", | |
| input=input_data | |
| ) | |
| progress(0.8, desc="πΌοΈ Finalizing...") | |
| # Handle the output - output is already a URL string or FileObject | |
| if output: | |
| # Check if output has a url attribute (FileObject) | |
| if hasattr(output, 'url'): | |
| # If url is a method, call it; if it's a property, just access it | |
| image_url = output.url() if callable(output.url) else output.url | |
| # If output is already a string URL | |
| elif isinstance(output, str): | |
| image_url = output | |
| # If output is a list of URLs | |
| elif isinstance(output, list) and len(output) > 0: | |
| # Check first item in list | |
| first_item = output[0] | |
| if hasattr(first_item, 'url'): | |
| image_url = first_item.url() if callable(first_item.url) else first_item.url | |
| else: | |
| image_url = first_item | |
| else: | |
| raise ValueError(f"Unexpected output format from Replicate: {type(output)}") | |
| # Download the image from URL | |
| response = requests.get(image_url) | |
| response.raise_for_status() | |
| # Save to temporary file | |
| img = Image.open(BytesIO(response.content)) | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmpfile: | |
| img.save(tmpfile.name) | |
| progress(1.0, desc="β Complete!") | |
| return tmpfile.name | |
| else: | |
| raise ValueError("No output received from Replicate API") | |
| except Exception as e: | |
| print(f"Multi-image error details: {e}") | |
| print(f"Output value: {output if 'output' in locals() else 'Not set'}") | |
| print(f"Output type: {type(output) if 'output' in locals() else 'Not set'}") | |
| raise gr.Error(f"Image generation failed: {e}") | |
| # --- Gradio App UI --- | |
| css = ''' | |
| /* Header Styling */ | |
| .main-header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| padding: 2rem; | |
| border-radius: 1rem; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.1); | |
| } | |
| .header-title { | |
| font-size: 2.5rem !important; | |
| font-weight: bold; | |
| color: white; | |
| text-align: center; | |
| margin: 0 !important; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.2); | |
| } | |
| .header-subtitle { | |
| color: rgba(255,255,255,0.9); | |
| text-align: center; | |
| margin-top: 0.5rem !important; | |
| font-size: 1.1rem; | |
| } | |
| /* Card Styling */ | |
| .card { | |
| background: white; | |
| border-radius: 1rem; | |
| padding: 1.5rem; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| border: 1px solid rgba(0,0,0,0.05); | |
| } | |
| .dark .card { | |
| background: #1f2937; | |
| border: 1px solid #374151; | |
| } | |
| /* Tab Styling */ | |
| .tabs { | |
| border-radius: 0.5rem; | |
| overflow: hidden; | |
| margin-bottom: 1rem; | |
| } | |
| .tabitem { | |
| padding: 1rem !important; | |
| } | |
| button.selected { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| color: white !important; | |
| } | |
| /* Button Styling */ | |
| .generate-btn { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| border: none !important; | |
| color: white !important; | |
| font-size: 1.1rem !important; | |
| font-weight: 600 !important; | |
| padding: 0.8rem 2rem !important; | |
| border-radius: 0.5rem !important; | |
| cursor: pointer !important; | |
| transition: all 0.3s ease !important; | |
| width: 100% !important; | |
| margin-top: 1rem !important; | |
| } | |
| .generate-btn:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4) !important; | |
| } | |
| .use-btn { | |
| background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; | |
| border: none !important; | |
| color: white !important; | |
| font-weight: 600 !important; | |
| padding: 0.6rem 1.5rem !important; | |
| border-radius: 0.5rem !important; | |
| cursor: pointer !important; | |
| transition: all 0.3s ease !important; | |
| width: 100% !important; | |
| } | |
| .use-btn:hover { | |
| transform: translateY(-1px) !important; | |
| box-shadow: 0 5px 15px rgba(16, 185, 129, 0.4) !important; | |
| } | |
| /* Input Styling */ | |
| .prompt-input textarea { | |
| border-radius: 0.5rem !important; | |
| border: 2px solid #e5e7eb !important; | |
| padding: 0.8rem !important; | |
| font-size: 1rem !important; | |
| transition: border-color 0.3s ease !important; | |
| } | |
| .prompt-input textarea:focus { | |
| border-color: #667eea !important; | |
| outline: none !important; | |
| } | |
| .dark .prompt-input textarea { | |
| border-color: #374151 !important; | |
| background: #1f2937 !important; | |
| } | |
| /* Image Output Styling */ | |
| #output { | |
| border-radius: 0.5rem !important; | |
| overflow: hidden !important; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important; | |
| } | |
| /* Progress Bar Styling */ | |
| .progress-bar { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| } | |
| /* Examples Styling */ | |
| .examples { | |
| background: #f9fafb; | |
| border-radius: 0.5rem; | |
| padding: 1rem; | |
| margin-top: 1rem; | |
| } | |
| .dark .examples { | |
| background: #1f2937; | |
| } | |
| /* Login Message Styling */ | |
| .login-message { | |
| background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); | |
| border-radius: 1rem; | |
| padding: 2rem; | |
| text-align: center; | |
| border: 2px solid #f59e0b; | |
| } | |
| .dark .login-message { | |
| background: linear-gradient(135deg, #7c2d12 0%, #92400e 100%); | |
| border-color: #f59e0b; | |
| } | |
| /* Emoji Animations */ | |
| @keyframes bounce { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-10px); } | |
| } | |
| .emoji-icon { | |
| display: inline-block; | |
| animation: bounce 2s infinite; | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 768px) { | |
| .header-title { | |
| font-size: 2rem !important; | |
| } | |
| .main-container { | |
| padding: 1rem !important; | |
| } | |
| } | |
| ''' | |
| with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo: | |
| # Header | |
| gr.HTML(''' | |
| <div class="main-header"> | |
| <h1 class="header-title"> | |
| π Real Nano Banana | |
| </h1> | |
| <p class="header-subtitle"> | |
| AI Image Generator powered by Google Nano Banana | |
| </p> | |
| </div> | |
| ''') | |
| # Login Notice | |
| gr.HTML(''' | |
| <div style="background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%); | |
| border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; | |
| border-left: 4px solid #0284c7;"> | |
| <p style="margin: 0; color: #075985; font-weight: 600;"> | |
| π Please sign in with your Hugging Face account to use this service. | |
| </p> | |
| </div> | |
| ''') | |
| login_message = gr.Markdown(visible=False) | |
| main_interface = gr.Column(visible=False, elem_classes="main-container") | |
| with main_interface: | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.HTML('<div class="card">') | |
| # Mode Selection | |
| gr.HTML('<h3 style="margin-top: 0;">πΈ Select Mode</h3>') | |
| active_tab_state = gr.State(value="single") | |
| with gr.Tabs(elem_classes="tabs") as tabs: | |
| with gr.TabItem("πΌοΈ Single Image", id="single") as single_tab: | |
| image_input = gr.Image( | |
| type="filepath", | |
| label="Input Image (Optional)", | |
| elem_classes="image-input" | |
| ) | |
| gr.HTML(''' | |
| <p style="text-align: center; color: #6b7280; font-size: 0.9rem; margin-top: 0.5rem;"> | |
| π‘ Leave empty for text-to-image generation | |
| </p> | |
| ''') | |
| with gr.TabItem("π¨ Multiple Images", id="multiple") as multi_tab: | |
| gallery_input = gr.Gallery( | |
| label="Input Images (Max 2 images)", | |
| file_types=["image"], | |
| elem_classes="gallery-input" | |
| ) | |
| gr.HTML(''' | |
| <p style="text-align: center; color: #6b7280; font-size: 0.9rem; margin-top: 0.5rem;"> | |
| π‘ Upload up to 2 images for combination/editing | |
| </p> | |
| ''') | |
| # Prompt Input | |
| gr.HTML('<h3>βοΈ Prompt</h3>') | |
| prompt_input = gr.Textbox( | |
| label="", | |
| info="Describe what you want the AI to generate", | |
| placeholder="e.g., A delicious pizza, a cat in space, futuristic cityscape...", | |
| lines=3, | |
| elem_classes="prompt-input" | |
| ) | |
| # Generate Button | |
| generate_button = gr.Button( | |
| "π Generate", | |
| variant="primary", | |
| elem_classes="generate-btn" | |
| ) | |
| # Examples | |
| with gr.Accordion("π‘ Example Prompts", open=False): | |
| gr.Examples( | |
| examples=[ | |
| ["A delicious looking pizza with melting cheese"], | |
| ["A cat in a spacesuit walking on the moon surface"], | |
| ["Cyberpunk city at night with neon lights"], | |
| ["Japanese garden with cherry blossoms in spring"], | |
| ["Fantasy wizard tower in a magical world"], | |
| ["Make the scene more dramatic and cinematic"], | |
| ["Transform this into a watercolor painting style"], | |
| ], | |
| inputs=prompt_input | |
| ) | |
| gr.HTML('</div>') | |
| with gr.Column(scale=1): | |
| gr.HTML('<div class="card">') | |
| gr.HTML('<h3 style="margin-top: 0;">π¨ Generated Result</h3>') | |
| output_image = gr.Image( | |
| label="", | |
| interactive=False, | |
| elem_id="output" | |
| ) | |
| use_image_button = gr.Button( | |
| "β»οΈ Use this image for next edit", | |
| elem_classes="use-btn", | |
| visible=False | |
| ) | |
| # Tips | |
| gr.HTML(''' | |
| <div style="background: #f0f9ff; border-radius: 0.5rem; padding: 1rem; margin-top: 1rem;"> | |
| <h4 style="margin-top: 0; color: #0369a1;">π‘ Tips</h4> | |
| <ul style="margin: 0; padding-left: 1.5rem; color: #0c4a6e;"> | |
| <li>Use specific and detailed prompts for better results</li> | |
| <li>You can reuse generated images for iterative improvements</li> | |
| <li>Multiple image mode supports up to 2 images for combination</li> | |
| <li>English prompts tend to produce better results</li> | |
| </ul> | |
| </div> | |
| ''') | |
| gr.HTML('</div>') | |
| # Footer | |
| gr.HTML(''' | |
| <div style="text-align: center; margin-top: 2rem; padding: 1rem; | |
| border-top: 1px solid #e5e7eb;"> | |
| <p style="color: #6b7280;"> | |
| Made with π using Replicate API | Powered by Google Nano Banana | |
| </p> | |
| </div> | |
| ''') | |
| login_button = gr.LoginButton() | |
| # --- Event Handlers --- | |
| def unified_generator( | |
| prompt: str, | |
| single_image: Optional[str], | |
| multi_images: Optional[List[str]], | |
| active_tab: str, | |
| oauth_token: Optional[gr.OAuthToken] = None, | |
| ): | |
| if not verify_login_status(oauth_token): | |
| raise gr.Error("Login required. Please click the 'Sign in with Hugging Face' button at the top.") | |
| if not prompt: | |
| raise gr.Error("Please enter a prompt.") | |
| if active_tab == "multiple" and multi_images: | |
| result = run_multi_image_logic(prompt, multi_images) | |
| else: | |
| result = run_single_image_logic(prompt, single_image) | |
| return result, gr.update(visible=True) | |
| single_tab.select(lambda: "single", None, active_tab_state) | |
| multi_tab.select(lambda: "multiple", None, active_tab_state) | |
| generate_button.click( | |
| unified_generator, | |
| inputs=[prompt_input, image_input, gallery_input, active_tab_state], | |
| outputs=[output_image, use_image_button], | |
| ) | |
| use_image_button.click( | |
| lambda img: (img, gr.update(visible=False)), | |
| inputs=[output_image], | |
| outputs=[image_input, use_image_button] | |
| ) | |
| # --- Access Control Logic --- | |
| def control_access( | |
| profile: Optional[gr.OAuthProfile] = None, | |
| oauth_token: Optional[gr.OAuthToken] = None | |
| ): | |
| if not profile: | |
| return gr.update(visible=False), gr.update(visible=False) | |
| if verify_login_status(oauth_token): | |
| return gr.update(visible=True), gr.update(visible=False) | |
| else: | |
| message = ''' | |
| <div class="login-message"> | |
| <h2>π Login Required</h2> | |
| <p style="font-size: 1.1rem; margin: 1rem 0;"> | |
| Please sign in with your Hugging Face account to use this AI image generation tool. | |
| </p> | |
| <p style="margin: 1rem 0;"> | |
| After logging in, you can access: | |
| </p> | |
| <ul style="text-align: left; display: inline-block; margin: 1rem 0;"> | |
| <li>π High-quality image generation via Google Nano Banana</li> | |
| <li>β‘ Fast image generation and editing</li> | |
| <li>π¨ Text-to-image conversion</li> | |
| <li>π§ Multiple image editing and combining</li> | |
| </ul> | |
| <p style="margin-top: 1.5rem; font-weight: bold;"> | |
| Click the "Sign in with Hugging Face" button at the top to get started! | |
| </p> | |
| </div> | |
| ''' | |
| return gr.update(visible=False), gr.update(visible=True, value=message) | |
| demo.load(control_access, inputs=None, outputs=[main_interface, login_message]) | |
| if __name__ == "__main__": | |
| demo.queue(max_size=None, default_concurrency_limit=None) | |
| demo.launch() |