import importlib from pathlib import Path from types import SimpleNamespace from unittest.mock import patch from tests.support.blender import BlenderTestCase class PreferencesTest(BlenderTestCase): def test_apple_silicon_architectures_support_coreml(self): properties = self.anyimage.properties for architecture in ("arm64", "aarch64"): with ( self.subTest(architecture=architecture), patch.object( self.anyimage.runtime, "environment_manifest", return_value={}, ), patch.object(properties.sys, "platform", "darwin"), patch.object( properties.platform, "machine", return_value=architecture, ), ): self.assertEqual( properties.supported_devices(), ["cpu", "coreml"], ) def test_non_apple_silicon_mac_uses_cpu(self): properties = self.anyimage.properties with ( patch.object( self.anyimage.runtime, "environment_manifest", return_value={}, ), patch.object(properties.sys, "platform", "darwin"), patch.object( properties.platform, "machine", return_value="x86_64", ), ): self.assertEqual(properties.supported_devices(), ["cpu"]) def test_manifest_coreml_requires_apple_silicon(self): properties = self.anyimage.properties manifest = {"devices": ["coreml", "cpu"]} with ( patch.object( self.anyimage.runtime, "environment_manifest", return_value=manifest, ), patch.object(properties.sys, "platform", "linux"), ): self.assertEqual(properties.supported_devices(), ["cpu"]) with ( patch.object( self.anyimage.runtime, "environment_manifest", return_value=manifest, ), patch.object(properties.sys, "platform", "darwin"), patch.object( properties.platform, "machine", return_value="aarch64", ), ): self.assertEqual( properties.supported_devices(), ["cpu", "coreml"], ) def test_manifest_rejects_old_mps_device(self): properties = self.anyimage.properties with ( patch.object( self.anyimage.runtime, "environment_manifest", return_value={"devices": ["mps", "cpu"]}, ), patch.object(properties.sys, "platform", "darwin"), patch.object( properties.platform, "machine", return_value="arm64", ), ): self.assertEqual(properties.supported_devices(), ["cpu"]) def test_windows_keeps_default_directml_device(self): properties = self.anyimage.properties with ( patch.object( self.anyimage.runtime, "environment_manifest", return_value={}, ), patch.object(properties.sys, "platform", "win32"), ): self.assertEqual( properties.supported_devices(), ["directml", "cpu"], ) def test_geometry_model_choices_match_moge_catalog(self): properties = self.anyimage.properties expected = tuple(properties.shared_model_catalog.MOGE_MODELS) geometry = properties.geometry_model_enum_items(None, None) self.assertEqual(tuple(item[0] for item in geometry), expected) def test_global_ai_input_size_defaults_and_clamps(self): preference_module = self.anyimage.preferences preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences self.assertEqual( preference_module.configured_max_ai_input_size(), 2048, ) preferences.max_ai_input_size = 10000 self.assertEqual( preference_module.configured_max_ai_input_size(), 8192, ) preferences.max_ai_input_size = 1 self.assertEqual( preference_module.configured_max_ai_input_size(), 256, ) def test_cutout_mesh_resolution_defaults_and_clamps_independently(self): preference_module = self.anyimage.preferences preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences self.assertEqual( preference_module.configured_max_cutout_mesh_resolution(), 1024, ) preferences.max_cutout_mesh_resolution = 2048 self.assertEqual( preference_module.configured_max_cutout_mesh_resolution(), 1024, ) preferences.max_cutout_mesh_resolution = 1 self.assertEqual( preference_module.configured_max_cutout_mesh_resolution(), 4, ) def test_frame_resolution_defaults_and_clamps_independently(self): preference_module = self.anyimage.preferences preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences self.assertEqual(preference_module.configured_max_frame_resolution(), 2048) preferences.max_frame_resolution = 10000 self.assertEqual(preference_module.configured_max_frame_resolution(), 8192) preferences.max_frame_resolution = 1 self.assertEqual(preference_module.configured_max_frame_resolution(), 64) def test_preferences_draws_upscale_model_download(self): preference_module = importlib.import_module("anyimage.preferences") downloads = [] properties = [] labels = [] boxes = [] class Layout: enabled = True def prop(self, _owner, name, **_options): properties.append(name) def separator(self, **_options): return None def label(self, **_options): labels.append(_options.get("text")) def row(self, **_options): return self def split(self, **_options): return self def column(self, **_options): return self def box(self): boxes.append(self) return self def operator(self, identifier, **_options): operator = SimpleNamespace(model="") if identifier == "anyimage.download_model": downloads.append(operator) return operator preferences = preference_module.AnyImagePreferences() preferences.layout = Layout() preferences.geometry_model = "MOGE2_VITS_NORMAL" preferences.background_model = "BEN2_BASE" preferences.upscale_model = "SAFMN_REAL_X4" catalog = tuple( { "key": key, "label": key, "family": family, "enabled": True, "ready": False, } for key, family in ( ("MOGE2_VITS_NORMAL", "moge"), ("BEN2_BASE", "ben2"), ("REALESRGAN_GENERAL_WDN_X4V3", "upscale"), ("SAFMN_REAL_X4", "upscale"), ) ) with ( patch.object( self.anyimage.properties, "ai_status", return_value={ "environment_ready": True, "missing_models": (), "ready": True, }, ), patch.object( self.anyimage.properties, "model_catalog", return_value=catalog, ), patch.object(self.anyimage.runtime, "server_busy", return_value=False), ): preferences.draw(None) self.assertEqual( [download.model for download in downloads], ["MOGE2_VITS_NORMAL", "BEN2_BASE", "SAFMN_REAL_X4"], ) self.assertEqual( [ label for label in labels if label in { "Server", "AI", "Tools", "Frame Tool", "Depth & Normal", "Cutout Tool", } ], [ "Server", "AI", "Tools", "Frame Tool", "Depth & Normal", "Cutout Tool", ], ) self.assertEqual(len(boxes), 4) self.assertIn("Shading", labels) self.assertIn("adapt_material_to_view_transform", properties) adaptation = preference_module.AnyImagePreferences.__annotations__[ "adapt_material_to_view_transform" ] self.assertTrue(adaptation["default"]) self.assertNotIn("update", adaptation) self.assertIn(preference_module.AnyImagePreferences, self.anyimage.CLASSES) self.assertNotIn("Normal Model", labels) self.assertIn("background_model", properties) self.assertIn("upscale_model", properties) self.assertIn("max_ai_input_size", properties) self.assertIn("max_frame_resolution", properties) self.assertIn("max_cutout_mesh_resolution", properties) self.assertIn("balloon_profile_algorithm", properties) balloon_profile = preference_module.AnyImagePreferences.__annotations__[ "balloon_profile_algorithm" ] self.assertEqual(balloon_profile["default"], "POISSON") self.assertEqual( tuple(item[0] for item in balloon_profile["items"]), ("POISSON", "TEDDY"), ) self.assertEqual( preference_module.AnyImagePreferences.__annotations__["upscale_model"][ "name" ], "Upscale Image Model", ) self.assertEqual( preference_module.AnyImagePreferences.__annotations__["geometry_model"][ "name" ], "Generate Depth Map Model", ) self.assertEqual( preference_module.AnyImagePreferences.__annotations__["background_model"][ "name" ], "Remove Background Model", ) def test_ready_models_show_their_ai_feature(self): preference_module = importlib.import_module("anyimage.preferences") labels = [] class Layout: def row(self, **_options): return self def label(self, **options): labels.append((options.get("text"), options.get("icon"))) preferences = preference_module.AnyImagePreferences() model = {"ready": True} for ready_label in ( "AI Generate Depth Map", "AI Remove Background", "AI Upscale Image", ): preferences._draw_model_status(Layout(), model, ready_label) self.assertEqual( labels, [ ("AI Generate Depth Map", "CHECKMARK"), ("AI Remove Background", "CHECKMARK"), ("AI Upscale Image", "CHECKMARK"), ], ) def test_preferences_hide_ai_until_server_is_installed(self): preference_module = importlib.import_module("anyimage.preferences") labels = [] operators = [] class Layout: enabled = True def box(self): return self def label(self, **options): labels.append(options.get("text")) def prop(self, *_args, **_options): return None def row(self, **_options): return self def separator(self, **_options): return None def operator(self, identifier, **_options): operators.append(identifier) return SimpleNamespace(model="") preferences = preference_module.AnyImagePreferences() preferences.layout = Layout() with ( patch.object( self.anyimage.properties, "ai_status", return_value={ "environment_ready": False, "missing_models": (), "ready": False, }, ), patch.object(self.anyimage.properties, "model_catalog", return_value=()), patch.object(self.anyimage.runtime, "server_busy", return_value=False), ): preferences.draw(None) self.assertEqual( labels, ["Server", "Shading", "Tools", "Frame Tool", "Depth & Normal", "Cutout Tool"], ) self.assertEqual( operators, ["anyimage.setup_ai_environment", "anyimage.open_server_log"], ) def test_depth_plane_request_includes_the_global_ai_input_limit(self): operator = self.convert_to_plane.GenerateDepthPlane() operator.plane_type = "DEPTH" operator.input_path = __file__ operator.analysis_input_path = __file__ operator.source_object_name = "Source" operator.mesh_detail = 0 operator.model = self.anyimage.properties.DEFAULT_MOGE2_MODEL_KEY operator.resolution_level = 5 operator.max_input_size = 1536 context = SimpleNamespace( scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=1)) ) with ( patch.object(self.convert_to_plane, "require_environment"), patch.object(self.convert_to_plane, "require_model"), patch.object(self.convert_to_plane, "require_input_path", return_value=Path("input.png")), patch.object(self.convert_to_plane, "production_device", return_value="CPU"), ): parameters = operator.request(context) self.assertEqual(parameters["max_input_size"], 1536) self.assertNotIn("shape", parameters) self.assertNotIn("polygon", parameters) def test_upscale_execute_reads_model_and_size_from_preferences(self): source_image = SimpleNamespace( name="Original", source="FILE", anyimage_upscale_source=None, ) source_object = SimpleNamespace( name="Source", type="EMPTY", empty_display_type="IMAGE", data=source_image, image_user=None, ) context = SimpleNamespace( object=source_object, scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=1)), ) operator = self.upscale.UpscaleImage() with ( patch.object( self.upscale, "configured_upscale_model", return_value="SAFMN_REAL_X4", ), patch.object( self.upscale, "configured_max_ai_input_size", return_value=1024, ), patch.object( self.upscale.ImageEditTarget, "prepare", return_value=(Path("input.png"), False), ), patch.object( self.runtime.JobOperatorBase, "execute", return_value={"RUNNING_MODAL"}, ), ): result = operator.execute(context) self.assertEqual(result, {"RUNNING_MODAL"}) self.assertEqual(operator.model, "SAFMN_REAL_X4") self.assertEqual(operator.max_input_size, 1024)