Compare commits

..
Author SHA1 Message Date
ausboss a4d0b18920 Pika H3 Flex: Fast test opens plainly off, and examples can't ship a half-set switch
The Fast test row set the video loader to 1 MP when off, but the workflow ships the loader at 0.41 MP, so the row opened striped 'changed' with neither side picked. Its off side is now 0.41.

workflow_contract now fails any Workflow Switches row whose saved values sit on neither side, are half on and half off, or disagree with whether its nodes run.
2026-11-05 01:51:48 +00:00
ausboss bd8ead7ef9 Pika H3 Flex: show how to dress the person in your video, and name the video by what it is
- Recipes: copy-moves recipes say Picture 1 is your person; new recipe for a hat or outfit on the person in your video.
- Your pictures: Picture 1 opens as person, change its job for a place, a hat or a thing.
- Settings: steps for a clip from pictures and for a new hat or outfit, beside the swap steps.
- Video loader, switch and callout: 'Your clip' instead of 'Swap or copy moves'.
- H3 Prompt Writer: with only an outfit or thing picture and a swap or copy-moves video, the error says to load a still of the video's person as Picture 1.
2026-11-05 01:16:45 +00:00
ausboss 3fd491c839 Pika H3 Flex: preview picture, a frame from a clip the workflow made 2026-11-04 10:11:32 -06:00
ausboss 93bf3dc325 Pika H3 Flex: the swap steps set the video job; drop the unfilled version line 2026-11-04 09:58:18 -06:00
ausboss 5753f4048a Pika H3 Flex: round the clip size to 32 px, which H3 needs, so no band wraps in at the bottom 2026-11-04 09:56:55 -06:00
ausboss 841bfa7d06 Pika H3 Flex: the Settings note covers swapping two people; Voice and Song saved blank 2026-11-04 09:09:52 -06:00
ausboss 0ffb6a576b H3 Prompt Writer: a swap puts each person picture in one person's place 2026-11-03 20:57:51 -06:00
ausboss 5656144a9f Add the Pika H3 Flex example workflow 2026-11-03 20:29:04 -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 8779 additions and 248 deletions
+10
View File
@@ -4,6 +4,16 @@ All notable changes to ComfyUI-AusBoss are documented here.
## Unreleased
- **New example: Pika H3 Flex.** One MiniMax H3 workflow for pictures,
video and sound. Give each picture a job on H3 Prompt Writer, or switch on
Video to swap someone into a clip or copy its moves. Keep the clip's sound
carries the clip's own words and music through a swap, and lets them steer
the mouths. Video swaps stop at 10 seconds and come out at about 480p,
where they hold best. Voice and Song use the new Load Audio node.
- **H3 Prompt Writer swaps two people.** With Swap the person and two person
pictures, Picture 1 takes the place of the person on the left and Picture 2
the person on the right. Type who goes to change that, one line each, such
as `@1 the man in the hat` and `@2 the woman in red`.
- **New node: Load Audio.** Load a sound file and keep only the part you
want. The node shows the sound as a wave: drag the IN and OUT handles to
trim it, and press play to hear just that part. Set Max length to the
@@ -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"
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

