From 3b2cfada78afd930369d024f95dfd9ebd497037a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Thu, 27 Nov 2025 12:51:33 +0100 Subject: [PATCH] feat: add video support to history tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Video processing now saves to history database - History gallery displays both images and videos - Added type filter dropdown (All/Images/Videos) - Updated search and details to work with videos - Video entries show codec, frame count in details 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/ui/components/history_tab.py | 132 ++++++++++++++++++++++--------- src/ui/components/video_tab.py | 18 +++++ 2 files changed, 111 insertions(+), 39 deletions(-) diff --git a/src/ui/components/history_tab.py b/src/ui/components/history_tab.py index 7e04a82..016fc69 100644 --- a/src/ui/components/history_tab.py +++ b/src/ui/components/history_tab.py @@ -11,17 +11,30 @@ from src.storage.history import history_manager, HistoryItem logger = logging.getLogger(__name__) -def get_history_gallery() -> list[tuple[str, str]]: +def get_history_gallery(type_filter: str = "All") -> 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 + # Apply type filter + if type_filter == "Images" and item.type != "image": + continue + if type_filter == "Videos" and item.type != "video": + continue + output_path = Path(item.output_path) - if output_path.exists() and item.type == "image": + if not output_path.exists(): + continue + + # For images, show the image directly + if item.type == "image": caption = f"{item.output_filename}\n{item.output_width}x{item.output_height}" gallery_items.append((str(output_path), caption)) + # 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)) return gallery_items @@ -41,79 +54,111 @@ def get_history_stats() -> str: ) -def search_history(query: str) -> list[tuple[str, str]]: +def search_history(query: str, type_filter: str = "All") -> list[tuple[str, str]]: """Search history by filename.""" if not query: - return get_history_gallery() + return get_history_gallery(type_filter) items = history_manager.search(query) gallery_items = [] for item in items: + # Apply type filter + if type_filter == "Images" and item.type != "image": + continue + if type_filter == "Videos" and item.type != "video": + continue + output_path = Path(item.output_path) - if output_path.exists() and item.type == "image": + if not output_path.exists(): + continue + + if item.type == "image": caption = f"{item.output_filename}\n{item.output_width}x{item.output_height}" gallery_items.append((str(output_path), caption)) + elif item.type == "video": + 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]: +def get_item_details(evt: gr.SelectData, type_filter: str = "All") -> 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()] + # 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(image_items): + if evt.index >= len(filtered_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) + item = filtered_items[evt.index] + # Load images for slider (only for images) slider_images = None - if input_path.exists() and output_path.exists(): - slider_images = (str(input_path), str(output_path)) + 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)) # 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')}" - ) + 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')}" return slider_images, details -def delete_selected(evt: gr.SelectData) -> tuple[list, str]: +def delete_selected(evt: gr.SelectData, type_filter: str = "All") -> tuple[list, str]: """Delete selected history item.""" if evt.index is None: - return get_history_gallery(), "No item selected" + return get_history_gallery(type_filter), "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" + # 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) - item = image_items[evt.index] + if evt.index >= len(filtered_items): + return get_history_gallery(type_filter), "Item not found" + + item = filtered_items[evt.index] history_manager.delete_item(item.id) - return get_history_gallery(), f"Deleted: {item.output_filename}" + return get_history_gallery(type_filter), f"Deleted: {item.output_filename}" def create_history_tab(): @@ -186,17 +231,25 @@ def create_history_tab(): # Event handlers search_box.change( fn=search_history, - inputs=[search_box], + inputs=[search_box, filter_dropdown], + outputs=[gallery], + ) + + filter_dropdown.change( + fn=get_history_gallery, + inputs=[filter_dropdown], outputs=[gallery], ) gallery.select( fn=get_item_details, + inputs=[filter_dropdown], outputs=[comparison_slider, item_details], ) delete_btn.click( - fn=lambda: (get_history_gallery(), get_history_stats()), + fn=lambda f: (get_history_gallery(f), get_history_stats()), + inputs=[filter_dropdown], outputs=[gallery, stats_display], ) @@ -205,4 +258,5 @@ def create_history_tab(): "comparison_slider": comparison_slider, "item_details": item_details, "stats_display": stats_display, + "filter_dropdown": filter_dropdown, } diff --git a/src/ui/components/video_tab.py b/src/ui/components/video_tab.py index a465341..daf5b0e 100644 --- a/src/ui/components/video_tab.py +++ b/src/ui/components/video_tab.py @@ -18,6 +18,7 @@ from src.video.extractor import VideoExtractor, get_video_info from src.video.encoder import encode_video from src.video.audio import extract_audio, has_audio_stream from src.video.checkpoint import checkpoint_manager +from src.storage.history import history_manager logger = logging.getLogger(__name__) @@ -209,6 +210,23 @@ def process_video( output_size = output_path.stat().st_size / (1024 * 1024) # MB fps_actual = metadata.total_frames / elapsed + # Save to history + history_manager.add_item( + type="video", + input_path=str(video_path), + output_path=str(output_path), + model=model_name, + scale=scale, + face_enhance=face_enhance, + video_codec=codec, + processing_time_seconds=elapsed, + input_width=metadata.width, + input_height=metadata.height, + output_width=metadata.width * scale, + output_height=metadata.height * scale, + frames_count=metadata.total_frames, + ) + status = ( f"Completed in {elapsed/60:.1f} minutes ({fps_actual:.2f} fps)\n" f"Output: {output_path.name} ({output_size:.1f} MB)"