InvokeAI 6.10.0rc1__py3-none-any.whl → 6.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. invokeai/app/api/routers/model_manager.py +43 -1
  2. invokeai/app/invocations/fields.py +1 -1
  3. invokeai/app/invocations/flux2_denoise.py +499 -0
  4. invokeai/app/invocations/flux2_klein_model_loader.py +222 -0
  5. invokeai/app/invocations/flux2_klein_text_encoder.py +222 -0
  6. invokeai/app/invocations/flux2_vae_decode.py +106 -0
  7. invokeai/app/invocations/flux2_vae_encode.py +88 -0
  8. invokeai/app/invocations/flux_denoise.py +77 -3
  9. invokeai/app/invocations/flux_lora_loader.py +1 -1
  10. invokeai/app/invocations/flux_model_loader.py +2 -5
  11. invokeai/app/invocations/ideal_size.py +6 -1
  12. invokeai/app/invocations/metadata.py +4 -0
  13. invokeai/app/invocations/metadata_linked.py +47 -0
  14. invokeai/app/invocations/model.py +1 -0
  15. invokeai/app/invocations/pbr_maps.py +59 -0
  16. invokeai/app/invocations/z_image_denoise.py +244 -84
  17. invokeai/app/invocations/z_image_image_to_latents.py +9 -1
  18. invokeai/app/invocations/z_image_latents_to_image.py +9 -1
  19. invokeai/app/invocations/z_image_seed_variance_enhancer.py +110 -0
  20. invokeai/app/services/config/config_default.py +3 -1
  21. invokeai/app/services/invocation_stats/invocation_stats_common.py +6 -6
  22. invokeai/app/services/invocation_stats/invocation_stats_default.py +9 -4
  23. invokeai/app/services/model_manager/model_manager_default.py +7 -0
  24. invokeai/app/services/model_records/model_records_base.py +4 -2
  25. invokeai/app/services/shared/invocation_context.py +15 -0
  26. invokeai/app/services/shared/sqlite/sqlite_util.py +2 -0
  27. invokeai/app/services/shared/sqlite_migrator/migrations/migration_25.py +61 -0
  28. invokeai/app/util/step_callback.py +58 -2
  29. invokeai/backend/flux/denoise.py +338 -118
  30. invokeai/backend/flux/dype/__init__.py +31 -0
  31. invokeai/backend/flux/dype/base.py +260 -0
  32. invokeai/backend/flux/dype/embed.py +116 -0
  33. invokeai/backend/flux/dype/presets.py +148 -0
  34. invokeai/backend/flux/dype/rope.py +110 -0
  35. invokeai/backend/flux/extensions/dype_extension.py +91 -0
  36. invokeai/backend/flux/schedulers.py +62 -0
  37. invokeai/backend/flux/util.py +35 -1
  38. invokeai/backend/flux2/__init__.py +4 -0
  39. invokeai/backend/flux2/denoise.py +280 -0
  40. invokeai/backend/flux2/ref_image_extension.py +294 -0
  41. invokeai/backend/flux2/sampling_utils.py +209 -0
  42. invokeai/backend/image_util/pbr_maps/architecture/block.py +367 -0
  43. invokeai/backend/image_util/pbr_maps/architecture/pbr_rrdb_net.py +70 -0
  44. invokeai/backend/image_util/pbr_maps/pbr_maps.py +141 -0
  45. invokeai/backend/image_util/pbr_maps/utils/image_ops.py +93 -0
  46. invokeai/backend/model_manager/configs/factory.py +19 -1
  47. invokeai/backend/model_manager/configs/lora.py +36 -0
  48. invokeai/backend/model_manager/configs/main.py +395 -3
  49. invokeai/backend/model_manager/configs/qwen3_encoder.py +116 -7
  50. invokeai/backend/model_manager/configs/vae.py +104 -2
  51. invokeai/backend/model_manager/load/model_cache/model_cache.py +107 -2
  52. invokeai/backend/model_manager/load/model_loaders/cogview4.py +2 -1
  53. invokeai/backend/model_manager/load/model_loaders/flux.py +1020 -8
  54. invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py +4 -2
  55. invokeai/backend/model_manager/load/model_loaders/onnx.py +1 -0
  56. invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py +2 -1
  57. invokeai/backend/model_manager/load/model_loaders/z_image.py +158 -31
  58. invokeai/backend/model_manager/starter_models.py +141 -4
  59. invokeai/backend/model_manager/taxonomy.py +31 -4
  60. invokeai/backend/model_manager/util/select_hf_files.py +3 -2
  61. invokeai/backend/patches/lora_conversions/z_image_lora_conversion_utils.py +39 -5
  62. invokeai/backend/quantization/gguf/ggml_tensor.py +15 -4
  63. invokeai/backend/util/vae_working_memory.py +0 -2
  64. invokeai/backend/z_image/extensions/regional_prompting_extension.py +10 -12
  65. invokeai/frontend/web/dist/assets/App-D13dX7be.js +161 -0
  66. invokeai/frontend/web/dist/assets/{browser-ponyfill-DHZxq1nk.js → browser-ponyfill-u_ZjhQTI.js} +1 -1
  67. invokeai/frontend/web/dist/assets/index-BB0nHmDe.js +530 -0
  68. invokeai/frontend/web/dist/index.html +1 -1
  69. invokeai/frontend/web/dist/locales/en-GB.json +1 -0
  70. invokeai/frontend/web/dist/locales/en.json +85 -6
  71. invokeai/frontend/web/dist/locales/it.json +135 -15
  72. invokeai/frontend/web/dist/locales/ru.json +11 -11
  73. invokeai/version/invokeai_version.py +1 -1
  74. {invokeai-6.10.0rc1.dist-info → invokeai-6.11.0.dist-info}/METADATA +8 -2
  75. {invokeai-6.10.0rc1.dist-info → invokeai-6.11.0.dist-info}/RECORD +81 -57
  76. {invokeai-6.10.0rc1.dist-info → invokeai-6.11.0.dist-info}/WHEEL +1 -1
  77. invokeai/frontend/web/dist/assets/App-CYhlZO3Q.js +0 -161
  78. invokeai/frontend/web/dist/assets/index-dgSJAY--.js +0 -530
  79. {invokeai-6.10.0rc1.dist-info → invokeai-6.11.0.dist-info}/entry_points.txt +0 -0
  80. {invokeai-6.10.0rc1.dist-info → invokeai-6.11.0.dist-info}/licenses/LICENSE +0 -0
  81. {invokeai-6.10.0rc1.dist-info → invokeai-6.11.0.dist-info}/licenses/LICENSE-SD1+SD2.txt +0 -0
  82. {invokeai-6.10.0rc1.dist-info → invokeai-6.11.0.dist-info}/licenses/LICENSE-SDXL.txt +0 -0
  83. {invokeai-6.10.0rc1.dist-info → invokeai-6.11.0.dist-info}/top_level.txt +0 -0