File diff suppressed because it is too large Load Diff
@@ -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"
-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.
+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 () {
-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." };
}
}
+75 -12
View File
@@ -37,6 +37,12 @@ NEEDS_A_PERSON = {
MOVES: "To copy the moves, connect a picture of your person and pick person for it.",
SWAP: "To swap the person, connect a picture of the new person and pick person for it.",
}
# A hat or a shirt with no person picture: the user wants the video's own person to wear it.
DRESS_THE_VIDEO_PERSON = (
"To put this on the person in your video, load a still of them from the video as Picture 1 "
"and pick person for it. Give the hat or outfit its own picture, pick outfit or thing, and type "
"\"@1 wears @2\" with video job swap the person."
)
# (what the label is, how it is kept). {picture} is H3's number for the picture.
JOB_WORDS = {
@@ -88,14 +94,20 @@ MOVES_KEPT = (
"keep the source performer's face, body, clothes, props, background and colours out of the target.\n"
"<Video 1>: attribute_transfer - apply only its camera movement to the target scene."
)
SWAP_IS = (
"<Video 1> supplies the place, the action, the timing, the camera movement and the look.\n"
"{replaced} is {who} in <Video 1>, selected for replacement."
)
SWAP_KEPT = (
SWAP_VIDEO = "<Video 1> supplies the place, the action, the timing, the camera movement and the look."
SWAP_IS = "{replaced} is {who} in <Video 1>, selected for replacement."
SWAP_VIDEO_KEPT = (
"<Video 1>: partially_preserved - keep the place, the action, the timing, the camera movement and the look; "
"change only the person.\n"
"{replaced}: removed - replace {who} with {main} throughout."
"change only the {people}."
)
SWAP_KEPT = "{replaced}: removed - replace {who} with {main} throughout."
ONE_GOES = "the original person"
# Where each person stands when two or more go and the script doesn't say, from left to right like the pictures.
SPOTS = {2: ("left", "right"), 3: ("left", "centre", "right"), 4: ("far left", "left", "right", "far right")}
SAYS_WHO = re.compile(
r"^(?:is|are|replaces|takes\s+the\s+place\s+of|goes\s+in\s+for|swaps?\s+(?:in\s+)?for|in\s+place\s+of|for|as)\b\s*"
r"|^[:=\-\u2013\u2014>]+\s*",
re.I,
)
SWAPPED = "attribute_transfer - apply the defined appearance to the original performance; the background of {picture} is not transferred."
KEEP_IS = (
@@ -115,6 +127,11 @@ REVOICE_KEPT = (
)
def lower_first(text):
first = text.split(" ", 1)[0]
return text if text[:1] == "<" or (len(first) > 1 and first.isupper()) else text[:1].lower() + text[1:]
def subject_label(number):
return f"<Subject {number}>"
@@ -135,11 +152,16 @@ class Picture:
subject: int = 0 # A picture that only marks the last frame has none.
def swapped_in(jobs):
"""How many people a swap puts in: one for each person or sheet picture."""
return max(1, sum(job in (PERSON, SHEET) for job in jobs.values()))
def number_pictures(jobs, video=""):
"""Connected pictures in order, from {number on the node: job}. In a swap
the person being replaced comes first, as <Subject 1>."""
the people being replaced come first, as <Subject 1>, <Subject 2> and on."""
pictures = []
subjects = 1 if video == SWAP else 0
subjects = swapped_in(jobs) if video == SWAP else 0
for picture, slot in enumerate(sorted(jobs), 1):
subjects += jobs[slot] != END
pictures.append(Picture(slot, jobs[slot], picture, 0 if jobs[slot] == END else subjects))
@@ -154,8 +176,12 @@ class Cast:
self.video = video
last = max((picture.subject for picture in pictures), default=0)
self.moves = subject_label(last + 1) if video == MOVES else ""
# In a swap each person or sheet picture takes one person's place in the video, in picture order. The script
# can name who goes: "the child in the seat", or "@1 the man on the left. @2 the woman in red".
self.swappers = [picture for picture in pictures if picture.job in (PERSON, SHEET)] if video == SWAP else []
goes = len(self.swappers)
self.whos = [f"the person on the {spot}" for spot in SPOTS[goes]] if goes > 1 else [ONE_GOES]
self.replaced = subject_label(1) if video == SWAP else ""
self.who = "the original person" # In a swap the script can name who goes: "the child in the seat".
self.in_video = subject_label(last + 1) if video in (KEEP, REVOICE) else ""
self.people = [picture for picture in pictures if picture.job in (PERSON, SHEET, START)]
self.subjects = {picture.slot: subject_label(picture.subject) for picture in pictures if picture.subject}
@@ -163,6 +189,39 @@ class Cast:
def of(self, *jobs):
return [picture for picture in self.pictures if picture.job in jobs]
@property
def who(self):
return self.whos[0]
@who.setter
def who(self, words):
self.whos[0] = words
@property
def swaps(self):
"""(who goes, the words for them, who comes in) for each person a swap replaces."""
coming = [subject_label(picture.subject) for picture in self.swappers] or [self.main]
return [(subject_label(number), who, new) for number, (who, new) in enumerate(zip(self.whos, coming), 1)]
def name_who_goes(self, lines):
"""Who each new person replaces, from the lines typed in a swap. With one, the words are who goes. With more,
each @ names one, or else the lines go in picture order; anything else keeps left to right."""
lines = [line.strip() for line in lines if line.strip(" .")]
text = " ".join(lines)
if len(self.whos) == 1:
self.who = lower_first(SAYS_WHO.sub("", TOKEN.sub("", text).strip()).rstrip(". ") or ONE_GOES)
return
slots = [picture.slot for picture in self.swappers]
pieces = re.split(r"@(\d)\b", text)
named = {int(slot): words for slot, words in zip(pieces[1::2], pieces[2::2]) if int(slot) in slots}
if not named:
named = dict(zip(slots, lines)) if len(lines) == len(slots) else {}
for index, slot in enumerate(slots):
words = SAYS_WHO.sub("", named.get(slot, "").strip(" .,;")).strip(" .,;")
words = re.sub(r"\s+and$", "", words)
if words:
self.whos[index] = lower_first(words)
@property
def main(self):
"""Who a line is about when it names nobody."""
@@ -188,6 +247,8 @@ class Cast:
if not self.pictures and not self.video:
return NOTHING_IN
if self.video in NEEDS_A_PERSON and not self.people:
if self.of(OUTFIT, THING):
return DRESS_THE_VIDEO_PERSON
return NEEDS_A_PERSON[self.video]
typed = sorted({int(number) for number in TOKEN.findall(text)} - {0})
slots = {picture.slot: picture for picture in self.pictures}
@@ -222,7 +283,8 @@ class Cast:
ends = self.ends
lines = []
if self.video == SWAP:
lines.append(SWAP_IS.format(replaced=self.replaced, who=self.who))
lines.append(SWAP_VIDEO)
lines += [SWAP_IS.format(replaced=gone, who=who) for gone, who, _ in self.swaps]
worn = set(wearers.values())
for picture in self.pictures:
if picture.job == END:
@@ -276,7 +338,8 @@ class Cast:
lines = []
if self.video == SWAP:
lines.append(SWAP_KEPT.format(replaced=self.replaced, who=self.who, main=self.main))
lines.append(SWAP_VIDEO_KEPT.format(people="people" if len(self.swaps) > 1 else "person"))
lines += [SWAP_KEPT.format(replaced=gone, who=who, main=new) for gone, who, new in self.swaps]
for picture in self.pictures:
if picture.job == END:
continue
+2 -2
View File
@@ -13,7 +13,7 @@ import re
import numpy as np
from ._h3_cast_helpers import KEEP, NEEDS_NO_WORDS, REVOICE, SWAP, Cast, number_pictures
from ._h3_script_helpers import ACTION_GOES_ON, Moment, add_moments, add_sound, lower_first, read_script, script_parts, speakers
from ._h3_script_helpers import ACTION_GOES_ON, Moment, add_moments, add_sound, read_script, script_parts, speakers
SECTIONS = (
"subject_definitions",
@@ -184,7 +184,7 @@ def write_prompt(script, seconds=15.0, camera="", look="", sound="", language="E
if not scene.moments and video not in NEEDS_NO_WORDS:
raise ValueError(NO_SCRIPT)
if video == SWAP and scene.moments: # Typed words in a swap say which person goes, for a clip with two.
cast.who = lower_first(" ".join(moment.text for moment in scene.moments).rstrip(". "))
cast.name_who_goes([moment.text for moment in scene.moments])
scene.moments = []
if not scene.moments:
scene.moments.append(Moment(ACTION_GOES_ON if video == KEEP else ""))
+8 -7
View File
@@ -25,7 +25,10 @@ from ._h3_cast_helpers import (
SOUND_GOES_ON_KEPT,
THING,
TOKEN,
ONE_GOES,
SPOTS,
Cast,
lower_first,
number_pictures,
picture_label,
subject_label,
@@ -124,7 +127,6 @@ VIDEO_LOOKS = {
KEEP: "The target video keeps the look, colours and camera of <Video 1>.",
REVOICE: "The target video keeps the look, light and camera of <Video 1>.",
}
SPOTS = {2: ("left", "right"), 3: ("left", "centre", "right"), 4: ("far left", "left", "right", "far right")}
NAMES_A_SPOT = re.compile(r"\b(?:left|right|behind|in\s+front|foreground|background)\b", re.I)
OPENS_ON_THEM = re.compile(r"^(?:She|He)\b")
FOLLOWS_A_COMMA = re.compile(r"^(?:A|An|The|She|He|They|It|Her|His|Their|Its|This|That|These|Those|There|Someone|Everyone|Nobody)\b")
@@ -539,9 +541,6 @@ def stamp(seconds):
return f"{int(minutes):02d}:{rest:06.3f}"
def lower_first(text):
first = text.split(" ", 1)[0]
return text if text[:1] == "<" or (len(first) > 1 and first.isupper()) else text[:1].lower() + text[1:]
def after_comma(text):
@@ -669,7 +668,8 @@ def before_the_action(number, text, cast, wearers):
words = OPENS_ON if cast.ends else OPENING
notes.append(words.format(subject=subject_label(opens.subject), picture=picture_label(opens.picture)))
if cast.video == SWAP:
notes += [TAKES_OVER.format(main=cast.main, who=cast.who), ONE_PERSON if cast.who == "the original person" else OTHERS_STAY]
notes += [TAKES_OVER.format(main=new, who=who) for _, who, new in cast.swaps]
notes.append(ONE_PERSON if cast.whos == [ONE_GOES] else OTHERS_STAY)
if number == 1 and cast.video == KEEP:
notes += [CARRIES_ON]
if number == 1 and cast.video == REVOICE:
@@ -678,7 +678,7 @@ def before_the_action(number, text, cast, wearers):
if places and not any(place in text for place in places):
notes.append(IN_THE_PLACE.format(place=places[0]))
people = people_in(text, cast)
if len(people) > 1 and not NAMES_A_SPOT.search(text):
if len(people) > 1 and not NAMES_A_SPOT.search(text) and cast.video != SWAP: # A swap's people stand where the video has them.
spots = [f"{person} is on the {spot}" for person, spot in zip(people, SPOTS[len(people)])]
notes.append(sentence(", ".join(spots[:-1]) + f" and {spots[-1]} of the frame"))
for slot, wearer in wearers.items():
@@ -786,7 +786,8 @@ def task_of(cast):
def summary_of(script, cast, new_places, soundtrack):
task = task_of(cast)
if cast.video == SWAP:
return f"[{task}] Replace {cast.replaced} with {cast.main} while preserving the original action, place, camera and look of <Video 1>."
swaps = " and ".join(f"{gone} with {new}" for gone, _, new in cast.swaps)
return f"[{task}] Replace {swaps} while preserving the original action, place, camera and look of <Video 1>."
if cast.video == REVOICE:
return (
f"[{task}] The target video is an edited version of <Video 1>. The picture, the timing and the lip movement "
+54
View File
@@ -63,6 +63,58 @@ def named_copy_problems(node: dict) -> list[str]:
return [f"node {node['id']} has stale named widget values: {', '.join(stale)}"] if stale else []
def saved_widget(node: dict, name: str):
"""A widget's saved value by name, or KeyError when the node keeps no named copy of it."""
values = node.get("widgets_values")
if isinstance(values, dict) and name in values:
return values[name]
named = node.get("widgets_values_named")
if isinstance(named, dict) and name in named:
return named[name]
raise KeyError(name)
def switch_row_problems(card: dict, by_id: dict) -> list[str]:
"""A Workflow Switches row opens striped "changed", with neither off nor
on picked, when the values it sets are saved half one way and half the
other, or when its nodes run one way and its values say the other. Every
row of an example has to open plainly off or on."""
settings = (card.get("properties") or {}).get("ausboss_workflow_switches") or {}
problems = []
for row in settings.get("switches") or []:
title = row.get("title", row.get("id"))
sides = set()
on, off = row.get("on") or {}, row.get("off") or {}
for node_id, widgets in on.items():
node = by_id.get(int(node_id)) if str(node_id).isdigit() else None
if node is None or not isinstance(widgets, dict):
continue
for name, on_value in widgets.items():
off_value = (off.get(node_id) or {}).get(name)
lora_rows = isinstance(on_value, str) and on_value.startswith("[")
if isinstance(on_value, (dict, list)) or lora_rows or on_value == off_value:
continue # a LoRA stack is matched row by row, and a shared value says nothing
try:
saved = saved_widget(node, name)
except KeyError:
continue
if saved == on_value:
sides.add("on")
elif saved == off_value:
sides.add("off")
else:
sides.add(f"neither ({node_id} {name} is {saved!r}, not {on_value!r} or {off_value!r})")
members = [by_id[i] for i in row.get("nodes") or [] if i in by_id]
modes = {"on" if m.get("mode", 0) == 0 else "off" for m in members}
if len(modes) > 1:
problems.append(f"switch card {card['id']} row '{title}': some of its nodes are on and some off")
odd = sorted(side for side in sides if side not in ("on", "off"))
if odd or len(sides) > 1 or (sides and modes and sides != modes):
detail = "; ".join(odd) if odd else f"values say {'/'.join(sorted(sides))}, nodes say {'/'.join(sorted(modes)) or '-'}"
problems.append(f"switch card {card['id']} row '{title}' opens as changed: {detail}")
return problems
def workflow_problems(data: dict) -> list[str]:
problems = []
nodes = data.get("nodes", [])
@@ -112,6 +164,8 @@ def workflow_problems(data: dict) -> list[str]:
problems.append(f"saved zoom {scale} is below {MIN_ZOOM}; widget text would not draw")
for n in nodes:
problems.extend(named_copy_problems(n))
if n["type"] == "AUSBOSS_NODES_WorkflowSwitches":
problems.extend(switch_row_problems(n, by_id))
if n["type"] in CORE_LORA_LOADERS:
problems.append(f"node {n['id']} loads a LoRA with core {n['type']}; examples use LoRA Loader 🆎")
selected = selected_files(n)
-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");
});
+28
View File
@@ -652,6 +652,12 @@ class VideoJobTests(unittest.TestCase):
cast = Cast(number_pictures({1: "place"}, "copy its moves"), "copy its moves")
self.assertIn("To copy the moves", cast.problem("She dances."))
def test_a_hat_with_no_person_picture_says_how_to_put_it_on_the_video_person(self):
for job in ("outfit", "thing"):
for video in ("swap the person", "copy its moves"):
cast = Cast(number_pictures({1: job}, video), video)
self.assertIn("load a still of them from the video as Picture 1", cast.problem("He wears @1."))
def test_a_swap_with_nobody_to_put_in_says_so(self):
with self.assertRaisesRegex(ValueError, "new person"):
write_prompt("", jobs={}, video="swap the person")
@@ -676,6 +682,28 @@ class VideoJobTests(unittest.TestCase):
self.assertIn("Everyone else stays", parts["detailed_description"])
self.assertNotIn("Show one person", parts["detailed_description"])
def test_a_swap_with_two_people_puts_each_in_one_place(self):
parts = parts_of("", jobs={1: "person", 2: "person"}, video="swap the person")
self.assertIn("Replace <Subject 1> with <Subject 3> and <Subject 2> with <Subject 4>", parts["summary"])
self.assertIn("<Subject 1> is the person on the left in <Video 1>", parts["subject_definitions"])
self.assertIn("<Subject 2> is the person on the right in <Video 1>", parts["subject_definitions"])
self.assertIn("<Subject 4> is the person in <Picture 2>", parts["subject_definitions"])
self.assertIn("replace the person on the right with <Subject 4>", parts["retention_analysis"])
self.assertIn("change only the people", parts["retention_analysis"])
self.assertIn("<Subject 4> takes the place of the person on the right", parts["detailed_description"])
self.assertNotIn("Show one person", parts["detailed_description"])
def test_a_swap_with_two_people_reads_who_goes_from_the_script(self):
for script in ("@2 is the woman in red. @1 replaces the man in the hat", "The man in the hat.\nThe woman in red."):
parts = parts_of(script, jobs={1: "person", 2: "character sheet"}, video="swap the person")
self.assertIn("replace the man in the hat with <Subject 3>", parts["retention_analysis"])
self.assertIn("replace the woman in red with <Subject 4>", parts["retention_analysis"])
self.assertNotIn("@", join_sections(parts))
def test_one_swap_takes_the_at_out_of_who_goes(self):
parts = parts_of("@1 replaces the child in the seat", jobs={1: "person"}, video="swap the person")
self.assertIn("replace the child in the seat with <Subject 2>", parts["retention_analysis"])
def test_keeping_going_needs_no_words_and_no_picture(self):
parts = parts_of("", jobs={}, video="keep going")
self.assertTrue(parts["summary"].startswith("[video continuation]"))
+18
View File
@@ -38,6 +38,24 @@ class WorkflowContractTests(unittest.TestCase):
graph["nodes"][0]["widgets_values_named"] = {"video": "someone_else.mp4", "start_seconds": 0, "upload": ""}
self.assertIn("stale named widget values: video", " ".join(workflow_problems(graph)))
def test_a_switch_row_must_open_plainly_off_or_on(self):
graph = good_graph()
graph["nodes"][0]["widgets_values"] = {"megapixels": 0.41}
card = {"id": 4, "type": "AUSBOSS_NODES_WorkflowSwitches", "pos": [0, 300], "size": [100, 40], "properties": {
"ausboss_workflow_switches": {"switches": [{"id": 9, "title": "Fast test", "nodes": [],
"on": {"1": {"megapixels": 0.26}}, "off": {"1": {"megapixels": 1}}}]}}}
graph["nodes"].append(card)
graph["last_node_id"] = 4
graph["groups"][0]["bounding"] = [-20, 40, 320, 320]
self.assertIn("row 'Fast test' opens as changed", " ".join(workflow_problems(graph)))
card["properties"]["ausboss_workflow_switches"]["switches"][0]["off"] = {"1": {"megapixels": 0.41}}
self.assertEqual(workflow_problems(graph), [])
graph["nodes"][1]["widgets_values"] = {"steps": 8}
graph["nodes"][1]["mode"] = 4
card["properties"]["ausboss_workflow_switches"]["switches"].append(
{"id": 10, "title": "Turbo", "nodes": [2], "on": {"2": {"steps": 8}}, "off": {"2": {"steps": 20}}})
self.assertIn("row 'Turbo' opens as changed: values say on, nodes say off", " ".join(workflow_problems(graph)))
def test_workflow_id_and_zoom(self):
graph = good_graph()
graph["id"] = "3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b"