Blur Bake
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
"""Core module exports."""
|
||||
|
||||
from .async_bake_generator import AsyncBakeGenerator, get_bake_generator
|
||||
from .async_generator import AsyncMaskGenerator, get_generator
|
||||
from .compositor_setup import create_mask_blur_node_tree, get_or_create_blur_node_tree
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Async blur bake generator using Thread + Queue + Timer pattern.
|
||||
|
||||
This module mirrors AsyncMaskGenerator behavior for bake-and-swap workflow,
|
||||
so Blender UI remains responsive during server-side bake processing.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import queue
|
||||
from typing import Optional, Callable
|
||||
|
||||
# Will be imported when running inside Blender
|
||||
bpy = None
|
||||
|
||||
|
||||
class AsyncBakeGenerator:
|
||||
"""Asynchronous bake generator for non-blocking blur bake tasks."""
|
||||
|
||||
def __init__(self):
|
||||
self.result_queue: queue.Queue = queue.Queue()
|
||||
self.progress_queue: queue.Queue = queue.Queue()
|
||||
self.worker_thread: Optional[threading.Thread] = None
|
||||
self.is_running: bool = False
|
||||
self.total_frames: int = 0
|
||||
self.current_frame: int = 0
|
||||
self._on_complete: Optional[Callable] = None
|
||||
self._on_progress: Optional[Callable] = None
|
||||
|
||||
def start(
|
||||
self,
|
||||
video_path: str,
|
||||
mask_path: str,
|
||||
output_path: str,
|
||||
blur_size: int,
|
||||
fmt: str,
|
||||
on_complete: Optional[Callable] = None,
|
||||
on_progress: Optional[Callable] = None,
|
||||
):
|
||||
"""Start asynchronous bake request and progress polling."""
|
||||
global bpy
|
||||
import bpy as _bpy
|
||||
|
||||
bpy = _bpy
|
||||
|
||||
if self.is_running:
|
||||
raise RuntimeError("Blur bake already in progress")
|
||||
|
||||
self.is_running = True
|
||||
self.total_frames = 0
|
||||
self.current_frame = 0
|
||||
self._on_complete = on_complete
|
||||
self._on_progress = on_progress
|
||||
|
||||
self.worker_thread = threading.Thread(
|
||||
target=self._worker,
|
||||
args=(video_path, mask_path, output_path, blur_size, fmt),
|
||||
daemon=True,
|
||||
)
|
||||
self.worker_thread.start()
|
||||
|
||||
bpy.app.timers.register(
|
||||
self._check_progress,
|
||||
first_interval=0.1,
|
||||
)
|
||||
|
||||
def cancel(self):
|
||||
"""Cancel the current bake processing."""
|
||||
self.is_running = False
|
||||
if self.worker_thread and self.worker_thread.is_alive():
|
||||
self.worker_thread.join(timeout=2.0)
|
||||
|
||||
def _worker(
|
||||
self,
|
||||
video_path: str,
|
||||
mask_path: str,
|
||||
output_path: str,
|
||||
blur_size: int,
|
||||
fmt: str,
|
||||
):
|
||||
import time
|
||||
from .inference_client import get_client
|
||||
|
||||
task_id = None
|
||||
try:
|
||||
client = get_client()
|
||||
task_id = client.bake_blur(
|
||||
video_path=video_path,
|
||||
mask_path=mask_path,
|
||||
output_path=output_path,
|
||||
blur_size=blur_size,
|
||||
fmt=fmt,
|
||||
)
|
||||
|
||||
while self.is_running:
|
||||
status = client.get_task_status(task_id)
|
||||
state = status.get("status")
|
||||
|
||||
total = status.get("total", 0)
|
||||
if total > 0:
|
||||
self.total_frames = total
|
||||
|
||||
progress = status.get("progress", 0)
|
||||
if progress >= 0:
|
||||
self.progress_queue.put(("progress", progress))
|
||||
|
||||
if state == "completed":
|
||||
result_path = status.get("result_path", output_path)
|
||||
self.result_queue.put(("done", result_path))
|
||||
return
|
||||
if state == "failed":
|
||||
error_msg = status.get("message", "Unknown server error")
|
||||
self.result_queue.put(("error", error_msg))
|
||||
return
|
||||
if state == "cancelled":
|
||||
self.result_queue.put(("cancelled", None))
|
||||
return
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# Local cancel path
|
||||
if task_id:
|
||||
client.cancel_task(task_id)
|
||||
self.result_queue.put(("cancelled", None))
|
||||
|
||||
except Exception as e:
|
||||
self.result_queue.put(("error", str(e)))
|
||||
|
||||
def _check_progress(self) -> Optional[float]:
|
||||
while not self.progress_queue.empty():
|
||||
try:
|
||||
msg_type, data = self.progress_queue.get_nowait()
|
||||
if msg_type == "progress":
|
||||
self.current_frame = data
|
||||
if self._on_progress:
|
||||
self._on_progress(self.current_frame, self.total_frames)
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
if not self.result_queue.empty():
|
||||
try:
|
||||
msg_type, data = self.result_queue.get_nowait()
|
||||
self.is_running = False
|
||||
if self._on_complete:
|
||||
self._on_complete(msg_type, data)
|
||||
return None
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
if self.is_running:
|
||||
return 0.1
|
||||
return None
|
||||
|
||||
|
||||
_bake_generator: Optional[AsyncBakeGenerator] = None
|
||||
|
||||
|
||||
def get_bake_generator() -> AsyncBakeGenerator:
|
||||
global _bake_generator
|
||||
if _bake_generator is None:
|
||||
_bake_generator = AsyncBakeGenerator()
|
||||
return _bake_generator
|
||||
@@ -9,6 +9,7 @@ import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
@@ -96,9 +97,16 @@ class InferenceClient:
|
||||
self.log_file = open(self.log_file_path, "w", buffering=1) # Line buffered
|
||||
print(f"[FaceMask] Server log: {self.log_file_path}")
|
||||
|
||||
# Start process with 'python' command (will use venv if PATH is set correctly)
|
||||
# Start server with explicit Python executable when available.
|
||||
python_executable = "python"
|
||||
venv_python = os.path.join(venv_bin, "python")
|
||||
if os.path.isfile(venv_python):
|
||||
python_executable = venv_python
|
||||
else:
|
||||
python_executable = sys.executable
|
||||
|
||||
self.server_process = subprocess.Popen(
|
||||
["python", "-u", server_script], # -u for unbuffered output
|
||||
[python_executable, "-u", server_script], # -u for unbuffered output
|
||||
cwd=root_dir,
|
||||
text=True,
|
||||
env=server_env,
|
||||
@@ -241,6 +249,45 @@ class InferenceClient:
|
||||
except urllib.error.HTTPError:
|
||||
return {"status": "unknown"}
|
||||
|
||||
def bake_blur(
|
||||
self,
|
||||
video_path: str,
|
||||
mask_path: str,
|
||||
output_path: str,
|
||||
blur_size: int,
|
||||
fmt: str,
|
||||
) -> str:
|
||||
"""
|
||||
Request blur bake for a source video + mask video.
|
||||
|
||||
Returns:
|
||||
task_id (str)
|
||||
"""
|
||||
if not self.is_server_running():
|
||||
self.start_server()
|
||||
|
||||
data = {
|
||||
"video_path": video_path,
|
||||
"mask_path": mask_path,
|
||||
"output_path": output_path,
|
||||
"blur_size": blur_size,
|
||||
"format": fmt,
|
||||
}
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{self.SERVER_URL}/bake_blur",
|
||||
data=json.dumps(data).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
return result["id"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise RuntimeError(f"Server error: {e.read().decode('utf-8')}")
|
||||
|
||||
def cancel_task(self, task_id: str):
|
||||
"""Cancel a task."""
|
||||
try:
|
||||
|
||||
+39
-21
@@ -47,6 +47,37 @@ def get_server_status() -> Dict:
|
||||
return result
|
||||
|
||||
|
||||
def get_cache_root() -> str:
|
||||
"""
|
||||
Resolve cache root directory from scene setting or defaults.
|
||||
|
||||
Priority:
|
||||
1) Scene setting: facemask_cache_dir (if non-empty)
|
||||
2) Saved blend file directory + .mask_cache
|
||||
3) Temp directory + blender_mask_cache
|
||||
"""
|
||||
import bpy
|
||||
|
||||
scene = getattr(bpy.context, "scene", None)
|
||||
cache_setting = ""
|
||||
if scene is not None:
|
||||
cache_setting = (getattr(scene, "facemask_cache_dir", "") or "").strip()
|
||||
|
||||
if cache_setting:
|
||||
return bpy.path.abspath(cache_setting)
|
||||
|
||||
blend_file = bpy.data.filepath
|
||||
if blend_file:
|
||||
project_dir = os.path.dirname(blend_file)
|
||||
return os.path.join(project_dir, ".mask_cache")
|
||||
return os.path.join(tempfile.gettempdir(), "blender_mask_cache")
|
||||
|
||||
|
||||
def get_cache_dir_for_strip(strip_name: str) -> str:
|
||||
"""Get cache directory path for a specific strip."""
|
||||
return os.path.join(get_cache_root(), strip_name)
|
||||
|
||||
|
||||
def get_cache_info(strip_name: Optional[str] = None) -> Tuple[str, int, int]:
|
||||
"""
|
||||
Get cache directory information.
|
||||
@@ -59,22 +90,10 @@ def get_cache_info(strip_name: Optional[str] = None) -> Tuple[str, int, int]:
|
||||
"""
|
||||
import bpy
|
||||
|
||||
blend_file = bpy.data.filepath
|
||||
|
||||
if strip_name:
|
||||
# Get cache for specific strip
|
||||
if blend_file:
|
||||
project_dir = os.path.dirname(blend_file)
|
||||
cache_path = os.path.join(project_dir, ".mask_cache", strip_name)
|
||||
else:
|
||||
cache_path = os.path.join(tempfile.gettempdir(), "blender_mask_cache", strip_name)
|
||||
cache_path = get_cache_dir_for_strip(strip_name)
|
||||
else:
|
||||
# Get cache root
|
||||
if blend_file:
|
||||
project_dir = os.path.dirname(blend_file)
|
||||
cache_path = os.path.join(project_dir, ".mask_cache")
|
||||
else:
|
||||
cache_path = os.path.join(tempfile.gettempdir(), "blender_mask_cache")
|
||||
cache_path = get_cache_root()
|
||||
|
||||
# Calculate size and count
|
||||
total_size = 0
|
||||
@@ -83,13 +102,12 @@ def get_cache_info(strip_name: Optional[str] = None) -> Tuple[str, int, int]:
|
||||
if os.path.exists(cache_path):
|
||||
for root, dirs, files in os.walk(cache_path):
|
||||
for file in files:
|
||||
if file.endswith('.png'): # Only count mask images
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
total_size += os.path.getsize(file_path)
|
||||
file_count += 1
|
||||
except OSError:
|
||||
pass
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
total_size += os.path.getsize(file_path)
|
||||
file_count += 1
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return cache_path, total_size, file_count
|
||||
|
||||
|
||||
Reference in New Issue
Block a user