feat: initial implementation of Real-ESRGAN Web UI

Full-featured Gradio 6.0+ web interface for Real-ESRGAN image/video
upscaling, optimized for RTX 4090 (24GB VRAM).

Features:
- Image upscaling with before/after comparison (ImageSlider)
- Video upscaling with progress tracking and checkpoint/resume
- Face enhancement via GFPGAN integration
- Multiple codecs: H.264, H.265, AV1 (with NVENC support)
- Batch processing queue with SQLite persistence
- Processing history gallery
- Custom dark theme
- Auto-download of model weights

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-27 11:56:59 +01:00
commit a6d20cf087
28 changed files with 4273 additions and 0 deletions

View File

@@ -0,0 +1,208 @@
"""History gallery tab component."""
import logging
from pathlib import Path
from typing import Optional
import gradio as gr
from src.storage.history import history_manager, HistoryItem
logger = logging.getLogger(__name__)
def get_history_gallery() -> list[tuple[str, str]]:
"""Get history items for gallery display."""
items = history_manager.get_recent(limit=100)
gallery_items = []
for item in items:
# Use output path for display
output_path = Path(item.output_path)
if output_path.exists() and item.type == "image":
caption = f"{item.output_filename}\n{item.output_width}x{item.output_height}"
gallery_items.append((str(output_path), caption))
return gallery_items
def get_history_stats() -> str:
"""Get history statistics."""
stats = history_manager.get_statistics()
return (
f"**History Statistics**\n\n"
f"- Total Items: {stats['total_items']}\n"
f"- Images: {stats['images']}\n"
f"- Videos: {stats['videos']}\n"
f"- Total Processing Time: {stats['total_processing_time']/60:.1f} min\n"
f"- Total Input Size: {stats['total_input_size_mb']:.1f} MB\n"
f"- Total Output Size: {stats['total_output_size_mb']:.1f} MB"
)
def search_history(query: str) -> list[tuple[str, str]]:
"""Search history by filename."""
if not query:
return get_history_gallery()
items = history_manager.search(query)
gallery_items = []
for item in items:
output_path = Path(item.output_path)
if output_path.exists() and item.type == "image":
caption = f"{item.output_filename}\n{item.output_width}x{item.output_height}"
gallery_items.append((str(output_path), caption))
return gallery_items
def get_item_details(evt: gr.SelectData) -> tuple[Optional[tuple], str]:
"""Get details for selected history item."""
if evt.index is None:
return None, "Select an item to view details"
items = history_manager.get_recent(limit=100)
# Filter to only images (same as gallery)
image_items = [i for i in items if i.type == "image" and Path(i.output_path).exists()]
if evt.index >= len(image_items):
return None, "Item not found"
item = image_items[evt.index]
# Load images for slider
input_path = Path(item.input_path)
output_path = Path(item.output_path)
slider_images = None
if input_path.exists() and output_path.exists():
slider_images = (str(input_path), str(output_path))
# Format details
details = (
f"**{item.output_filename}**\n\n"
f"- **Type:** {item.type.title()}\n"
f"- **Model:** {item.model}\n"
f"- **Scale:** {item.scale}x\n"
f"- **Face Enhancement:** {'Yes' if item.face_enhance else 'No'}\n"
f"- **Input:** {item.input_width}x{item.input_height}\n"
f"- **Output:** {item.output_width}x{item.output_height}\n"
f"- **Processing Time:** {item.processing_time_seconds:.1f}s\n"
f"- **Input Size:** {item.input_size_bytes/1024:.1f} KB\n"
f"- **Output Size:** {item.output_size_bytes/1024:.1f} KB\n"
f"- **Created:** {item.created_at.strftime('%Y-%m-%d %H:%M')}"
)
return slider_images, details
def delete_selected(evt: gr.SelectData) -> tuple[list, str]:
"""Delete selected history item."""
if evt.index is None:
return get_history_gallery(), "No item selected"
items = history_manager.get_recent(limit=100)
image_items = [i for i in items if i.type == "image" and Path(i.output_path).exists()]
if evt.index >= len(image_items):
return get_history_gallery(), "Item not found"
item = image_items[evt.index]
history_manager.delete_item(item.id)
return get_history_gallery(), f"Deleted: {item.output_filename}"
def create_history_tab():
"""Create the history gallery tab component."""
with gr.Tab("History", id="history-tab"):
gr.Markdown("## Processing History")
with gr.Row():
# Search
search_box = gr.Textbox(
label="Search",
placeholder="Search by filename...",
scale=2,
)
filter_dropdown = gr.Dropdown(
choices=["All", "Images", "Videos"],
value="All",
label="Filter",
scale=1,
)
with gr.Row():
# Gallery
with gr.Column(scale=2):
gallery = gr.Gallery(
value=get_history_gallery(),
label="History",
columns=4,
object_fit="cover",
height=400,
allow_preview=True,
)
# Details panel
with gr.Column(scale=1):
stats_display = gr.Markdown(
get_history_stats(),
elem_classes=["info-card"],
)
# Selected item details
with gr.Row():
with gr.Column(scale=2):
try:
from gradio_imageslider import ImageSlider
comparison_slider = ImageSlider(
label="Before / After",
type="filepath",
)
except ImportError:
comparison_slider = gr.Image(
label="Selected Image",
type="filepath",
)
with gr.Column(scale=1):
item_details = gr.Markdown(
"Select an item to view details",
elem_classes=["info-card"],
)
with gr.Row():
download_btn = gr.Button("Download", variant="secondary")
delete_btn = gr.Button("Delete", variant="stop")
status_text = gr.Markdown("")
# Event handlers
search_box.change(
fn=search_history,
inputs=[search_box],
outputs=[gallery],
)
gallery.select(
fn=get_item_details,
outputs=[comparison_slider, item_details],
)
delete_btn.click(
fn=lambda: (get_history_gallery(), get_history_stats()),
outputs=[gallery, stats_display],
)
return {
"gallery": gallery,
"comparison_slider": comparison_slider,
"item_details": item_details,
"stats_display": stats_display,
}