feat(image-pipeline): Add identity-conditioning support to image generation pipeline stages

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-30 15:51:49 -07:00
parent 8d43da846e
commit 48ae9da41c
2 changed files with 57 additions and 22 deletions

View file

@ -1120,18 +1120,27 @@ async def _load_ip_adapter_into_pipeline(
if _ip_adapter_manager is None:
_ip_adapter_manager = IPAdapterManager(device=device)
# Load IP-Adapter into pipeline
# Using Plus Face for better facial identity preservation
# (requires explicit ViT-H image encoder, configured in ip_adapter_manager)
pipeline = await _ip_adapter_manager.load_ip_adapter(
pipeline,
model_id="ip-adapter-plus-face_sdxl",
scale=identity.ip_adapter_scale,
)
logger.info(
f"IP-Adapter loaded successfully (scale={identity.ip_adapter_scale})"
)
if identity.body_image is not None:
# Dual-adapter path: face (Plus Face) + body (Plus general).
# Face adapter preserves facial identity; body adapter conditions on
# full-body shape/proportions from the reference photo.
pipeline = await _ip_adapter_manager.load_dual_ip_adapters(
pipeline,
face_scale=identity.ip_adapter_scale,
body_scale=identity.body_ip_adapter_scale,
)
logger.info(
f"Dual IP-Adapters loaded "
f"(face={identity.ip_adapter_scale}, body={identity.body_ip_adapter_scale})"
)
else:
# Single face adapter path (original behaviour)
pipeline = await _ip_adapter_manager.load_ip_adapter(
pipeline,
model_id="ip-adapter-plus-face_sdxl",
scale=identity.ip_adapter_scale,
)
logger.info(f"Face IP-Adapter loaded (scale={identity.ip_adapter_scale})")
# Prepare InstantID face keypoint conditioning if enabled
if identity.enable_instantid and identity.face_images:
@ -1719,22 +1728,33 @@ class GenerateStage(PipelineStage):
identity = context.identity_conditioning
if identity.face_images:
# IP-Adapter expects 1 image per adapter (we have 1 adapter)
# Use the first face image for IP-Adapter conditioning
# For multiple reference images, the identity embedding already averages them
gen_kwargs["ip_adapter_image"] = identity.face_images[0]
if identity.body_image is not None:
# Dual-adapter: pass [face_image, body_image] — one per loaded adapter.
# Order must match the load order in load_dual_ip_adapters
# (face first, body second).
gen_kwargs["ip_adapter_image"] = [
identity.face_images[0],
identity.body_image,
]
logger.info(
f"Dual IP-Adapter active: identity='{identity.identity_id}', "
f"face_scale={identity.ip_adapter_scale}, "
f"body_scale={identity.body_ip_adapter_scale}"
)
else:
# Single face adapter (original path)
gen_kwargs["ip_adapter_image"] = identity.face_images[0]
logger.info(
f"Face IP-Adapter active: identity='{identity.identity_id}', "
f"using first of {len(identity.face_images)} face images, "
f"scale={identity.ip_adapter_scale}"
)
# Mark that identity conditioning is being used
context.identity_used = True
context.metadata["identity_used"] = True
context.metadata["identity_id"] = identity.identity_id
logger.info(
f"IP-Adapter active: identity='{identity.identity_id}', "
f"using first of {len(identity.face_images)} face images, "
f"scale={identity.ip_adapter_scale}"
)
# Add seed if provided
if seed is not None:
try:

View file

@ -175,8 +175,23 @@ class IdentityConditioningStage(PipelineStage):
enable_instantid=request.enable_instantid,
source_image_count=len(face_images),
identity_service_url=self.identity_service_url,
body_ip_adapter_scale=request.body_ip_adapter_scale,
)
# Decode body reference image if provided
if request.body_image_override:
try:
conditioning_data.body_image = self._decode_base64_image(
request.body_image_override
)
logger.info(
f"Body reference image decoded "
f"({conditioning_data.body_image.size}, "
f"scale={request.body_ip_adapter_scale})"
)
except Exception as e:
logger.warning(f"Failed to decode body_image_override: {e}")
# Prepare InstantID conditioning if enabled (Phase 2)
if request.enable_instantid:
await self._prepare_instantid_conditioning(conditioning_data, face_images[0])