Batch処理

This commit is contained in:
2026-02-22 04:36:28 +09:00
parent d67265aa39
commit 0fdff5423e
9 changed files with 733 additions and 137 deletions
+280
View File
@@ -0,0 +1,280 @@
"""
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
+17
View File
@@ -247,6 +247,23 @@ class InferenceClient:
except urllib.error.HTTPError:
return {"status": "unknown"}
def get_video_info(self, video_path: str) -> Dict[str, Any]:
"""Get video metadata from the inference server."""
if not self.is_server_running():
self.start_server()
req = urllib.request.Request(
f"{self.SERVER_URL}/video_info",
data=json.dumps({"video_path": video_path}).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as e:
raise RuntimeError(f"Server error: {e.read().decode('utf-8')}")
def bake_blur(
self,
video_path: str,
+9
View File
@@ -83,6 +83,15 @@ def get_detections_path_for_strip(strip_name: str) -> str:
return os.path.join(get_cache_dir_for_strip(strip_name), "detections.msgpack")
def check_detection_cache(strip_name: str) -> bool:
"""Detection cache ファイルが存在し有効かどうか確認する。"""
path = get_detections_path_for_strip(strip_name)
try:
return os.path.exists(path) and os.path.getsize(path) > 0
except OSError:
return False
def get_cache_info(strip_name: Optional[str] = None) -> Tuple[str, int, int]:
"""
Get cache directory information.