Split Cadrille and CAD-Recode nodes from the CADabra monorepo. Multi-modal CAD reconstruction from point clouds, images, or text. Original source: https://github.com/PozzettiAndrea/ComfyUI-CADabra
159 lines
5.9 KiB
Python
159 lines
5.9 KiB
Python
"""
|
|
CAD-Recode Model Architecture
|
|
Paper: CAD-Recode: Reverse Engineering CAD Code from Point Clouds (ICCV 2025)
|
|
Source: https://github.com/filaPro/cad-recode
|
|
|
|
This module contains the CADRecode model class which extends Qwen2ForCausalLM
|
|
with a FourierPointEncoder for processing 3D point clouds.
|
|
"""
|
|
|
|
import torch
|
|
from torch import nn
|
|
from transformers import Qwen2ForCausalLM, Qwen2Model, PreTrainedModel
|
|
from transformers.modeling_outputs import CausalLMOutputWithPast
|
|
|
|
|
|
class FourierPointEncoder(nn.Module):
|
|
"""
|
|
Encodes 3D point coordinates using Fourier features.
|
|
|
|
Transforms (x, y, z) coordinates into high-dimensional embeddings using
|
|
sinusoidal positional encoding, then projects to the model's hidden size.
|
|
|
|
Input: (batch, N, 3) point coordinates
|
|
Output: (batch, N, hidden_size) embeddings
|
|
"""
|
|
|
|
def __init__(self, hidden_size: int):
|
|
super().__init__()
|
|
# 8 frequency bands: 2^0, 2^1, ..., 2^7
|
|
frequencies = 2.0 ** torch.arange(8, dtype=torch.float32)
|
|
self.register_buffer('frequencies', frequencies, persistent=False)
|
|
# 3 coords + 3*8 sin + 3*8 cos = 51 dimensions
|
|
self.projection = nn.Linear(51, hidden_size)
|
|
|
|
def forward(self, points: torch.Tensor) -> torch.Tensor:
|
|
"""
|
|
Args:
|
|
points: (batch, N, 3) or (N, 3) point coordinates
|
|
|
|
Returns:
|
|
(batch, N, hidden_size) or (N, hidden_size) embeddings
|
|
"""
|
|
x = points
|
|
# Apply Fourier encoding: multiply by frequencies
|
|
x = (x.unsqueeze(-1) * self.frequencies).view(*x.shape[:-1], -1)
|
|
# Concatenate original coords with sin and cos features
|
|
x = torch.cat((points, x.sin(), x.cos()), dim=-1)
|
|
# Project to hidden size
|
|
x = self.projection(x)
|
|
return x
|
|
|
|
|
|
class CADRecode(Qwen2ForCausalLM):
|
|
"""
|
|
CAD-Recode: Point cloud to CadQuery code generation model.
|
|
|
|
Extends Qwen2-1.5B with a FourierPointEncoder to process 3D point clouds
|
|
alongside text tokens. The model generates executable CadQuery Python code
|
|
that reconstructs CAD models from point cloud input.
|
|
|
|
Usage:
|
|
model = CADRecode.from_pretrained('filapro/cad-recode-v1.5')
|
|
# Point embeddings replace pad tokens where attention_mask == -1
|
|
"""
|
|
|
|
def __init__(self, config):
|
|
PreTrainedModel.__init__(self, config)
|
|
self.model = Qwen2Model(config)
|
|
self.vocab_size = config.vocab_size
|
|
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
|
|
|
|
# Initialize point encoder in float32 for numerical stability
|
|
torch.set_default_dtype(torch.float32)
|
|
self.point_encoder = FourierPointEncoder(config.hidden_size)
|
|
torch.set_default_dtype(torch.bfloat16)
|
|
|
|
def forward(
|
|
self,
|
|
input_ids=None,
|
|
attention_mask=None,
|
|
point_cloud=None,
|
|
position_ids=None,
|
|
past_key_values=None,
|
|
inputs_embeds=None,
|
|
labels=None,
|
|
use_cache=None,
|
|
output_attentions=None,
|
|
output_hidden_states=None,
|
|
return_dict=None,
|
|
cache_position=None,
|
|
):
|
|
"""
|
|
Forward pass with point cloud integration.
|
|
|
|
Point cloud embeddings are inserted into the sequence where
|
|
attention_mask == -1 (replacing pad token embeddings).
|
|
"""
|
|
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
|
|
output_hidden_states = output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
|
|
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
|
|
|
# Concatenate point and text embeddings on first forward pass
|
|
if past_key_values is None or past_key_values.get_seq_length() == 0:
|
|
assert inputs_embeds is None
|
|
inputs_embeds = self.model.embed_tokens(input_ids)
|
|
point_embeds = self.point_encoder(point_cloud).bfloat16()
|
|
# Replace pad token positions (marked with -1) with point embeddings
|
|
inputs_embeds[attention_mask == -1] = point_embeds.reshape(-1, point_embeds.shape[2])
|
|
attention_mask[attention_mask == -1] = 1
|
|
input_ids = None
|
|
position_ids = None
|
|
|
|
# Run through transformer
|
|
outputs = self.model(
|
|
input_ids=input_ids,
|
|
attention_mask=attention_mask,
|
|
position_ids=position_ids,
|
|
past_key_values=past_key_values,
|
|
inputs_embeds=inputs_embeds,
|
|
use_cache=use_cache,
|
|
output_attentions=output_attentions,
|
|
output_hidden_states=output_hidden_states,
|
|
return_dict=return_dict,
|
|
cache_position=cache_position,
|
|
)
|
|
|
|
hidden_states = outputs[0]
|
|
logits = self.lm_head(hidden_states)
|
|
logits = logits.float()
|
|
|
|
loss = None
|
|
if labels is not None:
|
|
# Shift for next-token prediction
|
|
shift_logits = logits[..., :-1, :].contiguous()
|
|
shift_labels = labels[..., 1:].contiguous()
|
|
loss_fct = nn.CrossEntropyLoss()
|
|
shift_logits = shift_logits.view(-1, self.config.vocab_size)
|
|
shift_labels = shift_labels.view(-1)
|
|
shift_labels = shift_labels.to(shift_logits.device)
|
|
loss = loss_fct(shift_logits, shift_labels)
|
|
|
|
if not return_dict:
|
|
output = (logits,) + outputs[1:]
|
|
return (loss,) + output if loss is not None else output
|
|
|
|
return CausalLMOutputWithPast(
|
|
loss=loss,
|
|
logits=logits,
|
|
past_key_values=outputs.past_key_values,
|
|
hidden_states=outputs.hidden_states,
|
|
attentions=outputs.attentions,
|
|
)
|
|
|
|
def prepare_inputs_for_generation(self, *args, **kwargs):
|
|
"""Pass point_cloud through to forward during generation."""
|
|
model_inputs = super().prepare_inputs_for_generation(*args, **kwargs)
|
|
model_inputs['point_cloud'] = kwargs.get('point_cloud')
|
|
return model_inputs
|