Files
Junst-ComfyUI-Concept-Diffu…/concept_attention.py
T
Junst fbc90b8720 Fix Flux model 3D tensor requirement
- Fix 'Input img and txt tensors must have 3 dimensions' error
- Convert x tensor from 4D to 3D format: [batch, seq, dim]
- Flatten context embeddings to 3D format for Flux compatibility
- Remove spatial dimension handling that caused 4D tensors
- Ensure all inputs match Flux model's 3D tensor requirements
2025-09-24 16:54:42 +09:00

216 lines
9.7 KiB
Python

"""
Simplified Concept Attention for ComfyUI based on original ConceptAttention structure.
This version removes complex hook systems and uses direct attention capture.
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from PIL import Image
import einops
from typing import Dict, List, Optional, Tuple
import logging
logger = logging.getLogger(__name__)
class ConceptAttention:
"""
Simplified concept attention processor based on original ConceptAttention structure.
Uses direct attention capture without complex hook systems.
"""
def __init__(self, model, device="cuda"):
self.model = model
self.device = device
self.attention_outputs = {}
self.concept_embeddings = None
def extract_concept_embeddings(self, concepts: List[str], clip_model) -> torch.Tensor:
"""
Extract concept embeddings using ComfyUI's CLIP model.
Based on original embed_concepts function.
"""
try:
concept_embeddings = []
for concept in concepts:
# Use ComfyUI's CLIP.encode method
embedding = clip_model.encode(concept)
if hasattr(embedding, 'squeeze'):
embedding = embedding.squeeze()
concept_embeddings.append(embedding)
concept_embeddings = torch.stack(concept_embeddings).unsqueeze(0)
logger.info(f"Extracted embeddings for concepts: {concepts}")
return concept_embeddings
except Exception as e:
logger.error(f"Error extracting concept embeddings: {e}")
raise RuntimeError(f"Failed to extract concept embeddings: {e}")
def capture_attention_outputs(self, image: torch.Tensor, concepts: List[str],
clip_model, timestep: int = 0) -> Dict[str, torch.Tensor]:
"""
Capture attention outputs using direct model forward pass.
Based on original FluxGenerator approach.
"""
try:
# Extract concept embeddings
concept_embeddings = self.extract_concept_embeddings(concepts, clip_model)
self.concept_embeddings = concept_embeddings
# Prepare inputs for Flux model
batch_size = 1
height, width = image.shape[-2], image.shape[-1]
# Ensure minimum dimensions for Flux model
latent_height = max(height // 8, 32)
latent_width = max(width // 8, 32)
# Create inputs for Flux model - ensure 3D tensors
# Flux model expects 3D tensors: [batch, seq, dim]
x = torch.randn(batch_size, latent_height * latent_width, 4, device=self.device)
timestep_tensor = torch.tensor([timestep], device=self.device)
# Create context from concept embeddings - ensure 3D format
if len(concept_embeddings.shape) == 3: # [batch, seq, dim]
context = concept_embeddings # Already 3D
elif len(concept_embeddings.shape) == 4: # [batch, seq, dim1, dim2]
# Flatten to 3D: [batch, seq, dim1*dim2]
batch, seq, dim1, dim2 = concept_embeddings.shape
context = concept_embeddings.view(batch, seq, dim1 * dim2)
elif len(concept_embeddings.shape) == 5: # [batch, batch, seq, dim1, dim2]
# Remove extra batch dimension and flatten
context = concept_embeddings.squeeze(0) # Remove first batch dimension
if len(context.shape) == 4: # [seq, dim1, dim2]
seq, dim1, dim2 = context.shape
context = context.view(1, seq, dim1 * dim2) # Add batch dimension back
else:
# For other shapes, try to reshape to 3D
if len(concept_embeddings.shape) == 2: # [seq, dim]
context = concept_embeddings.unsqueeze(0) # [1, seq, dim]
else:
context = concept_embeddings.unsqueeze(0) # Add batch dimension if needed
# Create guidance vector
y = torch.zeros(batch_size, 512, device=self.device)
logger.info(f"Running Flux model forward pass with inputs:")
logger.info(f"- x: {x.shape}")
logger.info(f"- timestep: {timestep_tensor.shape}")
logger.info(f"- context: {context.shape}")
logger.info(f"- y: {y.shape}")
# Run model forward pass to capture attention
with torch.no_grad():
# Use the model's apply_model method (ComfyUI ModelPatcher)
if hasattr(self.model, 'apply_model'):
output = self.model.apply_model(x, timestep_tensor, context, y)
elif hasattr(self.model, 'model') and hasattr(self.model.model, 'apply_model'):
# Nested model access
output = self.model.model.apply_model(x, timestep_tensor, context, y)
else:
# Try to access the underlying diffusion model
if hasattr(self.model, 'model'):
diffusion_model = self.model.model
if hasattr(diffusion_model, 'apply_model'):
output = diffusion_model.apply_model(x, timestep_tensor, context, y)
else:
raise RuntimeError("Cannot find apply_model method in model")
else:
raise RuntimeError("Cannot access model for forward pass")
# Store the output as attention
self.attention_outputs['flux_output'] = output
logger.info(f"Captured attention output: {output.shape}")
return self.attention_outputs
except Exception as e:
logger.error(f"Error capturing attention outputs: {e}")
raise RuntimeError(f"Failed to capture attention outputs: {e}")
def create_concept_maps(self, concepts: List[str], image_shape: Tuple[int, int]) -> Dict[str, torch.Tensor]:
"""
Create concept maps from attention outputs.
Based on original concept_attention_pipeline approach.
"""
try:
if not self.attention_outputs:
raise RuntimeError("No attention outputs available")
concept_maps = {}
attention_output = list(self.attention_outputs.values())[0] # Get first output
# Reshape attention output to spatial dimensions
if len(attention_output.shape) == 3: # [batch, seq, dim]
batch_size, seq_len, dim = attention_output.shape
# Reshape to spatial format
spatial_size = int(np.sqrt(seq_len))
if spatial_size * spatial_size == seq_len:
attention_spatial = attention_output.view(batch_size, spatial_size, spatial_size, dim)
else:
# Fallback to square root
spatial_size = int(np.sqrt(seq_len))
attention_spatial = attention_output[:, :spatial_size*spatial_size].view(batch_size, spatial_size, spatial_size, dim)
else:
attention_spatial = attention_output
# Create concept maps for each concept
for i, concept in enumerate(concepts):
if i < attention_spatial.shape[-1]:
# Extract concept-specific attention
concept_attention = attention_spatial[..., i]
# Resize to target image size
target_h, target_w = image_shape
concept_attention_resized = F.interpolate(
concept_attention.unsqueeze(0).unsqueeze(0),
size=(target_h, target_w),
mode='bilinear',
align_corners=False
).squeeze()
concept_maps[concept] = concept_attention_resized
logger.info(f"Created concept map for '{concept}': {concept_attention_resized.shape}")
return concept_maps
except Exception as e:
logger.error(f"Error creating concept maps: {e}")
raise RuntimeError(f"Failed to create concept maps: {e}")
class ConceptAttentionProcessor:
"""
Simplified processor for ComfyUI integration.
"""
def __init__(self, model, device="cuda"):
self.concept_attention = ConceptAttention(model, device)
self.device = device
def process_image(self, image: torch.Tensor, concepts: List[str],
clip_model) -> Dict[str, torch.Tensor]:
"""
Process image and extract concept attention maps.
"""
try:
logger.info(f"Processing image with concepts: {concepts}")
# Capture attention outputs
attention_outputs = self.concept_attention.capture_attention_outputs(
image, concepts, clip_model
)
# Create concept maps
image_shape = (image.shape[-2], image.shape[-1])
concept_maps = self.concept_attention.create_concept_maps(concepts, image_shape)
logger.info(f"Created concept maps for: {list(concept_maps.keys())}")
return concept_maps
except Exception as e:
logger.error(f"Error in SimpleConceptAttentionProcessor: {e}")
raise RuntimeError(f"Concept attention processing failed: {e}")