@@ -32,6 +32,13 @@ from invokeai.app.services.shared.invocation_context import InvocationContext
32
32
  from invokeai.backend.flux.controlnet.instantx_controlnet_flux import InstantXControlNetFlux
33
33
  from invokeai.backend.flux.controlnet.xlabs_controlnet_flux import XLabsControlNetFlux
34
34
  from invokeai.backend.flux.denoise import denoise
35
+ from invokeai.backend.flux.dype.presets import (
36
+ DYPE_PRESET_LABELS,
37
+ DYPE_PRESET_OFF,
38
+ DyPEPreset,
39
+ get_dype_config_from_preset,
40
+ )
41
+ from invokeai.backend.flux.extensions.dype_extension import DyPEExtension
35
42
  from invokeai.backend.flux.extensions.instantx_controlnet_extension import InstantXControlNetExtension
36
43
  from invokeai.backend.flux.extensions.kontext_extension import KontextExtension
37
44
  from invokeai.backend.flux.extensions.regional_prompting_extension import RegionalPromptingExtension
@@ -47,6 +54,7 @@ from invokeai.backend.flux.sampling_utils import (
47
54
  pack,
48
55
  unpack,
49
56
  )
57
+ from invokeai.backend.flux.schedulers import FLUX_SCHEDULER_LABELS, FLUX_SCHEDULER_MAP, FLUX_SCHEDULER_NAME_VALUES
50
58
  from invokeai.backend.flux.text_conditioning import FluxReduxConditioning, FluxTextConditioning
