mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-30 08:10:21 +08:00
Compare commits
8 Commits
8ca620f394
...
336f19e318
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
336f19e318 | ||
|
|
8f40b43e02 | ||
|
|
3b832231bb | ||
|
|
be518db5a7 | ||
|
|
80441eb15e | ||
|
|
07f2462eae | ||
|
|
d150440466 | ||
|
|
aa2ab2415c |
@ -14,6 +14,8 @@
|
||||
[![][github-downloads-shield]][github-downloads-link]
|
||||
[![][github-downloads-latest-shield]][github-downloads-link]
|
||||
|
||||
[中文版 README](README_CN.md)
|
||||
|
||||
[matrix-shield]: https://img.shields.io/badge/Matrix-000000?style=flat&logo=matrix&logoColor=white
|
||||
[matrix-url]: https://app.element.io/#/room/%23comfyui_space%3Amatrix.org
|
||||
[website-shield]: https://img.shields.io/badge/ComfyOrg-4285F4?style=flat
|
||||
|
||||
455
README_CN.md
Normal file
455
README_CN.md
Normal file
@ -0,0 +1,455 @@
|
||||
<div align="center">
|
||||
|
||||
# ComfyUI
|
||||
**最强大、模块化的可视化 AI 引擎和应用。**
|
||||
|
||||
|
||||
[![Website][website-shield]][website-url]
|
||||
[![Dynamic JSON Badge][discord-shield]][discord-url]
|
||||
[![Twitter][twitter-shield]][twitter-url]
|
||||
[![Matrix][matrix-shield]][matrix-url]
|
||||
<br>
|
||||
[![][github-release-shield]][github-release-link]
|
||||
[![][github-release-date-shield]][github-release-link]
|
||||
[![][github-downloads-shield]][github-downloads-link]
|
||||
[![][github-downloads-latest-shield]][github-downloads-link]
|
||||
|
||||
[matrix-shield]: https://img.shields.io/badge/Matrix-000000?style=flat&logo=matrix&logoColor=white
|
||||
[matrix-url]: https://app.element.io/#/room/%23comfyui_space%3Amatrix.org
|
||||
[website-shield]: https://img.shields.io/badge/ComfyOrg-4285F4?style=flat
|
||||
[website-url]: https://www.comfy.org/
|
||||
<!-- Workaround to display total user from https://github.com/badges/shields/issues/4500#issuecomment-2060079995 -->
|
||||
[discord-shield]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2Fcomfyorg%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&logo=discord&logoColor=white&label=Discord&color=green&suffix=%20total
|
||||
[discord-url]: https://www.comfy.org/discord
|
||||
[twitter-shield]: https://img.shields.io/twitter/follow/ComfyUI
|
||||
[twitter-url]: https://x.com/ComfyUI
|
||||
|
||||
[github-release-shield]: https://img.shields.io/github/v/release/comfyanonymous/ComfyUI?style=flat&sort=semver
|
||||
[github-release-link]: https://github.com/comfyanonymous/ComfyUI/releases
|
||||
[github-release-date-shield]: https://img.shields.io/github/release-date/comfyanonymous/ComfyUI?style=flat
|
||||
[github-downloads-shield]: https://img.shields.io/github/downloads/comfyanonymous/ComfyUI/total?style=flat
|
||||
[github-downloads-latest-shield]: https://img.shields.io/github/downloads/comfyanonymous/ComfyUI/latest/total?style=flat&label=downloads%40latest
|
||||
[github-downloads-link]: https://github.com/comfyanonymous/ComfyUI/releases
|
||||
|
||||

