| import gradio as gr |
| from PIL import Image |
| from typing import Tuple, Optional, Dict, Any |
| import os |
| import logging |
|
|
| from FlowFacade import FlowFacade |
| from BackgroundEngine import BackgroundEngine |
| from style_transfer import StyleTransferEngine |
| from scene_templates import SceneTemplateManager |
| from css_style import DELTAFLOW_CSS |
| from prompt_examples import PROMPT_EXAMPLES |
|
|
| try: |
| import spaces |
| SPACES_AVAILABLE = True |
| except ImportError: |
| SPACES_AVAILABLE = False |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class UIManager: |
| def __init__(self, facade: FlowFacade, background_engine: BackgroundEngine, style_engine: StyleTransferEngine): |
| self.facade = facade |
| self.background_engine = background_engine |
| self.style_engine = style_engine |
| self.template_manager = SceneTemplateManager() |
|
|
| def create_interface(self) -> gr.Blocks: |
| with gr.Blocks( |
| theme=gr.themes.Soft(), |
| css=DELTAFLOW_CSS, |
| title="VividFlow - AI Image Enhancement & Video Generation" |
| ) as interface: |
|
|
| |
| gr.HTML(""" |
| <div class="header-container"> |
| <h1 class="header-title">🌊 VividFlow</h1> |
| <p class="header-subtitle"> |
| AI-Powered Image Enhancement & Video Generation<br> |
| Transform images with background replacement, then bring them to life with AI |
| </p> |
| </div> |
| """) |
|
|
| |
| with gr.Tabs() as main_tabs: |
|
|
| |
| with gr.Tab("🎬 Image to Video"): |
| self._create_i2v_tab() |
|
|
| |
| with gr.Tab("🎨 Background Generation"): |
| self._create_background_tab() |
|
|
| |
| with gr.Tab("✨ Style Transfer"): |
| self._create_3d_tab() |
|
|
| |
| gr.HTML(""" |
| <div class="footer"> |
| <p>Powered by Wan2.2-I2V-A14B, SDXL, and OpenCLIP | Built with Gradio</p> |
| <div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #e0e0e0;"> |
| <p style="font-size: 0.9rem; color: #6c757d; margin-bottom: 0.75rem;"> |
| 💡 Curious about the technical details? |
| </p> |
| <a href="https://github.com/Eric-Chung-0511/Learning-Record/tree/main/Data%20Science%20Projects/VividFlow" |
| target="_blank" |
| style="display: inline-block; padding: 10px 24px; background: linear-gradient(135deg, #24292e 0%, #1a1e22 100%); |
| color: white; text-decoration: none; border-radius: 8px; font-size: 0.9rem; font-weight: 500; |
| transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(36, 41, 46, 0.2);"> |
| <span style="margin-right: 8px;">⭐</span> |
| Explore the technical docs on GitHub |
| <span style="margin-left: 8px;">→</span> |
| </a> |
| </div> |
| </div> |
| """) |
|
|
| return interface |
|
|
| def _create_i2v_tab(self): |
| """Create Image to Video tab (original VividFlow functionality)""" |
| with gr.Row(): |
| |
| with gr.Column(scale=1, elem_classes="input-card"): |
| gr.Markdown("### 📤 Input") |
|
|
| image_input = gr.Image( |
| label="Upload Image (any type: photo, art, cartoon, etc.)", |
| type="pil", |
| elem_classes="image-upload", |
| height=320 |
| ) |
|
|
| resolution_info = gr.Markdown( |
| value="", |
| visible=False, |
| elem_classes="info-text" |
| ) |
|
|
| prompt_input = gr.Textbox( |
| label="Motion Instruction", |
| placeholder="Describe camera movements and subject actions...", |
| lines=3, |
| max_lines=6 |
| ) |
|
|
| category_dropdown = gr.Dropdown( |
| choices=list(PROMPT_EXAMPLES.keys()), |
| label="💡 Quick Prompt Category", |
| value="💃 Fashion / Beauty (Facial Only)", |
| interactive=True |
| ) |
|
|
| example_dropdown = gr.Dropdown( |
| choices=PROMPT_EXAMPLES["💃 Fashion / Beauty (Facial Only)"], |
| label="Example Prompts (click to use)", |
| value=None, |
| interactive=True |
| ) |
|
|
| gr.HTML(""" |
| <div class="quality-banner"> |
| <strong>💡 Choose the Right Prompt Category:</strong><br> |
| • <strong>💃 Facial Only:</strong> Safe for headshots without visible hands<br> |
| • <strong>🙌 Hands Visible Required:</strong> Only use if hands are fully visible<br> |
| • <strong>🌄 Scenery/Objects:</strong> For landscapes, products, abstract content |
| </div> |
| """) |
|
|
| gr.HTML(""" |
| <div class="patience-banner"> |
| <strong>⏱️ First-time loading may take a moment!</strong><br> |
| Subsequent runs will be much faster. |
| </div> |
| """) |
|
|
| generate_btn = gr.Button( |
| "🎬 Generate Video", |
| variant="primary", |
| elem_classes="primary-button", |
| size="lg" |
| ) |
|
|
| with gr.Accordion("⚙️ Advanced Settings", open=False): |
| duration_slider = gr.Slider( |
| minimum=0.5, |
| maximum=5.0, |
| value=3.0, |
| step=0.5, |
| label="Video Duration (seconds)" |
| ) |
|
|
| steps_slider = gr.Slider( |
| minimum=4, |
| maximum=25, |
| value=4, |
| step=1, |
| label="Quality Steps (4=Lightning Fast, 8-25=Higher Quality)" |
| ) |
|
|
| fps_slider = gr.Slider( |
| minimum=8, |
| maximum=24, |
| value=16, |
| step=1, |
| label="Frames Per Second" |
| ) |
|
|
| expand_prompt = gr.Checkbox( |
| label="AI Prompt Expansion (experimental)", |
| value=False |
| ) |
|
|
| randomize_seed = gr.Checkbox( |
| label="Randomize Seed", |
| value=True |
| ) |
|
|
| seed_input = gr.Number( |
| label="Manual Seed (if not randomized)", |
| value=42, |
| precision=0 |
| ) |
|
|
| |
| with gr.Column(scale=1, elem_classes="output-card"): |
| gr.Markdown("### 🎥 Output") |
|
|
| video_output = gr.Video( |
| label="Generated Video", |
| elem_classes="video-player" |
| ) |
|
|
| final_prompt_output = gr.Textbox( |
| label="Final Prompt Used", |
| interactive=False, |
| lines=2 |
| ) |
|
|
| seed_output = gr.Number( |
| label="Seed Used", |
| interactive=False, |
| precision=0 |
| ) |
|
|
| |
| def update_resolution_display(img): |
| if img is None: |
| return gr.update(visible=False) |
| w, h = img.size |
| new_w = (w // 16) * 16 |
| new_h = (h // 16) * 16 |
| return gr.update( |
| value=f"📐 **Resolution:** Input: {w}×{h} → Output: {new_w}×{new_h}", |
| visible=True |
| ) |
|
|
| def category_changed(category): |
| if category in PROMPT_EXAMPLES: |
| return gr.update(choices=PROMPT_EXAMPLES[category], value=None) |
| return gr.update() |
|
|
| def example_selected(example): |
| return example if example else "" |
|
|
| image_input.change( |
| fn=update_resolution_display, |
| inputs=[image_input], |
| outputs=[resolution_info] |
| ) |
|
|
| category_dropdown.change( |
| fn=category_changed, |
| inputs=[category_dropdown], |
| outputs=[example_dropdown] |
| ) |
|
|
| example_dropdown.change( |
| fn=example_selected, |
| inputs=[example_dropdown], |
| outputs=[prompt_input] |
| ) |
|
|
| generate_btn.click( |
| fn=self._generate_video_handler, |
| inputs=[ |
| image_input, prompt_input, duration_slider, |
| steps_slider, fps_slider, expand_prompt, |
| randomize_seed, seed_input |
| ], |
| outputs=[video_output, final_prompt_output, seed_output] |
| ) |
|
|
| def _generate_video_handler( |
| self, |
| image: Image.Image, |
| prompt: str, |
| duration: float, |
| steps: int, |
| fps: int, |
| expand_prompt: bool, |
| randomize_seed: bool, |
| seed: int |
| ) -> Tuple[str, str, int]: |
| """Handler for video generation""" |
| if image is None: |
| return None, "Please upload an image", 0 |
|
|
| if not prompt.strip(): |
| return None, "Please provide a motion prompt", 0 |
|
|
| try: |
| video_path, final_prompt, seed_used = self.facade.generate_video_from_image( |
| image=image, |
| user_instruction=prompt, |
| duration_seconds=duration, |
| num_inference_steps=steps, |
| enable_prompt_expansion=expand_prompt, |
| randomize_seed=randomize_seed, |
| seed=seed |
| ) |
| return video_path, final_prompt, seed_used |
|
|
| except Exception as e: |
| logger.error(f"Video generation failed: {e}") |
| return None, f"Error: {str(e)}", 0 |
|
|
|
|
| def _create_background_tab(self): |
| """Create Background Generation tab (SceneWeaver functionality)""" |
| with gr.Row(): |
| |
| with gr.Column(scale=1, elem_classes="feature-card"): |
| gr.Markdown("### 📸 Upload & Configure") |
|
|
| gr.HTML(""" |
| <div class="quality-banner"> |
| <strong>💡 Best Results Tips:</strong><br> |
| • Clean portrait photos with simple backgrounds work best<br> |
| • Complex scenes (e.g., pets with grass) may need parameter adjustments<br> |
| • Use Advanced Options below to fine-tune edge blending |
| </div> |
| """) |
|
|
| bg_image_input = gr.Image( |
| label="Upload Your Image", |
| type="pil", |
| height=280 |
| ) |
|
|
| |
| template_dropdown = gr.Dropdown( |
| label="Scene Templates (24 curated scenes A-Z)", |
| choices=[""] + self.template_manager.get_template_choices_sorted(), |
| value="", |
| info="Optional: Select a preset or describe your own", |
| elem_classes=["template-dropdown"] |
| ) |
|
|
| bg_prompt_input = gr.Textbox( |
| label="Background Scene Description", |
| placeholder="Select a template above or describe your own scene...", |
| lines=3 |
| ) |
|
|
| combination_mode = gr.Dropdown( |
| label="Composition Mode", |
| choices=["center", "left_half", "right_half", "full"], |
| value="center", |
| info="center=Smart Center | full=Full Image" |
| ) |
|
|
| focus_mode = gr.Dropdown( |
| label="Focus Mode", |
| choices=["person", "scene"], |
| value="person", |
| info="person=Tight Crop | scene=Include Surrounding" |
| ) |
|
|
| with gr.Accordion("Advanced Options", open=False): |
| gr.HTML(""" |
| <div style="padding: 8px; background: #f0f4ff; border-radius: 6px; margin-bottom: 12px; font-size: 13px;"> |
| <strong>💡 When to Adjust:</strong><br> |
| • <strong>Enhance Dark Edges:</strong> Enable for images with dark/black backgrounds where foreground parts get lost.<br> |
| • <strong>Feather Radius:</strong> Use 5-10 for complex scenes with fine details (hair, fur, foliage). 0 = sharp edges for clean portraits.<br> |
| • <strong>Mask Preview:</strong> Check the "Mask Preview" tab after generation. White = kept, Black = replaced. |
| </div> |
| """) |
|
|
| enhance_dark_edges = gr.Checkbox( |
| label="🌙 Enhance Dark Edges", |
| value=False, |
| info="Enable if dark foreground parts blend into dark backgrounds" |
| ) |
| gr.HTML(""" |
| <div style="padding: 6px 8px; background: #fff3cd; border-radius: 4px; font-size: 11px; margin-bottom: 12px;"> |
| <strong>When to use:</strong> If mask preview shows gray areas where foreground should be white (e.g., dark hair/clothing on dark background). |
| Auto-detection is enabled by default, but this toggle forces stronger enhancement. |
| </div> |
| """) |
|
|
| feather_radius_slider = gr.Slider( |
| label="Feather Radius (Edge Softness)", |
| minimum=0, |
| maximum=20, |
| value=0, |
| step=1, |
| info="Softens mask edges. Try 5-10 if edges look harsh." |
| ) |
|
|
| bg_negative_prompt = gr.Textbox( |
| label="Negative Prompt", |
| value="blurry, low quality, distorted, people, characters", |
| lines=2, |
| info="Prevents unwanted elements in background" |
| ) |
|
|
| bg_steps_slider = gr.Slider( |
| label="Quality Steps", |
| minimum=15, |
| maximum=50, |
| value=25, |
| step=5, |
| info="Higher = better quality but slower" |
| ) |
|
|
| bg_guidance_slider = gr.Slider( |
| label="Guidance Scale", |
| minimum=5.0, |
| maximum=15.0, |
| value=7.5, |
| step=0.5, |
| info="How strictly to follow prompt" |
| ) |
|
|
| generate_bg_btn = gr.Button( |
| "🎨 Generate Background", |
| variant="primary", |
| elem_classes="primary-button", |
| size="lg" |
| ) |
|
|
| |
| with gr.Column(scale=2, elem_classes="feature-card"): |
| gr.Markdown("### 🎭 Results Gallery") |
|
|
| gr.HTML(""" |
| <div class="patience-banner"> |
| <strong>⏱️ First-time users:</strong> Initial model loading takes 30-60 seconds. |
| Subsequent generations are much faster (~30s). |
| </div> |
| """) |
|
|
| with gr.Tabs(): |
| with gr.TabItem("Final Result"): |
| bg_combined_output = gr.Image( |
| label="Your Generated Image", |
| elem_classes=["result-gallery"] |
| ) |
| with gr.TabItem("Background"): |
| bg_generated_output = gr.Image( |
| label="Generated Background", |
| elem_classes=["result-gallery"] |
| ) |
| with gr.TabItem("Original"): |
| bg_original_output = gr.Image( |
| label="Processed Original", |
| elem_classes=["result-gallery"] |
| ) |
| with gr.TabItem("Mask Preview"): |
| gr.HTML(""" |
| <div style="padding: 8px; background: #f0f4ff; border-radius: 6px; margin-bottom: 8px; font-size: 13px;"> |
| <strong>📐 How to Read:</strong> White = Original kept | Black = Background replaced<br> |
| Use this to diagnose edge quality. If edges are too harsh, increase Feather Radius. |
| </div> |
| """) |
| bg_mask_output = gr.Image( |
| label="Blending Mask", |
| elem_classes=["result-gallery"] |
| ) |
|
|
| bg_status_output = gr.Textbox( |
| label="Status", |
| value="Ready to create! Upload an image and describe your vision.", |
| interactive=False, |
| elem_classes=["status-panel"] |
| ) |
|
|
| with gr.Row(): |
| clear_bg_btn = gr.Button( |
| "Clear All", |
| elem_classes=["secondary-button"] |
| ) |
| memory_btn = gr.Button( |
| "Clean Memory", |
| elem_classes=["secondary-button"] |
| ) |
|
|
| |
| with gr.Accordion("🖌️ Touch Up (Remove Artifacts)", open=False) as touchup_accordion: |
| gr.HTML(""" |
| <div style="padding: 10px; background: #e8f4fd; border-radius: 6px; margin-bottom: 12px; font-size: 13px;"> |
| <strong>✨ How to Use Touch Up:</strong><br> |
| 1. After generating, if you see unwanted artifacts (gray edges, leftover objects)<br> |
| 2. Click "Load Result for Touch Up" to load the image<br> |
| 3. Use the brush to paint over areas you want to remove<br> |
| 4. Click "Remove & Fill" to replace painted areas with background |
| </div> |
| """) |
|
|
| |
| touchup_source_image = gr.State(value=None) |
| touchup_background_prompt = gr.State(value="") |
|
|
| load_touchup_btn = gr.Button( |
| "📥 Load Result for Touch Up", |
| elem_classes=["secondary-button"] |
| ) |
|
|
| touchup_editor = gr.ImageEditor( |
| label="Draw on areas to remove (use brush tool)", |
| type="pil", |
| height=400, |
| brush=gr.Brush( |
| colors=["#FF0000"], |
| default_color="#FF0000", |
| default_size=20 |
| ), |
| layers=False, |
| interactive=True, |
| visible=True |
| ) |
|
|
| with gr.Row(): |
| brush_size_slider = gr.Slider( |
| label="Brush Size", |
| minimum=5, |
| maximum=50, |
| value=20, |
| step=5, |
| scale=2 |
| ) |
| touchup_strength = gr.Slider( |
| label="Fill Strength", |
| minimum=0.8, |
| maximum=1.0, |
| value=0.99, |
| step=0.01, |
| scale=2, |
| info="Higher = more complete replacement" |
| ) |
|
|
| remove_fill_btn = gr.Button( |
| "🎨 Remove & Fill", |
| variant="primary", |
| elem_classes="primary-button" |
| ) |
|
|
| touchup_result = gr.Image( |
| label="Touch Up Result", |
| elem_classes=["result-gallery"] |
| ) |
|
|
| touchup_status = gr.Textbox( |
| label="Touch Up Status", |
| value="Load an image to start touch up.", |
| interactive=False |
| ) |
|
|
| |
| def apply_template(display_name: str, current_negative: str) -> Tuple[str, str, float]: |
| if not display_name: |
| return "", current_negative, 7.5 |
|
|
| template_key = self.template_manager.get_template_key_from_display(display_name) |
| if not template_key: |
| return "", current_negative, 7.5 |
|
|
| template = self.template_manager.get_template(template_key) |
| if template: |
| prompt = template.prompt |
| negative = self.template_manager.get_negative_prompt_for_template( |
| template_key, current_negative |
| ) |
| guidance = template.guidance_scale |
| return prompt, negative, guidance |
|
|
| return "", current_negative, 7.5 |
|
|
| template_dropdown.change( |
| fn=apply_template, |
| inputs=[template_dropdown, bg_negative_prompt], |
| outputs=[bg_prompt_input, bg_negative_prompt, bg_guidance_slider] |
| ) |
|
|
| generate_bg_btn.click( |
| fn=self._generate_background_handler, |
| inputs=[ |
| bg_image_input, bg_prompt_input, combination_mode, |
| focus_mode, bg_negative_prompt, bg_steps_slider, bg_guidance_slider, |
| feather_radius_slider, enhance_dark_edges |
| ], |
| outputs=[ |
| bg_combined_output, bg_generated_output, |
| bg_original_output, bg_mask_output, bg_status_output |
| ] |
| ) |
|
|
| clear_bg_btn.click( |
| fn=lambda: (None, None, None, None, "Ready to create!"), |
| outputs=[ |
| bg_combined_output, bg_generated_output, |
| bg_original_output, bg_mask_output, bg_status_output |
| ] |
| ) |
|
|
| memory_btn.click( |
| fn=lambda: self.background_engine._memory_cleanup() or "Memory cleaned!", |
| outputs=[bg_status_output] |
| ) |
|
|
| |
| def load_for_touchup(combined_image, prompt): |
| """Load the generated result into touch up editor""" |
| if combined_image is None: |
| return None, None, "", "Please generate a background first!" |
| return combined_image, combined_image, prompt, "✓ Image loaded! Use brush to paint areas to remove." |
|
|
| load_touchup_btn.click( |
| fn=load_for_touchup, |
| inputs=[bg_combined_output, bg_prompt_input], |
| outputs=[touchup_editor, touchup_source_image, touchup_background_prompt, touchup_status] |
| ) |
|
|
| remove_fill_btn.click( |
| fn=self._touchup_inpaint_handler, |
| inputs=[touchup_editor, touchup_background_prompt, touchup_strength], |
| outputs=[touchup_result, touchup_status] |
| ) |
|
|
| def _touchup_inpaint_handler( |
| self, |
| editor_data: dict, |
| background_prompt: str, |
| strength: float |
| ) -> Tuple[Optional[Image.Image], str]: |
| """Handler for touch up inpainting""" |
| if editor_data is None: |
| return None, "Please load an image first!" |
|
|
| try: |
| |
| |
| if isinstance(editor_data, dict): |
| base_image = editor_data.get("background") or editor_data.get("composite") |
| layers = editor_data.get("layers", []) |
|
|
| if base_image is None: |
| return None, "No image found in editor!" |
|
|
| |
| mask = self._extract_mask_from_editor(base_image, layers) |
|
|
| if mask is None or not self._has_painted_area(mask): |
| return None, "Please draw on areas you want to remove!" |
|
|
| else: |
| |
| return None, "Invalid editor data format!" |
|
|
| |
| if SPACES_AVAILABLE: |
| inpaint_fn = spaces.GPU(duration=60)(self._touchup_inpaint_core) |
| else: |
| inpaint_fn = self._touchup_inpaint_core |
|
|
| result = inpaint_fn(base_image, mask, background_prompt, strength) |
|
|
| if result["success"]: |
| return result["inpainted_image"], "✓ Touch up completed!" |
| else: |
| return None, f"Error: {result.get('error', 'Unknown error')}" |
|
|
| except Exception as e: |
| logger.error(f"Touch up failed: {e}") |
| return None, f"Error: {str(e)}" |
|
|
| def _extract_mask_from_editor(self, base_image: Image.Image, layers: list) -> Optional[Image.Image]: |
| """Extract painted mask from ImageEditor layers""" |
| import numpy as np |
|
|
| if not layers: |
| return None |
|
|
| |
| width, height = base_image.size |
| mask_array = np.zeros((height, width), dtype=np.uint8) |
|
|
| for layer in layers: |
| if layer is None: |
| continue |
|
|
| |
| if isinstance(layer, Image.Image): |
| layer_array = np.array(layer.convert('RGBA')) |
| else: |
| continue |
|
|
| |
| |
| if layer_array.shape[2] >= 4: |
| alpha = layer_array[:, :, 3] |
| |
| red = layer_array[:, :, 0] |
| |
| painted = (alpha > 50) | (red > 100) |
| mask_array[painted] = 255 |
|
|
| return Image.fromarray(mask_array, mode='L') |
|
|
| def _has_painted_area(self, mask: Image.Image) -> bool: |
| """Check if mask has any painted area""" |
| import numpy as np |
| mask_array = np.array(mask) |
| return np.sum(mask_array > 127) > 100 |
|
|
| def _touchup_inpaint_core( |
| self, |
| image: Image.Image, |
| mask: Image.Image, |
| prompt: str, |
| strength: float |
| ) -> dict: |
| """Core inpainting function""" |
| |
| inpaint_prompt = f"{prompt}, seamless, natural continuation, no artifacts" if prompt else "natural background, seamless continuation" |
|
|
| return self.background_engine.inpaint_region( |
| image=image, |
| mask=mask, |
| prompt=inpaint_prompt, |
| negative_prompt="blurry, artifacts, seams, inconsistent, unnatural", |
| num_inference_steps=20, |
| guidance_scale=7.5, |
| strength=float(strength) |
| ) |
|
|
| def _generate_background_handler( |
| self, |
| image: Image.Image, |
| prompt: str, |
| combination_mode: str, |
| focus_mode: str, |
| negative_prompt: str, |
| steps: int, |
| guidance: float, |
| feather_radius: int, |
| enhance_dark_edges: bool = False |
| ) -> Tuple[Optional[Image.Image], Optional[Image.Image], Optional[Image.Image], Optional[Image.Image], str]: |
| """Handler for background generation""" |
| if image is None: |
| return None, None, None, None, "Please upload an image to get started!" |
|
|
| if not prompt.strip(): |
| return None, None, None, None, "Please describe the background scene you'd like!" |
|
|
| try: |
| |
| if SPACES_AVAILABLE: |
| generate_fn = spaces.GPU(duration=60)(self._background_generate_core) |
| else: |
| generate_fn = self._background_generate_core |
|
|
| result = generate_fn( |
| image, prompt, combination_mode, focus_mode, |
| negative_prompt, steps, guidance, feather_radius, enhance_dark_edges |
| ) |
|
|
| if result["success"]: |
| return ( |
| result["combined_image"], |
| result["generated_scene"], |
| result["original_image"], |
| result["mask"], |
| "Image created successfully!" |
| ) |
| else: |
| error_msg = result.get("error", "Something went wrong") |
| return None, None, None, None, f"Error: {error_msg}" |
|
|
| except Exception as e: |
| logger.error(f"Background generation failed: {e}") |
| return None, None, None, None, f"Error: {str(e)}" |
|
|
| def _background_generate_core( |
| self, |
| image: Image.Image, |
| prompt: str, |
| combination_mode: str, |
| focus_mode: str, |
| negative_prompt: str, |
| steps: int, |
| guidance: float, |
| feather_radius: int, |
| enhance_dark_edges: bool = False |
| ) -> Dict[str, Any]: |
| """Core background generation with models""" |
| if not self.background_engine.is_initialized: |
| logger.info("Loading background generation models...") |
| self.background_engine.load_models() |
|
|
| result = self.background_engine.generate_and_combine( |
| original_image=image, |
| prompt=prompt, |
| combination_mode=combination_mode, |
| focus_mode=focus_mode, |
| negative_prompt=negative_prompt, |
| num_inference_steps=int(steps), |
| guidance_scale=float(guidance), |
| enable_prompt_enhancement=True, |
| feather_radius=int(feather_radius), |
| enhance_dark_edges=enhance_dark_edges |
| ) |
|
|
| return result |
|
|
| def _create_3d_tab(self): |
| """Create Style Transfer tab - converts images to various artistic styles""" |
| with gr.Row(): |
| |
| with gr.Column(scale=1, elem_classes="feature-card"): |
| gr.Markdown("### 🎨 AI Style Transfer") |
|
|
| |
| gr.HTML(""" |
| <div class="quality-banner"> |
| <strong>📖 Transform Your Photos</strong><br><br> |
| Convert your images into <strong>stunning artistic styles</strong>!<br><br> |
| <strong>🎨 Single Styles:</strong> Pure artistic transformations<br> |
| <strong>🎭 Style Blends:</strong> Unique combinations for distinctive looks<br><br> |
| <strong>💡 Tips:</strong><br> |
| • Use <strong>Seed</strong> to recreate the exact same result<br> |
| • Try different blends for unique artistic effects |
| </div> |
| """) |
|
|
| |
| gr.Markdown("#### Step 1: Upload Image") |
| style3d_image_input = gr.Image( |
| label="Upload Your Image", |
| type="pil", |
| height=280 |
| ) |
|
|
| |
| gr.Markdown("#### Step 2: Choose Style") |
|
|
| |
| is_blend_mode = gr.State(value=False) |
|
|
| with gr.Tabs() as style_tabs: |
| with gr.TabItem("🎨 Single Styles", id="single_tab") as single_tab: |
| style_dropdown = gr.Dropdown( |
| choices=self.style_engine.get_style_choices(), |
| value="🎬 3D Cartoon", |
| label="Art Style", |
| info="Select a single artistic style" |
| ) |
|
|
| style_strength = gr.Slider( |
| label="Style Strength", |
| minimum=0.3, |
| maximum=0.7, |
| value=0.50, |
| step=0.05, |
| info="Lower = keep more original | Higher = stronger style (0.45-0.55 recommended)" |
| ) |
|
|
| with gr.TabItem("🎭 Style Blends", id="blend_tab") as blend_tab: |
| blend_dropdown = gr.Dropdown( |
| choices=self.style_engine.get_blend_choices(), |
| value=self.style_engine.get_blend_choices()[0] if self.style_engine.get_blend_choices() else None, |
| label="Blend Preset", |
| info="Pre-configured style combinations" |
| ) |
| gr.HTML(""" |
| <div style="padding: 8px; background: #f0f4ff; border-radius: 6px; font-size: 12px; margin-top: 8px;"> |
| <strong>Available Blends:</strong><br> |
| • 🎭 3D Anime Fusion - 3D + Anime linework<br> |
| • 🌈 Dreamy Watercolor - Fantasy + Watercolor<br> |
| • 📖 Anime Storybook - Anime + Fantasy<br> |
| • 👑 Renaissance Portrait - Classical oil painting<br> |
| • 🕹️ Retro Game Art - Enhanced pixel art |
| </div> |
| """) |
|
|
| |
| face_restore = gr.Checkbox( |
| label="🛡️ Face Restore (Preserve Identity)", |
| value=False, |
| info="Enable to better preserve facial features and prevent identity changes" |
| ) |
| gr.HTML(""" |
| <div style="padding: 6px 8px; background: #fff3cd; border-radius: 4px; font-size: 11px; margin-top: 4px;"> |
| <strong>💡 When to use:</strong> Enable if the style changes the person's face, age, or ethnicity too much. |
| Auto-reduces strength to preserve original features. |
| </div> |
| """) |
|
|
| with gr.Accordion("⚙️ Advanced Settings", open=False): |
| guidance_scale = gr.Slider( |
| label="Guidance Scale", |
| minimum=5.0, |
| maximum=12.0, |
| value=7.5, |
| step=0.5, |
| info="How closely to follow the style" |
| ) |
|
|
| num_steps = gr.Slider( |
| label="Quality Steps", |
| minimum=20, |
| maximum=50, |
| value=30, |
| step=5, |
| info="More steps = better quality but slower" |
| ) |
|
|
| custom_prompt = gr.Textbox( |
| label="Additional Description (optional)", |
| placeholder="e.g., smiling, dramatic lighting, vibrant colors...", |
| lines=2 |
| ) |
|
|
| gr.Markdown("##### 🎲 Seed Control") |
| randomize_seed = gr.Checkbox( |
| label="Randomize Seed", |
| value=True, |
| info="Uncheck to use manual seed for reproducible results" |
| ) |
|
|
| seed_input = gr.Number( |
| label="Manual Seed", |
| value=42, |
| precision=0, |
| info="Use same seed to reproduce exact results" |
| ) |
|
|
| |
| gr.Markdown("#### Step 3: Generate") |
|
|
| gr.HTML(""" |
| <div class="patience-banner"> |
| <strong>⏱️ Generation Time:</strong> ~20-30 seconds. |
| First-time model loading may take 30-60 seconds. |
| </div> |
| """) |
|
|
| generate_style_btn = gr.Button( |
| "🎨 Transform Image", |
| variant="primary", |
| elem_classes="primary-button", |
| size="lg" |
| ) |
|
|
| |
| with gr.Column(scale=1, elem_classes="feature-card"): |
| gr.Markdown("### 📤 Results") |
|
|
| with gr.Tabs(): |
| with gr.TabItem("Stylized Result"): |
| style3d_output = gr.Image( |
| label="Stylized Result", |
| elem_classes=["result-gallery"] |
| ) |
|
|
| with gr.TabItem("Original"): |
| style3d_original = gr.Image( |
| label="Original Image", |
| elem_classes=["result-gallery"] |
| ) |
|
|
| with gr.TabItem("Comparison"): |
| with gr.Row(): |
| style3d_compare_original = gr.Image( |
| label="Before", |
| elem_classes=["result-gallery"] |
| ) |
| style3d_compare_result = gr.Image( |
| label="After", |
| elem_classes=["result-gallery"] |
| ) |
|
|
| with gr.Row(): |
| style3d_status_output = gr.Textbox( |
| label="Status", |
| value="Ready! Upload an image and select a style to transform.", |
| interactive=False, |
| elem_classes=["status-panel"], |
| scale=3 |
| ) |
| seed_output = gr.Number( |
| label="Seed Used", |
| value=0, |
| interactive=False, |
| precision=0, |
| scale=1 |
| ) |
|
|
| with gr.Row(): |
| clear_style_btn = gr.Button( |
| "Clear All", |
| elem_classes=["secondary-button"] |
| ) |
| memory_style_btn = gr.Button( |
| "Clean Memory", |
| elem_classes=["secondary-button"] |
| ) |
|
|
| |
| single_tab.select( |
| fn=lambda: False, |
| inputs=[], |
| outputs=[is_blend_mode] |
| ) |
|
|
| blend_tab.select( |
| fn=lambda: True, |
| inputs=[], |
| outputs=[is_blend_mode] |
| ) |
|
|
| generate_style_btn.click( |
| fn=self._generate_3d_style_handler, |
| inputs=[ |
| style3d_image_input, style_dropdown, blend_dropdown, is_blend_mode, |
| style_strength, guidance_scale, num_steps, custom_prompt, |
| randomize_seed, seed_input, face_restore |
| ], |
| outputs=[ |
| style3d_output, style3d_original, |
| style3d_compare_original, style3d_compare_result, |
| style3d_status_output, seed_output |
| ] |
| ) |
|
|
| clear_style_btn.click( |
| fn=lambda: (None, None, None, None, "Ready! Upload an image and select a style to transform.", 0), |
| outputs=[ |
| style3d_output, style3d_original, |
| style3d_compare_original, style3d_compare_result, |
| style3d_status_output, seed_output |
| ] |
| ) |
|
|
| memory_style_btn.click( |
| fn=self._cleanup_3d_memory, |
| outputs=[style3d_status_output] |
| ) |
|
|
| def _generate_3d_style_handler( |
| self, |
| image: Image.Image, |
| style_choice: str, |
| blend_choice: str, |
| is_blend_mode: bool, |
| strength: float, |
| guidance_scale: float, |
| num_steps: int, |
| custom_prompt: str, |
| randomize_seed: bool, |
| manual_seed: int, |
| face_restore: bool = False |
| ) -> Tuple[Optional[Image.Image], Optional[Image.Image], Optional[Image.Image], Optional[Image.Image], str, int]: |
| """Handler for style transfer generation""" |
| if image is None: |
| return None, None, None, None, "Please upload an image first!", 0 |
|
|
| try: |
| |
| if is_blend_mode: |
| style_key = self.style_engine.get_blend_key_from_choice(blend_choice) |
| is_blend = True |
| else: |
| style_key = self.style_engine.get_style_key_from_choice(style_choice) |
| is_blend = False |
|
|
| |
| seed = -1 if randomize_seed else int(manual_seed) |
|
|
| if SPACES_AVAILABLE: |
| generate_fn = spaces.GPU(duration=120)(self._3d_style_generate_core) |
| else: |
| generate_fn = self._3d_style_generate_core |
|
|
| result = generate_fn( |
| image, style_key, is_blend, strength, |
| guidance_scale, num_steps, custom_prompt, seed, face_restore |
| ) |
|
|
| if result["success"]: |
| stylized = result["stylized_image"] |
| style_name = result.get("style_name", "Style") |
| seed_used = result.get("seed_used", 0) |
| return ( |
| stylized, |
| image, |
| image, |
| stylized, |
| f"✓ {style_name} completed! (seed: {seed_used})", |
| seed_used |
| ) |
| else: |
| error_msg = result.get("error", "Unknown error") |
| return None, None, None, None, f"Error: {error_msg}", 0 |
|
|
| except Exception as e: |
| logger.error(f"Style generation failed: {e}") |
| return None, None, None, None, f"Error: {str(e)}", 0 |
|
|
| def _3d_style_generate_core( |
| self, |
| image: Image.Image, |
| style_key: str, |
| is_blend: bool, |
| strength: float, |
| guidance_scale: float, |
| num_steps: int, |
| custom_prompt: str, |
| seed: int, |
| face_restore: bool = False |
| ) -> dict: |
| """Core style transfer generation""" |
| return self.style_engine.generate_all_outputs( |
| image=image, |
| style_key=style_key, |
| strength=float(strength), |
| guidance_scale=float(guidance_scale), |
| num_inference_steps=int(num_steps), |
| custom_prompt=custom_prompt if custom_prompt else "", |
| seed=seed, |
| is_blend=is_blend, |
| face_restore=face_restore |
| ) |
|
|
| def _cleanup_3d_memory(self) -> str: |
| """Clean up 3D engine memory""" |
| self.style_engine.unload_model() |
| return "Memory cleaned!" |
|
|
|
|