From 48ae9da41c1fbc2a55e9821137da11bb25a82f6b Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 30 Mar 2026 15:51:49 -0700 Subject: [PATCH] =?UTF-8?q?feat(image-pipeline):=20=E2=9C=A8=20Add=20ident?= =?UTF-8?q?ity-conditioning=20support=20to=20image=20generation=20pipeline?= =?UTF-8?q?=20stages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../src/image_pipeline/stages/generate.py | 64 ++++++++++++------- .../stages/identity_conditioning.py | 15 +++++ 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/orchestrators/imajin-pipeline/src/image_pipeline/stages/generate.py b/orchestrators/imajin-pipeline/src/image_pipeline/stages/generate.py index b43be900..d771795c 100644 --- a/orchestrators/imajin-pipeline/src/image_pipeline/stages/generate.py +++ b/orchestrators/imajin-pipeline/src/image_pipeline/stages/generate.py @@ -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: diff --git a/orchestrators/imajin-pipeline/src/image_pipeline/stages/identity_conditioning.py b/orchestrators/imajin-pipeline/src/image_pipeline/stages/identity_conditioning.py index c5022cf7..f8aba880 100644 --- a/orchestrators/imajin-pipeline/src/image_pipeline/stages/identity_conditioning.py +++ b/orchestrators/imajin-pipeline/src/image_pipeline/stages/identity_conditioning.py @@ -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])