| import argparse |
| import os |
| import re |
|
|
| os.environ.setdefault("SPCONV_ALGO", "native") |
| os.environ.setdefault("ATTN_BACKEND", "flash_attn") |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") |
|
|
| import numpy as np |
| import torch |
|
|
| from trellis.pipelines import ( |
| DVDTextToVoxelPipeline, |
| TrellisTextTo3DPipeline, |
| as_voxel_output, |
| export_cubified_voxels, |
| run_text_stage2_from_dvd_voxels, |
| ) |
| from trellis.utils import postprocessing_utils |
|
|
|
|
| def slugify(text: str, max_length: int = 48) -> str: |
| slug = re.sub(r"[^a-zA-Z0-9]+", "_", text.lower()).strip("_") |
| return (slug[:max_length].strip("_") or "text_prompt") |
|
|
|
|
| def parse_range(values: list[int], resolution: int) -> tuple[int, int]: |
| lo, hi = values |
| lo = max(0, min(resolution, int(lo))) |
| hi = max(0, min(resolution, int(hi))) |
| if lo >= hi: |
| raise ValueError(f"Invalid edit range {values}; expected start < end after clamping.") |
| return lo, hi |
|
|
|
|
| def load_voxel_coords(path): |
| ext = os.path.splitext(path)[1].lower() |
| if ext == ".npy": |
| data = np.load(path) |
| elif ext in {".pt", ".pth"}: |
| data = torch.load(path, map_location="cpu") |
| if isinstance(data, dict): |
| for key in ("coords", "voxels", "samples"): |
| if key in data: |
| data = data[key] |
| break |
| else: |
| raise ValueError(f"Unsupported voxel coord file extension: {ext}") |
| return torch.as_tensor(data) |
|
|
|
|
| def make_keep_mask(voxels, edit_x: tuple[int, int], edit_y: tuple[int, int], edit_z: tuple[int, int]): |
| keep_mask = torch.ones_like(voxels.samples, dtype=torch.bool) |
| keep_mask[:, edit_x[0]:edit_x[1], edit_y[0]:edit_y[1], edit_z[0]:edit_z[1]] = False |
| return keep_mask |
|
|
|
|
| def parse_args(): |
| parser = argparse.ArgumentParser(description="DVD text-conditioned voxel editing followed by TRELLIS stage 2.") |
| parser.add_argument("--prompt", required=True, help="Target text condition for voxel editing.") |
| parser.add_argument( |
| "--voxel-coords", |
| required=True, |
| help="Existing voxel coords in DVD convention. Supports .npy, .pt, and .pth.", |
| ) |
| parser.add_argument("--name", default=None, help="Output asset name. Defaults to a slug of the prompt and voxel file.") |
| parser.add_argument("--dvd-config", default="ckpts/dvd_text_BSP_ft.json", help="BSP fine-tuned DVD text config JSON.") |
| parser.add_argument( |
| "--dvd-checkpoint", |
| default="ckpts/dvd_text_BSP_ft.safetensors", |
| help="BSP fine-tuned DVD text safetensors checkpoint.", |
| ) |
| parser.add_argument("--trellis-model", default="microsoft/TRELLIS-text-large", help="TRELLIS text pipeline.") |
| parser.add_argument("--output-dir", default="example_results", help="Directory for generated assets.") |
| parser.add_argument("--resolution", type=int, default=64, help="Voxel grid resolution for loaded coords.") |
| parser.add_argument("--seed", type=int, default=0) |
| parser.add_argument("--device", default="cuda") |
| parser.add_argument("--dvd-steps", type=int, default=128) |
| parser.add_argument("--stage2-steps", type=int, default=25) |
| parser.add_argument("--stage2-cfg", type=float, default=5.0) |
| parser.add_argument("--edit-x", type=int, nargs=2, default=(0, 64), metavar=("START", "END")) |
| parser.add_argument("--edit-y", type=int, nargs=2, default=(0, 64), metavar=("START", "END")) |
| parser.add_argument("--edit-z", type=int, nargs=2, default=(32, 64), metavar=("START", "END")) |
| parser.add_argument("--skip-stage2", action="store_true", help="Only generate and save edited DVD voxels.") |
| return parser.parse_args() |
|
|
|
|
| def main(): |
| args = parse_args() |
| os.makedirs(args.output_dir, exist_ok=True) |
|
|
| voxel_name = os.path.splitext(os.path.basename(args.voxel_coords))[0] |
| name = args.name or f"{slugify(args.prompt)}_{voxel_name}" |
|
|
| dvd = DVDTextToVoxelPipeline.from_files( |
| args.dvd_config, |
| args.dvd_checkpoint, |
| resolution=args.resolution, |
| device=args.device, |
| ) |
| voxels = as_voxel_output(load_voxel_coords(args.voxel_coords), resolution=args.resolution) |
|
|
| edit_x = parse_range(args.edit_x, args.resolution) |
| edit_y = parse_range(args.edit_y, args.resolution) |
| edit_z = parse_range(args.edit_z, args.resolution) |
| keep_mask = make_keep_mask(voxels, edit_x, edit_y, edit_z) |
|
|
| edited_voxels = dvd.edit_voxels( |
| args.prompt, |
| voxels, |
| keep_mask=keep_mask, |
| seed=args.seed, |
| steps=args.dvd_steps, |
| ) |
|
|
| edited_coords_path = os.path.join(args.output_dir, f"voxel64_{name}_edited_dis.npy") |
| np.save(edited_coords_path, edited_voxels.coords_without_batch.numpy()) |
| export_cubified_voxels(edited_voxels, os.path.join(args.output_dir, f"{name}_edited_voxels.glb")) |
| print(f"Saved edited DVD coords: {edited_coords_path}") |
|
|
| if args.skip_stage2: |
| return |
|
|
| trellis = TrellisTextTo3DPipeline.from_pretrained(args.trellis_model) |
| trellis.to(args.device) |
| outputs = run_text_stage2_from_dvd_voxels( |
| trellis, |
| args.prompt, |
| edited_voxels, |
| seed=args.seed, |
| slat_sampler_params={ |
| "steps": args.stage2_steps, |
| "cfg_strength": args.stage2_cfg, |
| }, |
| ) |
|
|
| glb = postprocessing_utils.to_glb(outputs["gaussian"][0], outputs["mesh"][0]) |
| glb_path = os.path.join(args.output_dir, f"{name}_edited.glb") |
| glb.export(glb_path) |
| print(f"Saved edited GLB: {glb_path}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|