""" VSE Panel for Face Mask controls. Provides a sidebar panel in the Video Sequence Editor for controlling mask generation and blur application. """ import os import bpy from bpy.types import Panel from ..core.async_bake_generator import get_bake_generator from ..core.async_generator import get_generator from ..core.batch_processor import get_batch_processor from ..core.utils import ( get_server_status, get_cache_info, format_size, check_detection_cache, ) class SEQUENCER_PT_face_mask(Panel): """Panel for face mask blur controls.""" bl_label = "Face Mask" bl_idname = "SEQUENCER_PT_face_mask" bl_space_type = 'SEQUENCE_EDITOR' bl_region_type = 'UI' bl_category = "Face Mask" def draw(self, context): layout = self.layout scene = context.scene wm = context.window_manager seq_editor = context.scene.sequence_editor # Note: Blender 5.0 uses 'strips' instead of 'sequences' batch = get_batch_processor() generator = get_generator() bake_generator = get_bake_generator() # Batch progress (highest priority) if batch.is_running: self._draw_batch_progress(layout, wm, batch, generator, bake_generator) return # Show progress if generating masks if generator.is_running: self._draw_progress(layout, wm, generator) return # Show progress if baking blur if bake_generator.is_running: self._draw_bake_progress(layout, wm, bake_generator) return # Show primary controls first (top priority in UI) if seq_editor and seq_editor.active_strip: strip = seq_editor.active_strip if strip.type in {'MOVIE', 'IMAGE'}: self._draw_generation_controls(layout, context, strip) self._draw_blur_controls(layout, context, strip) else: layout.label(text="Select a video or image strip") else: layout.label(text="No strip selected") layout.separator() # Secondary sections self._draw_parameters(layout, scene) self._draw_server_status(layout) self._draw_cache_info(layout, context, seq_editor) self._draw_batch_controls(layout, context, seq_editor) def _draw_parameters(self, layout, scene): """Draw detection parameters.""" box = layout.box() box.label(text="Parameters", icon='PREFERENCES') col = box.column(align=True) col.prop(scene, "facemask_conf_threshold") col.prop(scene, "facemask_iou_threshold") def _draw_server_status(self, layout): """Draw server status and GPU info.""" box = layout.box() box.label(text="Server Status", icon='SYSTEM') status = get_server_status() # Server status row = box.row() if status['running']: row.label(text="Server:", icon='CHECKMARK') row.label(text="Running") else: row.label(text="Server:", icon='ERROR') row.label(text="Stopped") # GPU status if status['running']: row = box.row() if status['gpu_available']: row.label(text="GPU:", icon='CHECKMARK') gpu_name = status['gpu_device'] or "Available" # Truncate long GPU names if len(gpu_name) > 25: gpu_name = gpu_name[:22] + "..." row.label(text=gpu_name) else: row.label(text="GPU:", icon='ERROR') row.label(text="Not Available") def _draw_cache_info(self, layout, context, seq_editor): """Draw cache information and clear button.""" box = layout.box() box.label(text="Cache", icon='FILE_CACHE') # Get cache info if seq_editor and seq_editor.active_strip: strip_name = seq_editor.active_strip.name cache_path, total_size, file_count = get_cache_info(strip_name) else: cache_path, total_size, file_count = get_cache_info() # Cache info row = box.row() row.label(text="Size:") row.label(text=format_size(total_size)) row = box.row() row.label(text="Files:") row.label(text=str(file_count)) # Cache directory setting box.prop(context.scene, "facemask_cache_dir") # Clear cache buttons row = box.row(align=True) if seq_editor and seq_editor.active_strip: op = row.operator( "sequencer.clear_mask_cache", text="Clear Strip Cache", icon='TRASH', ) op.all_strips = False op = row.operator( "sequencer.clear_mask_cache", text="Clear All", icon='TRASH', ) op.all_strips = True def _draw_progress(self, layout, wm, generator): """Draw progress bar during generation.""" box = layout.box() box.label(text="Generating Masks...", icon='RENDER_ANIMATION') # Progress bar progress = wm.mask_progress / max(wm.mask_total, 1) box.progress( factor=progress, text=f"Frame {wm.mask_progress} / {wm.mask_total}", ) # Cancel button box.operator( "sequencer.cancel_mask_generation", text="Cancel", icon='CANCEL', ) def _draw_bake_progress(self, layout, wm, generator): """Draw progress bar during blur bake.""" box = layout.box() box.label(text="Baking Blur...", icon='RENDER_ANIMATION') progress = wm.bake_progress / max(wm.bake_total, 1) box.progress( factor=progress, text=f"Frame {wm.bake_progress} / {wm.bake_total}", ) box.operator( "sequencer.cancel_bake_blur", text="Cancel", icon='CANCEL', ) def _draw_batch_progress(self, layout, wm, batch, generator, bake_generator): """Draw batch bake progress.""" box = layout.box() if batch._mode == "mask_only": box.label(text="Batch Generating Cache...", icon='RENDER_ANIMATION') else: box.label(text="Batch Baking...", icon='RENDER_ANIMATION') # Overall progress total = max(wm.batch_total, 1) # Show n-1/total while current strip is in progress, n/total when moving to next done_count = max(wm.batch_current - 1, 0) overall_factor = done_count / total box.progress( factor=overall_factor, text=f"{wm.batch_current} / {wm.batch_total}", ) if wm.batch_current_name: box.label(text=f"Strip: {wm.batch_current_name}") # Inner progress (mask gen or bake) if generator.is_running: inner = wm.mask_progress / max(wm.mask_total, 1) box.progress( factor=inner, text=f"Detecting: {wm.mask_progress} / {wm.mask_total}", ) elif bake_generator.is_running: inner = wm.bake_progress / max(wm.bake_total, 1) box.progress( factor=inner, text=f"Baking: {wm.bake_progress} / {wm.bake_total}", ) box.operator( "sequencer.cancel_batch_bake", text="Cancel Batch", icon='CANCEL', ) def _draw_batch_controls(self, layout, context, seq_editor): """Draw batch bake button when multiple MOVIE/IMAGE strips are selected.""" if not seq_editor: return selected_movies = [s for s in seq_editor.strips if s.select and s.type in {"MOVIE", "IMAGE"}] if not selected_movies: return count = len(selected_movies) image_count = sum(1 for s in selected_movies if s.type == "IMAGE") video_count = sum(1 for s in selected_movies if s.type == "MOVIE") label = f"Batch ({count} selected, image: {image_count}, video: {video_count})" box = layout.box() box.label(text=label, icon='RENDER_ANIMATION') box.operator( "sequencer.batch_bake_selected", text="Batch Bake Selected", icon='RENDER_ANIMATION', ) box.operator( "sequencer.batch_regenerate_cache", text="Batch Regenerate Cache", icon='FILE_REFRESH', ) box.operator( "sequencer.batch_restore_original", text="Batch Restore Original", icon='LOOP_BACK', ) def _draw_generation_controls(self, layout, context, strip): """Draw mask generation controls.""" box = layout.box() box.label(text="Mask Generation", icon='MOD_MASK') # Info about selected strip row = box.row() row.label(text=f"Strip: {strip.name}") has_mask = check_detection_cache(strip.name) if has_mask: row = box.row() row.label(text="✓ Detection cache exists", icon='CHECKMARK') # Generate / Regenerate button if not has_mask: box.operator( "sequencer.generate_face_mask", text="Generate Detection Cache", icon='FACE_MAPS', ) else: op = box.operator( "sequencer.generate_face_mask", text="Regenerate Cache", icon='FILE_REFRESH', ) op.force = True def _draw_blur_controls(self, layout, context, strip): """Draw blur application controls.""" box = layout.box() box.label(text="Blur Bake", icon='MATFLUID') has_mask = check_detection_cache(strip.name) if not has_mask: box.label(text="Generate detection cache first", icon='INFO') return # Bake parameters col = box.column(align=True) col.prop(context.scene, "facemask_bake_blur_size") col.prop(context.scene, "facemask_bake_display_scale") if strip.type == "MOVIE": col.prop(context.scene, "facemask_bake_format") box.separator() baked_path = strip.get("facemask_baked_filepath", "") has_baked = bool(baked_path and os.path.exists(bpy.path.abspath(baked_path))) source_mode = strip.get("facemask_source_mode", "original") if not has_baked: # 初回: Bakeのみ box.operator( "sequencer.bake_and_swap_blur_source", text="Bake", icon='RENDER_STILL', ) else: # Bake済み: ソース切り替え + Re-bake row = box.row(align=True) if source_mode == "baked": row.operator( "sequencer.restore_original_source", text="Restore Original", icon='LOOP_BACK', ) else: row.operator( "sequencer.swap_to_baked_blur", text="Swap to Baked", icon='PLAY', ) row.operator( "sequencer.force_rebake_blur", text="Re-bake", icon='FILE_REFRESH', ) # Registration classes = [ SEQUENCER_PT_face_mask, ] def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)