360 lines
11 KiB
Python
360 lines
11 KiB
Python
"""
|
|
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)
|