51
59
  from invokeai.backend.model_manager.taxonomy import BaseModelType, FluxVariantType, ModelFormat, ModelType
52
60
  from invokeai.backend.patches.layer_patcher import LayerPatcher
@@ -63,7 +71,7 @@ from invokeai.backend.util.devices import TorchDevice
63
71
  title="FLUX Denoise",
64
72
  tags=["image", "flux"],
65
73
  category="image",
66
- version="4.1.0",
74
+ version="4.5.0",
67
75
  )
68
76
  class FluxDenoiseInvocation(BaseInvocation):
69
77
  """Run denoising process with a FLUX transformer model."""
@@ -132,6 +140,12 @@ class FluxDenoiseInvocation(BaseInvocation):
132
140
  num_steps: int = InputField(
133
141
  default=4, description="Number of diffusion steps. Recommended values are schnell: 4, dev: 50."
134
142
  )
143
+ scheduler: FLUX_SCHEDULER_NAME_VALUES = InputField(
144
+ default="euler",
145
+ description="Scheduler (sampler) for the denoising process. 'euler' is fast and standard. "
146
+ "'heun' is 2nd-order (better quality, 2x slower). 'lcm' is optimized for few steps.",
147
+ ui_choice_labels=FLUX_SCHEDULER_LABELS,
148
+ )
135
149
  guidance: float = InputField(
136
150
  default=4.0,
137
151
  description="The guidance strength. Higher values adhere more strictly to the prompt, and will produce less diverse images. FLUX dev only, ignored for schnell.",
@@ -159,6 +173,28 @@ class FluxDenoiseInvocation(BaseInvocation):
159
173
  input=Input.Connection,
160
174
  )
161
175
 
176
+ # DyPE (Dynamic Position Extrapolation) for high-resolution generation
177
+ dype_preset: DyPEPreset = InputField(
178
+ default=DYPE_PRESET_OFF,
179
+ description="DyPE preset for high-resolution generation. 'auto' enables automatically for resolutions > 1536px. '4k' uses optimized settings for 4K output.",
180
+ ui_order=100,
181
+ ui_choice_labels=DYPE_PRESET_LABELS,
182
+ )
183
+ dype_scale: Optional[float] = InputField(
184
+ default=None,
185
+ ge=0.0,
186
+ le=8.0,
187
+ description="DyPE magnitude (λs). Higher values = stronger extrapolation. Only used when dype_preset is not 'off'.",
188
+ ui_order=101,
189
+ )
190
+ dype_exponent: Optional[float] = InputField(
191
+ default=None,
192
+ ge=0.0,
193
+ le=1000.0,
194
+ description="DyPE decay speed (λt). Controls transition from low to high frequency detail. Only used when dype_preset is not 'off'.",
195
+ ui_order=102,
196
+ )
197
+
162
198
  @torch.no_grad()
163
199
  def invoke(self, context: InvocationContext) -> LatentsOutput:
164
200
  latents = self._run_diffusion(context)
@@ -232,8 +268,14 @@ class FluxDenoiseInvocation(BaseInvocation):
232
268
  )
233
269
 
234
270
  transformer_config = context.models.get_config(self.transformer.transformer)
235
- assert transformer_config.base is BaseModelType.Flux and transformer_config.type is ModelType.Main
236
- is_schnell = transformer_config.variant is FluxVariantType.Schnell
271
+ assert (
272
+ transformer_config.base in (BaseModelType.Flux, BaseModelType.Flux2)
273
+ and transformer_config.type is ModelType.Main
274
+ )
275
+ # Schnell is only for FLUX.1, FLUX.2 Klein behaves like Dev (with guidance)
276
+ is_schnell = (
277
+ transformer_config.base is BaseModelType.Flux and transformer_config.variant is FluxVariantType.Schnell
278
+ )
237
279
 
238
280
  # Calculate the timestep schedule.
239
281
  timesteps = get_schedule(
@@ -242,6 +284,12 @@ class FluxDenoiseInvocation(BaseInvocation):
242
284
  shift=not is_schnell,
243
285
  )
244
286
 
