Chuyển đến nội dung chính

Lesson 6: Multi-modal Generation — Combining Text + Image for Design Output

Build a pipeline combining text prompt + image reference: ControlNet conditioning, IP-Adapter + prompt fusion, multi-reference blending. Output 2–4 design variations.

🧠 AI & ML — Lesson 5 Lesson 6: Multi-modal Generation — Combination Text + Image for Design Output

AI in Action: Building an AI Platform for Fashion & Print-on-Demand

Part 2: AI Design Generation Engine

xdev.asia

Introduction

The real power of the platform lies in the ability to combine text + image — users can both describe in words and provide reference images. This article builds a multi-modal generation pipeline using IP-Adapter + ControlNet + text prompt fusion.


1. Multi-modal Pipeline architecture

User Input
├── Text Prompt: "cyberpunk cat with neon glasses"
└── Reference Image: [uploaded artwork]
        │
        ├── CLIP encode → style embedding
        ├── IP-Adapter → style conditioning
        └── Color extraction → color guidance
                │
                ▼
    ┌───────────────────────────┐
    │   SDXL + LoRA (fashion)   │
    │   + IP-Adapter (style)    │
    │   + ControlNet (layout)   │
    └───────────────────────────┘
                │
                ▼
        4 Design Variations

2. Prompt + Image Fusion Engine

class MultiModalGenerator:
    """Kết hợp text prompt + image reference để generate design"""

    def __init__(self):
        self.pipe = self._setup_pipeline()
        self.style_analyzer = StyleAnalyzer()
        self.color_extractor = ColorExtractor()

    def _setup_pipeline(self):
        pipe = StableDiffusionXLPipeline.from_pretrained(
            "stabilityai/stable-diffusion-xl-base-1.0",
            torch_dtype=torch.float16,
        )
        pipe.load_lora_weights("models/sdxl-fashion-lora-v2.1")
        pipe.load_ip_adapter(
            "h94/IP-Adapter",
            subfolder="sdxl_models",
            weight_name="ip-adapter-plus_sdxl_vit-h.safetensors",
        )
        pipe.to("cuda")
        return pipe

    async def generate(
        self,
        prompt: str | None = None,
        reference_image: Image.Image | None = None,
        num_variations: int = 4,
        style_strength: float = 0.5,
    ) -> GenerationResult:
        # Determine generation mode
        if prompt and reference_image:
            return await self._generate_multimodal(
                prompt, reference_image,
                num_variations, style_strength
            )
        elif prompt:
            return await self._generate_text_only(
                prompt, num_variations
            )
        elif reference_image:
            return await self._generate_image_only(
                reference_image, num_variations
            )
        else:
            raise ValueError("Cần ít nhất prompt hoặc reference image")

    async def _generate_multimodal(
        self,
        prompt: str,
        reference: Image.Image,
        num_variations: int,
        style_strength: float,
    ) -> GenerationResult:
        # 1. Analyze reference
        style = self.style_analyzer.classify_style(reference)
        colors = self.color_extractor.extract_palette(reference)

        # 2. Enhance prompt with reference info
        enhanced_prompt = self._fuse_prompt_with_reference(
            prompt, style, colors
        )

        # 3. Set IP-Adapter influence
        self.pipe.set_ip_adapter_scale(style_strength)

        # 4. Generate
        images = self.pipe(
            prompt=enhanced_prompt,
            negative_prompt=self.FASHION_NEGATIVE_PROMPT,
            ip_adapter_image=reference,
            num_images_per_prompt=num_variations,
            num_inference_steps=30,
            guidance_scale=7.5,
        ).images

        return GenerationResult(
            images=images,
            prompt_used=enhanced_prompt,
            style_analysis=style,
            colors=colors,
        )

    def _fuse_prompt_with_reference(
        self,
        user_prompt: str,
        style: dict,
        colors: list[dict],
    ) -> str:
        """Kết hợp prompt với thông tin từ reference"""
        top_style = list(style.keys())[0]
        color_names = [c["name"] for c in colors[:3]]

        fused = (
            f"{user_prompt}, "
            f"{top_style} style, "
            f"color palette: {', '.join(color_names)}, "
            f"t-shirt design, transparent background, print-ready"
        )
        return fused

3. ControlNet for Layout Control

from diffusers import ControlNetModel, StableDiffusionXLControlNetPipeline

class LayoutControlledGenerator:
    """Generate design với layout control từ ControlNet"""

    def __init__(self):
        controlnet = ControlNetModel.from_pretrained(
            "diffusers/controlnet-canny-sdxl-1.0",
            torch_dtype=torch.float16,
        )
        self.pipe = StableDiffusionXLControlNetPipeline.from_pretrained(
            "stabilityai/stable-diffusion-xl-base-1.0",
            controlnet=controlnet,
            torch_dtype=torch.float16,
        )
        self.pipe.load_lora_weights("models/sdxl-fashion-lora-v2.1")
        self.pipe.to("cuda")

    def generate_with_layout(
        self,
        prompt: str,
        layout_image: Image.Image,
        controlnet_scale: float = 0.5,
    ) -> list[Image.Image]:
        """
        Generate design theo layout guide

        layout_image: Ảnh canny edge hoặc sketch
        controlnet_scale: 0.3 (loose) → 0.8 (strict)
        """
        # Generate canny edge nếu input là ảnh thường
        control_image = self._prepare_control(layout_image)

        images = self.pipe(
            prompt=prompt,
            image=control_image,
            controlnet_conditioning_scale=controlnet_scale,
            num_images_per_prompt=4,
            num_inference_steps=30,
        ).images

        return images

    def _prepare_control(self, image: Image.Image) -> Image.Image:
        """Tạo control image (canny edge)"""
        import cv2
        import numpy as np

        img_array = np.array(image.convert("RGB"))
        gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
        edges = cv2.Canny(gray, 100, 200)
        return Image.fromarray(edges)

4. Multi-Reference Blending

class MultiReferenceBlender:
    """Kết hợp nhiều reference images"""

    def blend_references(
        self,
        references: list[Image.Image],
        weights: list[float] | None = None,
    ) -> torch.Tensor:
        """
        Blend CLIP embeddings từ nhiều references

        references: List ảnh tham khảo
        weights: Trọng số mỗi reference (sum = 1.0)
        """
        if weights is None:
            weights = [1.0 / len(references)] * len(references)

        embeddings = []
        for ref in references:
            emb = self.style_analyzer.get_image_embedding(ref)
            embeddings.append(emb)

        # Weighted average
        blended = sum(
            w * emb for w, emb in zip(weights, embeddings)
        )
        blended = blended / blended.norm(dim=-1, keepdim=True)

        return blended

Summary

Multi-modal generation pipeline:

  1. Prompt + Image fusion — combine text intent with visual reference
  2. IP-Adapter — controllable style transfer with adjustable strength
  3. ControlNet — layout control for garment-aware placement
  4. Multi-reference blending — blending multiple references by weight

Next article: Prompt Engineering for Fashion — building template systems, bilingual handling and variation strategies.