One 32 GB card (RTX 5090) for the agent; fix the hf download commands; which setup for which GPU
#4
by yycc - opened
- COMFYUI.md +4 -2
- README.md +21 -3
- comfyui/ComfyUI-Viggle-Remake/nodes.py +43 -2
- mcp/README.md +44 -3
- mcp/comfy_worker/remake_worker.py +262 -0
- mcp/comfy_worker/run_worker.sh +11 -0
- mcp/comfy_worker/setup.sh +22 -0
- mcp/viggle_remake_mcp/common.py +2 -2
- mcp/viggle_remake_mcp/kit.py +1 -1
COMFYUI.md
CHANGED
|
@@ -13,6 +13,7 @@ starting noise for a seed. Everything else is stock ComfyUI. Tested with ComfyUI
|
|
| 13 |
|
| 14 |
```bash
|
| 15 |
cd ComfyUI
|
|
|
|
| 16 |
|
| 17 |
# the base model, as Comfy-Org's single-file repack (about 124 GB)
|
| 18 |
hf download Comfy-Org/MiniMax-H3 \
|
|
@@ -23,7 +24,7 @@ hf download Comfy-Org/MiniMax-H3 \
|
|
| 23 |
--local-dir models
|
| 24 |
|
| 25 |
# the LoRAs, the nodes, the workflows and the example inputs from this repository
|
| 26 |
-
hf download Viggle/Viggle-Remake --include "comfyui/*" "examples/inputs/*" --local-dir ./Viggle-Remake
|
| 27 |
cp -r Viggle-Remake/comfyui/ComfyUI-Viggle-Remake custom_nodes/
|
| 28 |
cp Viggle-Remake/comfyui/viggle_remake_*_comfyui.safetensors models/loras/
|
| 29 |
```
|
|
@@ -205,7 +206,8 @@ We measured on B200s. A second process held the rest of the card, so ComfyUI saw
|
|
| 205 |
- **Why GGUF needs the flag.** ComfyUI-GGUF puts as much of the model on the card as ComfyUI's memory estimate allows, and for MiniMax-H3 that estimate leaves out the reference and keyframe tokens.
|
| 206 |
- When the weights fit but the activations do not, ComfyUI aborts with `CUDA error: invalid argument` instead of falling back to offloading.
|
| 207 |
- `--reserve-vram N` keeps N GB free for the activations. If a run aborts, raise N.
|
| 208 |
-
- **Why int8 stops at 1280×736 × 226 frames.** Without a CUDA 13 build of PyTorch, comfy-kitchen falls back to a plain int8 matmul. That matmul keeps 32-bit sums for the whole layer and assembles the output in pieces, so one layer briefly needs several times the memory of its output. At 1280×736 × 226 frames this ran out of memory even with 80 GB free, while bf16 + LoRA fit in 80 GB and Q8_0 ran in 24 GB with `--reserve-vram 20`. We have not tested a CUDA 13 build.
|
|
|
|
| 209 |
- **Q4_K_M is the smallest file.** It is visibly further from the reference than int8 (see Quality).
|
| 210 |
|
| 211 |
### Quality
|
|
|
|
| 13 |
|
| 14 |
```bash
|
| 15 |
cd ComfyUI
|
| 16 |
+
pip install -U "huggingface_hub>=1.0" # the hf CLI; older versions keep only the last --include
|
| 17 |
|
| 18 |
# the base model, as Comfy-Org's single-file repack (about 124 GB)
|
| 19 |
hf download Comfy-Org/MiniMax-H3 \
|
|
|
|
| 24 |
--local-dir models
|
| 25 |
|
| 26 |
# the LoRAs, the nodes, the workflows and the example inputs from this repository
|
| 27 |
+
hf download Viggle/Viggle-Remake --include "comfyui/*" --include "examples/inputs/*" --local-dir ./Viggle-Remake
|
| 28 |
cp -r Viggle-Remake/comfyui/ComfyUI-Viggle-Remake custom_nodes/
|
| 29 |
cp Viggle-Remake/comfyui/viggle_remake_*_comfyui.safetensors models/loras/
|
| 30 |
```
|
|
|
|
| 206 |
- **Why GGUF needs the flag.** ComfyUI-GGUF puts as much of the model on the card as ComfyUI's memory estimate allows, and for MiniMax-H3 that estimate leaves out the reference and keyframe tokens.
|
| 207 |
- When the weights fit but the activations do not, ComfyUI aborts with `CUDA error: invalid argument` instead of falling back to offloading.
|
| 208 |
- `--reserve-vram N` keeps N GB free for the activations. If a run aborts, raise N.
|
| 209 |
+
- **Why int8 stops at 1280×736 × 226 frames.** Without a CUDA 13 build of PyTorch, comfy-kitchen falls back to a plain int8 matmul. That matmul keeps 32-bit sums for the whole layer and assembles the output in pieces, so one layer briefly needs several times the memory of its output. At 1280×736 × 226 frames this ran out of memory even with 80 GB free, while bf16 + LoRA fit in 80 GB and Q8_0 ran in 24 GB with `--reserve-vram 20`. We have not tested the graphs above with a CUDA 13 build, but see the next point.
|
| 210 |
+
- **A CUDA 13 build on a 32 GB card.** With torch 2.9.1+cu130 on an RTX 5090, int8 renders 1280×736 at up to 345 frames (about 260 s per step) through the MCP's ComfyUI worker, [`mcp/comfy_worker/`](mcp/comfy_worker), which also runs each block's feed-forward in row chunks and starts ComfyUI with `--vram-headroom 4`.
|
| 211 |
- **Q4_K_M is the smallest file.** It is visibly further from the reference than int8 (see Quality).
|
| 212 |
|
| 213 |
### Quality
|
README.md
CHANGED
|
@@ -53,15 +53,28 @@ swaps, removes and inserts objects, and changes the setting, the weather or the
|
|
| 53 |
|
| 54 |
> **Just want to use it?** The [Viggle app](https://viggle.ai/meme/app/home?ui=v2) runs Remake for you. The rest of this section is for running it on your own GPUs.
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
You need the base model (about 144 GB of its files) and this repository's LoRAs and sampler:
|
| 57 |
|
| 58 |
```bash
|
|
|
|
| 59 |
hf download MiniMaxAI/MiniMax-H3 --local-dir ./MiniMax-H3 \
|
| 60 |
-
--include "modular_model_index.json" "transformer/*" "text_encoder/*"
|
| 61 |
-
|
|
|
|
| 62 |
|
| 63 |
hf download Viggle/Viggle-Remake --local-dir ./Viggle-Remake \
|
| 64 |
-
--include "lora/*" "inference/*" "examples/*"
|
|
|
|
| 65 |
pip install -r Viggle-Remake/requirements.txt # plus an ffmpeg binary on PATH
|
| 66 |
|
| 67 |
python Viggle-Remake/inference/sample.py --mode swap \
|
|
@@ -83,6 +96,11 @@ the LoRA did not load.
|
|
| 83 |
|
| 84 |
Node pack, workflows, and int8/GGUF files down to 12 GB: [`COMFYUI.md`](COMFYUI.md).
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
## Agent: the MCP server
|
| 87 |
|
| 88 |
[`mcp/`](mcp) lets a coding agent (Claude Code, codex) remake a whole film: it cuts the windows, paints
|
|
|
|
| 53 |
|
| 54 |
> **Just want to use it?** The [Viggle app](https://viggle.ai/meme/app/home?ui=v2) runs Remake for you. The rest of this section is for running it on your own GPUs.
|
| 55 |
|
| 56 |
+
**Which setup for your machine:**
|
| 57 |
+
|
| 58 |
+
| your GPU | what to run |
|
| 59 |
+
|---|---|
|
| 60 |
+
| none | the [Viggle app](https://viggle.ai/meme/app/home?ui=v2) |
|
| 61 |
+
| one 12–24 GB card | ComfyUI with an int8 or GGUF file, workflows by hand (no agent): [Smaller GPUs](COMFYUI.md#smaller-gpus-int8-and-gguf) |
|
| 62 |
+
| one 32 GB card (RTX 5090) | the agent with the ComfyUI worker, 1280×736 up to 345 frames, with about 60 GB of RAM: [One 32 GB card](mcp/README.md#one-32-gb-card-the-comfyui-worker) |
|
| 63 |
+
| 80 GB | `sample.py` below, or ComfyUI with bf16 + LoRA: [ComfyUI](COMFYUI.md#comfyui) |
|
| 64 |
+
| 96 GB or more | the same, or the [agent](#agent-the-mcp-server) with `viggle-remake-worker` |
|
| 65 |
+
|
| 66 |
You need the base model (about 144 GB of its files) and this repository's LoRAs and sampler:
|
| 67 |
|
| 68 |
```bash
|
| 69 |
+
pip install -U "huggingface_hub>=1.0" # the hf CLI; older versions keep only the last --include
|
| 70 |
hf download MiniMaxAI/MiniMax-H3 --local-dir ./MiniMax-H3 \
|
| 71 |
+
--include "modular_model_index.json" --include "transformer/*" --include "text_encoder/*" \
|
| 72 |
+
--include "tokenizer/*" --include "processor/*" --include "vae/*" --include "audio_vae/*" \
|
| 73 |
+
--include "scheduler/*" --include "audio_scheduler/*" --include "docs/*"
|
| 74 |
|
| 75 |
hf download Viggle/Viggle-Remake --local-dir ./Viggle-Remake \
|
| 76 |
+
--include "lora/*" --include "inference/*" --include "examples/*" \
|
| 77 |
+
--include "requirements.txt" # 5.5 GB; the whole repo is ~210 GB
|
| 78 |
pip install -r Viggle-Remake/requirements.txt # plus an ffmpeg binary on PATH
|
| 79 |
|
| 80 |
python Viggle-Remake/inference/sample.py --mode swap \
|
|
|
|
| 96 |
|
| 97 |
Node pack, workflows, and int8/GGUF files down to 12 GB: [`COMFYUI.md`](COMFYUI.md).
|
| 98 |
|
| 99 |
+
To run the agent below on one 32 GB card such as an RTX 5090, use the ComfyUI render worker in
|
| 100 |
+
[`mcp/comfy_worker/`](mcp/comfy_worker) instead of `viggle-remake-worker`: a 1280×736 window of 345 frames renders in
|
| 101 |
+
about 28 minutes, and White House Funk runs end to end in about 3 hours. See
|
| 102 |
+
[One 32 GB card](mcp/README.md#one-32-gb-card-the-comfyui-worker).
|
| 103 |
+
|
| 104 |
## Agent: the MCP server
|
| 105 |
|
| 106 |
[`mcp/`](mcp) lets a coding agent (Claude Code, codex) remake a whole film: it cuts the windows, paints
|
comfyui/ComfyUI-Viggle-Remake/nodes.py
CHANGED
|
@@ -9,6 +9,7 @@ RemakeConditioning the source clip as <Video 1>, each image as <Picture N> and
|
|
| 9 |
clean target audio for the whole denoise
|
| 10 |
RemakeKeyframeUntil drops the keyframes once sigma falls below `keyframe_until` (0.9 = the last two of six forwards)
|
| 11 |
RemakeNoise draws the starting noise in the reference script's order, so a seed gives the same video
|
|
|
|
| 12 |
|
| 13 |
Load the LoRA with the stock LoraLoaderBypassModelOnly. LoraLoaderModelOnly merges it into the bf16 weights,
|
| 14 |
where rounding drops part of the update and the output comes out softer.
|
|
@@ -211,8 +212,48 @@ class RemakeNoise:
|
|
| 211 |
return (Noise(seed, skip),)
|
| 212 |
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
NODE_CLASS_MAPPINGS = {"RemakeConditioning": RemakeConditioning, "RemakeKeyframeUntil": RemakeKeyframeUntil,
|
| 215 |
-
"RemakeNoise": RemakeNoise}
|
| 216 |
NODE_DISPLAY_NAME_MAPPINGS = {"RemakeConditioning": "Viggle Remake Conditioning",
|
| 217 |
"RemakeKeyframeUntil": "Viggle Remake Keyframe Until",
|
| 218 |
-
"RemakeNoise": "Viggle Remake Noise"
|
|
|
|
|
|
| 9 |
clean target audio for the whole denoise
|
| 10 |
RemakeKeyframeUntil drops the keyframes once sigma falls below `keyframe_until` (0.9 = the last two of six forwards)
|
| 11 |
RemakeNoise draws the starting noise in the reference script's order, so a seed gives the same video
|
| 12 |
+
RemakePrefix a hop: the previous take's frames open this render, held clean like the soundtrack
|
| 13 |
|
| 14 |
Load the LoRA with the stock LoraLoaderBypassModelOnly. LoraLoaderModelOnly merges it into the bf16 weights,
|
| 15 |
where rounding drops part of the update and the output comes out softer.
|
|
|
|
| 212 |
return (Noise(seed, skip),)
|
| 213 |
|
| 214 |
|
| 215 |
+
class RemakePrefix:
|
| 216 |
+
@classmethod
|
| 217 |
+
def INPUT_TYPES(cls):
|
| 218 |
+
return {"required": {
|
| 219 |
+
"vae": ("VAE",),
|
| 220 |
+
"latent": ("LATENT",),
|
| 221 |
+
"prefix": ("IMAGE", {"tooltip": "The previous take's frames, on this render's canvas."}),
|
| 222 |
+
"prefix_frames": ("INT", {"default": 17, "min": 17, "max": 323, "step": 17,
|
| 223 |
+
"tooltip": "How many of its frames open this render, a multiple of 17."}),
|
| 224 |
+
"prefix_at": ("INT", {"default": -1, "min": -1, "max": 100000,
|
| 225 |
+
"tooltip": "The previous take's frame they start at; -1: its last prefix_frames."}),
|
| 226 |
+
}}
|
| 227 |
+
|
| 228 |
+
RETURN_TYPES = ("LATENT",)
|
| 229 |
+
FUNCTION = "run"
|
| 230 |
+
CATEGORY = "Viggle Remake"
|
| 231 |
+
|
| 232 |
+
def run(self, vae, latent, prefix, prefix_frames, prefix_at):
|
| 233 |
+
# frames [at, at + k) of the previous take are this render's first k frames: their latents go in clean with
|
| 234 |
+
# noise_mask 0, so like the held soundtrack they enter at the condition timestep, are never stepped and come
|
| 235 |
+
# out unchanged (the reference worker's prefix rows)
|
| 236 |
+
k = prefix_frames
|
| 237 |
+
at = len(prefix) - k if prefix_at < 0 else prefix_at
|
| 238 |
+
px = prefix[at:at + k, ..., :3]
|
| 239 |
+
video, audio = latent["samples"].unbind()
|
| 240 |
+
vmask, amask = latent["noise_mask"].unbind()
|
| 241 |
+
n = k // 17 * 5
|
| 242 |
+
assert k % 17 == 0 and n < video.shape[2], f"prefix_frames must be 17m and shorter than the render, got {k}"
|
| 243 |
+
assert tuple(px.shape[:3]) == (k, video.shape[3] * 16, video.shape[4] * 16), \
|
| 244 |
+
f"the prefix must be {k} frames on the {video.shape[4] * 16}x{video.shape[3] * 16} canvas, got {tuple(px.shape)}"
|
| 245 |
+
# the VAE encodes 17-frame clips and drops the last 3 latents: pad to 17m + 5, keep the first 5m
|
| 246 |
+
clean = vae.encode(torch.cat([px, px[-1:].repeat(5, 1, 1, 1)]))[:, :, :n]
|
| 247 |
+
video, vmask = video.clone(), vmask.clone()
|
| 248 |
+
video[:, :, :n] = clean.to(video)
|
| 249 |
+
vmask[:, :, :n] = 0
|
| 250 |
+
return ({**latent, "samples": comfy.nested_tensor.NestedTensor((video, audio)),
|
| 251 |
+
"noise_mask": comfy.nested_tensor.NestedTensor((vmask, amask))},)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
NODE_CLASS_MAPPINGS = {"RemakeConditioning": RemakeConditioning, "RemakeKeyframeUntil": RemakeKeyframeUntil,
|
| 255 |
+
"RemakeNoise": RemakeNoise, "RemakePrefix": RemakePrefix}
|
| 256 |
NODE_DISPLAY_NAME_MAPPINGS = {"RemakeConditioning": "Viggle Remake Conditioning",
|
| 257 |
"RemakeKeyframeUntil": "Viggle Remake Keyframe Until",
|
| 258 |
+
"RemakeNoise": "Viggle Remake Noise",
|
| 259 |
+
"RemakePrefix": "Viggle Remake Prefix"}
|
mcp/README.md
CHANGED
|
@@ -12,6 +12,15 @@ painted, writes the six-section prompt per render window, queues the renders, re
|
|
| 12 |
final video. The server gives it 27 tools for that (28 in pool image mode; see [Tools](#tools)) and enforces the
|
| 13 |
budgets. The rules the agent follows are in [`AGENTS.md`](viggle_remake_mcp/data/AGENTS.md).
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
## How it fits together
|
| 16 |
|
| 17 |
```
|
|
@@ -57,7 +66,8 @@ budgets. The rules the agent follows are in [`AGENTS.md`](viggle_remake_mcp/data
|
|
| 57 |
|
| 58 |
## Install
|
| 59 |
|
| 60 |
-
Requires Python ≥ 3.10, and `ffmpeg` and `ffprobe` on `PATH`.
|
|
|
|
| 61 |
|
| 62 |
```bash
|
| 63 |
hf download Viggle/Viggle-Remake --include "mcp/*" --local-dir viggle-remake # under 1 MB plus the example's 210 MB
|
|
@@ -173,8 +183,39 @@ One worker per GPU, bf16. A worker measures its card's free memory at start and
|
|
| 173 |
A longer one-take becomes a chain of windows, each continuing the previous one's take, so a smaller card renders the same
|
| 174 |
clip with more seams. Each worker also keeps the text encoder in host RAM: about 58 GiB of RAM per worker.
|
| 175 |
|
| 176 |
-
|
| 177 |
-
measurements behind the limits are in [`WORKER_NOTES.md`](WORKER_NOTES.md#memory).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
### Writing your own worker
|
| 180 |
|
|
|
|
| 12 |
final video. The server gives it 27 tools for that (28 in pool image mode; see [Tools](#tools)) and enforces the
|
| 13 |
budgets. The rules the agent follows are in [`AGENTS.md`](viggle_remake_mcp/data/AGENTS.md).
|
| 14 |
|
| 15 |
+
**What you need:**
|
| 16 |
+
- **an agent**: Claude Code, codex, or any MCP client;
|
| 17 |
+
- **a painter** for the keyframes: either a codex login with a ChatGPT subscription (`REMAKE_PAINT=pool`, no API
|
| 18 |
+
spend), or a key for an OpenAI-compatible images API (the default; see [Cost](#cost));
|
| 19 |
+
- **render workers**: `viggle-remake-worker` on a card with 96 GB or more, or the
|
| 20 |
+
[ComfyUI worker](#one-32-gb-card-the-comfyui-worker) on one 32 GB card such as an RTX 5090 (Linux, about 60 GB of
|
| 21 |
+
RAM). The server itself needs no GPU. Cards below 32 GB are not tested with the agent; use ComfyUI by hand
|
| 22 |
+
([`COMFYUI.md`](../COMFYUI.md#smaller-gpus-int8-and-gguf)).
|
| 23 |
+
|
| 24 |
## How it fits together
|
| 25 |
|
| 26 |
```
|
|
|
|
| 66 |
|
| 67 |
## Install
|
| 68 |
|
| 69 |
+
Requires Python ≥ 3.10, and `ffmpeg` 5.1 or newer and `ffprobe` on `PATH` (Ubuntu 22.04 ships 4.4: use a static
|
| 70 |
+
build).
|
| 71 |
|
| 72 |
```bash
|
| 73 |
hf download Viggle/Viggle-Remake --include "mcp/*" --local-dir viggle-remake # under 1 MB plus the example's 210 MB
|
|
|
|
| 183 |
A longer one-take becomes a chain of windows, each continuing the previous one's take, so a smaller card renders the same
|
| 184 |
clip with more seams. Each worker also keeps the text encoder in host RAM: about 58 GiB of RAM per worker.
|
| 185 |
|
| 186 |
+
This worker does not offload or quantize; on a smaller GPU use the ComfyUI worker below. Peak memory per length and
|
| 187 |
+
the measurements behind the limits are in [`WORKER_NOTES.md`](WORKER_NOTES.md#memory).
|
| 188 |
+
|
| 189 |
+
#### One 32 GB card: the ComfyUI worker
|
| 190 |
+
|
| 191 |
+
[`comfy_worker/`](comfy_worker) is a second worker for the same queue, for one consumer card. It imports ComfyUI 0.42.0
|
| 192 |
+
as a library and runs the int8 per-mode transformers from [`int8/`](../int8) with ComfyUI's dynamic VRAM, which
|
| 193 |
+
memory-maps the checkpoints and streams in, layer by layer, whatever does not fit on the card. The server and the agent
|
| 194 |
+
are unchanged.
|
| 195 |
+
|
| 196 |
+
```bash
|
| 197 |
+
cd viggle-remake/mcp/comfy_worker
|
| 198 |
+
./setup.sh # once: ComfyUI 0.42.0, torch 2.9.1 + CUDA 13.0, the node pack, the int8 weights, the text encoder
|
| 199 |
+
bash run_worker.sh # in place of viggle-remake-worker, in the foreground; --gpu N picks the card
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
It needs Linux, an NVIDIA driver for CUDA 13, `uv`, `ffmpeg` 5.1 or newer, `hf`, about 70 GB of disk and 60 GB of RAM.
|
| 203 |
+
Measured on one RTX 5090 (32 GB) with 60 GB of RAM, an int8 swap job, six steps:
|
| 204 |
+
|
| 205 |
+
| canvas | frames | render |
|
| 206 |
+
|---|---|---|
|
| 207 |
+
| 832×480 | 226 | 3 min |
|
| 208 |
+
| 832×480 | 345 | 6 min |
|
| 209 |
+
| 1280×736 | 175 | 8 min |
|
| 210 |
+
| 1280×736 | 226 | 13 min |
|
| 211 |
+
| 1280×736 | 345 | 28 min; with four keyframes, VRAM at most 27.8 GB |
|
| 212 |
+
|
| 213 |
+
A step is almost all attention, so a 345-frame window costs four times a 175-frame one; `--max-736 175` trades more
|
| 214 |
+
seams for speed. White House Funk ran end to end on one 5090 in 2 h 58 min. How it differs from the bf16 worker:
|
| 215 |
+
- the int8 transformer of the job's mode, one at a time; a job of the other mode swaps it;
|
| 216 |
+
- ComfyUI keeps 4 GB of VRAM free (`--vram-headroom 4`);
|
| 217 |
+
- after an OOM or a CUDA error the worker writes the `.err` and exits, and `run_worker.sh` restarts it;
|
| 218 |
+
- hops go through the node pack's `RemakePrefix` node.
|
| 219 |
|
| 220 |
### Writing your own worker
|
| 221 |
|
mcp/comfy_worker/remake_worker.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
"""Viggle Remake render worker on ComfyUI as a library: one process, no ComfyUI server.
|
| 3 |
+
|
| 4 |
+
ComfyUI/venv/bin/python remake_worker.py [--gpu 0] [--home ~/.viggle-remake] [--max-480 345] [--max-736 345] \
|
| 5 |
+
[-- <ComfyUI flags, e.g. --vram-headroom 6>]
|
| 6 |
+
|
| 7 |
+
Renders the viggle-remake MCP's file queue with the graph of comfyui/workflows/remake_*_api.json: the mode's int8
|
| 8 |
+
transformer -> RemakeKeyframeUntil -> BasicGuider, euler on the mode's six-step sigmas, RemakeConditioning +
|
| 9 |
+
RemakeNoise, and RemakePrefix for a hop. The queue protocol is the official worker's (viggle_remake_mcp/worker.py).
|
| 10 |
+
"""
|
| 11 |
+
import os
|
| 12 |
+
import sys
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
if "--gpu" in sys.argv[:-1]: # before torch
|
| 16 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = sys.argv[sys.argv.index("--gpu") + 1]
|
| 17 |
+
COMFY = Path(__file__).resolve().parent / "ComfyUI"
|
| 18 |
+
split = sys.argv.index("--") if "--" in sys.argv else len(sys.argv)
|
| 19 |
+
# --vram-headroom: dynamic VRAM keeps 4 GB free, else a 311-frame 736 window OOMed on a 2.8 GB attention output
|
| 20 |
+
# before the weights make room (a later flag after -- wins)
|
| 21 |
+
ours, sys.argv = sys.argv[1:split], [str(COMFY / "main.py"), "--vram-headroom", "4", *sys.argv[split + 1:]]
|
| 22 |
+
sys.path.insert(0, str(COMFY))
|
| 23 |
+
sys.path.insert(0, str(COMFY / "custom_nodes"))
|
| 24 |
+
|
| 25 |
+
import comfy.options # noqa: E402
|
| 26 |
+
|
| 27 |
+
comfy.options.enable_args_parsing()
|
| 28 |
+
import cuda_malloc # noqa: E402,F401 as ComfyUI's own main does before torch
|
| 29 |
+
import main # noqa: E402,F401 ComfyUI's setup (logging, dynamic VRAM, model paths); its server starts only as __main__
|
| 30 |
+
|
| 31 |
+
import argparse # noqa: E402
|
| 32 |
+
import gc # noqa: E402
|
| 33 |
+
import importlib # noqa: E402
|
| 34 |
+
import json # noqa: E402
|
| 35 |
+
import resource # noqa: E402
|
| 36 |
+
import socket # noqa: E402
|
| 37 |
+
import subprocess # noqa: E402
|
| 38 |
+
import threading # noqa: E402
|
| 39 |
+
import time # noqa: E402
|
| 40 |
+
import traceback # noqa: E402
|
| 41 |
+
from fractions import Fraction # noqa: E402
|
| 42 |
+
|
| 43 |
+
import numpy as np # noqa: E402
|
| 44 |
+
import torch # noqa: E402
|
| 45 |
+
from PIL import Image # noqa: E402
|
| 46 |
+
|
| 47 |
+
import comfy.ldm.minimax.model as H3 # noqa: E402
|
| 48 |
+
import comfy.model_management as mm # noqa: E402
|
| 49 |
+
import comfy.samplers # noqa: E402
|
| 50 |
+
import nodes # noqa: E402
|
| 51 |
+
from comfy_api.latest import InputImpl, Types # noqa: E402
|
| 52 |
+
from comfy_extras.nodes_audio import vae_decode_audio # noqa: E402
|
| 53 |
+
from comfy_extras.nodes_custom_sampler import Guider_Basic # noqa: E402
|
| 54 |
+
|
| 55 |
+
R = importlib.import_module("ComfyUI-Viggle-Remake.nodes")
|
| 56 |
+
|
| 57 |
+
# Each block's MLP over the packed sequence FF_ROWS rows at a time, as the reference worker does: its S x 28672 bf16
|
| 58 |
+
# fc1 output alone is 11 GiB at 345 frames on the 736 canvas, which a 32 GB card does not have. int8 quantizes the
|
| 59 |
+
# activations per row, so the output is the same.
|
| 60 |
+
FF_ROWS = 16384
|
| 61 |
+
_mlp = H3.MLP.forward
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def mlp_rows(self, x):
|
| 65 |
+
if x.shape[-2] <= FF_ROWS:
|
| 66 |
+
return _mlp(self, x)
|
| 67 |
+
out = torch.empty_like(x)
|
| 68 |
+
for a in range(0, x.shape[-2], FF_ROWS):
|
| 69 |
+
out[..., a:a + FF_ROWS, :] = _mlp(self, x[..., a:a + FF_ROWS, :])
|
| 70 |
+
return out
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
H3.MLP.forward = mlp_rows
|
| 74 |
+
|
| 75 |
+
MODES = ("swap", "edit")
|
| 76 |
+
FRAMES = range(90, 346, 17) # 17n + 5, 24 fps
|
| 77 |
+
AREA = {480: 832 * 480, 736: 1280 * 736}
|
| 78 |
+
SIGMAS = {"swap": [1.0, 0.990476, 0.965243, 0.923077, 0.8, 0.631579, 0.0],
|
| 79 |
+
"edit": [1.0, 0.983607, 0.96, 0.923077, 0.857143, 0.705882, 0.0]}
|
| 80 |
+
UNET = {m: f"viggle_remake_{m}_pruned_int8_convrot.safetensors" for m in MODES}
|
| 81 |
+
TE = "qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors"
|
| 82 |
+
VAE, AUDIO_VAE = "minimax_h3_video_vae_fp16.safetensors", "minimax_h3_audio_vae_fp32.safetensors"
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def main_loop():
|
| 86 |
+
parser = argparse.ArgumentParser()
|
| 87 |
+
parser.add_argument("--gpu", default=None, help="card index; default: CUDA_VISIBLE_DEVICES as set")
|
| 88 |
+
parser.add_argument("--home", default=os.environ.get("REMAKE_HOME", "~/.viggle-remake"))
|
| 89 |
+
parser.add_argument("--max-480", type=int, default=345, help="longest render at 832x480 (17n+5, 0 = none)")
|
| 90 |
+
parser.add_argument("--max-736", type=int, default=345, help="longest render at 1280x736 (17n+5, 0 = none)")
|
| 91 |
+
args = parser.parse_args(ours)
|
| 92 |
+
gpu = os.environ.get("CUDA_VISIBLE_DEVICES", "0")
|
| 93 |
+
host = socket.gethostname()
|
| 94 |
+
home = Path(args.home).expanduser()
|
| 95 |
+
queue, workers = home / "queue", home / "workers"
|
| 96 |
+
for p in (queue, workers):
|
| 97 |
+
p.mkdir(parents=True, exist_ok=True)
|
| 98 |
+
limit = {480: args.max_480, 736: args.max_736}
|
| 99 |
+
state = {"job": None}
|
| 100 |
+
beat_path, lock = workers / f"{host}_{gpu}.json", threading.Lock()
|
| 101 |
+
|
| 102 |
+
def beat():
|
| 103 |
+
with lock:
|
| 104 |
+
tmp = beat_path.with_suffix(f".{os.getpid()}.tmp")
|
| 105 |
+
tmp.write_text(json.dumps({"t": time.time(), "host": host, "gpu": gpu, "pid": os.getpid(),
|
| 106 |
+
"job": state["job"], "modes": list(MODES),
|
| 107 |
+
"max_frames": {str(k): v for k, v in limit.items()}}))
|
| 108 |
+
tmp.rename(beat_path)
|
| 109 |
+
|
| 110 |
+
def beat_loop():
|
| 111 |
+
while True:
|
| 112 |
+
try:
|
| 113 |
+
beat()
|
| 114 |
+
except OSError: # a transient fs error must not stop the heartbeat
|
| 115 |
+
pass
|
| 116 |
+
time.sleep(10)
|
| 117 |
+
|
| 118 |
+
t0 = time.time()
|
| 119 |
+
clip = nodes.CLIPLoader().load_clip(TE, type="minimax")[0]
|
| 120 |
+
vae = nodes.VAELoader().load_vae(VAE)[0]
|
| 121 |
+
audio_vae = nodes.VAELoader().load_vae(AUDIO_VAE)[0]
|
| 122 |
+
loaded = {}
|
| 123 |
+
|
| 124 |
+
def model(m):
|
| 125 |
+
# one transformer at a time (21 GB each): a mode switch drops the other one first
|
| 126 |
+
if m not in loaded:
|
| 127 |
+
loaded.clear()
|
| 128 |
+
mm.unload_all_models()
|
| 129 |
+
gc.collect()
|
| 130 |
+
mm.soft_empty_cache()
|
| 131 |
+
loaded[m] = nodes.UNETLoader().load_unet(UNET[m], "default")[0]
|
| 132 |
+
return loaded[m]
|
| 133 |
+
|
| 134 |
+
model("swap")
|
| 135 |
+
print(f"loaded in {time.time() - t0:.0f}s; max_frames {limit}", flush=True)
|
| 136 |
+
threading.Thread(target=beat_loop, daemon=True).start()
|
| 137 |
+
|
| 138 |
+
@torch.inference_mode() # as ComfyUI's executor runs every node; without it autograd keeps the VAE's activations
|
| 139 |
+
def render(job):
|
| 140 |
+
m, nf, tier, refs, seed = job["mode"], job["num_frames"], job["tier"], job["refs"], job["seed"]
|
| 141 |
+
assert m in MODES, f"mode must be one of {MODES}, got {m!r}"
|
| 142 |
+
assert nf in FRAMES, f"num_frames must be 17n+5 in 90..345, got {nf}"
|
| 143 |
+
assert tier in limit, f"tier must be one of {list(limit)}, got {tier}"
|
| 144 |
+
if nf > limit[tier]:
|
| 145 |
+
raise ValueError(f"num_frames {nf} exceeds this worker's max {limit[tier]} at tier {tier}")
|
| 146 |
+
assert len(refs) <= 4, f"the node pack takes at most 4 refs, got {len(refs)}"
|
| 147 |
+
pin = job.get("pin") or []
|
| 148 |
+
assert len(pin) in (0, len(refs)) and all(0 <= k < nf for k in pin), (pin, len(refs), nf)
|
| 149 |
+
|
| 150 |
+
source = InputImpl.VideoFromFile(job["cond"]).get_components().images
|
| 151 |
+
h, w = source.shape[1], source.shape[2]
|
| 152 |
+
assert h % 32 == 0 and w % 32 == 0 and h * w <= AREA[tier], \
|
| 153 |
+
f"cond must be multiples of 32 with at most {AREA[tier]} pixels at tier {tier}, got {w}x{h}"
|
| 154 |
+
# the soundtrack, as RemakeConditioning takes it (it pads and cuts to the render)
|
| 155 |
+
pcm = subprocess.run(["ffmpeg", "-loglevel", "error", "-i", job["audio"], "-t", f"{nf / 24:.3f}", "-vn",
|
| 156 |
+
"-f", "f32le", "-ac", "2", "-ar", str(R.AUDIO_SR), "-"],
|
| 157 |
+
capture_output=True, check=True).stdout
|
| 158 |
+
audio = {"waveform": torch.frombuffer(bytearray(pcm), dtype=torch.float32).view(-1, 2).T[None],
|
| 159 |
+
"sample_rate": R.AUDIO_SR}
|
| 160 |
+
ims = [torch.from_numpy(np.array(Image.open(x).convert("RGB"))).float()[None] / 255 for x in refs]
|
| 161 |
+
ims += [None] * (4 - len(ims))
|
| 162 |
+
# short_edge = the cond's own: it is already on its canvas, so nothing is resized
|
| 163 |
+
positive, latent = R.RemakeConditioning().run(clip, vae, audio_vae, source, job["prompt"], ims[0],
|
| 164 |
+
" ".join(map(str, pin)), nf, min(h, w), audio, *ims[1:])
|
| 165 |
+
if job.get("prefix"):
|
| 166 |
+
prev = InputImpl.VideoFromFile(job["prefix"]).get_components().images
|
| 167 |
+
latent = R.RemakePrefix().run(vae, latent, prev, job.get("prefix_frames", 17), job.get("prefix_at", -1))[0]
|
| 168 |
+
|
| 169 |
+
guider = Guider_Basic(R.RemakeKeyframeUntil().run(model(m), job.get("keyframe_until", 0.9))[0])
|
| 170 |
+
guider.set_conds(positive)
|
| 171 |
+
noise = R.RemakeNoise().run(positive, seed)[0]
|
| 172 |
+
samples = guider.sample(noise.generate_noise(latent), latent["samples"], comfy.samplers.sampler_object("euler"),
|
| 173 |
+
torch.tensor(SIGMAS[m]), denoise_mask=latent["noise_mask"], seed=seed)
|
| 174 |
+
out = {"samples": samples.to(mm.intermediate_device())}
|
| 175 |
+
video = InputImpl.VideoFromComponents(Types.VideoComponents(
|
| 176 |
+
images=nodes.VAEDecode().decode(vae, out)[0], audio=vae_decode_audio(audio_vae, out), frame_rate=Fraction(24)))
|
| 177 |
+
dst = Path(job["out"])
|
| 178 |
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
| 179 |
+
tmp = dst.with_name(f".{dst.stem}.{os.getpid()}.mp4")
|
| 180 |
+
video.save_to(str(tmp), format=Types.VideoContainer.MP4, codec=Types.VideoCodec.H264)
|
| 181 |
+
tmp.rename(dst)
|
| 182 |
+
|
| 183 |
+
def live_beats():
|
| 184 |
+
beats = []
|
| 185 |
+
for p in workers.glob("*.json"):
|
| 186 |
+
try:
|
| 187 |
+
hb = json.loads(p.read_text())
|
| 188 |
+
except (OSError, ValueError):
|
| 189 |
+
continue
|
| 190 |
+
if time.time() - hb["t"] < 120:
|
| 191 |
+
beats.append(hb)
|
| 192 |
+
return beats
|
| 193 |
+
|
| 194 |
+
def requeue_orphans(beats):
|
| 195 |
+
live = {hb["job"] for hb in beats}
|
| 196 |
+
for run in queue.glob("*.run"):
|
| 197 |
+
try:
|
| 198 |
+
if time.time() - run.stat().st_mtime > 120 and run.stem not in live:
|
| 199 |
+
run.rename(run.with_suffix(".json"))
|
| 200 |
+
print(f"{run.stem}: orphaned, requeued", flush=True)
|
| 201 |
+
except FileNotFoundError:
|
| 202 |
+
pass
|
| 203 |
+
|
| 204 |
+
def next_job(beats):
|
| 205 |
+
others = [hb for hb in beats if (hb["host"], hb["gpu"]) != (host, gpu)]
|
| 206 |
+
for p in sorted(queue.glob("*.json")):
|
| 207 |
+
try:
|
| 208 |
+
job = json.loads(p.read_text())
|
| 209 |
+
except (OSError, ValueError): # claimed meanwhile, or still being written
|
| 210 |
+
continue
|
| 211 |
+
if job.get("prefix") and not Path(job["prefix"]).exists():
|
| 212 |
+
continue
|
| 213 |
+
tier, nf = job.get("tier"), job.get("num_frames", 0)
|
| 214 |
+
if nf > limit.get(tier, 0) and any(
|
| 215 |
+
job.get("mode") in hb["modes"] and hb["max_frames"].get(str(tier), 0) >= nf for hb in others):
|
| 216 |
+
continue # too long for this card, not for another live worker: leave it to that one
|
| 217 |
+
run = p.with_suffix(".run")
|
| 218 |
+
try: # touch first: rename keeps the mtime, and a .run older than 120 s that no heartbeat names is requeued
|
| 219 |
+
os.utime(p)
|
| 220 |
+
p.rename(run)
|
| 221 |
+
except FileNotFoundError:
|
| 222 |
+
continue
|
| 223 |
+
return run
|
| 224 |
+
return None
|
| 225 |
+
|
| 226 |
+
while True:
|
| 227 |
+
beats = live_beats()
|
| 228 |
+
requeue_orphans(beats)
|
| 229 |
+
run = next_job(beats)
|
| 230 |
+
if run is None:
|
| 231 |
+
time.sleep(2)
|
| 232 |
+
continue
|
| 233 |
+
state["job"] = run.stem
|
| 234 |
+
beat()
|
| 235 |
+
try:
|
| 236 |
+
job = json.loads(run.read_text())
|
| 237 |
+
t = time.time()
|
| 238 |
+
render(job)
|
| 239 |
+
info = {"out": job["out"], "secs": round(time.time() - t, 1),
|
| 240 |
+
"peak_rss_gib": round(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 2**20, 1),
|
| 241 |
+
"host": host, "gpu": gpu, "mode": job["mode"], "sigmas": SIGMAS[job["mode"]]}
|
| 242 |
+
tmp = run.with_suffix(f".{os.getpid()}.tmp")
|
| 243 |
+
tmp.write_text(json.dumps(info))
|
| 244 |
+
tmp.rename(run.with_suffix(".done"))
|
| 245 |
+
print(f"{run.stem}: {info}", flush=True)
|
| 246 |
+
except Exception:
|
| 247 |
+
tb = traceback.format_exc()
|
| 248 |
+
run.with_suffix(".err").write_text(tb)
|
| 249 |
+
run.unlink(missing_ok=True)
|
| 250 |
+
print(f"{run.stem}: FAILED\n{tb}", flush=True)
|
| 251 |
+
# the context is gone after a CUDA error, and after an OOM freeing the exception's tensors aborts the
|
| 252 |
+
# process: exit before that happens (run_worker.sh restarts the worker)
|
| 253 |
+
if "CUDA error" in tb or "OutOfMemoryError" in tb:
|
| 254 |
+
os._exit(1)
|
| 255 |
+
run.unlink(missing_ok=True)
|
| 256 |
+
state["job"] = None
|
| 257 |
+
gc.collect()
|
| 258 |
+
mm.soft_empty_cache()
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
if __name__ == "__main__":
|
| 262 |
+
main_loop()
|
mcp/comfy_worker/run_worker.sh
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Viggle Remake worker in the foreground; restarted whenever it exits (it exits on purpose after a CUDA error or an OOM).
|
| 3 |
+
# bash run_worker.sh [worker flags] [-- ComfyUI flags] Ctrl-C stops it.
|
| 4 |
+
cd "$(dirname "$0")"
|
| 5 |
+
trap exit INT
|
| 6 |
+
ulimit -c 0 # a crash would otherwise dump a core of the whole address space (minutes, GBs)
|
| 7 |
+
while true; do
|
| 8 |
+
ComfyUI/venv/bin/python remake_worker.py "$@"
|
| 9 |
+
echo "worker exited ($?), restarting in 5 s"
|
| 10 |
+
sleep 5
|
| 11 |
+
done
|
mcp/comfy_worker/setup.sh
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# One-time install of the ComfyUI render worker, next to this script: ComfyUI 0.42.0 with torch 2.9.1 / CUDA 13.0, the
|
| 3 |
+
# repository's node pack linked in, the int8 transformers and the text encoder and VAEs. SKIP_MODELS=1 skips the weights.
|
| 4 |
+
set -e
|
| 5 |
+
cd "$(dirname "$0")"
|
| 6 |
+
REPO=$(realpath ../..)
|
| 7 |
+
export UV_HTTP_TIMEOUT=600
|
| 8 |
+
[ -d ComfyUI ] || git clone -q --depth 1 --branch v0.42.0 https://github.com/comfyanonymous/ComfyUI.git
|
| 9 |
+
[ -x ComfyUI/venv/bin/python ] || uv venv -q --python 3.12 ComfyUI/venv
|
| 10 |
+
uv pip install -q --python ComfyUI/venv/bin/python torch==2.9.1 torchvision==0.24.1 torchaudio==2.9.1 \
|
| 11 |
+
--index-url https://download.pytorch.org/whl/cu130
|
| 12 |
+
uv pip install -q --python ComfyUI/venv/bin/python -r ComfyUI/requirements.txt
|
| 13 |
+
M=(--include "comfyui/ComfyUI-Viggle-Remake/*")
|
| 14 |
+
[ -n "$SKIP_MODELS" ] || M+=(--include "int8/*")
|
| 15 |
+
hf download Viggle/Viggle-Remake "${M[@]}" --local-dir "$REPO"
|
| 16 |
+
ln -sfn "$REPO/comfyui/ComfyUI-Viggle-Remake" ComfyUI/custom_nodes/ComfyUI-Viggle-Remake
|
| 17 |
+
if [ -z "$SKIP_MODELS" ]; then
|
| 18 |
+
hf download Comfy-Org/MiniMax-H3 text_encoders/qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors \
|
| 19 |
+
vae/minimax_h3_video_vae_fp16.safetensors vae/minimax_h3_audio_vae_fp32.safetensors --local-dir ComfyUI/models
|
| 20 |
+
ln -sf "$REPO"/int8/*.safetensors ComfyUI/models/diffusion_models/
|
| 21 |
+
fi
|
| 22 |
+
echo "done. Start the worker with: bash $PWD/run_worker.sh"
|
mcp/viggle_remake_mcp/common.py
CHANGED
|
@@ -60,14 +60,14 @@ def frames(video, w, h):
|
|
| 60 |
def pick(video, fs, W, H):
|
| 61 |
"""Only frames `fs` of a video, decoded at W x H (a full decode of a 2-minute cond is 3.4 GB)."""
|
| 62 |
sel = "+".join(f"eq(n\\,{f})" for f in sorted(set(fs)))
|
| 63 |
-
raw = subprocess.run(["ffmpeg", "-v", "error", "-i", str(video), "-vf", f"select={sel},scale={W}:{H}", "-
|
| 64 |
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"], capture_output=True, check=True).stdout
|
| 65 |
return dict(zip(sorted(set(fs)), np.frombuffer(raw, np.uint8).reshape(-1, H, W, 3)))
|
| 66 |
|
| 67 |
|
| 68 |
def seg(video, a, b, W, H):
|
| 69 |
raw = subprocess.run(["ffmpeg", "-v", "error", "-i", str(video), "-vf", f"select=between(n\\,{a}\\,{b - 1}),scale={W}:{H}",
|
| 70 |
-
"-
|
| 71 |
return np.frombuffer(raw, np.uint8).reshape(-1, H, W, 3)
|
| 72 |
|
| 73 |
|
|
|
|
| 60 |
def pick(video, fs, W, H):
|
| 61 |
"""Only frames `fs` of a video, decoded at W x H (a full decode of a 2-minute cond is 3.4 GB)."""
|
| 62 |
sel = "+".join(f"eq(n\\,{f})" for f in sorted(set(fs)))
|
| 63 |
+
raw = subprocess.run(["ffmpeg", "-v", "error", "-i", str(video), "-vf", f"select={sel},scale={W}:{H}", "-fps_mode", "passthrough",
|
| 64 |
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"], capture_output=True, check=True).stdout
|
| 65 |
return dict(zip(sorted(set(fs)), np.frombuffer(raw, np.uint8).reshape(-1, H, W, 3)))
|
| 66 |
|
| 67 |
|
| 68 |
def seg(video, a, b, W, H):
|
| 69 |
raw = subprocess.run(["ffmpeg", "-v", "error", "-i", str(video), "-vf", f"select=between(n\\,{a}\\,{b - 1}),scale={W}:{H}",
|
| 70 |
+
"-fps_mode", "passthrough", "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], capture_output=True, check=True).stdout
|
| 71 |
return np.frombuffer(raw, np.uint8).reshape(-1, H, W, 3)
|
| 72 |
|
| 73 |
|
mcp/viggle_remake_mcp/kit.py
CHANGED
|
@@ -604,7 +604,7 @@ def unbar(d, v, canvas):
|
|
| 604 |
if not m:
|
| 605 |
continue
|
| 606 |
raw = subprocess.run(["ffmpeg", "-v", "error", "-i", str(cond(d, canvas)), "-vf", f"scale={W}:{H},crop={crop(m)},"
|
| 607 |
-
"scale=48:48,format=gray", "-
|
| 608 |
capture_output=True, check=True).stdout
|
| 609 |
dark = np.percentile(np.frombuffer(raw, np.uint8).reshape(-1, 48 * 48)[:n], 98, axis=1) < 14
|
| 610 |
for i in np.flatnonzero(dark):
|
|
|
|
| 604 |
if not m:
|
| 605 |
continue
|
| 606 |
raw = subprocess.run(["ffmpeg", "-v", "error", "-i", str(cond(d, canvas)), "-vf", f"scale={W}:{H},crop={crop(m)},"
|
| 607 |
+
"scale=48:48,format=gray", "-fps_mode", "passthrough", "-f", "rawvideo", "-pix_fmt", "gray", "-"],
|
| 608 |
capture_output=True, check=True).stdout
|
| 609 |
dark = np.percentile(np.frombuffer(raw, np.uint8).reshape(-1, 48 * 48)[:n], 98, axis=1) < 14
|
| 610 |
for i in np.flatnonzero(dark):
|