287
+ # Create scheduler if not using default euler
288
+ scheduler = None
289
+ if self.scheduler in FLUX_SCHEDULER_MAP:
290
+ scheduler_class = FLUX_SCHEDULER_MAP[self.scheduler]
291
+ scheduler = scheduler_class(num_train_timesteps=1000)
292
+
245
293
  # Clip the timesteps schedule based on denoising_start and denoising_end.
246
294
  timesteps = clip_timestep_schedule_fractional(timesteps, self.denoising_start, self.denoising_end)
247
295
 
@@ -409,6 +457,30 @@ class FluxDenoiseInvocation(BaseInvocation):
409
457
  kontext_extension.ensure_batch_size(x.shape[0])
410
458
  img_cond_seq, img_cond_seq_ids = kontext_extension.kontext_latents, kontext_extension.kontext_ids
411
459
 
460
+ # Prepare DyPE extension for high-resolution generation
461
+ dype_extension: DyPEExtension | None = None
462
+ dype_config = get_dype_config_from_preset(
463
+ preset=self.dype_preset,
464
+ width=self.width,
465
+ height=self.height,
466
+ custom_scale=self.dype_scale,
467
+ custom_exponent=self.dype_exponent,
468
+ )
469
+ if dype_config is not None:
470
+ dype_extension = DyPEExtension(
471
+ config=dype_config,
472
+ target_height=self.height,
473
+ target_width=self.width,
474
+ )
475
+ context.logger.info(
476
+ f"DyPE enabled: resolution={self.width}x{self.height}, preset={self.dype_preset}, "
477
+ f"method={dype_config.method}, scale={dype_config.dype_scale:.2f}, "
478
+ f"exponent={dype_config.dype_exponent:.2f}, start_sigma={dype_config.dype_start_sigma:.2f}, "
479
+ f"base_resolution={dype_config.base_resolution}"
480
+ )
481
+ else:
482
+ context.logger.debug(f"DyPE disabled: resolution={self.width}x{self.height}, preset={self.dype_preset}")
483
+
412
484
  x = denoise(
413
485
  model=transformer,
414
486
  img=x,
@@ -426,6 +498,8 @@ class FluxDenoiseInvocation(BaseInvocation):
426
498
  img_cond=img_cond,
427
499
  img_cond_seq=img_cond_seq,
428
500
  img_cond_seq_ids=img_cond_seq_ids,
501
+ dype_extension=dype_extension,
502
+ scheduler=scheduler,
429
503
  )
430
504
 
431
505
  x = unpack(x.float(), self.height, self.width)
@@ -162,7 +162,7 @@ class FLUXLoRACollectionLoader(BaseInvocation):
162
162
  if not context.models.exists(lora.lora.key):
163
163
  raise Exception(f"Unknown lora: {lora.lora.key}!")
164
164
 
165
- assert lora.lora.base is BaseModelType.Flux
165
+ assert lora.lora.base in (BaseModelType.Flux, BaseModelType.Flux2)
166
166
 
167
167
  added_loras.append(lora.lora.key)
168
168
 
@@ -6,7 +6,7 @@ from invokeai.app.invocations.baseinvocation import (
6
6
  invocation,
7
7
  invocation_output,
8
8
  )
9
- from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
9
+ from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField
10
10
  from invokeai.app.invocations.model import CLIPField, ModelIdentifierField, T5EncoderField, TransformerField, VAEField
11
11
  from invokeai.app.services.shared.invocation_context import InvocationContext
12
12
  from invokeai.app.util.t5_model_identifier import (
@@ -37,28 +37,25 @@ class FluxModelLoaderOutput(BaseInvocationOutput):
37
37
  title="Main Model - FLUX",
38
38
  tags=["model", "flux"],
39
39
  category="model",
40
- version="1.0.6",
40
+ version="1.0.7",
41
41
  )
42
42
  class FluxModelLoaderInvocation(BaseInvocation):
43
43
  """Loads a flux base model, outputting its submodels."""
44
44
 
45
45
  model: ModelIdentifierField = InputField(
46
46
  description=FieldDescriptions.flux_model,
47
- input=Input.Direct,
48
47
  ui_model_base=BaseModelType.Flux,
49
48
  ui_model_type=ModelType.Main,
50
49
  )
51
50
 
