Add tools and tests for AnyImage model management and documentation

- Implement sync_models.py for preparing and uploading ONNX artifacts.
- Create build_node.py for building and validating Blender scripts.
- Add docs.py for building and previewing documentation with Mike or MkDocs.
- Introduce model_exports.py for exporting PyTorch models to ONNX format.
- Develop pack.py for packaging the project with dependencies for different platforms.
- Add test_sync_models.py to validate sync_models functionality.
- Add test_tool_commands.py to ensure repository tools have correct command scripts.
- Initialize tools package with __init__.py.
This commit is contained in:
2026-08-31 15:15:33 +08:00
parent 377eee43f5
commit e7aeeb21d4
99 changed files with 1953 additions and 1676 deletions

Binary file not shown.

View File

@@ -0,0 +1 @@
"""Shared Blender-side image, node, object, interaction, and AI Job helpers."""

View File

@@ -2,9 +2,9 @@ from pathlib import Path
import bpy
from ...preferences import addon_preferences
from ...properties import ai_ready, model_ready, model_record
from ...runtime import runtime
from ..preferences import addon_preferences
from ..properties import ai_ready, model_ready, model_record
from ..runtime import runtime
BEN2_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".webp"}
BEN2_VIDEO_SUFFIXES = {

View File

@@ -3,7 +3,7 @@ import math
import bpy
from ...properties import ai_ready, ai_setup_label, ai_status
from ..properties import ai_ready, ai_setup_label, ai_status
from .image_data import (
image_empty_bounds,
is_image_empty,

View File

@@ -0,0 +1,47 @@
from pathlib import Path
import bpy
NODE_ASSET_NAME = "O_AnyImage.blend"
def node_asset_path():
return Path(__file__).resolve().parents[1] / "assets" / NODE_ASSET_NAME
def load_node_group(group_name):
existing = bpy.data.node_groups.get(group_name)
if existing is not None:
return existing
asset_path = node_asset_path()
if not asset_path.is_file():
raise RuntimeError(f"Missing node asset: {asset_path}")
if bpy.app.version < (5, 0, 0):
directory = str(asset_path) + "/NodeTree/"
result = bpy.ops.wm.append(
filepath=directory + group_name,
directory=directory,
filename=group_name,
link=False,
do_reuse_local_id=True,
)
if "FINISHED" not in result:
raise RuntimeError("Unable to append the node asset")
node_group = bpy.data.node_groups.get(group_name)
if node_group is None:
raise RuntimeError("Appended node asset is unavailable")
node_group.use_fake_user = True
return node_group
with bpy.data.libraries.load(str(asset_path), link=True) as (source, target):
if group_name not in source.node_groups:
raise RuntimeError(f"Node asset is missing {group_name}")
target.node_groups = [group_name]
linked_group = target.node_groups[0]
node_group = bpy.data.pack_linked_ids_hierarchy(root_id=linked_group)
if linked_group != node_group and linked_group.users == 0:
bpy.data.node_groups.remove(linked_group)
node_group.use_fake_user = True
return node_group

View File

@@ -1,5 +1,3 @@
from pathlib import Path
import bpy
from .image_data import (
@@ -8,50 +6,7 @@ from .image_data import (
image_pixels,
is_animated_image,
)
NODE_ASSET_NAME = "O_AnyImage.blend"
def node_asset_path():
return Path(__file__).resolve().parents[2] / "assets" / NODE_ASSET_NAME
def load_asset_node_group(group_name):
existing = bpy.data.node_groups.get(group_name)
if existing is not None:
return existing
asset_path = node_asset_path()
if not asset_path.is_file():
raise RuntimeError(f"Missing node asset: {asset_path}")
if bpy.app.version < (5, 0, 0):
directory = str(asset_path) + "/NodeTree/"
result = bpy.ops.wm.append(
filepath=directory + group_name,
directory=directory,
filename=group_name,
link=False,
do_reuse_local_id=True,
)
if "FINISHED" not in result:
raise RuntimeError("Unable to append the node asset")
node_group = bpy.data.node_groups.get(group_name)
if node_group is None:
raise RuntimeError("Appended node asset is unavailable")
node_group.use_fake_user = True
return node_group
with bpy.data.libraries.load(str(asset_path), link=True) as (source, target):
if group_name not in source.node_groups:
raise RuntimeError(f"Node asset is missing {group_name}")
target.node_groups = [group_name]
linked_group = target.node_groups[0]
node_group = bpy.data.pack_linked_ids_hierarchy(root_id=linked_group)
if linked_group != node_group and linked_group.users == 0:
bpy.data.node_groups.remove(linked_group)
node_group.use_fake_user = True
return node_group
from .node import load_node_group
def modifier_input_identifier(node_group, name):
@@ -139,7 +94,7 @@ def material_node_group(depth_plane=False):
if depth_plane
else IMAGE_MATERIAL_NODE_GROUP_NAME
)
return load_asset_node_group(group_name)
return load_node_group(group_name)
def create_image_material(
@@ -224,7 +179,7 @@ def create_image_material(
links.new(normal_texture.outputs["Color"], material_inputs.inputs["Normal"])
if shadeless:
shader = nodes.new("ShaderNodeGroup")
shader.node_tree = load_asset_node_group(SHADELESS_NODE_GROUP_NAME)
shader.node_tree = load_node_group(SHADELESS_NODE_GROUP_NAME)
shader.name = SHADELESS_NODE_GROUP_NAME
transparent = nodes.new("ShaderNodeBsdfTransparent")
mix_shader = nodes.new("ShaderNodeMixShader")

View File

@@ -1,6 +1,6 @@
import bpy
from .operators.common.image_data import is_image_empty
from .common.image_data import is_image_empty
from .operators.upscale import upscale_output_size

View File

@@ -8,7 +8,7 @@ from ..properties import (
model_download_size,
model_record,
)
from .common.ai_job import require_environment
from ..common.ai_job import require_environment
def format_download_size(size):

View File

@@ -1 +0,0 @@
"""Shared Blender-side image data, interaction, mesh result, and AI Job helpers."""

View File

@@ -8,7 +8,7 @@ from ...server.media.resolution import (
MAX_MAX_AI_INPUT_SIZE,
MIN_MAX_AI_INPUT_SIZE,
)
from ..common.ai_job import (
from ...common.ai_job import (
invoke_ai_setup_if_needed,
moge2_parameters,
production_device,
@@ -16,7 +16,7 @@ from ..common.ai_job import (
require_input_path,
require_model,
)
from ..common.image_data import (
from ...common.image_data import (
cleanup_image_input,
image_user_settings,
is_image_empty,
@@ -24,7 +24,7 @@ from ..common.image_data import (
source_start_frame,
video_total_frames,
)
from ..common.mesh_result import create_image_material
from ...common.object import create_image_material
from .object import (
create_color_image,
create_depth_plane_from_result,

View File

@@ -2,8 +2,8 @@
import bpy
from ..common.mesh_result import modifier_input_identifier, set_modifier_input
from .nodes import mesh_plane_node_group
from ...common.object import modifier_input_identifier, set_modifier_input
from ...common.node import load_node_group
MESH_PLANE_NODE_GROUP_NAME = "O Mesh Plane"
@@ -41,7 +41,7 @@ def add_mesh_plane_processing(
subdivisions=0,
thickness=0.0,
):
node_group = mesh_plane_node_group()
node_group = load_node_group(MESH_PLANE_NODE_GROUP_NAME)
modifier = plane_object.modifiers.new(
name=MESH_PLANE_NODE_GROUP_NAME,
type="NODES",

View File

@@ -1,23 +0,0 @@
"""Asset-backed Geometry Nodes for Plane conversion."""
from ..common.mesh_result import load_asset_node_group
MESH_PLANE_NODE_GROUP_NAME = "O Mesh Plane"
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
def mesh_plane_node_group():
return load_asset_node_group(MESH_PLANE_NODE_GROUP_NAME)
def depth_plane_node_group():
return load_asset_node_group(DEPTH_PLANE_NODE_GROUP_NAME)
def plane_node_group(shape):
if shape == "PLANE":
return mesh_plane_node_group()
if shape == "DEPTH_PLANE":
return depth_plane_node_group()
raise ValueError(f"Unsupported Plane shape: {shape}")

View File

@@ -2,26 +2,29 @@
import bpy
from ..common.image_data import (
from ...common.image_data import (
color_data_name,
image_empty_bounds,
image_pixels,
load_color_result_image,
require_image_empty,
)
from ..common.depth_data import (
from ...common.depth_data import (
depth_uniform_scale,
load_depth_metadata,
load_depth_result_image,
)
from ..common.mesh_result import (
from ...common.object import (
create_image_material,
finalize_object_result,
modifier_input_identifier,
set_modifier_input,
)
from ...common.node import load_node_group
from .mesh_plane import create_mesh_plane_mesh, create_mesh_plane_object
from .nodes import depth_plane_node_group
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
def create_color_image(source_object):
@@ -71,7 +74,7 @@ def create_depth_plane_object(
plane.matrix_world = source_object.matrix_world.copy()
plane["anyimage_mesh_shape"] = "DEPTH_PLANE"
node_group = depth_plane_node_group()
node_group = load_node_group(DEPTH_PLANE_NODE_GROUP_NAME)
modifier = plane.modifiers.new(name=node_group.name, type="NODES")
modifier.node_group = node_group
modifier.show_group_selector = False

View File

@@ -5,7 +5,7 @@ from dataclasses import dataclass
from ...preferences import addon_preferences
from ...server.media.resolution import limited_image_size
from ..common.image_data import image_empty_bounds
from ...common.image_data import image_empty_bounds
@dataclass(frozen=True)

View File

@@ -4,9 +4,10 @@ from ...preferences import (
MAX_CUTOUT_MESH_RESOLUTION,
MIN_CUTOUT_MESH_RESOLUTION,
)
from ..common.image_data import image_rgba
from ..common.image_region import rasterize_region_path
from ...common.image_data import image_rgba
from ...common.image_region import rasterize_region_path
from .geometry import CUTOUT_ALPHA_THRESHOLD
from .shape import CUTOUT_SHAPE_LABELS
def validate_cutout_region(
@@ -76,7 +77,7 @@ def open_shape_pie(
_pie_button(
pie,
"SURFACE",
"Surface",
CUTOUT_SHAPE_LABELS["SURFACE"],
"MESH_GRID",
source_name,
region_path_json,
@@ -87,7 +88,7 @@ def open_shape_pie(
_pie_button(
pie,
"BALLOON",
"Balloon",
CUTOUT_SHAPE_LABELS["BALLOON"],
"MESH_UVSPHERE",
source_name,
region_path_json,
@@ -98,7 +99,7 @@ def open_shape_pie(
_pie_button(
pie,
"DEPTH_BALLOON",
"Depth Balloon",
CUTOUT_SHAPE_LABELS["DEPTH_BALLOON"],
"META_BALL",
source_name,
region_path_json,
@@ -109,7 +110,7 @@ def open_shape_pie(
_pie_button(
pie,
"DEPTH_SURFACE",
"Depth Surface",
CUTOUT_SHAPE_LABELS["DEPTH_SURFACE"],
"SURFACE_NCURVE",
source_name,
region_path_json,

View File

@@ -22,7 +22,7 @@ from ...server.media.resolution import (
MAX_MAX_AI_INPUT_SIZE,
MIN_MAX_AI_INPUT_SIZE,
)
from ..common.ai_job import (
from ...common.ai_job import (
invoke_ai_setup_if_needed,
moge2_parameters,
production_device,
@@ -30,7 +30,7 @@ from ..common.ai_job import (
require_input_path,
require_model,
)
from ..common.image_data import (
from ...common.image_data import (
cleanup_image_input,
image_rgba,
load_normal_result_image,
@@ -39,8 +39,8 @@ from ..common.image_data import (
source_start_frame,
video_total_frames,
)
from ..common.depth_data import load_depth_metadata, load_depth_result_image
from ..common.image_interaction import (
from ...common.depth_data import load_depth_metadata, load_depth_result_image
from ...common.image_interaction import (
ImageRegionGesture,
apply_region_settings,
draw_region_settings,
@@ -49,7 +49,7 @@ from ..common.image_interaction import (
resolve_image_edit_click,
serialize_matrix,
)
from ..common.image_region import (
from ...common.image_region import (
ImageEditWarning,
RegionMask,
RegionPath,

View File

@@ -4,19 +4,19 @@ from enum import StrEnum
import bpy
from ..common.image_data import (
from ...common.image_data import (
color_data_name,
image_rgba,
)
from ..common.depth_data import camera_depth_values, depth_uniform_scale
from ..common.mesh_result import (
from ...common.depth_data import camera_depth_values, depth_uniform_scale
from ...common.object import (
IMAGE_MATERIAL_NODE_GROUP_NAME,
create_image_material,
finalize_object_result,
load_asset_node_group,
modifier_input_identifier,
set_modifier_input,
)
from ...common.node import load_node_group
from .geometry import (
DEFAULT_DEPTH_BASELINE,
DEFAULT_DEPTH_DIRECTION,
@@ -123,7 +123,7 @@ def _cropped_color_image(source_object, region_mask, source_rgba):
def load_cutout_node_group():
return load_asset_node_group(CUTOUT_NODE_GROUP_NAME)
return load_node_group(CUTOUT_NODE_GROUP_NAME)
def _connect_balloon_normal_scale(material):

View File

@@ -4,7 +4,7 @@ import bpy
from ..runtime import JobOperatorBase
from ..properties import BEN2_MODEL_KEY, REALESRGAN_GENERAL_WDN_MODEL_KEY
from .common.ai_job import (
from ..common.ai_job import (
require_background_input_path,
da3_parameters,
require_input_path,

View File

@@ -1,6 +1,6 @@
import bpy
from ..common.image_interaction import draw_region_settings
from ...common.image_interaction import draw_region_settings
from .edit_mask import EditImageMask

View File

@@ -4,12 +4,12 @@ import bpy
from ...properties import BEN2_MODEL_KEY, refine_region_property
from ...runtime import JobOperatorBase
from ..common.ai_job import (
from ...common.ai_job import (
production_device,
require_environment,
require_model,
)
from ..common.image_data import (
from ...common.image_data import (
cleanup_image_input,
create_pixel_result_image,
image_pixels,
@@ -19,13 +19,13 @@ from ..common.image_data import (
require_image_empty,
video_total_frames,
)
from ..common.image_interaction import (
from ...common.image_interaction import (
ImageRegionGesture,
gesture_property,
image_edit_poll,
report_image_edit_exception,
)
from ..common.image_region import (
from ...common.image_region import (
ImageEditWarning,
RegionPath,
crop_pixels_to_alpha,

View File

@@ -1,6 +1,6 @@
import bpy
from ..common.image_interaction import draw_region_settings
from ...common.image_interaction import draw_region_settings
from .edit_mask import EditImageMask

View File

@@ -3,14 +3,14 @@ import math
import bpy
from ..common.image_interaction import (
from ...common.image_interaction import (
deserialize_matrix,
drawing_in_region,
screen_path_to_image_pixels,
serialize_matrix,
)
from ..common.image_region import crop_pixels_to_alpha
from ..common.image_data import (
from ...common.image_region import crop_pixels_to_alpha
from ...common.image_data import (
create_pixel_result_image,
image_base_name,
image_pixels,
@@ -19,7 +19,7 @@ from ..common.image_data import (
require_image_empty,
video_total_frames,
)
from ..common.image_interaction import (
from ...common.image_interaction import (
active_view3d_tool_id,
draw_dashed_line,
draw_polygon_fill,

View File

@@ -2,7 +2,7 @@ import bpy
from ..properties import BEN2_MODEL_KEY
from ..runtime import JobOperatorBase
from .common.ai_job import (
from ..common.ai_job import (
require_background_input_path,
invoke_ai_setup_if_needed,
production_device,
@@ -10,7 +10,7 @@ from .common.ai_job import (
require_model,
)
from .common.image_data import (
from ..common.image_data import (
cleanup_image_input,
default_extract_frames,
is_image_empty,
@@ -70,7 +70,7 @@ class RemoveImageBackground(bpy.types.Operator):
def apply_background_response(result, source_object_name):
from .common.image_data import (
from ..common.image_data import (
load_color_result_image,
replace_empty_image,
require_image_empty,

View File

@@ -8,14 +8,14 @@ from ..server.media.resolution import (
MIN_MAX_AI_INPUT_SIZE,
)
from ..runtime import JobOperatorBase
from .common.ai_job import (
from ..common.ai_job import (
require_input_path,
invoke_ai_setup_if_needed,
production_device,
require_environment,
require_model,
)
from .common.image_data import (
from ..common.image_data import (
cleanup_image_input,
default_extract_frames,
is_image_empty,