Compare commits

..
Author SHA1 Message Date
ausboss 3569a1a24e Load Audio: review fixes
- Only plain audio files are decoded; files that point the decoder at
  other files (playlists and the like) are refused.
- A wired Max length is previewed only when its value is known;
  otherwise the face says it is wired.
- Typing 0 for OUT keeps to the end of the file, as documented.
- Playback pauses in a hidden tab and LOOP restarts at the file's end.
- Help page and README no longer claim a late selection decodes fast.
2026-11-04 00:26:50 -06:00
ausboss 3b16d1e254 Load Audio: shorter wave, length moves off the wave next to OUT 2026-11-03 23:09:43 +00:00
ausboss dff58e6b88 Load Audio: open smaller and go down to 280 wide 2026-11-03 22:57:38 +00:00
ausboss ef06093d56 Merge remote-tracking branch 'origin/main' into feat/load-audio-trim
# Conflicts:
#	CHANGELOG.md
2026-11-03 22:40:25 +00:00
ausboss 8582a0219d Add Load Audio node: wave, trim, preview and max length
Load a sound file and keep only the part you want. The node shows the
whole file as a wave. Drag the IN and OUT handles (or type the times),
press play to hear just that part, and set Max length to the longest
sound the next step takes: anything past it turns red and is cut off,
so you see what comes out before you run. Max length can be wired.