52
51
  t5_encoder_model: ModelIdentifierField = InputField(
53
52
  description=FieldDescriptions.t5_encoder,
54
- input=Input.Direct,
55
53
  title="T5 Encoder",
56
54
  ui_model_type=ModelType.T5Encoder,
57
55
  )
58
56
 
59
57
  clip_embed_model: ModelIdentifierField = InputField(
60
58
  description=FieldDescriptions.clip_embed_model,
61
- input=Input.Direct,
62
59
  title="CLIP Embed",
63
60
  ui_model_type=ModelType.CLIPEmbed,
64
61
  )
@@ -46,7 +46,12 @@ class IdealSizeInvocation(BaseInvocation):
46
46
  dimension = 512
47
47
  elif unet_config.base == BaseModelType.StableDiffusion2:
48
48
  dimension = 768
49
- elif unet_config.base in (BaseModelType.StableDiffusionXL, BaseModelType.Flux, BaseModelType.StableDiffusion3):
49
+ elif unet_config.base in (
50
+ BaseModelType.StableDiffusionXL,
51
+ BaseModelType.Flux,
52
+ BaseModelType.Flux2,
53
+ BaseModelType.StableDiffusion3,
54
+ ):
50
55
  dimension = 1024
51
56
  else:
52
57
  raise ValueError(f"Unsupported model type: {unet_config.base}")
@@ -150,6 +150,10 @@ GENERATION_MODES = Literal[
150
150
  "flux_img2img",
151
151
  "flux_inpaint",
152
152
  "flux_outpaint",
153
+ "flux2_txt2img",
154
+ "flux2_img2img",
155
+ "flux2_inpaint",
156
+ "flux2_outpaint",
153
157
  "sd3_txt2img",
154
158
  "sd3_img2img",
155
159
  "sd3_inpaint",
@@ -52,6 +52,7 @@ from invokeai.app.invocations.primitives import (
52
52
  )
53
53
  from invokeai.app.invocations.scheduler import SchedulerOutput
54
54
  from invokeai.app.invocations.t2i_adapter import T2IAdapterField, T2IAdapterInvocation
55
+ from invokeai.app.invocations.z_image_denoise import ZImageDenoiseInvocation
55
56
  from invokeai.app.services.shared.invocation_context import InvocationContext
56
57
  from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
57
58
  from invokeai.backend.stable_diffusion.schedulers.schedulers import SCHEDULER_NAME_VALUES
@@ -729,6 +730,52 @@ class FluxDenoiseLatentsMetaInvocation(FluxDenoiseInvocation, WithMetadata):
729
730
  return LatentsMetaOutput(**params, metadata=MetadataField.model_validate(md))
730
731
 
731
732
 
733
+ @invocation(
734
+ "z_image_denoise_meta",
735
+ title=f"{ZImageDenoiseInvocation.UIConfig.title} + Metadata",
736
+ tags=["z-image", "latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"],
737
+ category="latents",
738
+ version="1.0.0",
739
+ )
740
+ class ZImageDenoiseMetaInvocation(ZImageDenoiseInvocation, WithMetadata):
741
+ """Run denoising process with a Z-Image transformer model + metadata."""
742
+
743
+ def invoke(self, context: InvocationContext) -> LatentsMetaOutput:
744
+ def _loras_to_json(obj: Union[Any, list[Any]]):
745
+ if not isinstance(obj, list):
746
+ obj = [obj]
747
+
748
+ output: list[dict[str, Any]] = []
749
+ for item in obj:
750
+ output.append(
751
+ LoRAMetadataField(
752
+ model=item.lora,
753
+ weight=item.weight,
754
+ ).model_dump(exclude_none=True, exclude={"id", "type", "is_intermediate", "use_cache"})
755
+ )
756
+ return output
757
+
758
+ obj = super().invoke(context)
759
+
760
+ md: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
761
+ md.update({"width": obj.width})
762
+ md.update({"height": obj.height})
763
+ md.update({"steps": self.steps})
764
+ md.update({"guidance": self.guidance_scale})
765
+ md.update({"denoising_start": self.denoising_start})
766
+ md.update({"denoising_end": self.denoising_end})
767
+ md.update({"scheduler": self.scheduler})
768
+ md.update({"model": self.transformer.transformer})
769
+ md.update({"seed": self.seed})
770
+ if len(self.transformer.loras) > 0:
771
+ md.update({"loras": _loras_to_json(self.transformer.loras)})
772
+
773
+ params = obj.__dict__.copy()
774
+ del params["type"]
775
+
776
+ return LatentsMetaOutput(**params, metadata=MetadataField.model_validate(md))
777
+
778
+
732
779
  @invocation(
733
780
  "metadata_to_vae",
734
781
  title="Metadata To VAE",
@@ -510,6 +510,7 @@ class VAELoaderInvocation(BaseInvocation):
510
510
  BaseModelType.StableDiffusionXL,
511
511
  BaseModelType.StableDiffusion3,
512
512
  BaseModelType.Flux,
513
+ BaseModelType.Flux2,
513
514
  ],
514
515
  ui_model_type=ModelType.VAE,
515
516
  )
