Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4d0b18920 | ||
|
|
bd8ead7ef9 | ||
|
|
3fd491c839 | ||
|
|
93bf3dc325 | ||
|
|
5753f4048a | ||
|
|
841bfa7d06 | ||
|
|
0ffb6a576b | ||
|
|
5656144a9f | ||
|
|
3b16d1e254 | ||
|
|
dff58e6b88 | ||
|
|
ef06093d56 | ||
|
|
8582a0219d |
@@ -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"
|
||||
|
||||
@@ -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
@@ -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 () {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 ""))
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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]"))
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user