Only the picked part is decoded. Outputs the audio and its seconds.
Help page, README, changelog, Chinese names, tests and the API snapshot
are included.
2026-11-03 20:51:32 +00:00
17 changed files with 145 additions and 251 deletions
+1 -1
View File
@@ -166,7 +166,7 @@ It returns the frames, audio, frame count, fps, width, height, duration and a co
Upload a sound file, or pick one from the Source list, and the node shows it as a wave. Drag the IN and OUT handles to keep only the part you want, or type the times. Press play to hear just that part. Set **Max length** to the longest sound the next step can take, such as a video's length: anything past it turns red and is cut off, so you see what comes out before you run. Click the red length next to OUT to move OUT to the cut.
It returns the `audio` you kept and its length in `seconds`. Only that part is decoded. [Help page](js/docs/AUSBOSS_NODES_LoadAudio.md).
It returns the `audio` you kept and its length in `seconds`. Decoding reads from the beginning to the cut, so later selections take longer to load. [Help page](js/docs/AUSBOSS_NODES_LoadAudio.md).
### Video Crop + Rotate + Pad → Frame
@@ -1913,7 +1913,7 @@
"The painted area, grown a little, is filled with flat gray, so Krea 2 knows where to paint. The plain crop goes along too."
],
"widgets_values_named": {
"text": "The painted area, grown a little, is filled with flat gray, so Krea 2 knows where to paint. The plain crop goes along too."
"text": "The painted area, grown a little, is filled with flat gray. Krea 2 never sees what you are replacing."
},
"color": "#1f2c38",
"bgcolor": "#2a3b4a"
@@ -3052,7 +3052,7 @@
"The gray-filled crop is the reference. Painting starts from the plain crop, inside the noise mask: the painted area, widened by 24 px."
],
"widgets_values_named": {
"text": "The gray-filled crop is the reference. Painting starts from the plain crop, inside the noise mask: the painted area, widened by 24 px."
"text": "The gray-filled crop is the reference. The noise mask is the grown painted area, widened by 24 px. Krea 2 paints only inside it."
},
"color": "#1f2c38",
"bgcolor": "#2a3b4a"
@@ -2139,10 +2139,10 @@
"title": "Note",
"properties": {},
"widgets_values": [
"Blend in fades the model's picture into yours just inside its edge. Tone match is fully on, so the new area is adjusted to your picture's tone."
"Blend in fades the model's picture into yours just inside its edge, with no tone shift. The new area stays as painted."
],
"widgets_values_named": {
"text": "Blend in fades the model's picture into yours just inside its edge. Tone match is fully on, so the new area is adjusted to your picture's tone."
"text": "Blend in fades the model's picture into yours just inside its edge, with no tone shift. The new area stays as painted."
},
"color": "#1f2c38",
"bgcolor": "#2a3b4a"
@@ -37,10 +37,10 @@
"Node name for S&R": "AUSBOSS_NODES_WorkflowNote"
},
"widgets_values": [
"{\"v\": 1, \"title\": \"Qwen Image 2.1 Inpaint\", \"subtitle\": \"Click a thing in your picture and say what to change. Only that part is repainted.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"1. Load your image. You can crop it right there with the blue handles.\\n2. On **Mask by Touch**, click the thing you want to change. It lights up before you click.\\n3. In **What to change**, say it the way you would ask a person: \\\"Replace the cup with a glass of orange juice.\\\"\\n4. Press **Run**.\\n\\nOnly the picked part is repainted. Everything outside it stays your own picture.\\n\\n### Tips\\n- Wrong thing picked? The arrows under the picture swap it for the next thing it could mean.\\n- Name everything that should go: \\\"the iced coffee and its straw\\\". What you do not name can stay.\\n- To remove something, pick it and write \\\"Remove the cup.\\\" Pick its shadow too, or the shadow stays.\\n- To add something, switch to **Paint** and paint where it should go, with some room. Say how big: \\\"Add small black sunglasses on her eyes.\\\"\\n- Not what you wanted? Change **Seed** and run it again. It stays fixed between runs.\\n\\n### Switches\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA from the list below. Off: 25 steps without it, about twice as long.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.12.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"sam3.1_multiplex_fp16.safetensors\", \"dir\": \"checkpoints\", \"size\": \"1.75 GB\", \"url\": \"https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors\", \"note\": \"by Meta, packaged by Comfy-Org\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"optional, for Fast · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_MaskByTouch\"}], \"links\": [{\"label\": \"YouTube · this workflow\", \"url\": \"https://youtu.be/iMgaiXoB0v4\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
"{\"v\": 1, \"title\": \"Qwen Image 2.1 Inpaint\", \"subtitle\": \"Click a thing in your picture and say what to change. Only that part is repainted.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"1. Load your image. You can crop it right there with the blue handles.\\n2. On **Mask by Touch**, click the thing you want to change. It lights up before you click.\\n3. In **What to change**, say it the way you would ask a person: \\\"Replace the cup with a glass of orange juice.\\\"\\n4. Press **Run**.\\n\\nOnly the picked part is repainted. Everything outside it stays your own picture.\\n\\n### Tips\\n- Wrong thing picked? The arrows under the picture swap it for the next thing it could mean.\\n- Name everything that should go: \\\"the iced coffee and its straw\\\". What you do not name can stay.\\n- To remove something, pick it and write \\\"Remove the cup.\\\" Pick its shadow too, or the shadow stays.\\n- To add something, switch to **Paint** and paint where it should go, with some room. Say how big: \\\"Add small black sunglasses on her eyes.\\\"\\n- Not what you wanted? Run it again. The seed changes each run.\\n\\n### Switches\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA from the list below. Off: 25 steps without it, about twice as long.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.12.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"sam3.1_multiplex_fp16.safetensors\", \"dir\": \"checkpoints\", \"size\": \"1.75 GB\", \"url\": \"https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors\", \"note\": \"by Meta, packaged by Comfy-Org\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"optional, for Fast · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_MaskByTouch\"}], \"links\": [{\"label\": \"YouTube · this workflow\", \"url\": \"https://youtu.be/iMgaiXoB0v4\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
],
"widgets_values_named": {
"note": "{\"v\": 1, \"title\": \"Qwen Image 2.1 Inpaint\", \"subtitle\": \"Click a thing in your picture and say what to change. Only that part is repainted.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"1. Load your image. You can crop it right there with the blue handles.\\n2. On **Mask by Touch**, click the thing you want to change. It lights up before you click.\\n3. In **What to change**, say it the way you would ask a person: \\\"Replace the cup with a glass of orange juice.\\\"\\n4. Press **Run**.\\n\\nOnly the picked part is repainted. Everything outside it stays your own picture.\\n\\n### Tips\\n- Wrong thing picked? The arrows under the picture swap it for the next thing it could mean.\\n- Name everything that should go: \\\"the iced coffee and its straw\\\". What you do not name can stay.\\n- To remove something, pick it and write \\\"Remove the cup.\\\" Pick its shadow too, or the shadow stays.\\n- To add something, switch to **Paint** and paint where it should go, with some room. Say how big: \\\"Add small black sunglasses on her eyes.\\\"\\n- Not what you wanted? Change **Seed** and run it again. It stays fixed between runs.\\n\\n### Switches\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA from the list below. Off: 25 steps without it, about twice as long.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.12.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"sam3.1_multiplex_fp16.safetensors\", \"dir\": \"checkpoints\", \"size\": \"1.75 GB\", \"url\": \"https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors\", \"note\": \"by Meta, packaged by Comfy-Org\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"optional, for Fast · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_MaskByTouch\"}], \"links\": [{\"label\": \"YouTube · this workflow\", \"url\": \"https://youtu.be/iMgaiXoB0v4\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
"note": "{\"v\": 1, \"title\": \"Qwen Image 2.1 Inpaint\", \"subtitle\": \"Click a thing in your picture and say what to change. Only that part is repainted.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"1. Load your image. You can crop it right there with the blue handles.\\n2. On **Mask by Touch**, click the thing you want to change. It lights up before you click.\\n3. In **What to change**, say it the way you would ask a person: \\\"Replace the cup with a glass of orange juice.\\\"\\n4. Press **Run**.\\n\\nOnly the picked part is repainted. Everything outside it stays your own picture.\\n\\n### Tips\\n- Wrong thing picked? The arrows under the picture swap it for the next thing it could mean.\\n- Name everything that should go: \\\"the iced coffee and its straw\\\". What you do not name can stay.\\n- To remove something, pick it and write \\\"Remove the cup.\\\" Pick its shadow too, or the shadow stays.\\n- To add something, switch to **Paint** and paint where it should go, with some room. Say how big: \\\"Add small black sunglasses on her eyes.\\\"\\n- Not what you wanted? Run it again. The seed changes each run.\\n\\n### Switches\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA from the list below. Off: 25 steps without it, about twice as long.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.12.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"sam3.1_multiplex_fp16.safetensors\", \"dir\": \"checkpoints\", \"size\": \"1.75 GB\", \"url\": \"https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors\", \"note\": \"by Meta, packaged by Comfy-Org\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"optional, for Fast · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_MaskByTouch\"}], \"links\": [{\"label\": \"YouTube · this workflow\", \"url\": \"https://youtu.be/iMgaiXoB0v4\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
},
"color": "#14424d",
"bgcolor": "#161f21"
@@ -37,10 +37,10 @@
"ver": "2.9.0"
},
"widgets_values": [
"{\"v\": 1, \"title\": \"Qwen Image 2.1 Upscale + Restore\", \"subtitle\": \"Load a small, soft or worn picture. Qwen Image 2.1 redraws it sharp at the megapixels you set, in its own colors.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"[▶ AusBoss on YouTube](https://www.youtube.com/@ausbossdev) · videos on these nodes and workflows\\n\\n---\\n\\n1. Load your image. You can crop it right there with the blue handles.\\n2. Set **Megapixels**: the size of the result.\\n3. Press **Run**.\\n4. Slide **Before and after** to compare. The result is saved as a PNG.\\n\\n### What you get\\n\\n- Your picture redrawn sharp, at the size you asked for.\\n- Its own colors, also on a noisy or faded photo.\\n\\n### Megapixels\\n\\n- Up to about 2: one pass.\\n- Over that: redrawn in tiles of about 2 megapixels and joined. 4 takes two tiles, 8 takes four.\\n- The redraw limit for a small picture is 8 times its own megapixels or one roughly 2 megapixel tile, whichever is larger. The rest is enlarged the plain way.\\n- A picture of 1 megapixel or more only looks better when it grows about 1.6 times per side.\\n\\n### Switches\\n\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA. Off: 25 steps without it, about three times as long.\\n\\n### Tips\\n\\n- It is a redraw. Fine detail is made up to fit, so a face stays the same person, not the same pixels.\\n- Small lettering on signs and labels can come back misspelled.\\n- Each tile is matched to your picture's colors. To let Qwen's own colors through, lower **Keep colors** inside the box.\\n- If the run stops at a red **LoRA Loader**, a LoRA that is switched on is missing. Download it from the list below.\\n- Not tested on blur from a moving camera or missed focus.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.9.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"qwen-image-2.1-restore.safetensors\", \"dir\": \"loras\", \"size\": \"159 MB\", \"url\": \"https://huggingface.co/ausboss/Qwen-Image-2.1-Restore-LoRA/resolve/main/qwen-image-2.1-restore.safetensors\", \"note\": \"keeps the result close to your picture\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"for Fast on · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_TiledUpscale\"}], \"links\": [{\"label\": \"YouTube · ausbossdev\", \"url\": \"https://www.youtube.com/@ausbossdev\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
"{\"v\": 1, \"title\": \"Qwen Image 2.1 Upscale + Restore\", \"subtitle\": \"Load a small, soft or worn picture. Qwen Image 2.1 redraws it sharp at the megapixels you set, in its own colors.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"[▶ AusBoss on YouTube](https://www.youtube.com/@ausbossdev) · videos on these nodes and workflows\\n\\n---\\n\\n1. Load your image. You can crop it right there with the blue handles.\\n2. Set **Megapixels**: the size of the result.\\n3. Press **Run**.\\n4. Slide **Before and after** to compare. The result is saved as a PNG.\\n\\n### What you get\\n\\n- Your picture redrawn sharp, at the size you asked for.\\n- Its own colors, also on a noisy or faded photo.\\n\\n### Megapixels\\n\\n- Up to about 2: one pass.\\n- Over that: redrawn in tiles of about 2 megapixels and joined. 4 takes two tiles, 8 takes four.\\n- A small picture is redrawn at up to 8 times its own megapixels, then enlarged the plain way.\\n- A picture of 1 megapixel or more only looks better when it grows about 1.6 times per side.\\n\\n### Switches\\n\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA. Off: 25 steps without it, about three times as long.\\n\\n### Tips\\n\\n- It is a redraw. Fine detail is made up to fit, so a face stays the same person, not the same pixels.\\n- Small lettering on signs and labels can come back misspelled.\\n- Each tile is matched to your picture's colors. To let Qwen's own colors through, lower **Keep colors** inside the box.\\n- If the run stops at a red **LoRA Loader**, a LoRA that is switched on is missing. Download it from the list below.\\n- Not tested on blur from a moving camera or missed focus.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.9.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"qwen-image-2.1-restore.safetensors\", \"dir\": \"loras\", \"size\": \"159 MB\", \"url\": \"https://huggingface.co/ausboss/Qwen-Image-2.1-Restore-LoRA/resolve/main/qwen-image-2.1-restore.safetensors\", \"note\": \"keeps the result close to your picture\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"for Fast on · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_TiledUpscale\"}], \"links\": [{\"label\": \"YouTube · ausbossdev\", \"url\": \"https://www.youtube.com/@ausbossdev\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
],
"widgets_values_named": {
"note": "{\"v\": 1, \"title\": \"Qwen Image 2.1 Upscale + Restore\", \"subtitle\": \"Load a small, soft or worn picture. Qwen Image 2.1 redraws it sharp at the megapixels you set, in its own colors.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"[▶ AusBoss on YouTube](https://www.youtube.com/@ausbossdev) · videos on these nodes and workflows\\n\\n---\\n\\n1. Load your image. You can crop it right there with the blue handles.\\n2. Set **Megapixels**: the size of the result.\\n3. Press **Run**.\\n4. Slide **Before and after** to compare. The result is saved as a PNG.\\n\\n### What you get\\n\\n- Your picture redrawn sharp, at the size you asked for.\\n- Its own colors, also on a noisy or faded photo.\\n\\n### Megapixels\\n\\n- Up to about 2: one pass.\\n- Over that: redrawn in tiles of about 2 megapixels and joined. 4 takes two tiles, 8 takes four.\\n- The redraw limit for a small picture is 8 times its own megapixels or one roughly 2 megapixel tile, whichever is larger. The rest is enlarged the plain way.\\n- A picture of 1 megapixel or more only looks better when it grows about 1.6 times per side.\\n\\n### Switches\\n\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA. Off: 25 steps without it, about three times as long.\\n\\n### Tips\\n\\n- It is a redraw. Fine detail is made up to fit, so a face stays the same person, not the same pixels.\\n- Small lettering on signs and labels can come back misspelled.\\n- Each tile is matched to your picture's colors. To let Qwen's own colors through, lower **Keep colors** inside the box.\\n- If the run stops at a red **LoRA Loader**, a LoRA that is switched on is missing. Download it from the list below.\\n- Not tested on blur from a moving camera or missed focus.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.9.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"qwen-image-2.1-restore.safetensors\", \"dir\": \"loras\", \"size\": \"159 MB\", \"url\": \"https://huggingface.co/ausboss/Qwen-Image-2.1-Restore-LoRA/resolve/main/qwen-image-2.1-restore.safetensors\", \"note\": \"keeps the result close to your picture\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"for Fast on · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_TiledUpscale\"}], \"links\": [{\"label\": \"YouTube · ausbossdev\", \"url\": \"https://www.youtube.com/@ausbossdev\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
"note": "{\"v\": 1, \"title\": \"Qwen Image 2.1 Upscale + Restore\", \"subtitle\": \"Load a small, soft or worn picture. Qwen Image 2.1 redraws it sharp at the megapixels you set, in its own colors.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"[▶ AusBoss on YouTube](https://www.youtube.com/@ausbossdev) · videos on these nodes and workflows\\n\\n---\\n\\n1. Load your image. You can crop it right there with the blue handles.\\n2. Set **Megapixels**: the size of the result.\\n3. Press **Run**.\\n4. Slide **Before and after** to compare. The result is saved as a PNG.\\n\\n### What you get\\n\\n- Your picture redrawn sharp, at the size you asked for.\\n- Its own colors, also on a noisy or faded photo.\\n\\n### Megapixels\\n\\n- Up to about 2: one pass.\\n- Over that: redrawn in tiles of about 2 megapixels and joined. 4 takes two tiles, 8 takes four.\\n- A small picture is redrawn at up to 8 times its own megapixels, then enlarged the plain way.\\n- A picture of 1 megapixel or more only looks better when it grows about 1.6 times per side.\\n\\n### Switches\\n\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA. Off: 25 steps without it, about three times as long.\\n\\n### Tips\\n\\n- It is a redraw. Fine detail is made up to fit, so a face stays the same person, not the same pixels.\\n- Small lettering on signs and labels can come back misspelled.\\n- Each tile is matched to your picture's colors. To let Qwen's own colors through, lower **Keep colors** inside the box.\\n- If the run stops at a red **LoRA Loader**, a LoRA that is switched on is missing. Download it from the list below.\\n- Not tested on blur from a moving camera or missed focus.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.9.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"qwen-image-2.1-restore.safetensors\", \"dir\": \"loras\", \"size\": \"159 MB\", \"url\": \"https://huggingface.co/ausboss/Qwen-Image-2.1-Restore-LoRA/resolve/main/qwen-image-2.1-restore.safetensors\", \"note\": \"keeps the result close to your picture\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"for Fast on · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_TiledUpscale\"}], \"links\": [{\"label\": \"YouTube · ausbossdev\", \"url\": \"https://www.youtube.com/@ausbossdev\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
},
"color": "#14424d",
"bgcolor": "#161f21"
+4 -2
View File
@@ -44,8 +44,10 @@ Float 🆎, the wave shows the cut for that number; otherwise the length's toolt
## Technical details
- Only the picked part is decoded: decoding stops once the window is full,
so a short pick from a long recording stays quick.
- Only the picked part is returned. Decoding starts at the beginning,
discards sound before IN and stops at OUT or the Max length cut. A late
selection in a long recording takes longer to load.
- Preview playback pauses when you switch tabs or hide the page.
- The sound keeps its own sample rate and channels.
- A start past the end of the file stops the run with a message that says so.
- The wave is drawn in the browser from the file itself; nothing extra is
-9
View File
@@ -27,15 +27,6 @@ needs.
- The line underneath shows the last run's seed. The history saves with the
workflow.
## Compact look
The **gear** beside the number picks the look. **Compact** is one short
row: the number, a mode chip and a dice. Tap the chip to go Random → Fixed →
+1 → −1. The dice rolls a new seed and pins it. A long seed shrinks to fit.
Hover the number to see the last run's seed. The look is saved on the node,
and new Seed nodes start in the look you picked last. **Full** brings back
the whole card, with the seeds it ran with.
## Output
- **seed**: The seed, unchanged.
+15
View File
@@ -1,6 +1,21 @@
// Load Audio's window rules for the face, mirroring audio_window in
// nodes/_audio_trim_helpers.py so the wave shows exactly what one Run hands
// on. Pure: tests/load_audio_window.test.mjs runs it without a browser.
import { inputNumber } from "../shared/clip_rate.mjs";
import { dragTrimHandle } from "../load_video/trim_preview.mjs";
// Calculated outputs stay unknown until execution, even if the source has numbers.
export function wiredMax(node) {
const input = node.inputs?.find((slot) => slot.name === "max_seconds" || slot.widget?.name === "max_seconds");
if (input?.link == null) return { wired: false, value: null };
return { wired: true, value: inputNumber(node, "max_seconds", null) };
}
export function typedTrimBounds(duration, bounds, edge, seconds) {
// OUT 0 is the saved widget's "to the end", before handle clamping.
if (edge === "end" && seconds === 0) return { start: bounds.start, end: duration };
return dragTrimHandle(duration, bounds, edge, seconds / duration);
}
function finite(value, fallback = 0) {
const number = Number(value);
+14 -14
View File
@@ -16,7 +16,7 @@ import { parseImageReference } from "../shared/pad_canvas.mjs";
import { mediaViewQuery, splitMediaName } from "../shared/video_preview.mjs";
import { ensureVideoCss, hideCanvasWidget, makeToolButton } from "../shared/video_ui.mjs";
import { MIN_TRIM_SECONDS, dragTrimHandle, slideTrimWindow } from "../load_video/trim_preview.mjs";
import { grabZone, outputSummary, outputWindow, secondsLabel, wavePeaks } from "./audio_window.mjs";
import { grabZone, outputSummary, outputWindow, secondsLabel, typedTrimBounds, wavePeaks, wiredMax } from "./audio_window.mjs";
const NODE_NAME = "AUSBOSS_NODES_LoadAudio";
const PANEL_WIDGET = "ausboss_load_audio_wave";
@@ -50,18 +50,6 @@ function hideCoreAudio(node) {
}
}
// A wired Max length: the number the wire carries, when the far end is a
// node that shows it (Float, Integer, a Primitive). null when it can't be known.
function wiredMax(node) {
const input = node.inputs?.find((slot) => slot?.widget?.name === "max_seconds");
if (!input || input.link == null) return { wired: false, value: null };
const graph = node.graph;
const link = graph?.getLink?.(input.link) ?? graph?.links?.get?.(input.link) ?? graph?.links?.[input.link];
const origin = link ? graph?.getNodeById?.(link.origin_id) : null;
const shown = origin?.widgets?.find((widget) => typeof widget?.value === "number");
return { wired: true, value: shown ? Number(shown.value) : null };
}
function maxSeconds(state) {
const wired = wiredMax(state.node);
if (wired.wired) return { value: wired.value ?? 0, fromLink: true, known: wired.value != null };
@@ -317,12 +305,24 @@ function installPlayer(state) {
state.raf = requestAnimationFrame(tick);
};
audio.addEventListener("play", () => {
// Hidden tabs suspend animation frames, so never keep playing there.
if (document.hidden) { audio.pause(); return; }
state.playButton.textContent = "❚❚";
state.playButton.classList.add("active");
if (state.raf) cancelAnimationFrame(state.raf);
state.raf = requestAnimationFrame(tick);
}, { signal: state.abort.signal });
audio.addEventListener("pause", stop, { signal: state.abort.signal });
document.addEventListener("visibilitychange", () => {
if (document.hidden) audio.pause();
}, { signal: state.abort.signal });
audio.addEventListener("ended", () => {
const win = currentWindow(state);
if (!document.hidden && loopEnabled(state.node) && win.length > 0) {
audio.currentTime = win.start;
audio.play().catch(() => stop());
}
}, { signal: state.abort.signal });
state.playButton.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
@@ -343,7 +343,7 @@ function commitTimeInput(state, edge, input) {
input.value = formatTimecode(edge === "start" ? win.start : win.end);
return;
}
writeTrim(state, dragTrimHandle(state.duration, { start: win.start, end: win.end }, edge, seconds / state.duration, MIN_TRIM_SECONDS));
writeTrim(state, typedTrimBounds(state.duration, { start: win.start, end: win.end }, edge, seconds));
notifyAusbossChange();
input.blur();
}
+12 -123
View File
@@ -19,8 +19,6 @@ import { copyToClipboard } from "../shared/clipboard.mjs";
import { hideInputsInDef, hideWidget } from "../shared/widget_visibility.mjs";
import { holdNodeMinWidth, pinVuePanelHeight } from "../shared/panel_layout.mjs";
import { formatSeed, pushSeed, seedFromExecuted } from "../shared/seed_history.mjs";
import { SEED_LOOK_MENU, SEED_LOOK_SCOPE, controlChip, nextControl, seedLook, setSeedLook } from "../shared/seed_look.mjs";
import { gearIconSvg, loadSettings, openSettingsMenu } from "../shared/settings_menu.mjs";
const NODE_CLASS = "AUSBOSS_NODES_Seed";
const CSS_ID = "ausboss-seed-css";
@@ -30,19 +28,8 @@ const PROPERTY = "ausboss_seed_history"; // [seed, newest first]
// in a double; a fresh seed rolled here obeys the same ceiling.
const ROLL_MAX = 0x4000000000000;
const CARD_HEIGHT = 158;
// The compact look is one row: 8px card padding, a 32px row, 8px padding.
const COMPACT_CARD_HEIGHT = 50;
const PANEL_INSETS = 20; // the frontend's wrapper insets
const PANEL_DECLARED_HEIGHT = CARD_HEIGHT + 20; // + the frontend's wrapper insets
const NODE_WIDTH = 300;
const COMPACT_NODE_WIDTH = 290;
function panelHeight(node) {
return (seedLook(node) === "compact" ? COMPACT_CARD_HEIGHT : CARD_HEIGHT) + PANEL_INSETS;
}
function nodeWidthFloor(node) {
return seedLook(node) === "compact" ? COMPACT_NODE_WIDTH : NODE_WIDTH;
}
let openMenu = null;
@@ -72,21 +59,6 @@ function ensureCss() {
.ausboss-seed-btn:disabled{opacity:.4;cursor:default;pointer-events:none;}
.ausboss-seed-hint{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:#6f8886;font-size:10.5px;text-align:center;}
.ausboss-seed-hint b{color:#9fe3dc;font-weight:600;font-family:ui-monospace,"SF Mono",Menlo,Consolas,monospace;}
.ausboss-seed-top{display:flex;align-items:center;gap:6px;min-width:0;}
.ausboss-seed-top .ausboss-seed-field{flex:1 1 auto;min-width:0;}
.ausboss-seed-gear{display:grid;place-items:center;}
.ausboss-seed-chip,.ausboss-seed-dice{display:none;flex:none;height:32px;border:1px solid #2a3437;border-radius:7px;background:#0f1516;color:#d8ecea;font:600 11.5px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;cursor:pointer;pointer-events:auto;}
.ausboss-seed-chip{min-width:56px;padding:0 7px;}
.ausboss-seed-chip.random{border-color:rgba(0,180,170,.55);color:${BRAND};}
.ausboss-seed-dice{width:32px;font-size:15px;}
.ausboss-seed-chip:hover,.ausboss-seed-dice:hover{border-color:${BRAND};color:#fff;}
.ausboss-seed-card.compact{gap:0;}
.ausboss-seed-card.compact .ausboss-seed-seg,.ausboss-seed-card.compact .ausboss-seed-row,.ausboss-seed-card.compact .ausboss-seed-hint{display:none;}
.ausboss-seed-card.compact .ausboss-seed-chip,.ausboss-seed-card.compact .ausboss-seed-dice{display:block;}
.ausboss-seed-card.compact .ausboss-seed-field{height:32px;padding:0 3px 0 9px;}
.ausboss-seed-card.compact .ausboss-seed-field input{font-size:14px;}
.ausboss-seed-card.compact .ausboss-seed-tag,.ausboss-seed-card.compact .ausboss-seed-icon:not(.ausboss-seed-gear){display:none;}
.ausboss-seed-card.compact .ausboss-seed-field input{font-size:13.5px;letter-spacing:0;}
.ausboss-seed-menu{position:fixed;z-index:10000;min-width:230px;padding:4px;border:1px solid #3a4047;border-radius:7px;background:#1c1f23;box-shadow:0 8px 28px rgba(0,0,0,.5);font:12px/1.3 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#d7dde2;}
.ausboss-seed-menu-head{padding:5px 8px 6px;border-bottom:1px solid #2c3238;color:#78908e;font:10px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:.08em;text-transform:uppercase;}
.ausboss-seed-menu-item{display:block;width:100%;padding:6px 8px;border:none;border-radius:5px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;}
@@ -152,7 +124,6 @@ function render(state) {
const linked = seedLinked(node);
const value = seedWidget(node)?.value;
if (document.activeElement !== state.input) state.input.value = formatSeed(Number(value));
fitCompactNumber(state);
state.input.disabled = linked;
state.field.classList.toggle("linked", linked);
state.field.title = linked ? "The seed comes from its input link" : "Click to type a seed";
@@ -165,11 +136,6 @@ function render(state) {
state.segStep.title = stepping
? "Click again to step the other way"
: "Increment the seed after each queue (click again for decrement)";
const chip = controlChip(control);
state.chip.textContent = chip.text;
state.chip.title = chip.title;
state.chip.classList.toggle("random", control === "randomize");
state.card.classList.toggle("compact", seedLook(node) === "compact");
state.useLast.disabled = !seeds.length;
state.more.disabled = seeds.length < 2;
state.hint.textContent = "";
@@ -179,8 +145,6 @@ function render(state) {
b.textContent = formatSeed(last);
state.hint.append(b);
if (control === "randomize") state.hint.append(" · rolls a new seed each queue");
// The compact look has no hint line: the last run shows on the number.
if (seedLook(node) === "compact") state.field.title = `${state.field.title}. Last run: ${formatSeed(last)}`;
} else {
state.hint.textContent = control === "randomize"
? "rolls a new seed each queue"
@@ -188,16 +152,6 @@ function render(state) {
}
}
// The compact row is narrow, so a long seed gets smaller digits instead of
// being cut off: monospace digits are about 0.6 of the font size wide.
function fitCompactNumber(state) {
const { input } = state;
if (seedLook(state.node) !== "compact") { input.style.fontSize = ""; return; }
const digits = Math.max(1, String(input.value || "").length);
const room = input.clientWidth || 100;
input.style.fontSize = `${Math.max(8, Math.min(13.5, room / (0.64 * digits))).toFixed(1)}px`;
}
function showMenu(state) {
closeMenu();
const seeds = history(state.node);
@@ -287,9 +241,7 @@ function buildPanel(node) {
input.inputMode = "numeric";
input.spellcheck = false;
const copy = button("ausboss-seed-icon", "⧉", "Copy the seed");
const gear = button("ausboss-seed-icon ausboss-seed-gear", "", "Seed settings: full or compact look");
gear.innerHTML = gearIconSvg(12);
field.append(tag, input, copy, gear);
field.append(tag, input, copy);
field.addEventListener("pointerdown", (event) => event.stopPropagation());
const seg = el("div", "ausboss-seed-seg");
@@ -305,16 +257,11 @@ function buildPanel(node) {
row.append(newSeed, useLast, more);
const hint = el("div", "ausboss-seed-hint");
// Compact look: the mode chip and a dice sit on the number's row.
const chip = button("ausboss-seed-chip", "Fixed");
const dice = button("ausboss-seed-dice", "🎲", "Roll a fresh seed and pin it (Fixed)");
const top = el("div", "ausboss-seed-top");
top.append(field, chip, dice);
card.append(top, seg, row, hint);
card.append(field, seg, row, hint);
root.append(card);
const state = (node.__ausbossSeed = {
node, root, card, field, tag, input, copy, gear, chip, dice, segRandom, segFixed, segStep, newSeed, useLast, more, hint,
node, root, field, tag, input, copy, segRandom, segFixed, segStep, newSeed, useLast, more, hint,
});
const commitTyped = () => {
@@ -357,31 +304,6 @@ function buildPanel(node) {
notifyAusbossChange();
});
newSeed.addEventListener("click", () => { setSeed(node, rollSeed()); render(state); });
dice.addEventListener("click", () => { setSeed(node, rollSeed()); render(state); });
// The chip walks the same modes as the full card's buttons, and starts a
// new run the same way: into Random rolls now, Fixed into +1 steps now.
chip.addEventListener("click", () => {
const current = controlWidget(node)?.value ?? "fixed";
const next = nextControl(current);
setControl(node, next);
if (!seedLinked(node)) {
const value = Number(seed.value) || 0;
if (next === "randomize") setSeed(node, rollSeed(), { pin: false });
else if (current === "fixed" && next === "increment") setSeed(node, value + 1, { pin: false });
}
render(state);
notifyAusbossChange();
});
gear.addEventListener("click", () => {
openSettingsMenu({
scope: SEED_LOOK_SCOPE,
schema: SEED_LOOK_MENU,
anchor: gear.getBoundingClientRect(),
title: "Seed settings",
initial: { look: seedLook(node) },
onChange: (values) => applyLook(state, values.look),
});
});
useLast.addEventListener("click", () => {
const seeds = history(node);
if (seeds.length) { setSeed(node, seeds[0]); render(state); }
@@ -396,7 +318,7 @@ function buildPanel(node) {
const widget = node.addDOMWidget(WIDGET_NAME, "ausboss_seed", root, {
serialize: false,
hideOnZoom: false,
getMinHeight: () => panelHeight(node),
getMinHeight: () => PANEL_DECLARED_HEIGHT,
});
keepDomWidgetWidthAuto(widget);
// Not saved with the workflow either: options.serialize only keeps it out
@@ -405,20 +327,18 @@ function buildPanel(node) {
// A constant-height card: pinned on purpose (see
// tests/panel_guards.test.mjs, fixedByDesign).
widget.computeSize = (width) => [
Math.max(nodeWidthFloor(node), Number(width || node.size?.[0] || nodeWidthFloor(node))),
panelHeight(node),
Math.max(NODE_WIDTH, Number(width || node.size?.[0] || NODE_WIDTH)),
PANEL_DECLARED_HEIGHT,
];
// Pinned in Nodes 2.0 as well, where a node can be made taller than its card.
pinVuePanelHeight(widget);
widget.options.minNodeSize = [nodeWidthFloor(node), 60];
widget.options.minNodeSize = [NODE_WIDTH, 120];
// The frontend skips a pinned card's width, so its floor is held here.
holdNodeMinWidth(widget, nodeWidthFloor(node));
state.widget = widget;
holdNodeMinWidth(widget, NODE_WIDTH);
new ResizeObserver(() => fitCompactNumber(state)).observe(input);
render(state);
node.setSize?.([
Math.max(node.size?.[0] ?? 0, nodeWidthFloor(node)),
Math.max(node.size?.[0] ?? 0, NODE_WIDTH),
node.computeSize?.()[1] ?? node.size?.[1],
]);
// The card is a fixed-height row: width may grow, but a corner drag
@@ -436,49 +356,18 @@ function buildPanel(node) {
return state;
}
// Switch looks: the card, the node's floor and its height follow. Going
// compact also makes the node as narrow as the compact row allows, and
// going full widens it back to the full card's floor.
function applyLook(state, look) {
const { node } = state;
const before = seedLook(node);
const after = setSeedLook(node, look);
if (before === after) return;
render(state);
if (state.widget) {
state.widget.options.minNodeSize = [nodeWidthFloor(node), 60];
holdNodeMinWidth(state.widget, nodeWidthFloor(node));
}
const width = after === "compact" ? COMPACT_NODE_WIDTH : Math.max(node.size?.[0] ?? 0, NODE_WIDTH);
node.setSize?.([width, node.computeSize?.()[1] ?? node.size?.[1]]);
node.setDirtyCanvas?.(true, true);
notifyAusbossChange();
}
app.registerExtension({
name: "AusBoss.Seed",
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== NODE_CLASS) return;
hideInputsInDef(nodeData, ["seed"]);
chainCallback(nodeType.prototype, "onNodeCreated", function () {
// A new node starts in the look last picked from any Seed's gear. A
// loaded one keeps its own: onConfigure restores its properties after.
if (loadSettings(SEED_LOOK_SCOPE, SEED_LOOK_MENU).look === "compact") setSeedLook(this, "compact");
const state = buildPanel(this);
if (state && seedLook(this) === "compact") {
this.setSize?.([COMPACT_NODE_WIDTH, this.computeSize?.()[1] ?? this.size?.[1]]);
}
buildPanel(this);
});
chainCallback(nodeType.prototype, "onConfigure", function () {
queueMicrotask(() => {
const state = buildPanel(this);
if (!state) return;
render(state);
// A saved compact Seed: lower the floor to the compact row's.
if (state.widget) {
state.widget.options.minNodeSize = [nodeWidthFloor(this), 60];
holdNodeMinWidth(state.widget, nodeWidthFloor(this));
}
if (state) render(state);
});
});
chainCallback(nodeType.prototype, "onConnectionsChange", function () {
+2 -2
View File
@@ -9,7 +9,7 @@ export function inputNumber(node, name, fallback) {
for (let depth = 0; depth < 16; depth++) {
if (seen.has(linkId)) return null;
seen.add(linkId);
const link = graph?.links?.[linkId] ?? graph?._links?.get?.(linkId);
const link = graph?.getLink?.(linkId) ?? graph?.links?.get?.(linkId) ?? graph?.links?.[linkId] ?? graph?._links?.get?.(linkId);
const source = graph?.getNodeById?.(link?.origin_id);
if (!source || (source.mode ?? 0) !== 0) return null;
if (source.type === "Reroute") {
@@ -18,7 +18,7 @@ export function inputNumber(node, name, fallback) {
continue;
}
if (!["PrimitiveFloat", "PrimitiveInt", "AUSBOSS_NODES_Integer", "AUSBOSS_NODES_Float"].includes(source.type) || link.origin_slot !== 0) return null;
if (source.inputs?.some((slot) => slot.name === "value" && slot.link != null)) return null;
if (source.inputs?.some((slot) => (slot.name === "value" || slot.widget?.name === "value") && slot.link != null)) return null;
const value = source.widgets?.find((widget) => widget.name === "value")?.value;
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
-61
View File
@@ -1,61 +0,0 @@
// Seed 🆎's two looks, picked from the gear on its card: the full card
// (the default), or "compact", one short row with the number, a mode chip
// and a new-seed button. The look is kept in the node's properties, so the
// node's inputs, saved values and the prompt it sends are the same in both,
// and the last look picked is what new Seed nodes start with.
//
// Data and decisions only (tests/seed_look.test.mjs); js/seed/index.js
// draws the card.
export const SEED_LOOK_PROPERTY = "ausboss_seed_look";
export const SEED_LOOKS = ["full", "compact"];
export const SEED_LOOK_SCOPE = "seed_look";
export function seedLook(node) {
return node?.properties?.[SEED_LOOK_PROPERTY] === "compact" ? "compact" : "full";
}
// The full card leaves no property behind, so a node set back to it is
// saved exactly as one that was never changed.
export function setSeedLook(node, look) {
if (!node) return "full";
node.properties ??= {};
if (look === "compact") node.properties[SEED_LOOK_PROPERTY] = "compact";
else delete node.properties[SEED_LOOK_PROPERTY];
return seedLook(node);
}
// The gear menu (js/shared/settings_menu.mjs). The pick is also kept as the
// look new Seed nodes start with.
export const SEED_LOOK_MENU = [
{
key: "look",
label: "Look",
type: "choice",
options: SEED_LOOKS,
default: "full",
hint: "How this Seed node looks. New Seed nodes start the same way.",
optionHints: {
full: "the whole card: mode buttons, New seed, Use last run, and the seeds it ran with.",
compact: "one short row: the number, a mode chip you tap to change, and a dice for a new seed.",
},
},
];
// The compact row's chip: tap to go Random -> Fixed -> Step +1 -> Step -1
// -> Random. Values are the frontend's control_after_generate choices.
const CYCLE = ["randomize", "fixed", "increment", "decrement"];
export function nextControl(control) {
const index = CYCLE.indexOf(control);
return CYCLE[(index + 1) % CYCLE.length] ?? "fixed";
}
export function controlChip(control) {
switch (control) {
case "randomize": return { text: "Random", title: "Rolls a new seed after each run. Tap for Fixed." };
case "increment": return { text: "+1", title: "Adds 1 after each run. Tap for -1." };
case "decrement": return { text: "−1", title: "Takes 1 off after each run. Tap for Random." };
default: return { text: "Fixed", title: "Keeps this seed for every run. Tap for +1." };
}
}
+5 -3
View File
@@ -3,8 +3,7 @@
The window rules live in ``audio_window`` so the node and its tests agree on
what one Run hands on; ``js/load_audio/audio_window.mjs`` mirrors them for the
face. Decoding counts samples from the start of the sound track and stops as
soon as the window is full, so a short pick from a long file never decodes
the whole file.
soon as the window is full. Samples before the window are decoded and discarded.
"""
from __future__ import annotations
@@ -107,7 +106,10 @@ def decode_audio_window(path: Path, start_seconds, end_seconds, max_seconds=0.0)
"""
window = audio_window(start_seconds, end_seconds, max_seconds)
start = window["start"]
with av.open(str(path)) as container:
# Read only this already-validated file. An empty protocol whitelist
# prevents demuxers from opening any referenced files or network URLs,
# including playlists disguised with an ordinary audio extension.
with path.open("rb") as source, av.open(source, options={"protocol_whitelist": ""}) as container:
stream = next((candidate for candidate in container.streams if candidate.type == "audio"), None)
if stream is None:
raise ValueError(f"{path.name} has no sound in it.")
+1 -1
View File
@@ -90,7 +90,7 @@ class AusBossLoadAudio:
async def load_audio(self, audio, start_seconds, end_seconds, max_seconds=0.0):
path = resolve_input_path(audio)
# Decoding blocks for as long as the pick is; a worker thread keeps
# Decoding reads from the beginning through OUT; a worker thread keeps
# ComfyUI's event loop answering, and carries the context ComfyUI
# reads to attribute an interrupt to this node.
sound = await asyncio.to_thread(
+38 -1
View File
@@ -1,7 +1,44 @@
import assert from "node:assert/strict";
import test from "node:test";
import { grabZone, outputSummary, outputWindow, wavePeaks } from "../js/load_audio/audio_window.mjs";
import { grabZone, outputSummary, outputWindow, typedTrimBounds, wavePeaks, wiredMax } from "../js/load_audio/audio_window.mjs";
function maxLink(source, originSlot = 0, linkStore = "object") {
const link = { origin_id: 1, origin_slot: originSlot };
const graph = { getNodeById: () => source };
if (linkStore === "map") graph.links = new Map([[2, link]]);
else if (linkStore === "getter") graph.getLink = () => link;
else graph.links = { 2: link };
return { inputs: [{ name: "max_seconds", widget: { name: "max_seconds" }, link: 2 }], graph };
}
test("wired Max length only reads the connected output of an unlinked literal", () => {
assert.deepEqual(wiredMax({}), { wired: false, value: null });
const video = { type: "AUSBOSS_NODES_LoadVideo", widgets: [{ name: "start_seconds", value: 4 }, { name: "end_seconds", value: 14 }] };
assert.deepEqual(wiredMax(maxLink(video, 7)), { wired: true, value: null });
for (const type of ["AUSBOSS_NODES_Float", "AUSBOSS_NODES_Integer", "PrimitiveFloat", "PrimitiveInt"]) {
const source = { type, widgets: [{ name: "unrelated", value: 4 }, { name: "value", value: 10 }] };
for (const store of ["object", "map", "getter"]) {
assert.deepEqual(wiredMax(maxLink(source, 0, store)), { wired: true, value: 10 });
}
assert.equal(wiredMax(maxLink(source, 1)).value, null);
source.inputs = [{ name: "value", widget: { name: "value" }, link: 9 }];
assert.equal(wiredMax(maxLink(source)).value, null);
source.inputs = [];
source.mode = 4;
assert.equal(wiredMax(maxLink(source)).value, null);
source.mode = 0;
source.widgets[1].value = NaN;
assert.equal(wiredMax(maxLink(source)).value, null);
}
});
test("typing OUT zero restores the file end while other typed times keep handle limits", () => {
assert.deepEqual(typedTrimBounds(10, { start: 2, end: 5 }, "end", 0), { start: 2, end: 10 });
assert.deepEqual(typedTrimBounds(10, { start: 2, end: 5 }, "end", 1), { start: 2, end: 2.1 });
assert.deepEqual(typedTrimBounds(10, { start: 2, end: 5 }, "end", 7), { start: 2, end: 7 });
assert.deepEqual(typedTrimBounds(10, { start: 2, end: 5 }, "start", 0), { start: 0, end: 5 });
});
test("the face shows what the node hands on", () => {
// Mirrors audio_window in nodes/_audio_trim_helpers.py.
-26
View File
@@ -1,26 +0,0 @@
import assert from "node:assert/strict";
import test from "node:test";
import { SEED_LOOK_PROPERTY, controlChip, nextControl, seedLook, setSeedLook } from "../js/shared/seed_look.mjs";
test("the look lives on the node, and full leaves nothing behind", () => {
const node = { properties: {} };
assert.equal(seedLook(node), "full");
assert.equal(setSeedLook(node, "compact"), "compact");
assert.equal(node.properties[SEED_LOOK_PROPERTY], "compact");
assert.equal(setSeedLook(node, "full"), "full");
assert.deepEqual(node.properties, {});
assert.equal(seedLook(null), "full");
assert.equal(seedLook({ properties: { [SEED_LOOK_PROPERTY]: "huge" } }), "full");
});
test("the chip walks Random, Fixed, +1, -1 and back", () => {
assert.equal(nextControl("randomize"), "fixed");
assert.equal(nextControl("fixed"), "increment");
assert.equal(nextControl("increment"), "decrement");
assert.equal(nextControl("decrement"), "randomize");
assert.equal(nextControl("something else"), "randomize");
assert.equal(controlChip("randomize").text, "Random");
assert.equal(controlChip("fixed").text, "Fixed");
assert.equal(controlChip(undefined).text, "Fixed");
});
+45
View File
@@ -86,5 +86,50 @@ class DecodeTests(unittest.TestCase):
decode_audio_window(self.path, 12.0, 0.0)
class ReferenceFileTests(unittest.TestCase):
def test_hls_cannot_read_a_segment_outside_the_source_folder(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
source_folder = root / "input"
source_folder.mkdir()
outside = root / "outside.aac"
with av.open(str(outside), "w") as container:
stream = container.add_stream("aac", rate=48000, layout="mono")
frame = av.AudioFrame.from_ndarray(
np.zeros((1, 48000), dtype=np.float32), format="fltp", layout="mono"
)
frame.sample_rate = 48000
for packet in stream.encode(frame):
container.mux(packet)
for packet in stream.encode(None):
container.mux(packet)
for reference in ("../outside.aac", outside.as_posix(), outside.as_uri()):
for suffix in (".m3u8", ".wav"):
with self.subTest(reference=reference, suffix=suffix):
playlist = source_folder / f"song{suffix}"
playlist.write_text(
"#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:2\n"
f"#EXTINF:1.0,\n{reference}\n#EXT-X-ENDLIST\n"
)
with self.assertRaises(av.error.FFmpegError):
decode_audio_window(playlist, 0, 0)
def test_concat_cannot_follow_references_even_with_an_audio_extension(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
source_folder = root / "input"
source_folder.mkdir()
outside = root / "outside.wav"
write_ramp(outside, 1)
# A safe-looking relative name can still lead outside the allowed roots.
(source_folder / "segment.wav").symlink_to(outside)
for suffix in (".ffconcat", ".wav", ".mp3"):
with self.subTest(suffix=suffix):
playlist = source_folder / f"song{suffix}"
playlist.write_text("ffconcat version 1.0\nfile 'segment.wav'\n")
with self.assertRaises(av.error.FFmpegError):
decode_audio_window(playlist, 0, 0)
if __name__ == "__main__":
unittest.main()