@@ -0,0 +1,59 @@
1
+ import pathlib
2
+ from typing import Literal
3
+
4
+ from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
5
+ from invokeai.app.invocations.fields import ImageField, InputField, OutputField, WithBoard, WithMetadata
6
+ from invokeai.app.services.shared.invocation_context import InvocationContext
7
+ from invokeai.backend.image_util.pbr_maps.architecture.pbr_rrdb_net import PBR_RRDB_Net
8
+ from invokeai.backend.image_util.pbr_maps.pbr_maps import NORMAL_MAP_MODEL, OTHER_MAP_MODEL, PBRMapsGenerator
9
+ from invokeai.backend.util.devices import TorchDevice
10
+
11
+
12
+ @invocation_output("pbr_maps-output")
13
+ class PBRMapsOutput(BaseInvocationOutput):
14
+ normal_map: ImageField = OutputField(default=None, description="The generated normal map")
15
+ roughness_map: ImageField = OutputField(default=None, description="The generated roughness map")
16
+ displacement_map: ImageField = OutputField(default=None, description="The generated displacement map")
17
+
18
+
19
+ @invocation("pbr_maps", title="PBR Maps", tags=["image", "material"], category="image", version="1.0.0")
20
+ class PBRMapsInvocation(BaseInvocation, WithMetadata, WithBoard):
21
+ """Generate Normal, Displacement and Roughness Map from a given image"""
22
+
23
+ image: ImageField = InputField(description="Input image")
24
+ tile_size: int = InputField(default=512, description="Tile size")
25
+ border_mode: Literal["none", "seamless", "mirror", "replicate"] = InputField(
26
+ default="none", description="Border mode to apply to eliminate any artifacts or seams"
27
+ )
28
+
29
+ def invoke(self, context: InvocationContext) -> PBRMapsOutput:
30
+ image_pil = context.images.get_pil(self.image.image_name, mode="RGB")
31
+
32
+ def loader(model_path: pathlib.Path):
33
+ return PBRMapsGenerator.load_model(model_path, TorchDevice.choose_torch_device())
34
+
35
+ torch_device = TorchDevice.choose_torch_device()
36
+
37
+ with (
38
+ context.models.load_remote_model(NORMAL_MAP_MODEL, loader) as normal_map_model,
39
+ context.models.load_remote_model(OTHER_MAP_MODEL, loader) as other_map_model,
40
+ ):
41
+ assert isinstance(normal_map_model, PBR_RRDB_Net)
42
+ assert isinstance(other_map_model, PBR_RRDB_Net)
43
+ pbr_pipeline = PBRMapsGenerator(normal_map_model, other_map_model, torch_device)
44
+ normal_map, roughness_map, displacement_map = pbr_pipeline.generate_maps(
45
+ image_pil, self.tile_size, self.border_mode
46
+ )
47
+
48
+ normal_map = context.images.save(normal_map)
49
+ normal_map_field = ImageField(image_name=normal_map.image_name)
50
+
51
+ roughness_map = context.images.save(roughness_map)
52
+ roughness_map_field = ImageField(image_name=roughness_map.image_name)
53
+
54
+ displacement_map = context.images.save(displacement_map)
55
+ displacement_map_field = ImageField(image_name=displacement_map.image_name)
56
+
57
+ return PBRMapsOutput(
58
+ normal_map=normal_map_field, roughness_map=roughness_map_field, displacement_map=displacement_map_field
59
+ )