Files
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

352 lines
15 KiB
Python

from typing import TYPE_CHECKING, Any, ClassVar, Literal
from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation
from invokeai.app.invocations.fields import (
FieldDescriptions,
ImageField,
InputField,
MetadataField,
WithBoard,
WithMetadata,
)
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import ImageCollectionOutput
from invokeai.app.services.external_generation.external_generation_common import (
ExternalGenerationRequest,
ExternalGenerationResult,
ExternalReferenceImage,
)
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalGenerationMode
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
if TYPE_CHECKING:
from invokeai.app.services.invocation_services import InvocationServices
class BaseExternalImageGenerationInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Generate images using an external provider."""
provider_id: ClassVar[str | None] = None
model: ModelIdentifierField = InputField(
description=FieldDescriptions.main_model,
ui_model_base=[BaseModelType.External],
ui_model_type=[ModelType.ExternalImageGenerator],
ui_model_format=[ModelFormat.ExternalApi],
)
mode: ExternalGenerationMode = InputField(
default="txt2img",
description="Generation mode. Not all modes are supported by every model; unsupported modes raise at runtime.",
)
prompt: str = InputField(description="Prompt")
seed: int | None = InputField(default=None, description=FieldDescriptions.seed)
num_images: int = InputField(default=1, gt=0, description="Number of images to generate")
width: int = InputField(default=1024, gt=0, description=FieldDescriptions.width)
height: int = InputField(default=1024, gt=0, description=FieldDescriptions.height)
image_size: str | None = InputField(default=None, description="Image size preset (e.g. 1K, 2K, 4K)")
init_image: ImageField | None = InputField(default=None, description="Init image for img2img/inpaint")
mask_image: ImageField | None = InputField(default=None, description="Mask image for inpaint")
reference_images: list[ImageField] = InputField(default=[], description="Reference images")
def _build_provider_options(self) -> dict[str, Any] | None:
"""Override in provider-specific subclasses to pass extra options."""
return None
def invoke(self, context: InvocationContext) -> ImageCollectionOutput:
model_config = context.models.get_config(self.model)
if not isinstance(model_config, ExternalApiModelConfig):
raise ValueError("Selected model is not an external API model")
if self.provider_id is not None and model_config.provider_id != self.provider_id:
raise ValueError(
f"Selected model provider '{model_config.provider_id}' does not match node provider '{self.provider_id}'"
)
init_image = None
if self.init_image is not None:
init_image = context.images.get_pil(self.init_image.image_name, mode="RGB")
mask_image = None
if self.mask_image is not None:
mask_image = context.images.get_pil(self.mask_image.image_name, mode="L")
reference_images: list[ExternalReferenceImage] = []
for image_field in self.reference_images:
reference_image = context.images.get_pil(image_field.image_name, mode="RGB")
reference_images.append(ExternalReferenceImage(image=reference_image))
request = ExternalGenerationRequest(
model=model_config,
mode=self.mode,
prompt=self.prompt,
seed=self.seed,
num_images=self.num_images,
width=self.width,
height=self.height,
image_size=self.image_size,
init_image=init_image,
mask_image=mask_image,
reference_images=reference_images,
metadata=self._build_request_metadata(),
provider_options=self._build_provider_options(),
)
result = context._services.external_generation.generate(request)
outputs: list[ImageField] = []
for generated in result.images:
metadata = self._build_output_metadata(model_config, result, generated.seed)
image_dto = context.images.save(image=generated.image, metadata=metadata)
outputs.append(ImageField(image_name=image_dto.image_name))
return ImageCollectionOutput(collection=outputs)
def invoke_internal(self, context: InvocationContext, services: "InvocationServices") -> BaseInvocationOutput:
"""Override default cache behavior so cache hits produce new gallery entries.
The standard invocation cache returns the cached output (with stale image_name
references) without re-running invoke(), which means no new images are saved
to the gallery on repeat invokes. For external API nodes — where the API call
is the expensive part — we want cache hits to skip the API call but still
produce fresh gallery entries by copying the cached images.
"""
if services.configuration.node_cache_size == 0 or not self.use_cache:
return super().invoke_internal(context, services)
key = services.invocation_cache.create_key(self)
cached_value = services.invocation_cache.get(key)
if cached_value is None:
services.logger.debug(f'Invocation cache miss for type "{self.get_type()}": {self.id}')
output = self.invoke(context)
services.invocation_cache.save(key, output)
return output
services.logger.debug(f'Invocation cache hit for type "{self.get_type()}": {self.id}, duplicating images')
if not isinstance(cached_value, ImageCollectionOutput):
return cached_value
outputs: list[ImageField] = []
for image_field in cached_value.collection:
cached_image = context.images.get_pil(image_field.image_name, mode="RGB")
image_dto = context.images.save(image=cached_image)
outputs.append(ImageField(image_name=image_dto.image_name))
return ImageCollectionOutput(collection=outputs)
def _build_request_metadata(self) -> dict[str, Any] | None:
if self.metadata is None:
return None
return self.metadata.root
def _build_output_metadata(
self,
model_config: ExternalApiModelConfig,
result: ExternalGenerationResult,
image_seed: int | None,
) -> MetadataField | None:
metadata: dict[str, Any] = {}
if self.metadata is not None:
metadata.update(self.metadata.root)
metadata.update(
{
"external_provider": model_config.provider_id,
"external_model_id": model_config.provider_model_id,
}
)
if self.image_size is not None:
metadata["image_size"] = self.image_size
provider_request_id = getattr(result, "provider_request_id", None)
if provider_request_id:
metadata["external_request_id"] = provider_request_id
provider_metadata = getattr(result, "provider_metadata", None)
if provider_metadata:
metadata["external_provider_metadata"] = provider_metadata
if image_seed is not None:
metadata["external_seed"] = image_seed
metadata.update(self._build_output_provider_metadata())
if not metadata:
return None
return MetadataField(root=metadata)
def _build_output_provider_metadata(self) -> dict[str, Any]:
"""Override in provider-specific subclasses to add recall-relevant fields to the image metadata."""
return {}
@invocation(
"openai_image_generation",
title="OpenAI Image Generation",
tags=["external", "generation", "openai"],
category="image",
version="1.0.0",
)
class OpenAIImageGenerationInvocation(BaseExternalImageGenerationInvocation):
"""Generate images using an OpenAI-hosted external model."""
provider_id = "openai"
model: ModelIdentifierField = InputField(
description=FieldDescriptions.main_model,
ui_model_base=[BaseModelType.External],
ui_model_type=[ModelType.ExternalImageGenerator],
ui_model_format=[ModelFormat.ExternalApi],
ui_model_provider_id=["openai"],
)
# OpenAI's API has no img2img/inpaint distinction — the edits endpoint is used
# automatically when reference images are provided. Hide mode and init_image
# (init_image is functionally identical to a reference image), and hide
# mask_image since no OpenAI model supports inpainting.
mode: ExternalGenerationMode = InputField(default="txt2img", description="Generation mode.", ui_hidden=True)
init_image: ImageField | None = InputField(
default=None, description="Init image (use reference_images instead)", ui_hidden=True
)
mask_image: ImageField | None = InputField(default=None, description="Mask image for inpaint", ui_hidden=True)
quality: Literal["auto", "high", "medium", "low"] = InputField(default="auto", description="Output image quality")
background: Literal["auto", "transparent", "opaque"] = InputField(
default="auto", description="Background transparency handling"
)
input_fidelity: Literal["low", "high"] | None = InputField(
default=None, description="Fidelity to source images (edits only)"
)
def _build_provider_options(self) -> dict[str, Any]:
options: dict[str, Any] = {
"quality": self.quality,
"background": self.background,
}
if self.input_fidelity is not None:
options["input_fidelity"] = self.input_fidelity
return options
def _build_output_provider_metadata(self) -> dict[str, Any]:
metadata: dict[str, Any] = {
"openai_quality": self.quality,
"openai_background": self.background,
}
if self.input_fidelity is not None:
metadata["openai_input_fidelity"] = self.input_fidelity
return metadata
@invocation(
"gemini_image_generation",
title="Gemini Image Generation",
tags=["external", "generation", "gemini"],
category="image",
version="1.0.0",
)
class GeminiImageGenerationInvocation(BaseExternalImageGenerationInvocation):
"""Generate images using a Gemini-hosted external model."""
provider_id = "gemini"
model: ModelIdentifierField = InputField(
description=FieldDescriptions.main_model,
ui_model_base=[BaseModelType.External],
ui_model_type=[ModelType.ExternalImageGenerator],
ui_model_format=[ModelFormat.ExternalApi],
ui_model_provider_id=["gemini"],
)
# Gemini only supports txt2img — hide mode/init_image/mask_image fields
# that are inherited from the base class but not usable with any Gemini model.
mode: ExternalGenerationMode = InputField(default="txt2img", description="Generation mode.", ui_hidden=True)
init_image: ImageField | None = InputField(
default=None, description="Init image for img2img/inpaint", ui_hidden=True
)
mask_image: ImageField | None = InputField(default=None, description="Mask image for inpaint", ui_hidden=True)
temperature: float | None = InputField(default=None, ge=0.0, le=2.0, description="Sampling temperature")
thinking_level: Literal["minimal", "high"] | None = InputField(
default=None, description="Thinking level for image generation"
)
def _build_provider_options(self) -> dict[str, Any] | None:
options: dict[str, Any] = {}
if self.temperature is not None:
options["temperature"] = self.temperature
if self.thinking_level is not None:
options["thinking_level"] = self.thinking_level
return options or None
def _build_output_provider_metadata(self) -> dict[str, Any]:
metadata: dict[str, Any] = {}
if self.temperature is not None:
metadata["gemini_temperature"] = self.temperature
if self.thinking_level is not None:
metadata["gemini_thinking_level"] = self.thinking_level
return metadata
@invocation(
"seedream_image_generation",
title="Seedream Image Generation",
tags=["external", "generation", "seedream"],
category="image",
version="1.1.0",
)
class SeedreamImageGenerationInvocation(BaseExternalImageGenerationInvocation):
"""Generate images using a BytePlus Seedream model."""
provider_id = "seedream"
model: ModelIdentifierField = InputField(
description=FieldDescriptions.main_model,
ui_model_base=[BaseModelType.External],
ui_model_type=[ModelType.ExternalImageGenerator],
ui_model_format=[ModelFormat.ExternalApi],
ui_model_provider_id=["seedream"],
)
# Seedream's API has only one endpoint and no inpaint support — mode is implicit
# from inputs (img2img happens automatically when init_image or reference_images
# are provided). Hide mode and mask_image since they have no effect.
mode: ExternalGenerationMode = InputField(default="txt2img", description="Generation mode.", ui_hidden=True)
mask_image: ImageField | None = InputField(default=None, description="Mask image for inpaint", ui_hidden=True)
watermark: bool = InputField(default=False, description="Add watermark to generated images")
optimize_prompt: bool = InputField(default=False, description="Let the model optimize the prompt before generation")
def _build_provider_options(self) -> dict[str, Any]:
return {
"watermark": self.watermark,
"optimize_prompt": self.optimize_prompt,
}
def _build_output_provider_metadata(self) -> dict[str, Any]:
return {
"seedream_watermark": self.watermark,
"seedream_optimize_prompt": self.optimize_prompt,
}
@invocation(
"alibabacloud_image_generation",
title="Alibaba Cloud DashScope Image Generation",
tags=["external", "generation", "alibabacloud", "dashscope"],
category="image",
version="1.0.0",
)
class AlibabaCloudImageGenerationInvocation(BaseExternalImageGenerationInvocation):
"""Generate images using an Alibaba Cloud DashScope external model."""
provider_id = "alibabacloud"
model: ModelIdentifierField = InputField(
description=FieldDescriptions.main_model,
ui_model_base=[BaseModelType.External],
ui_model_type=[ModelType.ExternalImageGenerator],
ui_model_format=[ModelFormat.ExternalApi],
ui_model_provider_id=["alibabacloud"],
)