|
||||
</div>
|
||||
|
||||
ComfyUI 让你能够使用基于图形/节点/流程图的界面设计和执行高级的 Stable Diffusion 管道。支持 Windows、Linux 和 macOS。
|
||||
|
||||
## 快速开始
|
||||
|
||||
#### [桌面应用程序](https://www.comfy.org/download)
|
||||
- 最简单的入门方式。
|
||||
- 支持 Windows 和 macOS。
|
||||
|
||||
#### [Windows 便携包](#安装)
|
||||
- 获取最新提交,完全便携。
|
||||
- 支持 Windows。
|
||||
|
||||
#### [手动安装](#手动安装-windows-linux)
|
||||
支持所有操作系统和 GPU 类型(NVIDIA、AMD、Intel、Apple Silicon、Ascend)。
|
||||
|
||||
## [示例](https://comfyanonymous.github.io/ComfyUI_examples/)
|
||||
查看 [示例工作流](https://comfyanonymous.github.io/ComfyUI_examples/) 了解 ComfyUI 能做什么。
|
||||
|
||||
## 特性
|
||||
- 节点/图形/流程图界面,无需编写任何代码即可实验和创建复杂的 Stable Diffusion 工作流。
|
||||
- 图像模型
|
||||
- SD1.x, SD2.x ([unCLIP](https://comfyanonymous.github.io/ComfyUI_examples/unclip/))
|
||||
- [SDXL](https://comfyanonymous.github.io/ComfyUI_examples/sdxl/), [SDXL Turbo](https://comfyanonymous.github.io/ComfyUI_examples/sdturbo/)
|
||||
- [Stable Cascade](https://comfyanonymous.github.io/ComfyUI_examples/stable_cascade/)
|
||||
- [SD3 和 SD3.5](https://comfyanonymous.github.io/ComfyUI_examples/sd3/)
|
||||
- Pixart Alpha 和 Sigma
|
||||
- [AuraFlow](https://comfyanonymous.github.io/ComfyUI_examples/aura_flow/)
|
||||
- [HunyuanDiT](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_dit/)
|
||||
- [Flux](https://comfyanonymous.github.io/ComfyUI_examples/flux/)
|
||||
- [Lumina Image 2.0](https://comfyanonymous.github.io/ComfyUI_examples/lumina2/)
|
||||
- [HiDream](https://comfyanonymous.github.io/ComfyUI_examples/hidream/)
|
||||
- [Qwen Image](https://comfyanonymous.github.io/ComfyUI_examples/qwen_image/)
|
||||
- [Hunyuan Image 2.1](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_image/)
|
||||
- [Flux 2](https://comfyanonymous.github.io/ComfyUI_examples/flux2/)
|
||||
- [Z Image](https://comfyanonymous.github.io/ComfyUI_examples/z_image/)
|
||||
- 图像编辑模型
|
||||
- [Omnigen 2](https://comfyanonymous.github.io/ComfyUI_examples/omnigen/)
|
||||
- [Flux Kontext](https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-kontext-image-editing-model)
|
||||
- [HiDream E1.1](https://comfyanonymous.github.io/ComfyUI_examples/hidream/#hidream-e11)
|
||||
- [Qwen Image Edit](https://comfyanonymous.github.io/ComfyUI_examples/qwen_image/#edit-model)
|
||||
- 视频模型
|
||||
- [Stable Video Diffusion](https://comfyanonymous.github.io/ComfyUI_examples/video/)
|
||||
- [Mochi](https://comfyanonymous.github.io/ComfyUI_examples/mochi/)
|
||||
- [LTX-Video](https://comfyanonymous.github.io/ComfyUI_examples/ltxv/)
|
||||
- [Hunyuan Video](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_video/)
|
||||
- [Wan 2.1](https://comfyanonymous.github.io/ComfyUI_examples/wan/)
|
||||
- [Wan 2.2](https://comfyanonymous.github.io/ComfyUI_examples/wan22/)
|
||||
- [Hunyuan Video 1.5](https://docs.comfy.org/tutorials/video/hunyuan/hunyuan-video-1-5)
|
||||
- 音频模型
|
||||
- [Stable Audio](https://comfyanonymous.github.io/ComfyUI_examples/audio/)
|
||||
- [ACE Step](https://comfyanonymous.github.io/ComfyUI_examples/audio/)
|
||||
- 3D 模型
|
||||
- [Hunyuan3D 2.0](https://docs.comfy.org/tutorials/3d/hunyuan3D-2)
|
||||
- 异步队列系统
|
||||
- 许多优化:仅重新执行执行之间更改的工作流部分。
|
||||
- 智能内存管理:可以通过智能卸载在低至 1GB vram 的 GPU 上自动运行大型模型。
|
||||
- 即使没有 GPU 也可以工作:```--cpu```(慢)
|
||||
- 可以加载 ckpt 和 safetensors:一体化检查点或独立扩散模型、VAE 和 CLIP 模型。
|
||||
- 安全加载 ckpt、pt、pth 等文件。
|
||||
- Embeddings/Textual inversion
|
||||
- [Loras (regular, locon and loha)](https://comfyanonymous.github.io/ComfyUI_examples/lora/)
|
||||
- [Hypernetworks](https://comfyanonymous.github.io/ComfyUI_examples/hypernetworks/)
|
||||
- 从生成的 PNG、WebP 和 FLAC 文件加载完整的工作流(带有种子)。
|
||||
- 将工作流保存/加载为 Json 文件。
|
||||
- 节点界面可用于创建复杂的工作流,如 [Hires fix](https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/) 或更高级的工作流。
|
||||
- [区域合成](https://comfyanonymous.github.io/ComfyUI_examples/area_composition/)
|
||||
- [Inpainting](https://comfyanonymous.github.io/ComfyUI_examples/inpaint/) 支持常规和 inpainting 模型。
|
||||
- [ControlNet 和 T2I-Adapter](https://comfyanonymous.github.io/ComfyUI_examples/controlnet/)
|
||||
- [放大模型 (ESRGAN, ESRGAN variants, SwinIR, Swin2SR, etc...)](https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/)
|
||||
- [GLIGEN](https://comfyanonymous.github.io/ComfyUI_examples/gligen/)
|
||||
- [模型合并](https://comfyanonymous.github.io/ComfyUI_examples/model_merging/)
|
||||
- [LCM 模型和 Loras](https://comfyanonymous.github.io/ComfyUI_examples/lcm/)
|
||||
- 使用 [TAESD](#如何显示高质量预览) 进行潜在预览
|
||||
- 完全离线工作:除非你想要,否则核心永远不会下载任何东西。
|
||||
- 可选的 API 节点,通过在线 [Comfy API](https://docs.comfy.org/tutorials/api-nodes/overview) 使用外部提供商的付费模型。
|
||||
- [配置文件](extra_model_paths.yaml.example) 用于设置模型的搜索路径。
|
||||
|
||||
工作流示例可以在 [示例页面](https://comfyanonymous.github.io/ComfyUI_examples/) 找到。
|
||||
|
||||
## 发布流程
|
||||
|
||||
ComfyUI 遵循每周发布周期,目标是周一,但由于模型发布或代码库的重大更改,这经常会发生变化。有三个相互关联的存储库:
|
||||
|
||||
1. **[ComfyUI Core](https://github.com/comfyanonymous/ComfyUI)**
|
||||
- 大约每周发布一个新的稳定版本(例如 v0.7.0)。
|
||||
- 稳定版本标签之外的提交可能非常不稳定,并破坏许多自定义节点。
|
||||
- 作为桌面版本的基础。
|
||||
|
||||
2. **[ComfyUI Desktop](https://github.com/Comfy-Org/desktop)**
|
||||
- 使用最新的稳定核心版本构建新版本。
|
||||
|
||||
3. **[ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend)**
|
||||
- 每周前端更新合并到核心存储库中。
|
||||
- 即将发布的核心版本的功能被冻结。
|
||||
- 下一个发布周期的开发继续进行。
|
||||
|
||||
## 快捷键
|
||||
|
||||
| 快捷键 | 说明 |
|
||||
|------------------------------------|--------------------------------------------------------------------------------------------------------------------|
|
||||
| `Ctrl` + `Enter` | 将当前图形排队生成 |
|
||||
| `Ctrl` + `Shift` + `Enter` | 将当前图形作为第一个排队生成 |
|
||||
| `Ctrl` + `Alt` + `Enter` | 取消当前生成 |
|
||||
| `Ctrl` + `Z`/`Ctrl` + `Y` | 撤销/重做 |
|
||||
| `Ctrl` + `S` | 保存工作流 |
|
||||
| `Ctrl` + `O` | 加载工作流 |
|
||||
| `Ctrl` + `A` | 选择所有节点 |
|
||||
| `Alt `+ `C` | 折叠/展开选定的节点 |
|
||||
| `Ctrl` + `M` | 静音/取消静音选定的节点 |
|
||||
| `Ctrl` + `B` | 绕过选定的节点(就像节点从图中移除并且连线重新连接通过一样) |
|
||||
| `Delete`/`Backspace` | 删除选定的节点 |
|
||||
| `Ctrl` + `Backspace` | 删除当前图形 |
|
||||
| `Space` | 按住并移动光标时移动画布 |
|
||||
| `Ctrl`/`Shift` + `Click` | 将点击的节点添加到选择中 |
|
||||
| `Ctrl` + `C`/`Ctrl` + `V` | 复制并粘贴选定的节点(不保持与未选定节点输出的连接) |
|
||||
| `Ctrl` + `C`/`Ctrl` + `Shift` + `V` | 复制并粘贴选定的节点(保持从未选定节点的输出到粘贴节点的输入的连接) |
|
||||
| `Shift` + `Drag` | 同时移动多个选定的节点 |
|
||||
| `Ctrl` + `D` | 加载默认图形 |
|
||||
| `Alt` + `+` | 画布放大 |
|
||||
| `Alt` + `-` | 画布缩小 |
|
||||
| `Ctrl` + `Shift` + LMB + 垂直拖动 | 画布放大/缩小 |
|
||||
| `P` | 固定/取消固定选定的节点 |
|
||||
| `Ctrl` + `G` | 组合选定的节点 |
|
||||
| `Q` | 切换队列的可见性 |
|
||||
| `H` | 切换历史记录的可见性 |
|
||||
| `R` | 刷新图形 |
|
||||
| `F` | 显示/隐藏菜单 |
|
||||
| `.` | 适应视图到选择(未选择任何内容时适应整个图形) |
|
||||
| 双击 LMB | 打开节点快速搜索面板 |
|
||||
| `Shift` + 拖动 | 一次移动多条连线 |
|
||||
| `Ctrl` + `Alt` + LMB | 断开点击插槽的所有连线 |
|
||||
|
||||
macOS 用户可以将 `Ctrl` 替换为 `Cmd`
|
||||
|
||||
# 安装
|
||||
|
||||
## Windows 便携版
|
||||
|
||||
在 [发布页面](https://github.com/comfyanonymous/ComfyUI/releases) 上有一个适用于 Windows 的便携式独立构建,应该可以在 Nvidia GPU 上运行,或者仅在 CPU 上运行。
|
||||
|
||||
### [直接下载链接](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia.7z)
|
||||
|
||||
只需下载,使用 [7-Zip](https://7-zip.org) 解压,或者在最近的 Windows 版本上使用资源管理器解压并运行。对于较小的模型,通常只需要将 checkpoints(巨大的 ckpt/safetensors 文件)放在:ComfyUI\models\checkpoints 中,但许多较大的模型有多个文件。请务必按照说明了解将它们放在 ComfyUI\models\ 的哪个子文件夹中。
|
||||
|
||||
如果解压有问题,请右键单击文件 -> 属性 -> 解除锁定
|
||||
|
||||
如果无法启动,请更新您的 Nvidia 驱动程序。
|
||||
|
||||
#### 替代下载:
|
||||
|
||||
[适用于 AMD GPU 的实验性便携版](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_amd.7z)
|
||||
|
||||
[带有 pytorch cuda 12.8 和 python 3.12 的便携版](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia_cu128.7z)。
|
||||
|
||||
[带有 pytorch cuda 12.6 和 python 3.12 的便携版](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia_cu126.7z)(支持 Nvidia 10 系列及更旧的 GPU)。
|
||||
|
||||
#### 如何在另一个 UI 和 ComfyUI 之间共享模型?
|
||||
|
||||
请参阅 [配置文件](extra_model_paths.yaml.example) 以设置模型的搜索路径。在独立的 Windows 构建中,您可以在 ComfyUI 目录中找到此文件。将此文件重命名为 extra_model_paths.yaml 并使用您喜欢的文本编辑器进行编辑。
|
||||
|
||||
|
||||
## [comfy-cli](https://docs.comfy.org/comfy-cli/getting-started)
|
||||
|
||||
您可以使用 comfy-cli 安装并启动 ComfyUI:
|
||||
```bash
|
||||
pip install comfy-cli
|
||||
comfy install
|
||||
```
|
||||
|
||||
## 手动安装 (Windows, Linux)
|
||||
|
||||
Python 3.14 可以工作,但您可能会遇到 torch 编译节点的问题。自由线程变体仍然缺少一些依赖项。
|
||||
|
||||
Python 3.13 支持得很好。如果您在 3.13 上遇到某些自定义节点依赖项的问题,可以尝试 3.12。
|
||||
|
||||
### 说明:
|
||||
|
||||
Git clone 此仓库。
|
||||
|
||||
将您的 SD checkpoints(巨大的 ckpt/safetensors 文件)放在:models/checkpoints
|
||||
|
||||
将您的 VAE 放在:models/vae
|
||||
|
||||
|
||||
### AMD GPU (Linux)
|
||||
|
||||
AMD 用户如果尚未安装,可以使用 pip 安装 rocm 和 pytorch,这是安装稳定版本的命令:
|
||||
|
||||
```pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.4```
|
||||
|
||||
这是安装带有 ROCm 7.0 的 nightly 版本的命令,可能会有一些性能改进:
|
||||
|
||||
```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/rocm7.1```
|
||||
|
||||
|
||||
### AMD GPU(实验性:Windows 和 Linux),仅限 RDNA 3, 3.5 和 4。
|
||||
|
||||
这些硬件支持比上面的构建少,但它们可以在 Windows 上运行。您还需要安装特定于您的硬件的 pytorch 版本。
|
||||
|
||||
RDNA 3 (RX 7000 系列):
|
||||
|
||||
```pip install --pre torch torchvision torchaudio --index-url https://rocm.nightlies.amd.com/v2/gfx110X-dgpu/```
|
||||
|
||||
RDNA 3.5 (Strix halo/Ryzen AI Max+ 365):
|
||||
|
||||
```pip install --pre torch torchvision torchaudio --index-url https://rocm.nightlies.amd.com/v2/gfx1151/```
|
||||
|
||||
RDNA 4 (RX 9000 系列):
|
||||
|
||||
```pip install --pre torch torchvision torchaudio --index-url https://rocm.nightlies.amd.com/v2/gfx120X-all/```
|
||||
|
||||
### Intel GPU (Windows 和 Linux)
|
||||
|
||||
Intel Arc GPU 用户可以使用 pip 安装具有 torch.xpu 支持的原生 PyTorch。更多信息可以在 [这里](https://pytorch.org/docs/main/notes/get_start_xpu.html) 找到。
|
||||
|
||||
1. 要安装 PyTorch xpu,请使用以下命令:
|
||||
|
||||
```pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/xpu```
|
||||
|
||||
这是安装 Pytorch xpu nightly 的命令,可能会有一些性能改进:
|
||||
|
||||
```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/xpu```
|
||||
|
||||
### NVIDIA
|
||||
|
||||
Nvidia 用户应使用此命令安装稳定的 pytorch:
|
||||
|
||||
```pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu130```
|
||||
|
||||
这是安装 pytorch nightly 的命令,可能会有性能改进。
|
||||
|
||||
```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu130```
|
||||
|
||||
#### 故障排除
|
||||
|
||||
如果您收到 "Torch not compiled with CUDA enabled" 错误,请使用以下命令卸载 torch:
|
||||
|
||||
```pip uninstall torch```
|
||||
|
||||
并再次使用上面的命令安装它。
|
||||
|
||||
### 依赖项
|
||||
|
||||
通过在 ComfyUI 文件夹中打开终端并运行以下命令来安装依赖项:
|
||||
|
||||
```pip install -r requirements.txt```
|
||||
|
||||
在此之后,您应该已经安装了所有内容,并可以继续运行 ComfyUI。
|
||||
|
||||
### 其他:
|
||||
|
||||
#### Apple Mac silicon
|
||||
|
||||
您可以在任何最近的 macOS 版本上在 Apple Mac silicon (M1 或 M2) 上安装 ComfyUI。
|
||||
|
||||
1. 安装 pytorch nightly。有关说明,请阅读 [Mac 上的加速 PyTorch 训练](https://developer.apple.com/metal/pytorch/) Apple 开发者指南(确保安装最新的 pytorch nightly)。
|
||||
1. 按照 Windows 和 Linux 的 [ComfyUI 手动安装](#手动安装-windows-linux) 说明进行操作。
|
||||
1. 安装 ComfyUI [依赖项](#依赖项)。如果您安装了另一个 Stable Diffusion UI,[您可能能够重用依赖项](#我已经安装了另一个-stable-diffusion-ui-我真的必须安装所有这些依赖项吗)。
|
||||
1. 通过运行 `python main.py` 启动 ComfyUI
|
||||
|
||||
> **注意**:请记住将您的模型、VAE、LoRA 等添加到相应的 Comfy 文件夹中,如 [ComfyUI 手动安装](#手动安装-windows-linux) 中所述。
|
||||
|
||||
#### Ascend NPU
|
||||
|
||||
对于与 Ascend Extension for PyTorch (torch_npu) 兼容的模型。首先,确保您的环境满足 [安装](https://ascend.github.io/docs/sources/ascend/quick_install.html) 页面上列出的先决条件。这是针对您的平台和安装方法的逐步指南:
|
||||
|
||||
1. 如果需要,首先安装 torch-npu 安装页面中指定的推荐或更新的 Linux 内核版本。
|
||||
2. 按照针对您特定平台的说明,继续安装 Ascend Basekit,其中包括驱动程序、固件和 CANN。
|
||||
3. 接下来,按照 [安装](https://ascend.github.io/docs/sources/pytorch/install.html#pytorch) 页面上的特定于平台的说明安装 torch-npu 的必要包。
|
||||
4. 最后,遵循 Linux 的 [ComfyUI 手动安装](#手动安装-windows-linux) 指南。安装所有组件后,您可以如前所述运行 ComfyUI。
|
||||
|
||||
#### Cambricon MLU
|
||||
|
||||
对于与 Cambricon Extension for PyTorch (torch_mlu) 兼容的模型。这是针对您的平台和安装方法的逐步指南:
|
||||
|
||||
1. 按照 [安装](https://www.cambricon.com/docs/sdk_1.15.0/cntoolkit_3.7.2/cntoolkit_install_3.7.2/index.html) 上的特定于平台的说明安装 Cambricon CNToolkit
|
||||
2. 接下来,按照 [安装](https://www.cambricon.com/docs/sdk_1.15.0/cambricon_pytorch_1.17.0/user_guide_1.9/index.html) 上的说明安装 PyTorch(torch_mlu)
|
||||
3. 通过运行 `python main.py` 启动 ComfyUI
|
||||
|
||||
#### Iluvatar Corex
|
||||
|
||||
对于与 Iluvatar Extension for PyTorch 兼容的模型。这是针对您的平台和安装方法的逐步指南:
|
||||
|
||||
1. 按照 [安装](https://support.iluvatar.com/#/DocumentCentre?id=1&nameCenter=2&productId=520117912052801536) 上的特定于平台的说明安装 Iluvatar Corex Toolkit
|
||||
2. 通过运行 `python main.py` 启动 ComfyUI
|
||||
|
||||
|
||||
## [ComfyUI-Manager](https://github.com/Comfy-Org/ComfyUI-Manager/tree/manager-v4)
|
||||
|
||||
**ComfyUI-Manager** 是一个扩展,允许您轻松安装、更新和管理 ComfyUI 的自定义节点。
|
||||
|
||||
### 设置
|
||||
|
||||
1. 安装管理器依赖项:
|
||||
```bash
|
||||
pip install -r manager_requirements.txt
|
||||
```
|
||||
|
||||
2. 运行 ComfyUI 时使用 `--enable-manager` 标志启用管理器:
|
||||
```bash
|
||||
python main.py --enable-manager
|
||||
```
|
||||
|
||||
### 命令行选项
|
||||
|
||||
| 标志 | 描述 |
|
||||
|------|-------------|
|
||||
| `--enable-manager` | 启用 ComfyUI-Manager |
|
||||
| `--enable-manager-legacy-ui` | 使用旧版管理器 UI 而不是新 UI(需要 `--enable-manager`) |
|
||||
| `--disable-manager-ui` | 禁用管理器 UI 和端点,同时保留后台功能,如安全检查和计划安装完成(需要 `--enable-manager`) |
|
||||
|
||||
|
||||
# 运行
|
||||
|
||||
```python main.py```
|
||||
|
||||
### 对于 ROCm 未正式支持的 AMD 卡
|
||||
|
||||
如果有问题,请尝试使用此命令运行它:
|
||||
|
||||
对于 6700, 6600 和可能的其他 RDNA2 或更旧的卡:```HSA_OVERRIDE_GFX_VERSION=10.3.0 python main.py```
|
||||
|
||||
对于 AMD 7600 和可能的其他 RDNA3 卡:```HSA_OVERRIDE_GFX_VERSION=11.0.0 python main.py```
|
||||
|
||||
### AMD ROCm 提示
|
||||
|
||||
您可以使用此命令在某些 AMD GPU 上的 ComfyUI 中启用最近 pytorch 的实验性内存高效注意力,它应该已经在 RDNA3 上默认启用。如果这提高了您 GPU 上最新 pytorch 的速度,请报告它,以便我可以默认启用它。
|
||||
|
||||
```TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1 python main.py --use-pytorch-cross-attention```
|
||||
|
||||
您也可以尝试设置此环境变量 `PYTORCH_TUNABLEOP_ENABLED=1`,这可能会加快速度,但代价是非常慢的初始运行。
|
||||
|
||||
# 注意事项
|
||||
|
||||
只有具有所有正确输入的输出的图形部分才会被执行。
|
||||
|
||||
只有从每次执行到下一次执行发生更改的图形部分才会被执行,如果您提交相同的图形两次,只有第一次会被执行。如果您更改图形的最后一部分,只有您更改的部分和依赖于它的部分会被执行。
|
||||
|
||||
将生成的 png 拖到网页上或加载它将为您提供完整的工作流,包括用于创建它的种子。
|
||||
|
||||
您可以使用 () 来更改单词或短语的强调,例如:(good code:1.2) 或 (bad code:0.8)。() 的默认强调是 1.1。要在实际提示中使用 () 字符,请像 \\( 或 \\) 一样转义它们。
|
||||
|
||||
您可以使用 {day|night} 进行通配符/动态提示。使用此语法 "{wild|card|test}" 将在每次您排队提示时由前端随机替换为 "wild"、"card" 或 "test"。要在实际提示中使用 {} 字符,请像:\\{ or \\} 一样转义它们。
|
||||
|
||||
动态提示还支持 C 风格的注释,如 `// comment` 或 `/* comment */`。
|
||||
|
||||
要在文本提示中使用文本反转概念/嵌入,请将它们放在 models/embeddings 目录中,并在 CLIPTextEncode 节点中像这样使用它们(您可以省略 .pt 扩展名):
|
||||
|
||||
```embedding:embedding_filename.pt```
|
||||
|
||||
|
||||
## 如何显示高质量预览?
|
||||
|
||||
使用 ```--preview-method auto``` 启用预览。
|
||||
|
||||
默认安装包括一种快速的潜在预览方法,分辨率较低。要使用 [TAESD](https://github.com/madebyollin/taesd) 启用更高质量的预览,请下载 [taesd_decoder.pth, taesdxl_decoder.pth, taesd3_decoder.pth 和 taef1_decoder.pth](https://github.com/madebyollin/taesd/) 并将它们放在 `models/vae_approx` 文件夹中。安装完成后,重新启动 ComfyUI 并使用 `--preview-method taesd` 启动它以启用高质量预览。
|
||||
|
||||
## 如何使用 TLS/SSL?
|
||||
通过运行以下命令生成自签名证书(不适合共享/生产使用)和密钥:`openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"`
|
||||
|
||||
使用 `--tls-keyfile key.pem --tls-certfile cert.pem` 启用 TLS/SSL,应用程序现在将可以通过 `https://...` 而不是 `http://...` 访问。
|
||||
|
||||
> 注意:Windows 用户可以使用 [alexisrolland/docker-openssl](https://github.com/alexisrolland/docker-openssl) 或 [第三方二进制发行版](https://wiki.openssl.org/index.php/Binaries) 之一来运行上面的命令示例。
|
||||
<br/><br/>如果您使用容器,请注意卷挂载 `-v` 可以是相对路径,因此 `... -v ".\:/openssl-certs" ...` 将在命令提示符或 powershell 终端的当前目录中创建密钥和证书文件。
|
||||
|
||||
## 支持和开发频道
|
||||
|
||||
[Discord](https://comfy.org/discord): 尝试 #help 或 #feedback 频道。
|
||||
|
||||
[Matrix space: #comfyui_space:matrix.org](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) (它像 discord 但是开源的)。
|
||||
|
||||
另请参阅:[https://www.comfy.org/](https://www.comfy.org/)
|
||||
|
||||
## 前端开发
|
||||
|
||||
截至 2024 年 8 月 15 日,我们已过渡到新的前端,该前端现在托管在一个单独的存储库中:[ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend)。此存储库现在在 `web/` 目录下托管编译后的 JS(来自 TS/Vue)。
|
||||
|
||||
### 报告问题和请求功能
|
||||
|
||||
对于与前端相关的任何错误、问题或功能请求,请使用 [ComfyUI Frontend 存储库](https://github.com/Comfy-Org/ComfyUI_frontend)。这将帮助我们更有效地管理和解决前端特定的问题。
|
||||
|
||||
### 使用最新前端
|
||||
|
||||
新前端现在是 ComfyUI 的默认前端。但是,请注意:
|
||||
|
||||
1. 主 ComfyUI 存储库中的前端每两周更新一次。
|
||||
2. 每日发布在单独的前端存储库中可用。
|
||||
|
||||
要使用最新的前端版本:
|
||||
|
||||
1. 对于最新的每日发布,使用此命令行参数启动 ComfyUI:
|
||||
|
||||
```
|
||||
--front-end-version Comfy-Org/ComfyUI_frontend@latest
|
||||
```
|
||||
|
||||
2. 对于特定版本,将 `latest` 替换为所需的版本号:
|
||||
|
||||
```
|
||||
--front-end-version Comfy-Org/ComfyUI_frontend@1.2.2
|
||||
```
|
||||
|
||||
这种方法允许您轻松地在稳定的每两周发布和前沿的每日更新之间切换,甚至可以切换到特定版本进行测试。
|
||||
|
||||
### 访问旧版前端
|
||||
|
||||
如果您因任何原因需要使用旧版前端,可以使用以下命令行参数访问它:
|
||||
|
||||
```
|
||||
--front-end-version Comfy-Org/ComfyUI_legacy_frontend@latest
|
||||
```
|
||||
|
||||
这将使用保存在 [ComfyUI Legacy Frontend 存储库](https://github.com/Comfy-Org/ComfyUI_legacy_frontend) 中的旧版前端快照。
|
||||
|
||||
# QA
|
||||
|
||||
### 我应该为此购买哪种 GPU?
|
||||
|
||||
[请参阅此页面以获取一些建议](https://github.com/comfyanonymous/ComfyUI/wiki/Which-GPU-should-I-buy-for-ComfyUI)
|
||||
15
comfy/sd.py
15
comfy/sd.py
@ -1014,6 +1014,7 @@ class CLIPType(Enum):
|
||||
KANDINSKY5 = 22
|
||||
KANDINSKY5_IMAGE = 23
|
||||
NEWBIE = 24
|
||||
FLUX2 = 25
|
||||
|
||||
|
||||
def load_clip(ckpt_paths, embedding_directory=None, clip_type=CLIPType.STABLE_DIFFUSION, model_options={}):
|
||||
@ -1046,6 +1047,7 @@ class TEModel(Enum):
|
||||
QWEN3_2B = 17
|
||||
GEMMA_3_12B = 18
|
||||
JINA_CLIP_2 = 19
|
||||
QWEN3_8B = 20
|
||||
|
||||
|
||||
def detect_te_model(sd):
|
||||
@ -1089,6 +1091,8 @@ def detect_te_model(sd):
|
||||
return TEModel.QWEN3_4B
|
||||
elif weight.shape[0] == 2048:
|
||||
return TEModel.QWEN3_2B
|
||||
elif weight.shape[0] == 4096:
|
||||
return TEModel.QWEN3_8B
|
||||
if weight.shape[0] == 5120:
|
||||
if "model.layers.39.post_attention_layernorm.weight" in sd:
|
||||
return TEModel.MISTRAL3_24B
|
||||
@ -1214,11 +1218,18 @@ def load_text_encoder_state_dicts(state_dicts=[], embedding_directory=None, clip
|
||||
clip_target.tokenizer = comfy.text_encoders.flux.Flux2Tokenizer
|
||||
tokenizer_data["tekken_model"] = clip_data[0].get("tekken_model", None)
|
||||
elif te_model == TEModel.QWEN3_4B:
|
||||
clip_target.clip = comfy.text_encoders.z_image.te(**llama_detect(clip_data))
|
||||
clip_target.tokenizer = comfy.text_encoders.z_image.ZImageTokenizer
|
||||
if clip_type == CLIPType.FLUX or clip_type == CLIPType.FLUX2:
|
||||
clip_target.clip = comfy.text_encoders.flux.klein_te(**llama_detect(clip_data), model_type="qwen3_4b")
|
||||
clip_target.tokenizer = comfy.text_encoders.flux.KleinTokenizer
|
||||
else:
|
||||
clip_target.clip = comfy.text_encoders.z_image.te(**llama_detect(clip_data))
|
||||
clip_target.tokenizer = comfy.text_encoders.z_image.ZImageTokenizer
|
||||
elif te_model == TEModel.QWEN3_2B:
|
||||
clip_target.clip = comfy.text_encoders.ovis.te(**llama_detect(clip_data))
|
||||
clip_target.tokenizer = comfy.text_encoders.ovis.OvisTokenizer
|
||||
elif te_model == TEModel.QWEN3_8B:
|
||||
clip_target.clip = comfy.text_encoders.flux.klein_te(**llama_detect(clip_data), model_type="qwen3_8b")
|
||||
clip_target.tokenizer = comfy.text_encoders.flux.KleinTokenizer8B
|
||||
elif te_model == TEModel.JINA_CLIP_2:
|
||||
clip_target.clip = comfy.text_encoders.jina_clip_2.JinaClip2TextModelWrapper
|
||||
clip_target.tokenizer = comfy.text_encoders.jina_clip_2.JinaClip2TokenizerWrapper
|
||||
|
||||
@ -3,7 +3,7 @@ import comfy.text_encoders.t5
|
||||
import comfy.text_encoders.sd3_clip
|
||||
import comfy.text_encoders.llama
|
||||
import comfy.model_management
|
||||
from transformers import T5TokenizerFast, LlamaTokenizerFast
|
||||
from transformers import T5TokenizerFast, LlamaTokenizerFast, Qwen2Tokenizer
|
||||
import torch
|
||||
import os
|
||||
import json
|
||||
@ -172,3 +172,60 @@ def flux2_te(dtype_llama=None, llama_quantization_metadata=None, pruned=False):
|
||||
model_options["num_layers"] = 30
|
||||
super().__init__(device=device, dtype=dtype, model_options=model_options)
|
||||
return Flux2TEModel_
|
||||
|
||||
class Qwen3Tokenizer(sd1_clip.SDTokenizer):
|
||||
def __init__(self, embedding_directory=None, tokenizer_data={}):
|
||||
tokenizer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "qwen25_tokenizer")
|
||||
super().__init__(tokenizer_path, pad_with_end=False, embedding_size=2560, embedding_key='qwen3_4b', tokenizer_class=Qwen2Tokenizer, has_start_token=False, has_end_token=False, pad_to_max_length=False, max_length=99999999, min_length=512, pad_token=151643, tokenizer_data=tokenizer_data)
|
||||
|
||||
class Qwen3Tokenizer8B(sd1_clip.SDTokenizer):
|
||||
def __init__(self, embedding_directory=None, tokenizer_data={}):
|
||||
tokenizer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "qwen25_tokenizer")
|
||||
super().__init__(tokenizer_path, pad_with_end=False, embedding_size=4096, embedding_key='qwen3_8b', tokenizer_class=Qwen2Tokenizer, has_start_token=False, has_end_token=False, pad_to_max_length=False, max_length=99999999, min_length=512, pad_token=151643, tokenizer_data=tokenizer_data)
|
||||
|
||||
class KleinTokenizer(sd1_clip.SD1Tokenizer):
|
||||
def __init__(self, embedding_directory=None, tokenizer_data={}, name="qwen3_4b"):
|
||||
if name == "qwen3_4b":
|
||||
tokenizer = Qwen3Tokenizer
|
||||
elif name == "qwen3_8b":
|
||||
tokenizer = Qwen3Tokenizer8B
|
||||
|
||||
super().__init__(embedding_directory=embedding_directory, tokenizer_data=tokenizer_data, name=name, tokenizer=tokenizer)
|
||||
self.llama_template = "<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n"
|
||||
|
||||
def tokenize_with_weights(self, text, return_word_ids=False, llama_template=None, **kwargs):
|
||||
if llama_template is None:
|
||||
llama_text = self.llama_template.format(text)
|
||||
else:
|
||||
llama_text = llama_template.format(text)
|
||||
|
||||
tokens = super().tokenize_with_weights(llama_text, return_word_ids=return_word_ids, disable_weights=True, **kwargs)
|
||||
return tokens
|
||||
|
||||
class KleinTokenizer8B(KleinTokenizer):
|
||||
def __init__(self, embedding_directory=None, tokenizer_data={}, name="qwen3_8b"):
|
||||
super().__init__(embedding_directory=embedding_directory, tokenizer_data=tokenizer_data, name=name)
|
||||
|
||||
class Qwen3_4BModel(sd1_clip.SDClipModel):
|
||||
def __init__(self, device="cpu", layer=[9, 18, 27], layer_idx=None, dtype=None, attention_mask=True, model_options={}):
|
||||
super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config={}, dtype=dtype, special_tokens={"pad": 151643}, layer_norm_hidden_state=False, model_class=comfy.text_encoders.llama.Qwen3_4B, enable_attention_masks=attention_mask, return_attention_masks=attention_mask, model_options=model_options)
|
||||
|
||||
class Qwen3_8BModel(sd1_clip.SDClipModel):
|
||||
def __init__(self, device="cpu", layer=[9, 18, 27], layer_idx=None, dtype=None, attention_mask=True, model_options={}):
|
||||
super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config={}, dtype=dtype, special_tokens={"pad": 151643}, layer_norm_hidden_state=False, model_class=comfy.text_encoders.llama.Qwen3_8B, enable_attention_masks=attention_mask, return_attention_masks=attention_mask, model_options=model_options)
|
||||
|
||||
def klein_te(dtype_llama=None, llama_quantization_metadata=None, model_type="qwen3_4b"):
|
||||
if model_type == "qwen3_4b":
|
||||
model = Qwen3_4BModel
|
||||
elif model_type == "qwen3_8b":
|
||||
model = Qwen3_8BModel
|
||||
|
||||
class Flux2TEModel_(Flux2TEModel):
|
||||
def __init__(self, device="cpu", dtype=None, model_options={}):
|
||||
if llama_quantization_metadata is not None:
|
||||
model_options = model_options.copy()
|
||||
model_options["quantization_metadata"] = llama_quantization_metadata
|
||||
if dtype_llama is not None:
|
||||
dtype = dtype_llama
|
||||
super().__init__(device=device, dtype=dtype, name=model_type, model_options=model_options, clip_model=model)
|
||||
return Flux2TEModel_
|
||||
|
||||
@ -99,6 +99,28 @@ class Qwen3_4BConfig:
|
||||
rope_scale = None
|
||||
final_norm: bool = True
|
||||
|
||||
@dataclass
|
||||
class Qwen3_8BConfig:
|
||||
vocab_size: int = 151936
|
||||
hidden_size: int = 4096
|
||||
intermediate_size: int = 12288
|
||||
num_hidden_layers: int = 36
|
||||
num_attention_heads: int = 32
|
||||
num_key_value_heads: int = 8
|
||||
max_position_embeddings: int = 40960
|
||||
rms_norm_eps: float = 1e-6
|
||||
rope_theta: float = 1000000.0
|
||||
transformer_type: str = "llama"
|
||||
head_dim = 128
|
||||
rms_norm_add = False
|
||||
mlp_activation = "silu"
|
||||
qkv_bias = False
|
||||
rope_dims = None
|
||||
q_norm = "gemma3"
|
||||
k_norm = "gemma3"
|
||||
rope_scale = None
|
||||
final_norm: bool = True
|
||||
|
||||
@dataclass
|
||||
class Ovis25_2BConfig:
|
||||
vocab_size: int = 151936
|
||||
@ -628,6 +650,15 @@ class Qwen3_4B(BaseLlama, torch.nn.Module):
|
||||
self.model = Llama2_(config, device=device, dtype=dtype, ops=operations)
|
||||
self.dtype = dtype
|
||||
|
||||
class Qwen3_8B(BaseLlama, torch.nn.Module):
|
||||
def __init__(self, config_dict, dtype, device, operations):
|
||||
super().__init__()
|
||||
config = Qwen3_8BConfig(**config_dict)
|
||||
self.num_layers = config.num_hidden_layers
|
||||
|
||||
self.model = Llama2_(config, device=device, dtype=dtype, ops=operations)
|
||||
self.dtype = dtype
|
||||
|
||||
class Ovis25_2B(BaseLlama, torch.nn.Module):
|
||||
def __init__(self, config_dict, dtype, device, operations):
|
||||
super().__init__()
|
||||
|
||||
@ -118,8 +118,9 @@ class LTXAVTEModel(torch.nn.Module):
|
||||
sdo = comfy.utils.state_dict_prefix_replace(sd, {"text_embedding_projection.aggregate_embed.weight": "text_embedding_projection.weight", "model.diffusion_model.video_embeddings_connector.": "video_embeddings_connector.", "model.diffusion_model.audio_embeddings_connector.": "audio_embeddings_connector."}, filter_keys=True)
|
||||
if len(sdo) == 0:
|
||||
sdo = sd
|
||||
|
||||
return self.load_state_dict(sdo, strict=False)
|
||||
missing, unexpected = self.load_state_dict(sdo, strict=False)
|
||||
missing = [k for k in missing if not k.startswith("gemma3_12b.")] # filter out keys that belong to the main gemma model
|
||||
return (missing, unexpected)
|
||||
|
||||
def memory_estimation_function(self, token_weight_pairs, device=None):
|
||||
constant = 6.0
|
||||
|
||||
@ -929,7 +929,9 @@ def bislerp(samples, width, height):
|
||||
return result.to(orig_dtype)
|
||||
|
||||
def lanczos(samples, width, height):
|
||||
images = [Image.fromarray(np.clip(255. * image.movedim(0, -1).cpu().numpy(), 0, 255).astype(np.uint8)) for image in samples]
|
||||
#the below API is strict and expects grayscale to be squeezed
|
||||
samples = samples.squeeze(1) if samples.shape[1] == 1 else samples.movedim(1, -1)
|
||||
images = [Image.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8)) for image in samples]
|
||||
images = [image.resize((width, height), resample=Image.Resampling.LANCZOS) for image in images]
|
||||
images = [torch.from_numpy(np.array(image).astype(np.float32) / 255.0).movedim(-1, 0) for image in images]
|
||||
result = torch.stack(images)
|
||||
|
||||
160
comfy_api_nodes/apis/meshy.py
Normal file
160
comfy_api_nodes/apis/meshy.py
Normal file
@ -0,0 +1,160 @@
|
||||
from typing import TypedDict
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from comfy_api.latest import Input
|
||||
|
||||
|
||||
class InputShouldRemesh(TypedDict):
|
||||
should_remesh: str
|
||||
topology: str
|
||||
target_polycount: int
|
||||
|
||||
|
||||
class InputShouldTexture(TypedDict):
|
||||
should_texture: str
|
||||
enable_pbr: bool
|
||||
texture_prompt: str
|
||||
texture_image: Input.Image | None
|
||||
|
||||
|
||||
class MeshyTaskResponse(BaseModel):
|
||||
result: str = Field(...)
|
||||
|
||||
|
||||
class MeshyTextToModelRequest(BaseModel):
|
||||
mode: str = Field("preview")
|
||||
prompt: str = Field(..., max_length=600)
|
||||
art_style: str = Field(..., description="'realistic' or 'sculpture'")
|
||||
ai_model: str = Field(...)
|
||||
topology: str | None = Field(..., description="'quad' or 'triangle'")
|
||||
target_polycount: int | None = Field(..., ge=100, le=300000)
|
||||
should_remesh: bool = Field(
|
||||
True,
|
||||
description="False returns the original mesh, ignoring topology and polycount.",
|
||||
)
|
||||
symmetry_mode: str = Field(..., description="'auto', 'off' or 'on'")
|
||||
pose_mode: str = Field(...)
|
||||
seed: int = Field(...)
|
||||
moderation: bool = Field(False)
|
||||
|
||||
|
||||
class MeshyRefineTask(BaseModel):
|
||||
mode: str = Field("refine")
|
||||
preview_task_id: str = Field(...)
|
||||
enable_pbr: bool | None = Field(...)
|
||||
texture_prompt: str | None = Field(...)
|
||||
texture_image_url: str | None = Field(...)
|
||||
ai_model: str = Field(...)
|
||||
moderation: bool = Field(False)
|
||||
|
||||
|
||||
class MeshyImageToModelRequest(BaseModel):
|
||||
image_url: str = Field(...)
|
||||
ai_model: str = Field(...)
|
||||
topology: str | None = Field(..., description="'quad' or 'triangle'")
|
||||
target_polycount: int | None = Field(..., ge=100, le=300000)
|
||||
symmetry_mode: str = Field(..., description="'auto', 'off' or 'on'")
|
||||
should_remesh: bool = Field(
|
||||
True,
|
||||
description="False returns the original mesh, ignoring topology and polycount.",
|
||||
)
|
||||
should_texture: bool = Field(...)
|
||||
enable_pbr: bool | None = Field(...)
|
||||
pose_mode: str = Field(...)
|
||||
texture_prompt: str | None = Field(None, max_length=600)
|
||||
texture_image_url: str | None = Field(None)
|
||||
seed: int = Field(...)
|
||||
moderation: bool = Field(False)
|
||||
|
||||
|
||||
class MeshyMultiImageToModelRequest(BaseModel):
|
||||
image_urls: list[str] = Field(...)
|
||||
ai_model: str = Field(...)
|
||||
topology: str | None = Field(..., description="'quad' or 'triangle'")
|
||||
target_polycount: int | None = Field(..., ge=100, le=300000)
|
||||
symmetry_mode: str = Field(..., description="'auto', 'off' or 'on'")
|
||||
should_remesh: bool = Field(
|
||||
True,
|
||||
description="False returns the original mesh, ignoring topology and polycount.",
|
||||
)
|
||||
should_texture: bool = Field(...)
|
||||
enable_pbr: bool | None = Field(...)
|
||||
pose_mode: str = Field(...)
|
||||
texture_prompt: str | None = Field(None, max_length=600)
|
||||
texture_image_url: str | None = Field(None)
|
||||
seed: int = Field(...)
|
||||
moderation: bool = Field(False)
|
||||
|
||||
|
||||
class MeshyRiggingRequest(BaseModel):
|
||||
input_task_id: str = Field(...)
|
||||
height_meters: float = Field(...)
|
||||
texture_image_url: str | None = Field(...)
|
||||
|
||||
|
||||
class MeshyAnimationRequest(BaseModel):
|
||||
rig_task_id: str = Field(...)
|
||||
action_id: int = Field(...)
|
||||
|
||||
|
||||
class MeshyTextureRequest(BaseModel):
|
||||
input_task_id: str = Field(...)
|
||||
ai_model: str = Field(...)
|
||||
enable_original_uv: bool = Field(...)
|
||||
enable_pbr: bool = Field(...)
|
||||
text_style_prompt: str | None = Field(...)
|
||||
image_style_url: str | None = Field(...)
|
||||
|
||||
|
||||
class MeshyModelsUrls(BaseModel):
|
||||
glb: str = Field("")
|
||||
|
||||
|
||||
class MeshyRiggedModelsUrls(BaseModel):
|
||||
rigged_character_glb_url: str = Field("")
|
||||
|
||||
|
||||
class MeshyAnimatedModelsUrls(BaseModel):
|
||||
animation_glb_url: str = Field("")
|
||||
|
||||
|
||||
class MeshyResultTextureUrls(BaseModel):
|
||||
base_color: str = Field(...)
|
||||
metallic: str | None = Field(None)
|
||||
normal: str | None = Field(None)
|
||||
roughness: str | None = Field(None)
|
||||
|
||||
|
||||
class MeshyTaskError(BaseModel):
|
||||
message: str | None = Field(None)
|
||||
|
||||
|
||||
class MeshyModelResult(BaseModel):
|
||||
id: str = Field(...)
|
||||
type: str = Field(...)
|
||||
model_urls: MeshyModelsUrls = Field(MeshyModelsUrls())
|
||||
thumbnail_url: str = Field(...)
|
||||
video_url: str | None = Field(None)
|
||||
status: str = Field(...)
|
||||
progress: int = Field(0)
|
||||
texture_urls: list[MeshyResultTextureUrls] | None = Field([])
|
||||
task_error: MeshyTaskError | None = Field(None)
|
||||
|
||||
|
||||
class MeshyRiggedResult(BaseModel):
|
||||
id: str = Field(...)
|
||||
type: str = Field(...)
|
||||
status: str = Field(...)
|
||||
progress: int = Field(0)
|
||||
result: MeshyRiggedModelsUrls = Field(MeshyRiggedModelsUrls())
|
||||
task_error: MeshyTaskError | None = Field(None)
|
||||
|
||||
|
||||
class MeshyAnimationResult(BaseModel):
|
||||
id: str = Field(...)
|
||||
type: str = Field(...)
|
||||
status: str = Field(...)
|
||||
progress: int = Field(0)
|
||||
result: MeshyAnimatedModelsUrls = Field(MeshyAnimatedModelsUrls())
|
||||
task_error: MeshyTaskError | None = Field(None)
|
||||
790
comfy_api_nodes/nodes_meshy.py
Normal file
790
comfy_api_nodes/nodes_meshy.py
Normal file
@ -0,0 +1,790 @@
|
||||
import os
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from comfy_api.latest import IO, ComfyExtension, Input
|
||||
from comfy_api_nodes.apis.meshy import (
|
||||
InputShouldRemesh,
|
||||
InputShouldTexture,
|
||||
MeshyAnimationRequest,
|
||||
MeshyAnimationResult,
|
||||
MeshyImageToModelRequest,
|
||||
MeshyModelResult,
|
||||
MeshyMultiImageToModelRequest,
|
||||
MeshyRefineTask,
|
||||
MeshyRiggedResult,
|
||||
MeshyRiggingRequest,
|
||||
MeshyTaskResponse,
|
||||
MeshyTextToModelRequest,
|
||||
MeshyTextureRequest,
|
||||
)
|
||||
from comfy_api_nodes.util import (
|
||||
ApiEndpoint,
|
||||
download_url_to_bytesio,
|
||||
poll_op,
|
||||
sync_op,
|
||||
upload_images_to_comfyapi,
|
||||
validate_string,
|
||||
)
|
||||
from folder_paths import get_output_directory
|
||||
|
||||
|
||||
class MeshyTextToModelNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="MeshyTextToModelNode",
|
||||
display_name="Meshy: Text to Model",
|
||||
category="api node/3d/Meshy",
|
||||
inputs=[
|
||||
IO.Combo.Input("model", options=["latest"]),
|
||||
IO.String.Input("prompt", multiline=True, default=""),
|
||||
IO.Combo.Input("style", options=["realistic", "sculpture"]),
|
||||
IO.DynamicCombo.Input(
|
||||
"should_remesh",
|
||||
options=[
|
||||
IO.DynamicCombo.Option(
|
||||
"true",
|
||||
[
|
||||
IO.Combo.Input("topology", options=["triangle", "quad"]),
|
||||
IO.Int.Input(
|
||||
"target_polycount",
|
||||
default=300000,
|
||||
min=100,
|
||||
max=300000,
|
||||
display_mode=IO.NumberDisplay.number,
|
||||
),
|
||||
],
|
||||
),
|
||||
IO.DynamicCombo.Option("false", []),
|
||||
],
|
||||
tooltip="When set to false, returns an unprocessed triangular mesh.",
|
||||
),
|
||||
IO.Combo.Input("symmetry_mode", options=["auto", "on", "off"]),
|
||||
IO.Combo.Input(
|
||||
"pose_mode",
|
||||
options=["", "A-pose", "T-pose"],
|
||||
tooltip="Specify the pose mode for the generated model.",
|
||||
),
|
||||
IO.Int.Input(
|
||||
"seed",
|
||||
default=0,
|
||||
min=0,
|
||||
max=2147483647,
|
||||
display_mode=IO.NumberDisplay.number,
|
||||
control_after_generate=True,
|
||||
tooltip="Seed controls whether the node should re-run; "
|
||||
"results are non-deterministic regardless of seed.",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
IO.String.Output(display_name="model_file"),
|
||||
IO.Custom("MESHY_TASK_ID").Output(display_name="meshy_task_id"),
|
||||
],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
is_api_node=True,
|
||||
is_output_node=True,
|
||||
price_badge=IO.PriceBadge(
|
||||
expr="""{"type":"usd","usd":0.8}""",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
model: str,
|
||||
prompt: str,
|
||||
style: str,
|
||||
should_remesh: InputShouldRemesh,
|
||||
symmetry_mode: str,
|
||||
pose_mode: str,
|
||||
seed: int,
|
||||
) -> IO.NodeOutput:
|
||||
validate_string(prompt, field_name="prompt", min_length=1, max_length=600)
|
||||
response = await sync_op(
|
||||
cls,
|
||||
ApiEndpoint(path="/proxy/meshy/openapi/v2/text-to-3d", method="POST"),
|
||||
response_model=MeshyTaskResponse,
|
||||
data=MeshyTextToModelRequest(
|
||||
prompt=prompt,
|
||||
art_style=style,
|
||||
ai_model=model,
|
||||
topology=should_remesh.get("topology", None),
|
||||
target_polycount=should_remesh.get("target_polycount", None),
|
||||
should_remesh=should_remesh["should_remesh"] == "true",
|
||||
symmetry_mode=symmetry_mode,
|
||||
pose_mode=pose_mode.lower(),
|
||||
seed=seed,
|
||||
),
|
||||
)
|
||||
result = await poll_op(
|
||||
cls,
|
||||
ApiEndpoint(path=f"/proxy/meshy/openapi/v2/text-to-3d/{response.result}"),
|
||||
response_model=MeshyModelResult,
|
||||
status_extractor=lambda r: r.status,
|
||||
progress_extractor=lambda r: r.progress,
|
||||
)
|
||||
model_file = f"meshy_model_{response.result}.glb"
|
||||
await download_url_to_bytesio(result.model_urls.glb, os.path.join(get_output_directory(), model_file))
|
||||
return IO.NodeOutput(model_file, response.result)
|
||||
|
||||
|
||||
class MeshyRefineNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="MeshyRefineNode",
|
||||
display_name="Meshy: Refine Draft Model",
|
||||
category="api node/3d/Meshy",
|
||||
description="Refine a previously created draft model.",
|
||||
inputs=[
|
||||
IO.Combo.Input("model", options=["latest"]),
|
||||
IO.Custom("MESHY_TASK_ID").Input("meshy_task_id"),
|
||||
IO.Boolean.Input(
|
||||
"enable_pbr",
|
||||
default=False,
|
||||
tooltip="Generate PBR Maps (metallic, roughness, normal) in addition to the base color. "
|
||||
"Note: this should be set to false when using Sculpture style, "
|
||||
"as Sculpture style generates its own set of PBR maps.",
|
||||
),
|
||||
IO.String.Input(
|
||||
"texture_prompt",
|
||||
default="",
|
||||
multiline=True,
|
||||
tooltip="Provide a text prompt to guide the texturing process. "
|
||||
"Maximum 600 characters. Cannot be used at the same time as 'texture_image'.",
|
||||
),
|
||||
IO.Image.Input(
|
||||
"texture_image",
|
||||
tooltip="Only one of 'texture_image' or 'texture_prompt' may be used at the same time.",
|
||||
optional=True,
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
IO.String.Output(display_name="model_file"),
|
||||
IO.Custom("MESHY_TASK_ID").Output(display_name="meshy_task_id"),
|
||||
],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
is_api_node=True,
|
||||
is_output_node=True,
|
||||
price_badge=IO.PriceBadge(
|
||||
expr="""{"type":"usd","usd":0.4}""",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
model: str,
|
||||
meshy_task_id: str,
|
||||
enable_pbr: bool,
|
||||
texture_prompt: str,
|
||||
texture_image: Input.Image | None = None,
|
||||
) -> IO.NodeOutput:
|
||||
if texture_prompt and texture_image is not None:
|
||||
raise ValueError("texture_prompt and texture_image cannot be used at the same time")
|
||||
texture_image_url = None
|
||||
if texture_prompt:
|
||||
validate_string(texture_prompt, field_name="texture_prompt", max_length=600)
|
||||
if texture_image is not None:
|
||||
texture_image_url = (await upload_images_to_comfyapi(cls, texture_image, wait_label="Uploading texture"))[0]
|
||||
response = await sync_op(
|
||||
cls,
|
||||
endpoint=ApiEndpoint(path="/proxy/meshy/openapi/v2/text-to-3d", method="POST"),
|
||||
response_model=MeshyTaskResponse,
|
||||
data=MeshyRefineTask(
|
||||
preview_task_id=meshy_task_id,
|
||||
enable_pbr=enable_pbr,
|
||||
texture_prompt=texture_prompt if texture_prompt else None,
|
||||
texture_image_url=texture_image_url,
|
||||
ai_model=model,
|
||||
),
|
||||
)
|
||||
result = await poll_op(
|
||||
cls,
|
||||
ApiEndpoint(path=f"/proxy/meshy/openapi/v2/text-to-3d/{response.result}"),
|
||||
response_model=MeshyModelResult,
|
||||
status_extractor=lambda r: r.status,
|
||||
progress_extractor=lambda r: r.progress,
|
||||
)
|
||||
model_file = f"meshy_model_{response.result}.glb"
|
||||
await download_url_to_bytesio(result.model_urls.glb, os.path.join(get_output_directory(), model_file))
|
||||
return IO.NodeOutput(model_file, response.result)
|
||||
|
||||
|
||||
class MeshyImageToModelNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="MeshyImageToModelNode",
|
||||
display_name="Meshy: Image to Model",
|
||||
category="api node/3d/Meshy",
|
||||
inputs=[
|
||||
IO.Combo.Input("model", options=["latest"]),
|
||||
IO.Image.Input("image"),
|
||||
IO.DynamicCombo.Input(
|
||||
"should_remesh",
|
||||
options=[
|
||||
IO.DynamicCombo.Option(
|
||||
"true",
|
||||
[
|
||||
IO.Combo.Input("topology", options=["triangle", "quad"]),
|
||||
IO.Int.Input(
|
||||
"target_polycount",
|
||||
default=300000,
|
||||
min=100,
|
||||
max=300000,
|
||||
display_mode=IO.NumberDisplay.number,
|
||||
),
|
||||
],
|
||||
),
|
||||
IO.DynamicCombo.Option("false", []),
|
||||
],
|
||||
tooltip="When set to false, returns an unprocessed triangular mesh.",
|
||||
),
|
||||
IO.Combo.Input("symmetry_mode", options=["auto", "on", "off"]),
|
||||
IO.DynamicCombo.Input(
|
||||
"should_texture",
|
||||
options=[
|
||||
IO.DynamicCombo.Option(
|
||||
"true",
|
||||
[
|
||||
IO.Boolean.Input(
|
||||
"enable_pbr",
|
||||
default=False,
|
||||
tooltip="Generate PBR Maps (metallic, roughness, normal) "
|
||||
"in addition to the base color.",
|
||||
),
|
||||
IO.String.Input(
|
||||
"texture_prompt",
|
||||
default="",
|
||||
multiline=True,
|
||||
tooltip="Provide a text prompt to guide the texturing process. "
|
||||
"Maximum 600 characters. Cannot be used at the same time as 'texture_image'.",
|
||||
),
|
||||
IO.Image.Input(
|
||||
"texture_image",
|
||||
tooltip="Only one of 'texture_image' or 'texture_prompt' "
|
||||
"may be used at the same time.",
|
||||
optional=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
IO.DynamicCombo.Option("false", []),
|
||||
],
|
||||
tooltip="Determines whether textures are generated. "
|
||||
"Setting it to false skips the texture phase and returns a mesh without textures.",
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"pose_mode",
|
||||
options=["", "A-pose", "T-pose"],
|
||||
tooltip="Specify the pose mode for the generated model.",
|
||||
),
|
||||
IO.Int.Input(
|
||||
"seed",
|
||||
default=0,
|
||||
min=0,
|
||||
max=2147483647,
|
||||
display_mode=IO.NumberDisplay.number,
|
||||
control_after_generate=True,
|
||||
tooltip="Seed controls whether the node should re-run; "
|
||||
"results are non-deterministic regardless of seed.",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
IO.String.Output(display_name="model_file"),
|
||||
IO.Custom("MESHY_TASK_ID").Output(display_name="meshy_task_id"),
|
||||
],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
is_api_node=True,
|
||||
is_output_node=True,
|
||||
price_badge=IO.PriceBadge(
|
||||
depends_on=IO.PriceBadgeDepends(widgets=["should_texture"]),
|
||||
expr="""
|
||||
(
|
||||
$prices := {"true": 1.2, "false": 0.8};
|
||||
{"type":"usd","usd": $lookup($prices, widgets.should_texture)}
|
||||
)
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
model: str,
|
||||
image: Input.Image,
|
||||
should_remesh: InputShouldRemesh,
|
||||
symmetry_mode: str,
|
||||
should_texture: InputShouldTexture,
|
||||
pose_mode: str,
|
||||
seed: int,
|
||||
) -> IO.NodeOutput:
|
||||
texture = should_texture["should_texture"] == "true"
|
||||
texture_image_url = texture_prompt = None
|
||||
if texture:
|
||||
if should_texture["texture_prompt"] and should_texture["texture_image"] is not None:
|
||||
raise ValueError("texture_prompt and texture_image cannot be used at the same time")
|
||||
if should_texture["texture_prompt"]:
|
||||
validate_string(should_texture["texture_prompt"], field_name="texture_prompt", max_length=600)
|
||||
texture_prompt = should_texture["texture_prompt"]
|
||||
if should_texture["texture_image"] is not None:
|
||||
texture_image_url = (
|
||||
await upload_images_to_comfyapi(
|
||||
cls, should_texture["texture_image"], wait_label="Uploading texture"
|
||||
)
|
||||
)[0]
|
||||
response = await sync_op(
|
||||
cls,
|
||||
ApiEndpoint(path="/proxy/meshy/openapi/v1/image-to-3d", method="POST"),
|
||||
response_model=MeshyTaskResponse,
|
||||
data=MeshyImageToModelRequest(
|
||||
image_url=(await upload_images_to_comfyapi(cls, image, wait_label="Uploading base image"))[0],
|
||||
ai_model=model,
|
||||
topology=should_remesh.get("topology", None),
|
||||
target_polycount=should_remesh.get("target_polycount", None),
|
||||
symmetry_mode=symmetry_mode,
|
||||
should_remesh=should_remesh["should_remesh"] == "true",
|
||||
should_texture=texture,
|
||||
enable_pbr=should_texture.get("enable_pbr", None),
|
||||
pose_mode=pose_mode.lower(),
|
||||
texture_prompt=texture_prompt,
|
||||
texture_image_url=texture_image_url,
|
||||
seed=seed,
|
||||
),
|
||||
)
|
||||
result = await poll_op(
|
||||
cls,
|
||||
ApiEndpoint(path=f"/proxy/meshy/openapi/v1/image-to-3d/{response.result}"),
|
||||
response_model=MeshyModelResult,
|
||||
status_extractor=lambda r: r.status,
|
||||
progress_extractor=lambda r: r.progress,
|
||||
)
|
||||
model_file = f"meshy_model_{response.result}.glb"
|
||||
await download_url_to_bytesio(result.model_urls.glb, os.path.join(get_output_directory(), model_file))
|
||||
return IO.NodeOutput(model_file, response.result)
|
||||
|
||||
|
||||
class MeshyMultiImageToModelNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="MeshyMultiImageToModelNode",
|
||||
display_name="Meshy: Multi-Image to Model",
|
||||
category="api node/3d/Meshy",
|
||||
inputs=[
|
||||
IO.Combo.Input("model", options=["latest"]),
|
||||
IO.Autogrow.Input(
|
||||
"images",
|
||||
template=IO.Autogrow.TemplatePrefix(IO.Image.Input("image"), prefix="image", min=2, max=4),
|
||||
),
|
||||
IO.DynamicCombo.Input(
|
||||
"should_remesh",
|
||||
options=[
|
||||
IO.DynamicCombo.Option(
|
||||
"true",
|
||||
[
|
||||
IO.Combo.Input("topology", options=["triangle", "quad"]),
|
||||
IO.Int.Input(
|
||||
"target_polycount",
|
||||
default=300000,
|
||||
min=100,
|
||||
max=300000,
|
||||
display_mode=IO.NumberDisplay.number,
|
||||
),
|
||||
],
|
||||
),
|
||||
IO.DynamicCombo.Option("false", []),
|
||||
],
|
||||
tooltip="When set to false, returns an unprocessed triangular mesh.",
|
||||
),
|
||||
IO.Combo.Input("symmetry_mode", options=["auto", "on", "off"]),
|
||||
IO.DynamicCombo.Input(
|
||||
"should_texture",
|
||||
options=[
|
||||
IO.DynamicCombo.Option(
|
||||
"true",
|
||||
[
|
||||
IO.Boolean.Input(
|
||||
"enable_pbr",
|
||||
default=False,
|
||||
tooltip="Generate PBR Maps (metallic, roughness, normal) "
|
||||
"in addition to the base color.",
|
||||
),
|
||||
IO.String.Input(
|
||||
"texture_prompt",
|
||||
default="",
|
||||
multiline=True,
|
||||
tooltip="Provide a text prompt to guide the texturing process. "
|
||||
"Maximum 600 characters. Cannot be used at the same time as 'texture_image'.",
|
||||
),
|
||||
IO.Image.Input(
|
||||
"texture_image",
|
||||
tooltip="Only one of 'texture_image' or 'texture_prompt' "
|
||||
"may be used at the same time.",
|
||||
optional=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
IO.DynamicCombo.Option("false", []),
|
||||
],
|
||||
tooltip="Determines whether textures are generated. "
|
||||
"Setting it to false skips the texture phase and returns a mesh without textures.",
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"pose_mode",
|
||||
options=["", "A-pose", "T-pose"],
|
||||
tooltip="Specify the pose mode for the generated model.",
|
||||
),
|
||||
IO.Int.Input(
|
||||
"seed",
|
||||
default=0,
|
||||
min=0,
|
||||
max=2147483647,
|
||||
display_mode=IO.NumberDisplay.number,
|
||||
control_after_generate=True,
|
||||
tooltip="Seed controls whether the node should re-run; "
|
||||
"results are non-deterministic regardless of seed.",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
IO.String.Output(display_name="model_file"),
|
||||
IO.Custom("MESHY_TASK_ID").Output(display_name="meshy_task_id"),
|
||||
],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
is_api_node=True,
|
||||
is_output_node=True,
|
||||
price_badge=IO.PriceBadge(
|
||||
depends_on=IO.PriceBadgeDepends(widgets=["should_texture"]),
|
||||
expr="""
|
||||
(
|
||||
$prices := {"true": 0.6, "false": 0.2};
|
||||
{"type":"usd","usd": $lookup($prices, widgets.should_texture)}
|
||||
)
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
model: str,
|
||||
images: IO.Autogrow.Type,
|
||||
should_remesh: InputShouldRemesh,
|
||||
symmetry_mode: str,
|
||||
should_texture: InputShouldTexture,
|
||||
pose_mode: str,
|
||||
seed: int,
|
||||
) -> IO.NodeOutput:
|
||||
texture = should_texture["should_texture"] == "true"
|
||||
texture_image_url = texture_prompt = None
|
||||
if texture:
|
||||
if should_texture["texture_prompt"] and should_texture["texture_image"] is not None:
|
||||
raise ValueError("texture_prompt and texture_image cannot be used at the same time")
|
||||
if should_texture["texture_prompt"]:
|
||||
validate_string(should_texture["texture_prompt"], field_name="texture_prompt", max_length=600)
|
||||
texture_prompt = should_texture["texture_prompt"]
|
||||
if should_texture["texture_image"] is not None:
|
||||
texture_image_url = (
|
||||
await upload_images_to_comfyapi(
|
||||
cls, should_texture["texture_image"], wait_label="Uploading texture"
|
||||
)
|
||||
)[0]
|
||||
response = await sync_op(
|
||||
cls,
|
||||
ApiEndpoint(path="/proxy/meshy/openapi/v1/multi-image-to-3d", method="POST"),
|
||||
response_model=MeshyTaskResponse,
|
||||
data=MeshyMultiImageToModelRequest(
|
||||
image_urls=await upload_images_to_comfyapi(
|
||||
cls, list(images.values()), wait_label="Uploading base images"
|
||||
),
|
||||
ai_model=model,
|
||||
topology=should_remesh.get("topology", None),
|
||||
target_polycount=should_remesh.get("target_polycount", None),
|
||||
symmetry_mode=symmetry_mode,
|
||||
should_remesh=should_remesh["should_remesh"] == "true",
|
||||
should_texture=texture,
|
||||
enable_pbr=should_texture.get("enable_pbr", None),
|
||||
pose_mode=pose_mode.lower(),
|
||||
texture_prompt=texture_prompt,
|
||||
texture_image_url=texture_image_url,
|
||||
seed=seed,
|
||||
),
|
||||
)
|
||||
result = await poll_op(
|
||||
cls,
|
||||
ApiEndpoint(path=f"/proxy/meshy/openapi/v1/multi-image-to-3d/{response.result}"),
|
||||
response_model=MeshyModelResult,
|
||||
status_extractor=lambda r: r.status,
|
||||
progress_extractor=lambda r: r.progress,
|
||||
)
|
||||
model_file = f"meshy_model_{response.result}.glb"
|
||||
await download_url_to_bytesio(result.model_urls.glb, os.path.join(get_output_directory(), model_file))
|
||||
return IO.NodeOutput(model_file, response.result)
|
||||
|
||||
|
||||
class MeshyRigModelNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="MeshyRigModelNode",
|
||||
display_name="Meshy: Rig Model",
|
||||
category="api node/3d/Meshy",
|
||||
description="Provides a rigged character in standard formats. "
|
||||
"Auto-rigging is currently not suitable for untextured meshes, non-humanoid assets, "
|
||||
"or humanoid assets with unclear limb and body structure.",
|
||||
inputs=[
|
||||
IO.Custom("MESHY_TASK_ID").Input("meshy_task_id"),
|
||||
IO.Float.Input(
|
||||
"height_meters",
|
||||
min=0.1,
|
||||
max=15.0,
|
||||
default=1.7,
|
||||
tooltip="The approximate height of the character model in meters. "
|
||||
"This aids in scaling and rigging accuracy.",
|
||||
),
|
||||
IO.Image.Input(
|
||||
"texture_image",
|
||||
tooltip="The model's UV-unwrapped base color texture image.",
|
||||
optional=True,
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
IO.String.Output(display_name="model_file"),
|
||||
IO.Custom("MESHY_RIGGED_TASK_ID").Output(display_name="rig_task_id"),
|
||||
],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
is_api_node=True,
|
||||
is_output_node=True,
|
||||
price_badge=IO.PriceBadge(
|
||||
expr="""{"type":"usd","usd":0.2}""",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
meshy_task_id: str,
|
||||
height_meters: float,
|
||||
texture_image: Input.Image | None = None,
|
||||
) -> IO.NodeOutput:
|
||||
texture_image_url = None
|
||||
if texture_image is not None:
|
||||
texture_image_url = (await upload_images_to_comfyapi(cls, texture_image, wait_label="Uploading texture"))[0]
|
||||
response = await sync_op(
|
||||
cls,
|
||||
endpoint=ApiEndpoint(path="/proxy/meshy/openapi/v1/rigging", method="POST"),
|
||||
response_model=MeshyTaskResponse,
|
||||
data=MeshyRiggingRequest(
|
||||
input_task_id=meshy_task_id,
|
||||
height_meters=height_meters,
|
||||
texture_image_url=texture_image_url,
|
||||
),
|
||||
)
|
||||
result = await poll_op(
|
||||
cls,
|
||||
ApiEndpoint(path=f"/proxy/meshy/openapi/v1/rigging/{response.result}"),
|
||||
response_model=MeshyRiggedResult,
|
||||
status_extractor=lambda r: r.status,
|
||||
progress_extractor=lambda r: r.progress,
|
||||
)
|
||||
model_file = f"meshy_model_{response.result}.glb"
|
||||
await download_url_to_bytesio(
|
||||
result.result.rigged_character_glb_url, os.path.join(get_output_directory(), model_file)
|
||||
)
|
||||
return IO.NodeOutput(model_file, response.result)
|
||||
|
||||
|
||||
class MeshyAnimateModelNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="MeshyAnimateModelNode",
|
||||
display_name="Meshy: Animate Model",
|
||||
category="api node/3d/Meshy",
|
||||
description="Apply a specific animation action to a previously rigged character.",
|
||||
inputs=[
|
||||
IO.Custom("MESHY_RIGGED_TASK_ID").Input("rig_task_id"),
|
||||
IO.Int.Input(
|
||||
"action_id",
|
||||
default=0,
|
||||
min=0,
|
||||
max=696,
|
||||
tooltip="Visit https://docs.meshy.ai/en/api/animation-library for a list of available values.",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
IO.String.Output(display_name="model_file"),
|
||||
],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
is_api_node=True,
|
||||
is_output_node=True,
|
||||
price_badge=IO.PriceBadge(
|
||||
expr="""{"type":"usd","usd":0.12}""",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
rig_task_id: str,
|
||||
action_id: int,
|
||||
) -> IO.NodeOutput:
|
||||
response = await sync_op(
|
||||
cls,
|
||||
endpoint=ApiEndpoint(path="/proxy/meshy/openapi/v1/animations", method="POST"),
|
||||
response_model=MeshyTaskResponse,
|
||||
data=MeshyAnimationRequest(
|
||||
rig_task_id=rig_task_id,
|
||||
action_id=action_id,
|
||||
),
|
||||
)
|
||||
result = await poll_op(
|
||||
cls,
|
||||
ApiEndpoint(path=f"/proxy/meshy/openapi/v1/animations/{response.result}"),
|
||||
response_model=MeshyAnimationResult,
|
||||
status_extractor=lambda r: r.status,
|
||||
progress_extractor=lambda r: r.progress,
|
||||
)
|
||||
model_file = f"meshy_model_{response.result}.glb"
|
||||
await download_url_to_bytesio(result.result.animation_glb_url, os.path.join(get_output_directory(), model_file))
|
||||
return IO.NodeOutput(model_file, response.result)
|
||||
|
||||
|
||||
class MeshyTextureNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="MeshyTextureNode",
|
||||
display_name="Meshy: Texture Model",
|
||||
category="api node/3d/Meshy",
|
||||
inputs=[
|
||||
IO.Combo.Input("model", options=["latest"]),
|
||||
IO.Custom("MESHY_TASK_ID").Input("meshy_task_id"),
|
||||
IO.Boolean.Input(
|
||||
"enable_original_uv",
|
||||
default=True,
|
||||
tooltip="Use the original UV of the model instead of generating new UVs. "
|
||||
"When enabled, Meshy preserves existing textures from the uploaded model. "
|
||||
"If the model has no original UV, the quality of the output might not be as good.",
|
||||
),
|
||||
IO.Boolean.Input("pbr", default=False),
|
||||
IO.String.Input(
|
||||
"text_style_prompt",
|
||||
default="",
|
||||
multiline=True,
|
||||
tooltip="Describe your desired texture style of the object using text. Maximum 600 characters."
|
||||
"Maximum 600 characters. Cannot be used at the same time as 'image_style'.",
|
||||
),
|
||||
IO.Image.Input(
|
||||
"image_style",
|
||||
optional=True,
|
||||
tooltip="A 2d image to guide the texturing process. "
|
||||
"Can not be used at the same time with 'text_style_prompt'.",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
IO.String.Output(display_name="model_file"),
|
||||
IO.Custom("MODEL_TASK_ID").Output(display_name="meshy_task_id"),
|
||||
],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
is_api_node=True,
|
||||
is_output_node=True,
|
||||
price_badge=IO.PriceBadge(
|
||||
expr="""{"type":"usd","usd":0.4}""",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
model: str,
|
||||
meshy_task_id: str,
|
||||
enable_original_uv: bool,
|
||||
pbr: bool,
|
||||
text_style_prompt: str,
|
||||
image_style: Input.Image | None = None,
|
||||
) -> IO.NodeOutput:
|
||||
if text_style_prompt and image_style is not None:
|
||||
raise ValueError("text_style_prompt and image_style cannot be used at the same time")
|
||||
if not text_style_prompt and image_style is None:
|
||||
raise ValueError("Either text_style_prompt or image_style is required")
|
||||
image_style_url = None
|
||||
if image_style is not None:
|
||||
image_style_url = (await upload_images_to_comfyapi(cls, image_style, wait_label="Uploading style"))[0]
|
||||
response = await sync_op(
|
||||
cls,
|
||||
endpoint=ApiEndpoint(path="/proxy/meshy/openapi/v1/retexture", method="POST"),
|
||||
response_model=MeshyTaskResponse,
|
||||
data=MeshyTextureRequest(
|
||||
input_task_id=meshy_task_id,
|
||||
ai_model=model,
|
||||
enable_original_uv=enable_original_uv,
|
||||
enable_pbr=pbr,
|
||||
text_style_prompt=text_style_prompt if text_style_prompt else None,
|
||||
image_style_url=image_style_url,
|
||||
),
|
||||
)
|
||||
result = await poll_op(
|
||||
cls,
|
||||
ApiEndpoint(path=f"/proxy/meshy/openapi/v1/retexture/{response.result}"),
|
||||
response_model=MeshyModelResult,
|
||||
status_extractor=lambda r: r.status,
|
||||
progress_extractor=lambda r: r.progress,
|
||||
)
|
||||
model_file = f"meshy_model_{response.result}.glb"
|
||||
await download_url_to_bytesio(result.model_urls.glb, os.path.join(get_output_directory(), model_file))
|
||||
return IO.NodeOutput(model_file, response.result)
|
||||
|
||||
|
||||
class MeshyExtension(ComfyExtension):
|
||||
@override
|
||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||
return [
|
||||
MeshyTextToModelNode,
|
||||
MeshyRefineNode,
|
||||
MeshyImageToModelNode,
|
||||
MeshyMultiImageToModelNode,
|
||||
MeshyRigModelNode,
|
||||
MeshyAnimateModelNode,
|
||||
MeshyTextureNode,
|
||||
]
|
||||
|
||||
|
||||
async def comfy_entrypoint() -> MeshyExtension:
|
||||
return MeshyExtension()
|
||||
@ -43,7 +43,7 @@ class UploadResponse(BaseModel):
|
||||
|
||||
async def upload_images_to_comfyapi(
|
||||
cls: type[IO.ComfyNode],
|
||||
image: torch.Tensor,
|
||||
image: torch.Tensor | list[torch.Tensor],
|
||||
*,
|
||||
max_images: int = 8,
|
||||
mime_type: str | None = None,
|
||||
@ -55,15 +55,28 @@ async def upload_images_to_comfyapi(
|
||||
Uploads images to ComfyUI API and returns download URLs.
|
||||
To upload multiple images, stack them in the batch dimension first.
|
||||
"""
|
||||
tensors: list[torch.Tensor] = []
|
||||
if isinstance(image, list):
|
||||
for img in image:
|
||||
is_batch = len(img.shape) > 3
|
||||
if is_batch:
|
||||
tensors.extend(img[i] for i in range(img.shape[0]))
|
||||
else:
|
||||
tensors.append(img)
|
||||
else:
|
||||
is_batch = len(image.shape) > 3
|
||||
if is_batch:
|
||||
tensors.extend(image[i] for i in range(image.shape[0]))
|
||||
else:
|
||||
tensors.append(image)
|
||||
|
||||
# if batched, try to upload each file if max_images is greater than 0
|
||||
download_urls: list[str] = []
|
||||
is_batch = len(image.shape) > 3
|
||||
batch_len = image.shape[0] if is_batch else 1
|
||||
num_to_upload = min(batch_len, max_images)
|
||||
num_to_upload = min(len(tensors), max_images)
|
||||
batch_start_ts = time.monotonic()
|
||||
|
||||
for idx in range(num_to_upload):
|
||||
tensor = image[idx] if is_batch else image
|
||||
tensor = tensors[idx]
|
||||
img_io = tensor_to_bytesio(tensor, total_pixels=total_pixels, mime_type=mime_type)
|
||||
|
||||
effective_label = wait_label
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by the build process when version is
|
||||
# updated in pyproject.toml.
|
||||
__version__ = "0.9.1"
|
||||
__version__ = "0.9.2"
|
||||
|
||||
2
nodes.py
2
nodes.py
@ -788,6 +788,7 @@ class VAELoader:
|
||||
|
||||
#TODO: scale factor?
|
||||
def load_vae(self, vae_name):
|
||||
metadata = None
|
||||
if vae_name == "pixel_space":
|
||||
sd = {}
|
||||
sd["pixel_space_vae"] = torch.tensor(1.0)
|
||||
@ -2400,6 +2401,7 @@ async def init_builtin_api_nodes():
|
||||
"nodes_sora.py",
|
||||
"nodes_topaz.py",
|
||||
"nodes_tripo.py",
|
||||
"nodes_meshy.py",
|
||||
"nodes_moonvalley.py",
|
||||
"nodes_rodin.py",
|
||||
"nodes_gemini.py",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "ComfyUI"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user