import struct import numpy as np import io from server import PromptServer, BinaryEventTypes from pathlib import Path from PIL import Image import base64 from io import BytesIO import torch import tempfile import os class Hy3DtoTD: #https://github.com/kijai/ComfyUI-Hunyuan3DWrapper @classmethod def INPUT_TYPES(s): return { "required": { "trimesh": ("TRIMESH",), "broadcast": ("BOOLEAN", {"default": False}), } } RETURN_TYPES = () FUNCTION = "send_mesh" OUTPUT_NODE = True CATEGORY = "TouchDesigner" def send_mesh(self, trimesh, broadcast): try: processed_mesh = self.process_mesh(trimesh) return self._send_to_td(processed_mesh, broadcast) except Exception as e: raise ValueError(f"Error processing mesh: {str(e)}") def process_mesh(self, mesh): try: import trimesh if isinstance(mesh, trimesh.Scene): meshes = [] for m in mesh.geometry.values(): if isinstance(m, trimesh.Trimesh): meshes.append(m) if meshes: mesh = trimesh.util.concatenate(meshes) else: raise ValueError("No valid mesh data found") if not isinstance(mesh, trimesh.Trimesh): raise ValueError("Input data is not a valid Trimesh object") mesh.fix_normals() if hasattr(mesh.visual, 'vertex_colors') and mesh.visual.vertex_colors is not None: pass elif hasattr(mesh.visual, 'uv') and mesh.visual.uv is not None: mesh.visual = mesh.visual.to_color() else: mesh.visual = trimesh.visual.ColorVisuals(mesh) mesh.visual.vertex_colors = np.tile([200, 200, 200, 255], (len(mesh.vertices), 1)) return mesh except ImportError: raise ImportError("Please install the trimesh library: pip install trimesh") except Exception as e: raise ValueError(f"Error processing mesh: {str(e)}") def _send_to_td(self, mesh, broadcast): try: import trimesh buffer = io.BytesIO() mesh.export(file_obj=buffer, file_type='ply') binary_data = buffer.getvalue() buffer.close() server = PromptServer.instance sid = None if broadcast else server.client_id server.send_sync(1000, binary_data, sid=sid) return {"ui": { "mesh": [{ "source": "websocket", "content-type": "model/ply", "type": "output", }] }} except ImportError: raise ImportError("Please install the trimesh library: pip install trimesh") except Exception as e: raise ValueError(f"Error sending mesh to TouchDesigner: {str(e)}") class Tripo3DtoTD: #https://github.com/VAST-AI-Research/ComfyUI-Tripo @classmethod def INPUT_TYPES(s): return { "required": { "model_file": ("STRING",), "broadcast": ("BOOLEAN", {"default": False}), } } RETURN_TYPES = () FUNCTION = "send_mesh" OUTPUT_NODE = True CATEGORY = "TouchDesigner" def send_mesh(self, model_file, broadcast): try: processed_mesh = self.process_mesh(model_file) return self._send_to_td(processed_mesh, broadcast) except Exception as e: raise ValueError(f"Error processing mesh: {str(e)}") def process_mesh(self, model_file): try: import trimesh if not model_file.startswith(('http://', 'https://')) and not os.path.isabs(model_file): try: import folder_paths output_dir = folder_paths.get_output_directory() local_path = os.path.join(output_dir, model_file) if os.path.exists(local_path): model_file = local_path except: pass mesh = trimesh.load(model_file) if isinstance(mesh, trimesh.Scene): meshes = [] for m in mesh.geometry.values(): if isinstance(m, trimesh.Trimesh): self._process_vertex_colors(m) meshes.append(m) if meshes: mesh = trimesh.util.concatenate(meshes) else: raise ValueError("No valid mesh data found") if not isinstance(mesh, trimesh.Trimesh): raise ValueError("Input data is not a valid Trimesh object") mesh.fix_normals() if not hasattr(mesh.visual, 'vertex_colors') or mesh.visual.vertex_colors is None: mesh.visual = trimesh.visual.ColorVisuals(mesh) mesh.visual.vertex_colors = np.tile([200, 200, 200, 255], (len(mesh.vertices), 1)) else: if mesh.visual.vertex_colors.max() <= 1.0: mesh.visual.vertex_colors = (mesh.visual.vertex_colors * 255).astype(np.uint8) else: mesh.visual.vertex_colors = mesh.visual.vertex_colors.astype(np.uint8) return mesh except ImportError: raise ImportError("Please install the trimesh library: pip install trimesh") except Exception as e: raise ValueError(f"Error processing mesh: {str(e)}") def _process_vertex_colors(self, m): try: import trimesh if not hasattr(m.visual, 'vertex_colors') or m.visual.vertex_colors is None: if hasattr(m.visual, 'material'): material = m.visual.material if hasattr(material, 'baseColorTexture') and material.baseColorTexture is not None: if hasattr(m.visual, 'uv') and m.visual.uv is not None: self._extract_colors_from_texture(m) elif hasattr(material, 'baseColorFactor'): color = np.array(material.baseColorFactor) if len(color) == 3: color = np.append(color, 1.0) color = np.clip(color, 0, 1) color = (color * 255).astype(np.uint8) m.visual = trimesh.visual.ColorVisuals(m) m.visual.vertex_colors = np.tile(color, (len(m.vertices), 1)) else: m.visual = trimesh.visual.ColorVisuals(m) m.visual.vertex_colors = np.tile([200, 200, 200, 255], (len(m.vertices), 1)) else: m.visual = trimesh.visual.ColorVisuals(m) m.visual.vertex_colors = np.tile([200, 200, 200, 255], (len(m.vertices), 1)) else: if m.visual.vertex_colors.max() <= 1.0: m.visual.vertex_colors = (m.visual.vertex_colors * 255).astype(np.uint8) else: m.visual.vertex_colors = m.visual.vertex_colors.astype(np.uint8) except ImportError: raise ImportError("Please install the trimesh library: pip install trimesh") except Exception as e: raise ValueError(f"Error processing vertex colors: {str(e)}") def _extract_colors_from_texture(self, m): try: import trimesh material = m.visual.material image = material.baseColorTexture if image is not None: image_data = np.array(image) if image_data.dtype != np.uint8: image_data = (np.clip(image_data, 0, 1) * 255).astype(np.uint8) uvs = m.visual.uv uvs = np.clip(uvs, 0, 1) h, w = image_data.shape[0], image_data.shape[1] i = ((1 - uvs[:, 1]) * (h - 1)).astype(int) j = (uvs[:, 0] * (w - 1)).astype(int) i = np.clip(i, 0, h - 1) j = np.clip(j, 0, w - 1) colors = image_data[i, j, :4] m.visual = trimesh.visual.ColorVisuals(m) m.visual.vertex_colors = colors else: m.visual = trimesh.visual.ColorVisuals(m) m.visual.vertex_colors = np.tile([200, 200, 200, 255], (len(m.vertices), 1)) except ImportError: raise ImportError("Please install the trimesh library: pip install trimesh") except Exception as e: raise ValueError(f"Error extracting colors from texture: {str(e)}") def _send_to_td(self, mesh, broadcast): try: if not hasattr(mesh.visual, 'vertex_colors') or mesh.visual.vertex_colors is None: colors = np.tile([200, 200, 200, 255], (len(mesh.vertices), 1)).astype(np.uint8) else: colors = mesh.visual.vertex_colors.astype(np.uint8) header = [ "ply", "format binary_little_endian 1.0", f"element vertex {len(mesh.vertices)}", "property float x", "property float y", "property float z", "property uchar red", "property uchar green", "property uchar blue", "property uchar alpha", "end_header\n" ] header = '\n'.join(header).encode('ascii') vertices = mesh.vertices.astype('I", type_num) bytesIO.write(header) image.save(bytesIO, format="JPEG", quality=quality) jpeg_bytes = bytesIO.getvalue() server = PromptServer.instance sid = None if broadcast else server.client_id server.send_sync( BinaryEventTypes.PREVIEW_IMAGE, jpeg_bytes, sid, ) results.append({ "source": "websocket", "content-type": "image/jpeg", "type": "output", }) return {"ui": {"images": results}} class Comfy3DPacktoTD: #https://github.com/MrForExample/ComfyUI-3D-Pack @classmethod def INPUT_TYPES(cls): return { "required": { "mesh": ("MESH",), "broadcast": ("BOOLEAN", {"default": False}), }, } RETURN_TYPES = () FUNCTION = "send_to_td" OUTPUT_NODE = True CATEGORY = "TouchDesigner" def send_to_td(self, mesh, broadcast): try: vertices = mesh.v.cpu().numpy().astype(np.float32) colors = None color_source = "none" if hasattr(mesh, 'albedo') and mesh.albedo is not None: albedo = mesh.albedo if hasattr(albedo, 'cpu'): albedo = albedo.cpu() if hasattr(albedo, 'numpy'): albedo = albedo.numpy() if len(albedo.shape) > 1: if hasattr(mesh, 'vt') and mesh.vt is not None: uvs = mesh.vt.cpu().numpy() uvs = np.clip(uvs, 0, 1) tex_h, tex_w = albedo.shape[:2] mapped_u, mapped_v = uvs[:, 0], uvs[:, 1] tex_x = mapped_u * (tex_w - 1) tex_y = mapped_v * (tex_h - 1) tex_x0 = np.floor(tex_x).astype(np.int32) tex_y0 = np.floor(tex_y).astype(np.int32) tex_x1 = np.minimum(tex_x0 + 1, tex_w - 1) tex_y1 = np.minimum(tex_y0 + 1, tex_h - 1) wx = tex_x - tex_x0 wy = tex_y - tex_y0 c00 = albedo[tex_y0, tex_x0] c01 = albedo[tex_y0, tex_x1] c10 = albedo[tex_y1, tex_x0] c11 = albedo[tex_y1, tex_x1] c0 = c00 * (1 - wx[:, np.newaxis]) + c01 * wx[:, np.newaxis] c1 = c10 * (1 - wx[:, np.newaxis]) + c11 * wx[:, np.newaxis] colors = c0 * (1 - wy[:, np.newaxis]) + c1 * wy[:, np.newaxis] color_source = "albedo_with_uv_no_v_flip" else: if albedo.shape[0] == len(vertices): colors = albedo color_source = "albedo_direct" else: colors = albedo color_source = "albedo_scalar" if colors is None and hasattr(mesh, 'vc') and mesh.vc is not None: colors = mesh.vc.cpu().numpy() color_source = "vertex_colors" if colors is None: positions = vertices - vertices.min(axis=0) positions = positions / positions.max(axis=0) if positions.max() > 0 else positions colors = positions color_source = "generated" if colors is not None: if colors.max() <= 1.0: colors = (colors * 255).astype(np.uint8) else: colors = np.clip(colors, 0, 255).astype(np.uint8) if colors.shape[1] == 3: alpha = np.full((len(vertices), 1), 255, dtype=np.uint8) colors = np.concatenate([colors, alpha], axis=1) header = [ "ply", "format binary_little_endian 1.0", f"element vertex {len(vertices)}", "property float x", "property float y", "property float z", "property uchar red", "property uchar green", "property uchar blue", "property uchar alpha", "end_header\n" ] header = '\n'.join(header).encode('ascii') binary_data = bytearray() for i in range(len(vertices)): binary_data.extend(struct.pack(' 0: mesh_obj = mesh[0] else: raise ValueError("Empty mesh list received") else: mesh_obj = mesh if hasattr(mesh_obj, 'vertices'): vertices = mesh_obj.vertices if hasattr(vertices, 'cpu'): vertices = vertices.cpu() if hasattr(vertices, 'numpy'): vertices = vertices.numpy() vertices = vertices.astype(np.float32) else: raise ValueError("Cannot find vertex data in mesh object") rotated_vertices = vertices.copy() rotated_vertices[:, 0] = -vertices[:, 0] rotated_vertices[:, 1], rotated_vertices[:, 2] = vertices[:, 2], vertices[:, 1] final_vertices = rotated_vertices.copy() final_vertices[:, 0] = rotated_vertices[:, 2] final_vertices[:, 2] = -rotated_vertices[:, 0] vertices = final_vertices colors = None if hasattr(mesh_obj, 'visual') and mesh_obj.visual is not None: visual = mesh_obj.visual if hasattr(visual, 'vertex_colors') and visual.vertex_colors is not None: colors = visual.vertex_colors if hasattr(colors, 'cpu'): colors = colors.cpu() if hasattr(colors, 'numpy'): colors = colors.numpy() elif hasattr(visual, 'face_colors') and visual.face_colors is not None: face_colors = visual.face_colors if hasattr(face_colors, 'cpu'): face_colors = face_colors.cpu() if hasattr(face_colors, 'numpy'): face_colors = face_colors.numpy() if hasattr(mesh_obj, 'faces') and mesh_obj.faces is not None: faces = mesh_obj.faces if hasattr(faces, 'cpu'): faces = faces.cpu() if hasattr(faces, 'numpy'): faces = faces.numpy() colors = np.zeros((len(vertices), 4), dtype=np.uint8) vertex_color_count = np.zeros(len(vertices), dtype=np.int32) for i, face in enumerate(faces): for vertex_idx in face: colors[vertex_idx] += face_colors[i] vertex_color_count[vertex_idx] += 1 for i in range(len(vertices)): if vertex_color_count[i] > 0: colors[i] = colors[i] // vertex_color_count[i] elif hasattr(visual, 'material') and visual.material is not None: material = visual.material if hasattr(material, 'diffuse') and material.diffuse is not None: diffuse = material.diffuse colors = np.tile(diffuse, (len(vertices), 1)) if colors is None and hasattr(mesh_obj, '_visual') and mesh_obj._visual is not None: visual = mesh_obj._visual if hasattr(visual, 'vertex_colors') and visual.vertex_colors is not None: colors = visual.vertex_colors if hasattr(colors, 'cpu'): colors = colors.cpu() if hasattr(colors, 'numpy'): colors = colors.numpy() if colors is None and hasattr(mesh_obj, 'visual') and hasattr(mesh_obj.visual, 'uv') and mesh_obj.visual.uv is not None: if hasattr(mesh_obj.visual, 'material') and hasattr(mesh_obj.visual.material, 'image'): uvs = mesh_obj.visual.uv image = mesh_obj.visual.material.image h, w = image.shape[:2] u = np.clip(uvs[:, 0], 0, 1) * (w - 1) v = np.clip(uvs[:, 1], 0, 1) * (h - 1) u_int = np.round(u).astype(np.int32) v_int = np.round(v).astype(np.int32) u_int = np.clip(u_int, 0, w - 1) v_int = np.clip(v_int, 0, h - 1) colors = image[v_int, u_int] if colors is None: np.random.seed(42) colors = np.random.randint(100, 200, size=(len(vertices), 3), dtype=np.uint8) alpha = np.full((len(vertices), 1), 255, dtype=np.uint8) colors = np.concatenate([colors, alpha], axis=1) if colors is not None: if len(colors.shape) == 1: colors = np.column_stack([colors, colors, colors]) if colors.max() <= 1.0 and colors.dtype != np.uint8: colors = (colors * 255).astype(np.uint8) else: colors = np.clip(colors, 0, 255).astype(np.uint8) if colors.shape[1] == 3: alpha = np.full((len(vertices), 1), 255, dtype=np.uint8) colors = np.concatenate([colors, alpha], axis=1) elif colors.shape[1] > 4: colors = colors[:, :4] if len(colors) != len(vertices): if len(colors) > len(vertices): colors = colors[:len(vertices)] else: last_color = colors[-1] padding = np.tile(last_color, (len(vertices) - len(colors), 1)) colors = np.vstack([colors, padding]) header = [ "ply", "format binary_little_endian 1.0", f"element vertex {len(vertices)}", "property float x", "property float y", "property float z", "property uchar red", "property uchar green", "property uchar blue", "property uchar alpha", "end_header\n" ] header = '\n'.join(header).encode('ascii') binary_data = bytearray() for i in range(len(vertices)): binary_data.extend(struct.pack(' 1: waveform = waveform[0:1, ...] waveform = waveform[0] with torch.no_grad(): max_abs = waveform.abs().amax() if torch.isfinite(max_abs) and max_abs > 1.0: waveform = waveform / max_abs wav_np = waveform.detach().cpu().float().numpy() if wav_np.ndim != 2: raise ValueError(f"Waveform must be [C, T] after squeeze, got {wav_np.shape}") wav_np = np.transpose(wav_np, (1, 0)) if format == "wav": audio_binary, content_type = self._encode_wav(wav_np, sample_rate) elif format == "mp3": audio_binary, content_type = self._encode_mp3(wav_np, sample_rate, bitrate_kbps) else: raise ValueError(f"Unsupported format: {format}") return self._send_to_td(audio_binary, content_type, broadcast) def _encode_wav(self, wav_np, sample_rate): import soundfile as sf buf = io.BytesIO() if wav_np.ndim == 1: wav_np = wav_np[:, None] sf.write(buf, wav_np, samplerate=sample_rate, subtype="PCM_16", format="WAV") audio_binary = buf.getvalue() buf.close() return audio_binary, "audio/wav" def _encode_mp3(self, wav_np, sample_rate, bitrate_kbps): import imageio_ffmpeg, subprocess with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as temp_file: temp_path = temp_file.name try: channels = wav_np.shape[1] if wav_np.ndim == 2 else 1 if wav_np.ndim == 1: wav_np = wav_np[:, None] channels = 1 pcm = wav_np.astype(np.float32, copy=False) if not pcm.flags["C_CONTIGUOUS"]: pcm = np.ascontiguousarray(pcm) cmd = [ imageio_ffmpeg.get_ffmpeg_exe(), "-y", "-f", "f32le", "-ac", str(channels), "-ar", str(sample_rate), "-i", "pipe:0", "-c:a", "libmp3lame", "-b:a", f"{int(bitrate_kbps)}k", temp_path ] proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) proc.stdin.write(pcm.tobytes()) proc.stdin.close() rc = proc.wait() if rc != 0: stderr = proc.stderr.read().decode("utf-8", errors="ignore") raise RuntimeError(f"ffmpeg failed (code {rc}): {stderr.strip()}") with open(temp_path, "rb") as f: audio_binary = f.read() finally: try: os.unlink(temp_path) except Exception: pass return audio_binary, "audio/mpeg" def _send_to_td(self, audio_binary, content_type, broadcast): server = PromptServer.instance sid = None if broadcast else server.client_id server.send_sync(1002, audio_binary, sid=sid) return {"ui": { "audio": [{ "source": "websocket", "content-type": content_type, "type": "output", }] }} class GaussianSplattingtoTD: @classmethod def INPUT_TYPES(s): return { "required": { "ply_path": ("STRING",), "broadcast": ("BOOLEAN", {"default": False}), } } RETURN_TYPES = () FUNCTION = "send_gaussian_splatting" OUTPUT_NODE = True CATEGORY = "TouchDesigner" def send_gaussian_splatting(self, ply_path, broadcast): try: gs_data = self.process_ply(ply_path) return self._send_to_td(gs_data, broadcast) except Exception as e: raise ValueError(f"Error processing Gaussian Splatting data: {str(e)}") def process_ply(self, ply_path): try: from plyfile import PlyData if not ply_path.startswith(('http://', 'https://')) and not os.path.isabs(ply_path): try: import folder_paths output_dir = folder_paths.get_output_directory() local_path = os.path.join(output_dir, ply_path) if os.path.exists(local_path): ply_path = local_path except: pass if not os.path.exists(ply_path): raise ValueError(f"PLY file not found: {ply_path}") plydata = PlyData.read(ply_path) vertex = plydata['vertex'] required_props = ['x', 'y', 'z', 'f_dc_0', 'f_dc_1', 'f_dc_2', 'opacity', 'scale_0', 'scale_1', 'scale_2', 'rot_0', 'rot_1', 'rot_2', 'rot_3'] missing_props = [prop for prop in required_props if prop not in vertex.data.dtype.names] if missing_props: raise ValueError(f"Missing properties in PLY file: {missing_props}") gs_data = { 'positions': np.stack([vertex['x'], vertex['y'], vertex['z']], axis=1).astype(np.float32), 'colors': np.stack([vertex['f_dc_0'], vertex['f_dc_1'], vertex['f_dc_2']], axis=1).astype(np.float32), 'opacity': vertex['opacity'].astype(np.float32), 'scales': np.stack([vertex['scale_0'], vertex['scale_1'], vertex['scale_2']], axis=1).astype(np.float32), 'rotations': np.stack([vertex['rot_0'], vertex['rot_1'], vertex['rot_2'], vertex['rot_3']], axis=1).astype(np.float32), } return gs_data except ImportError: raise ImportError("Please install the plyfile library: pip install plyfile") except Exception as e: raise ValueError(f"Error reading PLY file: {str(e)}") def _send_to_td(self, gs_data, broadcast): try: num_points = len(gs_data['positions']) header = [ "ply", "format binary_little_endian 1.0", f"element vertex {num_points}", "property float x", "property float y", "property float z", "property float f_dc_0", "property float f_dc_1", "property float f_dc_2", "property float opacity", "property float scale_0", "property float scale_1", "property float scale_2", "property float rot_0", "property float rot_1", "property float rot_2", "property float rot_3", "end_header\n" ] header = '\n'.join(header).encode('ascii') binary_data = bytearray() for i in range(num_points): binary_data.extend(struct.pack('