281 lines
10 KiB
Python
281 lines
10 KiB
Python
"""
|
||
Batch processor for sequential Generate+Bake across multiple VSE strips.
|
||
|
||
Uses timer-based async chaining so Blender's UI stays responsive.
|
||
"""
|
||
|
||
from typing import List, Optional, Callable, Any
|
||
|
||
# Lazy-imported inside Blender
|
||
bpy = None
|
||
|
||
|
||
class _DummyOperator:
|
||
"""Dummy operator object for _start_bake_impl calls."""
|
||
|
||
def report(self, level, msg):
|
||
print(f"[FaceMask] Batch: {msg}")
|
||
|
||
|
||
class BatchProcessor:
|
||
"""Manages sequential Generate Detection Cache → Bake across a list of strips."""
|
||
|
||
def __init__(self):
|
||
self.is_running: bool = False
|
||
self._mode: str = "full" # "full" or "mask_only"
|
||
self._strip_names: List[str] = []
|
||
self._current_idx: int = 0
|
||
self._context: Any = None
|
||
self._cancelled: bool = False
|
||
self._results: List[dict] = []
|
||
self._on_item_complete: Optional[Callable] = None # (idx, total, name, status)
|
||
self._on_all_complete: Optional[Callable] = None # (results)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Public API
|
||
# ------------------------------------------------------------------
|
||
|
||
def start(self, context, strips, on_item_complete=None, on_all_complete=None, mode="full"):
|
||
"""Start batch processing for the given strips.
|
||
|
||
mode:
|
||
"full" - マスク生成(キャッシュなければ)→ Bake
|
||
"mask_only" - キャッシュを無視してマスク生成のみ(Bakeしない)
|
||
"""
|
||
global bpy
|
||
import bpy as _bpy
|
||
bpy = _bpy
|
||
|
||
if self.is_running:
|
||
raise RuntimeError("Batch already running")
|
||
|
||
self.is_running = True
|
||
self._mode = mode
|
||
self._strip_names = [s.name for s in strips]
|
||
self._current_idx = 0
|
||
self._context = context
|
||
self._cancelled = False
|
||
self._results = []
|
||
self._on_item_complete = on_item_complete
|
||
self._on_all_complete = on_all_complete
|
||
|
||
wm = context.window_manager
|
||
wm.batch_current = 0
|
||
wm.batch_total = len(self._strip_names)
|
||
wm.batch_current_name = ""
|
||
|
||
bpy.app.timers.register(self._process_next, first_interval=0.0)
|
||
|
||
def cancel(self):
|
||
"""Cancel batch. Stops currently running mask gen / bake."""
|
||
self._cancelled = True
|
||
from .async_generator import get_generator
|
||
from .async_bake_generator import get_bake_generator
|
||
gen = get_generator()
|
||
bake_gen = get_bake_generator()
|
||
if gen.is_running:
|
||
gen.cancel()
|
||
if bake_gen.is_running:
|
||
bake_gen.cancel()
|
||
|
||
# ------------------------------------------------------------------
|
||
# Internal: queue stepping
|
||
# ------------------------------------------------------------------
|
||
|
||
def _process_next(self):
|
||
"""Process the next strip in the queue (called via timer)."""
|
||
if self._cancelled:
|
||
self._finish()
|
||
return None
|
||
|
||
if self._current_idx >= len(self._strip_names):
|
||
self._finish()
|
||
return None
|
||
|
||
strip_name = self._strip_names[self._current_idx]
|
||
seq_editor = self._context.scene.sequence_editor
|
||
strip = seq_editor.strips.get(strip_name)
|
||
|
||
if strip is None:
|
||
print(f"[FaceMask] Batch: strip not found, skipping: {strip_name}")
|
||
self._results.append({"strip": strip_name, "status": "skipped"})
|
||
if self._on_item_complete:
|
||
self._on_item_complete(self._current_idx, len(self._strip_names), strip_name, "skipped")
|
||
self._current_idx += 1
|
||
bpy.app.timers.register(self._process_next, first_interval=0.0)
|
||
return None
|
||
|
||
# Update wm progress labels
|
||
wm = self._context.window_manager
|
||
wm.batch_current = self._current_idx + 1
|
||
wm.batch_current_name = strip_name
|
||
for area in self._context.screen.areas:
|
||
if area.type == "SEQUENCE_EDITOR":
|
||
area.tag_redraw()
|
||
|
||
if self._mode == "mask_only":
|
||
# キャッシュを無視して常にマスク生成(Bakeしない)
|
||
self._start_mask_gen(strip)
|
||
else:
|
||
from .utils import check_detection_cache
|
||
if not check_detection_cache(strip.name):
|
||
self._start_mask_gen(strip)
|
||
else:
|
||
self._start_bake(strip)
|
||
|
||
return None # one-shot timer
|
||
|
||
def _schedule_next(self):
|
||
bpy.app.timers.register(self._process_next, first_interval=0.0)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Mask generation
|
||
# ------------------------------------------------------------------
|
||
|
||
def _start_mask_gen(self, strip):
|
||
from ..operators.generate_mask import start_mask_gen_for_strip
|
||
|
||
strip_name = strip.name
|
||
|
||
def on_complete(status, data):
|
||
self._on_mask_done(strip_name, status, data)
|
||
|
||
def on_progress(current, total):
|
||
wm = self._context.window_manager
|
||
wm.mask_progress = current
|
||
wm.mask_total = max(total, 1)
|
||
for area in self._context.screen.areas:
|
||
if area.type == "SEQUENCE_EDITOR":
|
||
area.tag_redraw()
|
||
|
||
try:
|
||
start_mask_gen_for_strip(self._context, strip, on_complete, on_progress)
|
||
print(f"[FaceMask] Batch: started mask gen for {strip_name}")
|
||
except Exception as e:
|
||
print(f"[FaceMask] Batch: failed to start mask gen for {strip_name}: {e}")
|
||
self._on_mask_done(strip_name, "error", str(e))
|
||
|
||
def _on_mask_done(self, strip_name, status, data):
|
||
if self._cancelled or status == "cancelled":
|
||
self._results.append({"strip": strip_name, "status": "cancelled"})
|
||
self._finish()
|
||
return
|
||
|
||
if status == "error":
|
||
print(f"[FaceMask] Batch: mask gen failed for {strip_name}: {data}")
|
||
self._results.append({"strip": strip_name, "status": "error", "reason": str(data)})
|
||
if self._on_item_complete:
|
||
self._on_item_complete(self._current_idx, len(self._strip_names), strip_name, "error")
|
||
self._current_idx += 1
|
||
self._schedule_next()
|
||
return
|
||
|
||
# Mask gen succeeded
|
||
if self._mode == "mask_only":
|
||
# Bakeしない:結果を記録して次へ
|
||
self._results.append({"strip": strip_name, "status": "done"})
|
||
if self._on_item_complete:
|
||
self._on_item_complete(self._current_idx, len(self._strip_names), strip_name, "done")
|
||
self._current_idx += 1
|
||
self._schedule_next()
|
||
return
|
||
|
||
# full mode: proceed to bake
|
||
seq_editor = self._context.scene.sequence_editor
|
||
strip = seq_editor.strips.get(strip_name)
|
||
if strip is None:
|
||
print(f"[FaceMask] Batch: strip removed after mask gen: {strip_name}")
|
||
self._results.append({"strip": strip_name, "status": "skipped"})
|
||
if self._on_item_complete:
|
||
self._on_item_complete(self._current_idx, len(self._strip_names), strip_name, "skipped")
|
||
self._current_idx += 1
|
||
self._schedule_next()
|
||
return
|
||
|
||
self._start_bake(strip)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Bake
|
||
# ------------------------------------------------------------------
|
||
|
||
def _start_bake(self, strip):
|
||
from .async_bake_generator import get_bake_generator
|
||
from ..operators.apply_blur import _start_bake_impl
|
||
|
||
strip_name = strip.name
|
||
|
||
def on_complete_extra(status, data):
|
||
self._on_bake_done(strip_name, status, data)
|
||
|
||
bake_gen = get_bake_generator()
|
||
result = _start_bake_impl(
|
||
_DummyOperator(),
|
||
self._context,
|
||
force=False,
|
||
strip=strip,
|
||
on_complete_extra=on_complete_extra,
|
||
)
|
||
|
||
if result == {"CANCELLED"}:
|
||
# Error starting bake
|
||
print(f"[FaceMask] Batch: bake failed to start for {strip_name}")
|
||
self._results.append({"strip": strip_name, "status": "error", "reason": "bake failed to start"})
|
||
if self._on_item_complete:
|
||
self._on_item_complete(self._current_idx, len(self._strip_names), strip_name, "error")
|
||
self._current_idx += 1
|
||
self._schedule_next()
|
||
elif not bake_gen.is_running:
|
||
# Cache hit: on_complete_extra was NOT called by _start_bake_impl
|
||
print(f"[FaceMask] Batch: bake cache hit for {strip_name}")
|
||
self._on_bake_done(strip_name, "done", None)
|
||
|
||
def _on_bake_done(self, strip_name, status, data):
|
||
if self._cancelled or status == "cancelled":
|
||
self._results.append({"strip": strip_name, "status": "cancelled"})
|
||
self._finish()
|
||
return
|
||
|
||
if status == "error":
|
||
print(f"[FaceMask] Batch: bake failed for {strip_name}: {data}")
|
||
self._results.append({"strip": strip_name, "status": "error", "reason": str(data)})
|
||
else:
|
||
self._results.append({"strip": strip_name, "status": "done"})
|
||
print(f"[FaceMask] Batch: completed {strip_name}")
|
||
|
||
if self._on_item_complete:
|
||
self._on_item_complete(self._current_idx, len(self._strip_names), strip_name, status)
|
||
|
||
self._current_idx += 1
|
||
self._schedule_next()
|
||
|
||
# ------------------------------------------------------------------
|
||
# Finish
|
||
# ------------------------------------------------------------------
|
||
|
||
def _finish(self):
|
||
self.is_running = False
|
||
wm = self._context.window_manager
|
||
wm.batch_current = 0
|
||
wm.batch_total = 0
|
||
wm.batch_current_name = ""
|
||
|
||
print(f"[FaceMask] Batch: all done. Results: {self._results}")
|
||
|
||
if self._on_all_complete:
|
||
self._on_all_complete(self._results)
|
||
|
||
for area in self._context.screen.areas:
|
||
if area.type == "SEQUENCE_EDITOR":
|
||
area.tag_redraw()
|
||
|
||
|
||
# Singleton
|
||
_batch_processor: Optional[BatchProcessor] = None
|
||
|
||
|
||
def get_batch_processor() -> BatchProcessor:
|
||
global _batch_processor
|
||
if _batch_processor is None:
|
||
_batch_processor = BatchProcessor()
|
||
return _batch_processor
|