Files
Andrea Pozzetti e114ecefdf Initial commit: Extract Cadrille + CAD-Recode from ComfyUI-CADabra
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
2025-12-19 23:02:15 +00:00

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