2025-11-27 11:56:59 +01:00
|
|
|
"""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__)
|
|
|
|
|
|
|
|
|
|
|
2025-11-27 12:51:33 +01:00
|
|
|
def get_history_gallery(type_filter: str = "All") -> list[tuple[str, str]]:
|
2025-11-27 11:56:59 +01:00
|
|
|
"""Get history items for gallery display."""
|
|
|
|
|
items = history_manager.get_recent(limit=100)
|
|
|
|
|
|
|
|
|
|
gallery_items = []
|
|
|
|
|
for item in items:
|
2025-11-27 12:51:33 +01:00
|
|
|
# Apply type filter
|
|
|
|
|
if type_filter == "Images" and item.type != "image":
|
|
|
|
|
continue
|
|
|
|
|
if type_filter == "Videos" and item.type != "video":
|
|
|
|
|
continue
|
|
|
|
|
|
2025-11-27 11:56:59 +01:00
|
|
|
output_path = Path(item.output_path)
|
2025-11-27 12:51:33 +01:00
|
|
|
if not output_path.exists():
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# For images, show the image directly
|
|
|
|
|
if item.type == "image":
|
2025-11-27 11:56:59 +01:00
|
|
|
caption = f"{item.output_filename}\n{item.output_width}x{item.output_height}"
|
|
|
|
|
gallery_items.append((str(output_path), caption))
|
2025-11-27 12:51:33 +01:00
|
|
|
# For videos, show output path (Gradio Gallery will handle video thumbnails)
|
|
|
|
|
elif item.type == "video":
|
|
|
|
|
caption = f"🎬 {item.output_filename}\n{item.output_width}x{item.output_height}"
|
|
|
|
|
gallery_items.append((str(output_path), caption))
|
2025-11-27 11:56:59 +01:00
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2025-11-27 12:51:33 +01:00
|
|
|
def search_history(query: str, type_filter: str = "All") -> list[tuple[str, str]]:
|
2025-11-27 11:56:59 +01:00
|
|
|
"""Search history by filename."""
|
|
|
|
|
if not query:
|
2025-11-27 12:51:33 +01:00
|
|
|
return get_history_gallery(type_filter)
|
2025-11-27 11:56:59 +01:00
|
|
|
|
|
|
|
|
items = history_manager.search(query)
|
|
|
|
|
|
|
|
|
|
gallery_items = []
|
|
|
|
|
for item in items:
|
2025-11-27 12:51:33 +01:00
|
|
|
# Apply type filter
|
|
|
|
|
if type_filter == "Images" and item.type != "image":
|
|
|
|
|
continue
|
|
|
|
|
if type_filter == "Videos" and item.type != "video":
|
|
|
|
|
continue
|
|
|
|
|
|
2025-11-27 11:56:59 +01:00
|
|
|
output_path = Path(item.output_path)
|
2025-11-27 12:51:33 +01:00
|
|
|
if not output_path.exists():
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if item.type == "image":
|
2025-11-27 11:56:59 +01:00
|
|
|
caption = f"{item.output_filename}\n{item.output_width}x{item.output_height}"
|
|
|
|
|
gallery_items.append((str(output_path), caption))
|
2025-11-27 12:51:33 +01:00
|
|
|
elif item.type == "video":
|
|
|
|
|
caption = f"🎬 {item.output_filename}\n{item.output_width}x{item.output_height}"
|
|
|
|
|
gallery_items.append((str(output_path), caption))
|
2025-11-27 11:56:59 +01:00
|
|
|
|
|
|
|
|
return gallery_items
|
|
|
|
|
|
|
|
|
|
|
2025-11-27 12:51:33 +01:00
|
|
|
def get_item_details(evt: gr.SelectData, type_filter: str = "All") -> tuple[Optional[tuple], str]:
|
2025-11-27 11:56:59 +01:00
|
|
|
"""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)
|
|
|
|
|
|
2025-11-27 12:51:33 +01:00
|
|
|
# Filter items same as gallery
|
|
|
|
|
filtered_items = []
|
|
|
|
|
for item in items:
|
|
|
|
|
if type_filter == "Images" and item.type != "image":
|
|
|
|
|
continue
|
|
|
|
|
if type_filter == "Videos" and item.type != "video":
|
|
|
|
|
continue
|
|
|
|
|
if Path(item.output_path).exists():
|
|
|
|
|
filtered_items.append(item)
|
|
|
|
|
|
|
|
|
|
if evt.index >= len(filtered_items):
|
2025-11-27 11:56:59 +01:00
|
|
|
return None, "Item not found"
|
|
|
|
|
|
2025-11-27 12:51:33 +01:00
|
|
|
item = filtered_items[evt.index]
|
2025-11-27 11:56:59 +01:00
|
|
|
|
2025-11-27 12:51:33 +01:00
|
|
|
# Load images for slider (only for images)
|
2025-11-27 11:56:59 +01:00
|
|
|
slider_images = None
|
2025-11-27 12:51:33 +01:00
|
|
|
if item.type == "image":
|
|
|
|
|
input_path = Path(item.input_path)
|
|
|
|
|
output_path = Path(item.output_path)
|
|
|
|
|
if input_path.exists() and output_path.exists():
|
|
|
|
|
slider_images = (str(input_path), str(output_path))
|
2025-11-27 11:56:59 +01:00
|
|
|
|
|
|
|
|
# Format details
|
2025-11-27 12:51:33 +01:00
|
|
|
details = f"**{item.output_filename}**\n\n"
|
|
|
|
|
details += f"- **Type:** {item.type.title()}\n"
|
|
|
|
|
details += f"- **Model:** {item.model}\n"
|
|
|
|
|
details += f"- **Scale:** {item.scale}x\n"
|
|
|
|
|
details += f"- **Face Enhancement:** {'Yes' if item.face_enhance else 'No'}\n"
|
|
|
|
|
|
|
|
|
|
if item.type == "video" and item.video_codec:
|
|
|
|
|
details += f"- **Codec:** {item.video_codec}\n"
|
|
|
|
|
if item.frames_count:
|
|
|
|
|
details += f"- **Frames:** {item.frames_count}\n"
|
|
|
|
|
|
|
|
|
|
details += f"- **Input:** {item.input_width}x{item.input_height}\n"
|
|
|
|
|
details += f"- **Output:** {item.output_width}x{item.output_height}\n"
|
|
|
|
|
details += f"- **Processing Time:** {item.processing_time_seconds:.1f}s\n"
|
|
|
|
|
details += f"- **Input Size:** {item.input_size_bytes/1024:.1f} KB\n"
|
|
|
|
|
details += f"- **Output Size:** {item.output_size_bytes/1024/1024:.1f} MB\n" if item.output_size_bytes > 1024*1024 else f"- **Output Size:** {item.output_size_bytes/1024:.1f} KB\n"
|
|
|
|
|
details += f"- **Created:** {item.created_at.strftime('%Y-%m-%d %H:%M')}"
|
2025-11-27 11:56:59 +01:00
|
|
|
|
|
|
|
|
return slider_images, details
|
|
|
|
|
|
|
|
|
|
|
2025-11-27 12:51:33 +01:00
|
|
|
def delete_selected(evt: gr.SelectData, type_filter: str = "All") -> tuple[list, str]:
|
2025-11-27 11:56:59 +01:00
|
|
|
"""Delete selected history item."""
|
|
|
|
|
if evt.index is None:
|
2025-11-27 12:51:33 +01:00
|
|
|
return get_history_gallery(type_filter), "No item selected"
|
2025-11-27 11:56:59 +01:00
|
|
|
|
|
|
|
|
items = history_manager.get_recent(limit=100)
|
|
|
|
|
|
2025-11-27 12:51:33 +01:00
|
|
|
# Filter items same as gallery
|
|
|
|
|
filtered_items = []
|
|
|
|
|
for item in items:
|
|
|
|
|
if type_filter == "Images" and item.type != "image":
|
|
|
|
|
continue
|
|
|
|
|
if type_filter == "Videos" and item.type != "video":
|
|
|
|
|
continue
|
|
|
|
|
if Path(item.output_path).exists():
|
|
|
|
|
filtered_items.append(item)
|
|
|
|
|
|
|
|
|
|
if evt.index >= len(filtered_items):
|
|
|
|
|
return get_history_gallery(type_filter), "Item not found"
|
2025-11-27 11:56:59 +01:00
|
|
|
|
2025-11-27 12:51:33 +01:00
|
|
|
item = filtered_items[evt.index]
|
2025-11-27 11:56:59 +01:00
|
|
|
history_manager.delete_item(item.id)
|
|
|
|
|
|
2025-11-27 12:51:33 +01:00
|
|
|
return get_history_gallery(type_filter), f"Deleted: {item.output_filename}"
|
2025-11-27 11:56:59 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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...",
|
2025-11-27 13:05:41 +01:00
|
|
|
scale=3,
|
2025-11-27 11:56:59 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
filter_dropdown = gr.Dropdown(
|
|
|
|
|
choices=["All", "Images", "Videos"],
|
|
|
|
|
value="All",
|
|
|
|
|
label="Filter",
|
|
|
|
|
scale=1,
|
|
|
|
|
)
|
|
|
|
|
|
2025-11-27 13:05:41 +01:00
|
|
|
refresh_btn = gr.Button("Refresh", variant="secondary", scale=1)
|
|
|
|
|
|
2025-11-27 11:56:59 +01:00
|
|
|
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,
|
2025-11-27 12:51:33 +01:00
|
|
|
inputs=[search_box, filter_dropdown],
|
|
|
|
|
outputs=[gallery],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
filter_dropdown.change(
|
|
|
|
|
fn=get_history_gallery,
|
|
|
|
|
inputs=[filter_dropdown],
|
2025-11-27 11:56:59 +01:00
|
|
|
outputs=[gallery],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
gallery.select(
|
|
|
|
|
fn=get_item_details,
|
2025-11-27 12:51:33 +01:00
|
|
|
inputs=[filter_dropdown],
|
2025-11-27 11:56:59 +01:00
|
|
|
outputs=[comparison_slider, item_details],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
delete_btn.click(
|
2025-11-27 12:51:33 +01:00
|
|
|
fn=lambda f: (get_history_gallery(f), get_history_stats()),
|
|
|
|
|
inputs=[filter_dropdown],
|
2025-11-27 11:56:59 +01:00
|
|
|
outputs=[gallery, stats_display],
|
|
|
|
|
)
|
|
|
|
|
|
2025-11-27 13:05:41 +01:00
|
|
|
refresh_btn.click(
|
|
|
|
|
fn=lambda f: (get_history_gallery(f), get_history_stats()),
|
|
|
|
|
inputs=[filter_dropdown],
|
|
|
|
|
outputs=[gallery, stats_display],
|
|
|
|
|
)
|
|
|
|
|
|
2025-11-27 11:56:59 +01:00
|
|
|
return {
|
|
|
|
|
"gallery": gallery,
|
|
|
|
|
"comparison_slider": comparison_slider,
|
|
|
|
|
"item_details": item_details,
|
|
|
|
|
"stats_display": stats_display,
|
2025-11-27 12:51:33 +01:00
|
|
|
"filter_dropdown": filter_dropdown,
|
2025-11-27 13:05:41 +01:00
|
|
|
"refresh_btn": refresh_btn,
|
2025-11-27 11:56:59 +01:00
|
|
|
}
|