Compare commits

..
Author SHA1 Message Date
ausboss f38ed1dc22 Fix four issues from a review of recent changes
- Crop For Inpaint: refuse a crop size that would need tens of GB of
  memory instead of trying to allocate it.
- H3 Prompt Writer timeline: retiming a line keeps its list number
  and recognises every timestamp form the backend reads.
- H3 Prompt Writer picture chips: the row grows when chips wrap, so
  they no longer spill over Length.
- Line up: clicking a resize handle without dragging no longer resizes
  the other selected nodes.
2026-11-04 00:28:37 -06:00
15 changed files with 276 additions and 26 deletions
@@ -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 -1
View File
@@ -253,7 +253,7 @@ function run(tool) {
// took the press. Every other selected node takes its size from where it
// stood when the button went down, so nothing builds up across steps.
const together = { enabled: true, before: null, grabbed: null, handleOf: null, sides: null, frame: 0 };
const together = { enabled: true, before: null, grabbed: null, handleOf: null, sides: null, frame: 0, resized: false };
function resizableSelection(canvas) {
const { nodes } = graphSets(canvas?.graph);
@@ -288,6 +288,8 @@ function syncSizes() {
const grabbed = together.grabbed;
if (!grabbed) return;
const was = together.before.get(grabbed);
together.resized ||= grabbed.size[0] !== was.size[0] || grabbed.size[1] !== was.size[1];
if (!together.resized) return;
together.sides ??= sharedSides(
[...together.before.values()].map(({ pos, size }) => ({ x: pos[0], y: pos[1], w: size[0], h: size[1] })),
);
@@ -348,6 +350,7 @@ function endTogether() {
together.grabbed = null;
together.handleOf = null;
together.sides = null;
together.resized = false;
}
function installTogether() {
+1 -1
View File
@@ -15,7 +15,7 @@ function ensureCss() {
const style = document.createElement("style");
style.id = CSS_ID;
style.textContent = `
.ausboss-chips{display:flex;flex-wrap:wrap;align-content:flex-start;gap:5px;height:100%;min-width:0;font:600 10.5px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
.ausboss-chips{display:flex;flex-wrap:wrap;align-content:flex-start;gap:5px;min-width:0;font:600 10.5px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
.ausboss-chip{position:relative;display:flex;align-items:center;gap:4px;height:24px;max-width:100%;padding:0 8px;box-sizing:border-box;border:1px solid #2a3437;border-radius:12px;background:#0b0f10;color:#d8ecea;white-space:nowrap}
.ausboss-chip:hover{border-color:${BRAND}}
.ausboss-chip img{flex:none;width:18px;height:18px;margin-left:-5px;border-radius:9px;object-fit:cover}
+16 -11
View File
@@ -31,16 +31,27 @@ export function stripRows(report) {
}));
}
// A time typed at the start of a line: 3s:, 3 s, at 3 seconds, [3s].
const TYPED_TIME = /^\[?(?:at\s+)?\d+(?:\.\d+)?\s*(?:seconds?|secs?|s)\b\]?\s*[:,\-]?\s*/i;
// Keep these in step with NUMBER and TIMED in nodes/_h3_script_helpers.py.
const LIST_PREFIX = /^(?:\d+[.)]|[-*•]|(?:first|second|third|fourth|fifth|sixth)(?:ly)?\b[:,]?|step\s+\d+[:.]?)\s*/i;
const UNIT = String.raw`(?:seconds?|secs?|s)\b`;
const DASH = String.raw`[-–—]`;
const TYPED_TIME = new RegExp(String.raw`^\[?(?:at\s+)?(?:\d+:\d\d(?:\.\d+)?|\d+(?:\.\d+)?\s*(?:${UNIT}|(?=${DASH}\s*\d)))(?:\s*${DASH}\s*\d+(?:\.\d+)?\s*(?:${UNIT})?)?\]?\s*[:,–—\-]?\s*`, "i");
function splitMoment(line) {
const indent = line.match(/^\s*/)[0];
const body = line.slice(indent.length);
const prefix = TYPED_TIME.test(body) ? "" : body.match(LIST_PREFIX)?.[0] ?? "";
const rest = body.slice(prefix.length);
const time = rest.match(TYPED_TIME)?.[0] ?? "";
return { lead: indent + prefix, time, words: rest.slice(time.length) };
}
// The script with one line set to start at a second: its old time, if any,
// gives way to the new one. line counts typed lines from 0.
export function retime(text, line, second) {
const lines = String(text ?? "").split("\n");
if (!Number.isInteger(line) || line < 0 || line >= lines.length) return String(text ?? "");
const lead = lines[line].match(/^\s*/)[0];
const words = lines[line].slice(lead.length).replace(TYPED_TIME, "");
const { lead, words } = splitMoment(lines[line]);
lines[line] = `${lead}${trimmed(second)}s: ${words}`;
return lines.join("\n");
}
@@ -88,13 +99,7 @@ export function rulerSeconds(seconds) {
export function moveMoment(text, slots, from, to) {
const lines = String(text ?? "").split("\n");
if (from === to || !slots.every((line) => Number.isInteger(line) && line >= 0 && line < lines.length)) return String(text ?? "");
const split = (line) => {
const lead = lines[line].match(/^\s*/)[0];
const body = lines[line].slice(lead.length);
const time = body.match(TYPED_TIME)?.[0] ?? "";
return { lead, time, words: body.slice(time.length) };
};
const parts = slots.map(split);
const parts = slots.map((line) => splitMoment(lines[line]));
const words = parts.map((part) => part.words);
const [moved] = words.splice(from, 1);
words.splice(Math.max(0, Math.min(to, words.length)), 0, moved);
+18
View File
@@ -22,6 +22,24 @@
// stack's rounded bottom border once went missing.
export const WIDGET_FRAME = 20;
// A wrapped row grows from its content, which must have a natural height.
// The row's own height feeds the card total and the sockets below it.
export function observePanelHeight(row, element, content, refresh) {
const minimum = Number(row.height) || 26;
const observer = new ResizeObserver(() => {
// offsetHeight stays in layout pixels when the canvas is zoomed.
const measured = content.offsetHeight;
if (!measured) return;
const height = Math.max(minimum, measured);
if (height === row.height) return;
row.height = height;
element.style.height = `${height}px`;
refresh();
});
observer.observe(content);
return observer;
}
// The frontend pads a DOM widget's minWidth before it becomes the node's
// floor: LGraphNode.computeSize adds the room a number widget's value box
// takes (BaseWidget.minValueWidth plus both arrows and margins - 104px on
+8 -3
View File
@@ -33,7 +33,7 @@
import { BRAND, chainCallback, keepDomWidgetWidthAuto, notifyAusbossChange } from "./index.mjs";
import { keepKeyInField } from "./canvas_passthrough.mjs";
import { createMediaPicker } from "./media_picker.mjs";
import { ensureNodeMinHeight, fillNodeHeight, holdNodeMinWidth, nodeHeightAfterCardChange, pinVuePanelHeight } from "./panel_layout.mjs";
import { ensureNodeMinHeight, fillNodeHeight, holdNodeMinWidth, nodeHeightAfterCardChange, observePanelHeight, pinVuePanelHeight } from "./panel_layout.mjs";
import { makeScrubInput, quantizeScrubValue, stepDecimals } from "./scrub_input.mjs";
import { limitsLabel, narrowedRange } from "./wired_limits.mjs";
import { hideWidget } from "./widget_visibility.mjs";
@@ -221,7 +221,7 @@ function isFloatWidget(widget) {
// - `top` lifts the widgets' sockets
// into the node's slot column
// { section: "Caption" }
// { panel: (node) => ({ element, sync(values) }), height }
// { panel: (node) => ({ element, sync(values) }), height, measure? }
// - the node's own display, on a row
// of the card
// { group: "advanced", label: "More" } - a disclosure; later rows with
@@ -526,11 +526,16 @@ export function mountWidgetCard(node, { rows, minWidth = 300, first = false, hid
root.append(caption); continue;
}
if (row.panel) {
// Measurements belong to this node, not the shared card definition.
const panelRow = { ...row };
const line = el("div", "ausboss-card-row panel");
line.style.height = `${rowHeight(row)}px`;
const panel = row.panel(node);
line.append(panel.element);
state.rows.push({ row, element: line, syncs: panel.sync ? [panel.sync] : [] });
state.rows.push({ row: panelRow, element: line, syncs: panel.sync ? [panel.sync] : [] });
if (row.measure && typeof ResizeObserver === "function") {
state.observers.push(observePanelHeight(panelRow, line, panel.element, refresh));
}
root.append(line); continue;
}
if (row.group !== undefined && row.widget === undefined && row.pair === undefined) {
+1 -1
View File
@@ -344,7 +344,7 @@ const CARDS = {
{ widget: "sound", label: "Sound", placeholder: "rain on a window" },
{ widget: "music", label: "Music", placeholder: "slow piano: music makes it louder" },
{ section: "Pictures and settings" },
{ panel: castChips, height: 30 },
{ panel: castChips, height: 30, measure: true },
{ widget: "seconds", label: "Length", suffix: "s" },
{ widget: "camera", label: "Camera", kind: "select" },
{ widget: "look", label: "Look", kind: "select" },
+18
View File
@@ -225,6 +225,20 @@ def _as_mask(
RESIZE_ALGORITHMS = ("bilinear", "bicubic", "area", "nearest")
# A crop keeps several float buffers alive together. Limit new picture
# buffers to 256 MiB or the source's size, before padding or resizing can
# multiply its footprint. A large source alone does not force a rejection.
_MAX_CROP_ELEMENTS = 64 * 1024 * 1024
def _check_crop_size(image: torch.Tensor, width: int, height: int) -> None:
if image.shape[0] * width * height * image.shape[3] > max(_MAX_CROP_ELEMENTS, image.numel()):
raise ValueError(
f"Crop For Inpaint: {width}x{height} across {image.shape[0]} picture(s) "
"is too large to process safely. Reduce the context, extension or "
"target size, or choose target sides with closer proportions."
)
def _resize_image(
image: torch.Tensor, width: int, height: int, algorithm: str = "bilinear"
@@ -420,6 +434,7 @@ def build_crop(
ext_u, ext_d = max(0, int(extend_up)), max(0, int(extend_down))
if ext_l or ext_r or ext_u or ext_d:
src_h, src_w = image.shape[1], image.shape[2]
_check_crop_size(image, src_w + ext_l + ext_r, src_h + ext_u + ext_d)
image = _replicate_pad_image(image, ext_l, ext_u, ext_r, ext_d)
extended = torch.ones(
(mask.shape[0], src_h + ext_u + ext_d, src_w + ext_l + ext_r),
@@ -487,7 +502,10 @@ def build_crop(
rect = expand_rect_to_multiple(rect, multiple)
rect = fit_rect(rect, width, height)
if use_target:
_check_crop_size(image, target_w, target_h)
left, top, right, bottom = rect_margins(rect, width, height)
_check_crop_size(image, width + left + right, height + top + bottom)
if left or top or right or bottom:
canvas = _replicate_pad_image(image, left, top, right, bottom)
else:
+39
View File
@@ -1,11 +1,50 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { runInNewContext } from "node:vm";
import { MIN_GAP, enoughFor, lineUp, matchWidth, sameSize, sharedSides, spreadEvenly } from "../js/shared/line_up.mjs";
const box = (x, y, w, h) => ({ x, y, w, h });
const rects = [box(10, 0, 100, 50), box(40, 100, 200, 80), box(0, 300, 60, 40)];
function resizeHandlers(layout = "column", classic = false) {
const make = (id, pos, size) => ({ id, pos, size, flags: {}, setSize(size) { this.size = size; }, computeSize: () => [100, 60] });
const first = make(1, [0, 0], [300, 100]);
const second = make(2, layout === "row" ? [500, 0] : [0, 200], [400, 150]);
const canvas = { graph: { _nodes: [first, second], _groups: [] }, selectedItems: new Set([first, second]), setDirty() {} };
const source = readFileSync(new URL("../js/line_up/index.js", import.meta.url), "utf8").replace(/^import .*;\n/gm, "");
const handlers = runInNewContext(`${source}\n({ onTogetherDown, syncSizes, onTogetherUp })`, {
app: { canvas, registerExtension() {} }, sharedSides, matchWidth,
cancelAnimationFrame() {},
});
const handle = { closest: () => ({ dataset: { nodeId: "1" } }) };
handlers.onTogetherDown({ button: 0, target: { closest: () => classic ? null : handle } });
if (classic) canvas.resizing_node = first;
return { ...handlers, first, second };
}
test("pressing and releasing a resize handle leaves other selected nodes unchanged", () => {
for (const classic of [false, true]) for (const layout of ["row", "column"]) {
const { syncSizes, onTogetherUp, second } = resizeHandlers(layout, classic);
syncSizes(); // A pointer move can arrive without a size change.
onTogetherUp();
assert.deepEqual(second.size, [400, 150]);
}
});
test("an actual resize still shares dimensions and can return to its starting size", () => {
for (const classic of [false, true]) {
const { syncSizes, onTogetherUp, first, second } = resizeHandlers("column", classic);
first.size = [350, 100];
syncSizes();
assert.deepEqual(Array.from(second.size), [350, 150]);
first.size = [300, 100];
onTogetherUp();
assert.deepEqual(Array.from(second.size), [300, 150]);
}
});
test("edges line up on the outermost edge of the selection", () => {
assert.deepEqual(lineUp(rects, "left").map((p) => p.x), [0, 0, 0]);
assert.deepEqual(lineUp(rects, "right").map((p) => p.x), [140, 40, 180]); // right edge 240 for all
+19 -1
View File
@@ -1,10 +1,28 @@
import assert from "node:assert/strict";
import test from "node:test";
import { stripCaption, stripRows, wiredJobs } from "../js/shared/moment_strip_math.mjs";
import { moveMoment, retime, stripCaption, stripRows, wiredJobs } from "../js/shared/moment_strip_math.mjs";
const row = (start, end, extra = {}) => ({ start, end, text: "She runs", cut: false, speech: false, ...extra });
test("retiming keeps the list prefix ahead of the new timestamp", () => {
for (const prefix of ["2. ", "2) ", "- ", "* ", "• ", "First, ", "secondly: ", "Step 2: "]) {
assert.equal(retime(` ${prefix}@1 sits.`, 0, 5), ` ${prefix}5s: @1 sits.`);
}
});
test("retiming replaces all supported timestamps and ranges", () => {
for (const prefix of ["", "2. "]) for (const time of ["3s:", "3 s,", "at 3 seconds,", "[3s]", "3.5s:", "at 3.5 secs,", "01:03.5:", "[at 1:03]", "3-6s:", "3s - 6 seconds,", "[3–6s]", "at 3—6 secs:"]) {
assert.equal(retime(`${prefix}${time} @1 sits.`, 0, 5), `${prefix}5s: @1 sits.`, time);
}
});
test("reordering timed list items leaves times and list prefixes in their slots", () => {
const text = "1. [0-3s] @1 walks.\n2) 00:03-6s: @1 sits.\n\nStep 3: at 6 seconds, @1 runs.";
assert.equal(moveMoment(text, [0, 1, 3], 2, 0),
"1. [0-3s] @1 runs.\n2) 00:03-6s: @1 walks.\n\nStep 3: at 6 seconds, @1 sits.");
});
test("a row says its second and its words, and its bar sits where it plays", () => {
const rows = stripRows({ seconds: 10, moments: [row(0, 2.5), row(2.5, 10)] });
assert.deepEqual(rows.map((moment) => moment.second), ["0", "2.5"]);
+110
View File
@@ -1,5 +1,9 @@
import assert from "node:assert/strict";
import test from "node:test";
import { readFileSync } from "node:fs";
import { runInNewContext } from "node:vm";
import * as cardMath from "../js/shared/widget_card_math.mjs";
import * as panelLayout from "../js/shared/panel_layout.mjs";
import {
ensureNodeMinHeight,
@@ -11,7 +15,113 @@ import {
measureLayoutWidthPadding,
nodeHeightAfterCardChange,
pinVuePanelHeight,
observePanelHeight,
} from "../js/shared/panel_layout.mjs";
import { cardHeight, rowTops } from "../js/shared/widget_card_math.mjs";
test("a measured card row updates its node and sockets without changing another node", () => {
const observed = [];
class PanelObserver {
constructor(callback) { this.callback = callback; this.disconnected = false; }
observe(content) { this.content = content; observed.push(this); }
disconnect() { this.disconnected = true; }
}
const element = () => ({
style: {}, classList: { add() {}, remove() {}, toggle() {} }, children: [],
append(...children) { this.children.push(...children); }, addEventListener() {},
});
const source = readFileSync(new URL("../js/shared/widget_card.mjs", import.meta.url), "utf8")
.replace(/^import [\s\S]*?;\n/gm, "").replace(/^export \* from .*;\n/gm, "").replace(/^export /gm, "");
const mount = runInNewContext(`${source}\nmountWidgetCard`, {
...cardMath, ...panelLayout, BRAND: "#00b4aa",
document: { getElementById: () => true, createElement: element },
ResizeObserver: PanelObserver,
observePanelHeight(row, line, content, refresh) {
// Run the real observer helper in the same test realm as the mock.
const saved = globalThis.ResizeObserver;
globalThis.ResizeObserver = PanelObserver;
try { return panelLayout.observePanelHeight(row, line, content, refresh); }
finally { globalThis.ResizeObserver = saved; }
},
chainCallback(target, name, callback) {
const previous = target[name];
target[name] = function (...args) { previous?.apply(this, args); callback.apply(this, args); };
},
keepDomWidgetWidthAuto() {}, pinVuePanelHeight() {}, holdNodeMinWidth() {},
hideWidget(widget) { widget.hidden = true; },
});
const definition = { panel: () => ({ element: { offsetHeight: 24 } }), height: 30, measure: true };
const make = () => {
const node = {
size: [420, 100], pos: [0, 0], widgets: [{ name: "seconds", type: "text", value: "8" }],
inputs: [{ name: "seconds", widget: { name: "seconds" }, link: null }],
addDOMWidget(name, type, root, options) {
const widget = { name, type, element: root, options, y: 0 };
this.widgets.push(widget);
return widget;
},
computeSize() { return this.__ausbossCard.widget.computeSize(420); },
setSize(size) { this.size = Array.from(size); },
};
return { node, state: mount(node, { rows: [definition, { widget: "seconds", kind: "text" }], minWidth: 420 }) };
};
const first = make();
const second = make();
const height = first.node.size[1];
const socketY = first.node.widgets[0].y;
if (observed[0]) {
observed[0].content.offsetHeight = 53;
observed[0].callback();
}
assert.equal(first.node.size[1], height + 23);
assert.equal(first.node.widgets[0].y, socketY + 23);
assert.equal(second.node.size[1], height);
assert.equal(definition.height, 30);
first.node.onRemoved();
assert.equal(observed[0].disconnected, true);
});
test("wrapped panel content moves the next row and grows the card, then shrinks again", () => {
const saved = globalThis.ResizeObserver;
let changed;
let disconnected = false;
globalThis.ResizeObserver = class {
constructor(callback) { changed = callback; }
observe(element) { assert.equal(element, content); }
disconnect() { disconnected = true; }
};
const row = { panel: () => {}, height: 30 };
const next = { widget: "seconds" };
const element = { style: {} };
let height = 24;
const content = { get offsetHeight() { return height; }, getBoundingClientRect: () => ({ height: height / 2 }) };
const initialHeight = cardHeight([row, next]);
const initialTop = rowTops([row, next])[1];
let refreshed = 0;
try {
const observer = observePanelHeight(row, element, content, () => { refreshed += 1; });
changed();
assert.equal(row.height, 30);
height = 53; // Two 24px chip rows and the 5px gap.
changed();
assert.equal(element.style.height, "53px");
assert.equal(cardHeight([row, next]), initialHeight + 23);
assert.equal(rowTops([row, next])[1], initialTop + 23);
changed();
assert.equal(refreshed, 1, "unchanged measurements do not resize again");
height = 0; // A hidden panel does not erase its minimum.
changed();
assert.equal(row.height, 53);
height = 24;
changed();
assert.equal(row.height, 30);
assert.equal(cardHeight([row, next]), initialHeight);
observer.disconnect();
assert.equal(disconnected, true);
} finally {
globalThis.ResizeObserver = saved;
}
});
test("fillNodeHeight drops computeSize and declares a floor with no ceiling", () => {
const widget = { computeSize: () => [0, 200], options: {} };
+34
View File
@@ -8,6 +8,7 @@ from pathlib import Path
import sys
import types
import unittest
from unittest.mock import patch
import torch
@@ -145,6 +146,39 @@ class GeometryTests(unittest.TestCase):
class CropContractTests(unittest.TestCase):
def test_size_guard_allows_a_large_source_without_amplifying_it(self):
image = torch.zeros((2, 32, 32, 4))
mask = torch.ones((1, 32, 32))
with patch.object(inpaint_helpers, "_MAX_CROP_ELEMENTS", 1024):
cropped, _, _ = build_crop(image, mask, 1.0, 0, 32)
self.assertEqual(cropped.shape, image.shape)
with self.assertRaisesRegex(ValueError, "too large"):
build_crop(image, mask, 1.0, 0, 32, target_width=64, target_height=64)
def test_extreme_target_aspects_fail_before_allocating_padding(self):
image = torch.zeros((1, 2048, 2048, 3))
mask = torch.ones((1, 2048, 2048))
for keep_inside in (False, True):
for width, height in ((16384, 32), (32, 16384)):
with self.subTest(keep_inside=keep_inside, target=(width, height)):
with patch.object(inpaint_helpers, "_replicate_pad_image", side_effect=AssertionError("unsafe allocation")) as pad:
with self.assertRaisesRegex(ValueError, "Crop For Inpaint.*too large"):
build_crop(image, mask, 1.0, 0, 32, target_width=width, target_height=height, keep_inside=keep_inside)
pad.assert_not_called()
def test_size_guard_covers_extension_native_context_and_sampler_output(self):
image = torch.zeros((1, 32, 32, 3))
mask = torch.ones((1, 32, 32))
for options in ({"extend_right": 100_000_000}, {"context_factor": 10000},
{"target_width": 16384, "target_height": 16384}):
with self.subTest(options=options):
with (
patch.object(inpaint_helpers, "_replicate_pad_image", side_effect=AssertionError("unsafe allocation")),
patch.object(inpaint_helpers, "_resize_image", side_effect=AssertionError("unsafe resize")),
):
with self.assertRaisesRegex(ValueError, "Crop For Inpaint.*too large"):
build_crop(image, mask, blend_pixels=0, output_multiple=32, **{"context_factor": 1.0, **options})
def test_stitcher_schema(self):
image = rand_image(1, 64, 96)
mask = box_mask(64, 96, 24, 40, 40, 56)