Files
sugarkworkandClaude Opus 4.7 76224d7455 Lower default confidence threshold from 0.5 to 0.25
0.5 was too conservative for the sensitive_detect models in practice -
real footage frequently has the target region detected at 0.3-0.4, and
users had to manually drop the slider every run. Match the value the
extract_segments.py CLI already used as its default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 10:59:21 +09:00

293 lines
11 KiB
Python

import os
import sys
import types
import numpy as np
import torch
from PIL import Image
import gradio as gr
from pathlib import Path
import glob
# ==========================================
# 1. ComfyUI Dependencies Mocking
# ==========================================
# node.py imports `folder_paths` which is specific to ComfyUI.
# We create a mock module and insert it into sys.modules.
current_dir = os.path.dirname(os.path.abspath(__file__))
models_dir = os.path.join(current_dir, "models")
dummy_output_dir = os.path.join(current_dir, "output")
if not os.path.exists(models_dir):
os.makedirs(models_dir)
if not os.path.exists(dummy_output_dir):
os.makedirs(dummy_output_dir)
# Create a mock folder_paths module
folder_paths = types.ModuleType("folder_paths")
folder_paths.models_dir = models_dir
# Global variable to dynamically set the output directory for folder_paths mock
_current_mock_output_dir = dummy_output_dir
def get_output_directory():
return _current_mock_output_dir
folder_paths.get_output_directory = get_output_directory
def get_save_image_path(filename_prefix, output_dir, width, height):
# Dummy implementation for standalone use
counter = 1
# Check existing files to increment counter (simplified)
while os.path.exists(os.path.join(output_dir, f"{filename_prefix}_{counter:05}.png")) or \
os.path.exists(os.path.join(output_dir, f"{filename_prefix}_{counter:05}.psd")):
counter += 1
return output_dir, filename_prefix, counter, "", filename_prefix
folder_paths.get_save_image_path = get_save_image_path
# Register the mock module
sys.modules["folder_paths"] = folder_paths
# ==========================================
# 2. Node Initialization
# ==========================================
# Now we can safely import node.py
from node import AutoMosaic
# Create a single global instance of AutoMosaic to reuse the model
mosaic_node = AutoMosaic()
# ==========================================
# 3. Gradio Interface Logic
# ==========================================
def _run_node_on_np(img_np, save_psd, filename_prefix, confidence, process_method, factor, target_class,
mask_expand=0.0):
"""Helper to convert numpy array to tensor and run the node."""
img_tensor = torch.from_numpy(img_np).unsqueeze(0)
result_dict = mosaic_node.process_image(
image=img_tensor,
save_psd=save_psd,
filename_prefix=filename_prefix,
confidence=confidence,
process_method=process_method,
factor=factor,
target_class=target_class,
mask_expand=mask_expand,
)
output_tensors = result_dict.get("result", (None,))[0]
if output_tensors is None:
return None
output_tensor = output_tensors[0]
out_np = (output_tensor.cpu().numpy() * 255).astype(np.uint8)
return out_np
def process_ui_image(image_input, save_psd, filename_prefix, confidence, process_method, factor, target_class,
mask_expand):
"""Adapter function for single image Gradio processing."""
global _current_mock_output_dir
_current_mock_output_dir = dummy_output_dir # Use default for single UI
if image_input is None:
return None, "Please upload an image."
if isinstance(image_input, np.ndarray):
img_np = image_input.astype(np.float32) / 255.0
else:
img_np = np.array(image_input).astype(np.float32) / 255.0
try:
out_np = _run_node_on_np(
img_np, save_psd, filename_prefix, confidence, process_method, factor, target_class,
mask_expand=mask_expand,
)
if out_np is None:
return None, "Error: Node returned no result tensor."
return out_np, "Processing successful!"
except Exception as e:
import traceback
traceback.print_exc()
return None, f"Error during processing: {str(e)}"
def process_batch_directory(input_dir, output_dir, recursive, save_psd, confidence, process_method, factor, target_class,
mask_expand):
"""Adapter function for batch processing a directory."""
global _current_mock_output_dir
if not input_dir or not os.path.exists(input_dir):
return f"Error: Input directory does not exist: {input_dir}"
if not output_dir:
return "Error: Output directory must be specified."
input_path = Path(input_dir)
out_base_path = Path(output_dir)
# Common image extensions Pillow can read
extensions = ('*.png', '*.jpg', '*.jpeg', '*.webp', '*.bmp')
image_files = []
for ext in extensions:
if recursive:
image_files.extend(input_path.rglob(ext))
else:
image_files.extend(input_path.glob(ext))
if not image_files:
return f"No common images found in {input_dir}"
success_count = 0
fail_count = 0
status_lines = []
status_lines.append(f"Found {len(image_files)} images. Starting processing...")
yield "\n".join(status_lines)
for i, img_file in enumerate(image_files):
try:
# Determine relative path to recreate structure
rel_path = img_file.relative_to(input_path)
target_out_dir = out_base_path / rel_path.parent
target_out_dir.mkdir(parents=True, exist_ok=True)
# The node uses `folder_paths.get_output_directory()` internally for saving PSD
_current_mock_output_dir = str(target_out_dir)
# Open via Pillow
with Image.open(img_file) as pil_img:
img_rgb = pil_img.convert('RGB')
img_np = np.array(img_rgb).astype(np.float32) / 255.0
filename_prefix = img_file.stem
out_np = _run_node_on_np(
img_np=img_np,
save_psd=save_psd,
filename_prefix=filename_prefix,
confidence=confidence,
process_method=process_method,
factor=factor,
target_class=target_class,
mask_expand=mask_expand,
)
if out_np is not None:
out_pil = Image.fromarray(out_np)
# Save just the composite PNG alongside PSD
out_png_path = target_out_dir / f"{filename_prefix}_mosaic.png"
out_pil.save(out_png_path)
success_count += 1
status_lines.append(f"[{i+1}/{len(image_files)}] OK: {rel_path}")
else:
fail_count += 1
status_lines.append(f"[{i+1}/{len(image_files)}] FAIL: {rel_path} (No tensor)")
except Exception as e:
fail_count += 1
status_lines.append(f"[{i+1}/{len(image_files)}] ERROR: {rel_path} - {str(e)}")
yield "\n".join(status_lines[-10:]) # Yield last 10 lines to keep UI somewhat responsive and not overload
status_lines.append("---")
status_lines.append(f"Batch Processing Complete! Success: {success_count}, Failures: {fail_count}")
status_lines.append(f"Output saved to: {out_base_path}")
yield "\n".join(status_lines)
# ==========================================
# 4. Gradio UI Layout
# ==========================================
# Common Options Components Builder to avoid repeating UI code
def build_processing_options_ui():
with gr.Group():
gr.Markdown("### Processing Options")
process_method = gr.Dropdown(
choices=["raw", "mosaic", "white", "blur"],
value="mosaic",
label="Process Method"
)
target_class = gr.Textbox(
value="pussy,penis",
label="Target Class (comma separated)"
)
confidence = gr.Slider(
minimum=0.01, maximum=1.0, step=0.01,
value=0.25,
label="Confidence Threshold"
)
factor = gr.Slider(
minimum=10, maximum=200, step=1,
value=100,
label="Factor (Block size / Blur strength)"
)
mask_expand = gr.Slider(
minimum=0.0, maximum=20.0, step=0.1,
value=0.0,
label="Mask Expand (% of long edge)"
)
save_psd = gr.Checkbox(label="Save PSD file", value=False)
return process_method, target_class, confidence, factor, mask_expand, save_psd
with gr.Blocks(title="ComfyUI AutoMosaic Standalone") as app:
gr.Markdown("# ComfyUI AutoMosaic - Standalone WebUI")
gr.Markdown("Test the `AutoMosaic` custom node functionality without booting up ComfyUI.")
with gr.Tabs():
# --- TAB 1: Single Image ---
with gr.Tab("Single Image"):
with gr.Row():
with gr.Column():
s_input_image = gr.Image(label="Input Image", type="numpy")
s_process_method, s_target_class, s_confidence, s_factor, s_mask_expand, s_save_psd = build_processing_options_ui()
s_filename_prefix = gr.Textbox(value="StandaloneMosaic", label="Filename Prefix (for PSD)")
s_run_btn = gr.Button("Process Image", variant="primary")
with gr.Column():
s_output_image = gr.Image(label="Result Image")
s_status_text = gr.Textbox(label="Status", interactive=False)
s_run_btn.click(
fn=process_ui_image,
inputs=[
s_input_image, s_save_psd, s_filename_prefix,
s_confidence, s_process_method, s_factor, s_target_class,
s_mask_expand,
],
outputs=[s_output_image, s_status_text]
)
# --- TAB 2: Batch Process ---
with gr.Tab("Batch Process"):
with gr.Row():
with gr.Column():
gr.Markdown("Batch process images from an input directory and save them, preserving structure.")
b_input_dir = gr.Textbox(label="Input Directory Path", placeholder="C:/images/input")
b_output_dir = gr.Textbox(label="Output Directory Path", placeholder="C:/images/output")
b_recursive = gr.Checkbox(label="Process Subdirectories (Recursive)", value=True)
b_process_method, b_target_class, b_confidence, b_factor, b_mask_expand, b_save_psd = build_processing_options_ui()
b_run_btn = gr.Button("Start Batch Processing", variant="primary")
with gr.Column():
b_status_text = gr.Textbox(label="Batch Status", lines=15, interactive=False)
b_run_btn.click(
fn=process_batch_directory,
inputs=[
b_input_dir, b_output_dir, b_recursive,
b_save_psd, b_confidence, b_process_method, b_factor, b_target_class,
b_mask_expand,
],
outputs=[b_status_text]
)
if __name__ == "__main__":
print(f"Models directory correctly mocked at {models_dir}")
print(f"Single Mode dummy outputs will be saved at {dummy_output_dir}")
app.launch()