diff --git a/README.md b/README.md index df859e6..52d7c89 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,13 @@ WanGP supports the Wan (and derived models), Hunyuan Video and LTV Video models **Follow DeepBeepMeep on Twitter/X to get the Latest News**: https://x.com/deepbeepmeep ## 🔥 Latest Updates : +### August 21 2025: WanGP v8.0 - the killer of seven + +- Qwen Image Edit : Flux Kontext challenger (prompt driven image edition). You should use it at high res (1080p) if you want to preserve identity of the original people / objects. It works with Qwen Lora Lightning 4 steps. I have also unlocked all the resolutions for Qwen models. Bonus Zone: support for multiple image compositions +- On demand Prompt Enhancer (need to be enabled in Configuration Tab) that you can use to Enhance a Text Prompt before starting a Generation. You can refine the Enhanced Prompt or change the original Prompt. +- Choice of a non censored Prompt Enhancer. Beware this is one is VRAM hungry and will require 12 GB of VRAM to work +- Memory Profile customizable per model : useful to set for instance Profile 3 (preload the model entirely in VRAM) with only Image Generation models, if you have 24 GB of VRAM. In that case Generation will be much faster because with Image generators (contrary to Video generators) as a lot of time is wasted in offloading +- Expert Guidance Mode: change the Guidance during the generation up to 2 times. Very useful with Wan 2.2 Ligthning to reduce the slow motion effect. The idea is to insert a CFG phase before the 2 accelerated phases that follow and have no Guidance. I have added the finetune *Wan2.2 Vace Lightning 3 Phases 14B* with a prebuilt configuration. Please note that it is a 8 steps process although the lora lightning is 4 steps. This expert guidance mode is also available with Wan 2.1. ### August 12 2025: WanGP v7.7777 - Lucky Day(s) diff --git a/defaults/hunyuan.json b/defaults/hunyuan.json index b02a7ea..a6ba832 100644 --- a/defaults/hunyuan.json +++ b/defaults/hunyuan.json @@ -5,7 +5,7 @@ "architecture" : "hunyuan", "description": "Probably the best text 2 video model available.", "URLs": [ - "https://huggingface.co/DeepBeepMeep/HunyuanVideo/resolve/main/hunyuan_video_720_bf16.safetensors.safetensors", + "https://huggingface.co/DeepBeepMeep/HunyuanVideo/resolve/main/hunyuan_video_720_bf16.safetensors", "https://huggingface.co/DeepBeepMeep/HunyuanVideo/resolve/main/hunyuan_video_720_quanto_int8.safetensors" ] } diff --git a/defaults/qwen_image_20B.json b/defaults/qwen_image_20B.json index 691afee..8e3fba5 100644 --- a/defaults/qwen_image_20B.json +++ b/defaults/qwen_image_20B.json @@ -2,12 +2,12 @@ "model": { "name": "Qwen Image 20B", "architecture": "qwen_image_20B", - "description": "Qwen Image is generative model that will very high quality images. It is one of the few models capable to generate in the image very long texts.", + "description": "Qwen Image is generative model that will generate very high quality images. It is one of the few models capable to generate in the image very long texts.", "URLs": [ "https://huggingface.co/DeepBeepMeep/Qwen_image/resolve/main/qwen_image_20B_bf16.safetensors", "https://huggingface.co/DeepBeepMeep/Qwen_image/resolve/main/qwen_image_20B_quanto_bf16_int8.safetensors" ], - "resolutions": [ ["1328x1328 (1:1)", "1328x1328"], + "xresolutions": [ ["1328x1328 (1:1)", "1328x1328"], ["1664x928 (16:9)", "1664x928"], ["928x1664 (9:16)", "928x1664"], ["1472x1140 (4:3)", "1472x1140"], @@ -16,6 +16,6 @@ "image_outputs": true }, "prompt": "draw a hat", - "resolution": "1280x720", + "resolution": "1920x1088", "batch_size": 1 } \ No newline at end of file diff --git a/defaults/qwen_image_edit_20B.json b/defaults/qwen_image_edit_20B.json new file mode 100644 index 0000000..183d418 --- /dev/null +++ b/defaults/qwen_image_edit_20B.json @@ -0,0 +1,19 @@ +{ + "model": { + "name": "Qwen Image Edit 20B", + "architecture": "qwen_image_edit_20B", + "description": "Qwen Image Edit is generative model that will generate very high quality images. It can be used to edit a Subject or combine multiple Subjects. It is one of the few models capable to generate in the image very long texts.", + "URLs": [ + "https://huggingface.co/DeepBeepMeep/Qwen_image/resolve/main/qwen_image_edit_20B_bf16.safetensors", + "https://huggingface.co/DeepBeepMeep/Qwen_image/resolve/main/qwen_image_edit_20B_quanto_bf16_int8.safetensors" + ], + "attention": { + "<89": "sdpa" + }, + "reference_image": true, + "image_outputs": true + }, + "prompt": "draw a hat", + "resolution": "1280x720", + "batch_size": 1 +} \ No newline at end of file diff --git a/defaults/vace_14B_lightning_3p_2_2.json b/defaults/vace_14B_lightning_3p_2_2.json new file mode 100644 index 0000000..cfa6ce5 --- /dev/null +++ b/defaults/vace_14B_lightning_3p_2_2.json @@ -0,0 +1,29 @@ +{ + "model": { + "name": "Wan2.2 Vace Lightning 3 Phases 14B", + "architecture": "vace_14B", + "modules": [ + "vace_14B" + ], + "description": "This finetune uses the Lightning 4 steps Loras Accelerator for Wan 2.2 but extend them to 8 steps in order to insert a CFG phase before the 2 accelerated phases with no Guidance. The ultimate goal is reduce the slow motion effect of these Loras Accelerators.", + "URLs": "t2v_2_2", + "URLs2": "t2v_2_2", + "loras": [ + "https://huggingface.co/DeepBeepMeep/Wan2.2/resolve/main/loras_accelerators/Wan2.2-Lightning_T2V-v1.1-A14B-4steps-lora_HIGH_fp16.safetensors", + "https://huggingface.co/DeepBeepMeep/Wan2.2/resolve/main/loras_accelerators/Wan2.2-Lightning_T2V-v1.1-A14B-4steps-lora_LOW_fp16.safetensors" + ], + "loras_multipliers": ["0;1;0", "0;0;1"], + "lock_guidance_phases": true, + "group": "wan2_2" + }, + "num_inference_steps": 8, + "guidance_phases": 3.1, + "guidance_scale": 3.5, + "guidance2_scale": 1, + "guidance3_scale": 1, + "switch_threshold": 965, + "switch_threshold2": 800, + "model_switch_phase": 2, + "flow_shift": 3, + "sample_solver": "euler" +} \ No newline at end of file diff --git a/docs/FINETUNES.md b/docs/FINETUNES.md index 32bc7c6..7f9dc3f 100644 --- a/docs/FINETUNES.md +++ b/docs/FINETUNES.md @@ -69,9 +69,25 @@ For instance if one adds a module *vace_14B* on top of a model with architecture -*visible* : by default assumed to be true. If set to false the model will no longer be visible. This can be useful if you create a finetune to override a default model and hide it. -*image_outputs* : turn any model that generates a video into a model that generates images. In fact it will adapt the user interface for image generation and ask the model to generate a video with a single frame. -In order to favor reusability the properties of *URLs*, *modules*, *loras* and *preload_URLs* can contain instead of a list of URLs a single text which corresponds to the id of a finetune or default model to reuse. +In order to favor reusability the properties of *URLs*, *modules*, *loras* and *preload_URLs* can contain instead of a list of URLs a single text which corresponds to the id of a finetune or default model to reuse. Instead of: +``` + "URLs": [ + "https://huggingface.co/DeepBeepMeep/Wan2.2/resolve/main/wan2.2_text2video_14B_high_mbf16.safetensors", + "https://huggingface.co/DeepBeepMeep/Wan2.2/resolve/main/wan2.2_text2video_14B_high_quanto_mbf16_int8.safetensors", + "https://huggingface.co/DeepBeepMeep/Wan2.2/resolve/main/wan2.2_text2video_14B_high_quanto_mfp16_int8.safetensors" + ], + "URLs2": [ + "https://huggingface.co/DeepBeepMeep/Wan2.2/resolve/main/wan2.2_text2video_14B_low_mbf16.safetensors", + "https://huggingface.co/DeepBeepMeep/Wan2.2/resolve/main/wan2.2_text2video_14B_low_quanto_mbf16_int8.safetensors", + "https://huggingface.co/DeepBeepMeep/Wan2.2/resolve/main/wan2.2_text2video_14B_low_quanto_mfp16_int8.safetensors" + ], +``` + You can write: +``` + "URLs": "t2v_2_2", + "URLs2": "t2v_2_2", +``` -For example let’s say you have defined a *t2v_fusionix.json* file which contains the URLs to download the finetune. In the *vace_fusionix.json* you can write « URLs » : « fusionix » to reuse automatically the URLS already defined in the correspond file. Example of **model** subtree ``` diff --git a/docs/LORAS.md b/docs/LORAS.md index 73a88ac..89b2e59 100644 --- a/docs/LORAS.md +++ b/docs/LORAS.md @@ -7,18 +7,21 @@ Loras (Low-Rank Adaptations) allow you to customize video generation models by a Loras are organized in different folders based on the model they're designed for: ### Wan Text-to-Video Models -- `loras/` - General t2v loras +- `loras/` - General t2v loras for Wan 2.1 (t2v only) and for all Wan 2.2 models +Optional sub folders: - `loras/1.3B/` - Loras specifically for 1.3B models +- `loras/5B/` - Loras specifically for 1.3B models - `loras/14B/` - Loras specifically for 14B models ### Wan Image-to-Video Models -- `loras_i2v/` - Image-to-video loras +- `loras_i2v/` - Image-to-video loras for Wan 2.1 ### Other Models - `loras_hunyuan/` - Hunyuan Video t2v loras - `loras_hunyuan_i2v/` - Hunyuan Video i2v loras - `loras_ltxv/` - LTX Video loras - `loras_flux/` - Flux loras +- `loras_qwen/` - Qwen loras ## Custom Lora Directory @@ -40,7 +43,9 @@ python wgp.py --lora-dir-hunyuan /path/to/hunyuan/loras --lora-dir-ltxv /path/to 2. Launch WanGP 3. In the Advanced Tab, select the "Loras" section 4. Check the loras you want to activate -5. Set multipliers for each lora (default is 1.0) +5. Set multipliers for each lora (default is 1.0 if multiplier is not mentioned) + +If you store loras in the loras folder once WanGP has been launched, click the *Refresh* button at the top so that it can become selectable. ### Lora Multipliers @@ -53,7 +58,7 @@ Multipliers control the strength of each lora's effect: - First lora: 1.2 strength - Second lora: 0.8 strength -#### Time-based Multipliers +#### Time-based and Phase-based Multipliers For dynamic effects over generation steps, use comma-separated values: ``` 0.9,0.8,0.7 @@ -75,7 +80,7 @@ Also with Wan 2.2, if you have two loras and you want the first one to be applie 1;0 0;1 ``` -As usual, you can use any float for of multiplier and have a multiplier varries throughout one phase for one Lora: +As usual, you can use any float for a multiplier and have a multiplier varries throughout one phase for one Lora: ``` 0.9,0.8;1.2,1.1,1 ``` @@ -87,7 +92,31 @@ Here is another example for two loras: 0.5;0,0.7 ``` -Note that the syntax for multipliers can also be used in a Finetune model definition file (except that each multiplier definition is a string in a json list) +If one of several of your Lora multipliers are phased based (that is with a ";") and there are also Loras Multipliers that are only time based (don't have a ";" but have a ",") the time only multiplier will ignore the phases. For instance, let's assume we have a 6 steps denoising process in the following example: + +``` +1;0 +0;1 +0.8,0.7,0.5 +``` +Here the first lora will be as expected only used with the High Noise model and the second lora only used with the Low noise model. However for the third Lora: for steps 1-2 the multiplier will be (regadless of the phase) 0.8 then for steps 3-4 the multiplier will be 0.7 and finally for steps 5-6 the multiplier will be 0.5 + +You can use phased Lora multipliers even if have a single model (that is without any High / Low models) as Lora multiplier phases are aligned with Guidance phases. Let's assume you have defined 3 guidance phases (for instance guidance=3, then guidance=1.5 and at last guidance=1 ): +``` +0;1;0 +0;0;1 +``` +In that case no lora will be applied during the first phase when guidance is 3. Then the fist lora will be only used when guidance is 1.5 and the second lora only when guidance is 1. + +Best of all you can combine 3 guidance phases with High / Low models. Let's take this practical example with *Lightning 4/8 steps loras accelerators for Wan 2.2* where we want to increase the motion by adding some guidance at the very beginning (in that case a first phase that lasts only 1 step should be sufficient): +``` +Guidances: 3.5, 1 and 1 +Model transition: Phase 2-3 +Loras Multipliers: 0;1;0 0;0;1 +``` +Here during the first phase with guidance 3.5, the High model will be used but there won't be any lora at all. Then during phase 2 only the High lora will be used (which requires to set the guidance to 1). At last in phase 3 WanGP will switch to the Low model and then only the Low lora will be used. + +*Note that the syntax for multipliers can also be used in a Finetune model definition file (except that each multiplier definition is a string in a json list)* ## Lora Presets Lora Presets are combinations of loras with predefined multipliers and prompts. @@ -125,15 +154,22 @@ WanGP supports multiple lora formats: ## Loras Accelerators Most Loras are used to apply a specific style or to alter the content of the output of the generated video. However some Loras have been designed to tranform a model into a distilled model which requires fewer steps to generate a video. +Loras accelerators usually require to the set the Guidance to 1. Don't forget to do it as not only the quality of the generate video will be bad but it will two times slower. -You will find most *Loras Accelerators* here: +You will find most *Loras Accelerators* below: +- Wan 2.1 https://huggingface.co/DeepBeepMeep/Wan2.1/tree/main/loras_accelerators +- Wan 2.2 +https://huggingface.co/DeepBeepMeep/Wan2.2/tree/main/loras_accelerators +- Qwen: +https://huggingface.co/DeepBeepMeep/Qwen_image/tree/main/loras_accelerators + ### Setup Instructions 1. Download the Lora 2. Place it in your `loras/` directory if it is a t2v lora or in the `loras_i2v/` directory if it isa i2v lora -## FusioniX (or FusionX) Lora +## FusioniX (or FusionX) Lora for Wan 2.1 / Wan 2.2 If you need just one Lora accelerator use this one. It is a combination of multiple Loras acelerators (including Causvid below) and style loras. It will not only accelerate the video generation but it will also improve the quality. There are two versions of this lora whether you use it for t2v or i2v ### Usage @@ -148,8 +184,8 @@ If you need just one Lora accelerator use this one. It is a combination of multi 5. Set generation steps from 8-10 6. Generate! -## Safe-Forcing lightx2v Lora (Video Generation Accelerator) -Safeforcing Lora has been created by Kijai from the Safe-Forcing lightx2v distilled Wan model and can generate videos with only 2 steps and offers also a 2x speed improvement since it doesnt require classifier free guidance. It works on both t2v and i2v models +## Self-Forcing lightx2v Lora (Video Generation Accelerator) for Wan 2.1 / Wan 2.2 +Selg forcing Lora has been created by Kijai from the Self-Forcing lightx2v distilled Wan model and can generate videos with only 2 steps and offers also a 2x speed improvement since it doesnt require classifier free guidance. It works on both t2v and i2v models You will find it under the name of *Wan21_T2V_14B_lightx2v_cfg_step_distill_lora_rank32.safetensors* ### Usage @@ -165,7 +201,7 @@ You will find it under the name of *Wan21_T2V_14B_lightx2v_cfg_step_distill_lora 6. Generate! -## CausVid Lora (Video Generation Accelerator) +## CausVid Lora (Video Generation Accelerator) for Wan 2.1 / Wan 2.2 CausVid is a distilled Wan model that generates videos in 4-12 steps with 2x speed improvement. ### Usage @@ -188,11 +224,10 @@ CausVid is a distilled Wan model that generates videos in 4-12 steps with 2x spe *Note: Lower steps = lower quality (especially motion)* -## AccVid Lora (Video Generation Accelerator) +## AccVid Lora (Video Generation Accelerator) for Wan 2.1 / Wan 2.2 AccVid is a distilled Wan model that generates videos with a 2x speed improvement since classifier free guidance is no longer needed (that is cfg = 1). - ### Usage 1. Select a Wan t2v model (e.g., Wan 2.1 text2video 13B or Vace 13B) or Wan i2v model 2. Enable Advanced Mode @@ -201,6 +236,21 @@ AccVid is a distilled Wan model that generates videos with a 2x speed improvemen - Set Shift Scale = 5 4. The number steps remain unchanged compared to what you would use with the original model but it will be two times faster since classifier free guidance is not needed +## Lightx2v 4 steps Lora (Video Generation Accelerator) for Wan 2.2 +This lora is in fact composed of two loras, one for the High model and one for the Low Wan 2.2 model. + +You need to select these two loras and set the following Loras multipliers: + +``` +1;0 0;1 (the High lora should be only enabled when only the High model is loaded, same for the Low lora) +``` + +Don't forget to set guidance to 1 ! +## Qwen Image Lightning 4 steps / Lightning 8 steps +Very powerful lora that you can use to reduce the number of steps from 30 to only 4 ! +Just install the lora in *lora_qwen* folder, select the lora and set Guidance to 1 and the number of steps to 4 or 8 + + https://huggingface.co/Kijai/WanVideo_comfy/blob/main/Wan21_T2V_14B_lightx2v_cfg_step_distill_lora_rank32.safetensors @@ -215,6 +265,7 @@ https://huggingface.co/Kijai/WanVideo_comfy/blob/main/Wan21_T2V_14B_lightx2v_cfg - Loras are loaded on-demand to save VRAM - Multiple loras can be used simultaneously - Time-based multipliers don't use extra memory +- The order of Loras doesn't matter (as long as the loras multipliers are in the right order of course !) ## Finding Loras @@ -266,6 +317,7 @@ In the video, a man is presented. The man is in a city and looks at his watch. ## Troubleshooting ### Lora Not Working +0. If it is a lora accelerator, Guidance should be set to 1 1. Check if lora is compatible with your model size (1.3B vs 14B) 2. Verify lora format is supported 3. Try different multiplier values @@ -287,12 +339,13 @@ In the video, a man is presented. The man is in a city and looks at his watch. ```bash # Lora-related command line options ---lora-dir path # Path to t2v loras directory +--lora-dir path # Path to t2v loras directory --lora-dir-i2v path # Path to i2v loras directory --lora-dir-hunyuan path # Path to Hunyuan t2v loras --lora-dir-hunyuan-i2v path # Path to Hunyuan i2v loras --lora-dir-ltxv path # Path to LTX Video loras --lora-dir-flux path # Path to Flux loras +--lora-dir-qwen path # Path to Qwen loras --lora-preset preset # Load preset on startup --check-loras # Filter incompatible loras ``` \ No newline at end of file diff --git a/models/flux/flux_handler.py b/models/flux/flux_handler.py index 9bdc0cf..8b59027 100644 --- a/models/flux/flux_handler.py +++ b/models/flux/flux_handler.py @@ -15,9 +15,7 @@ class family_handler(): "image_outputs" : True, "no_negative_prompt" : True, } - if flux_schnell: - model_def_output["no_guidance"] = True - else: + if not flux_schnell: model_def_output["embedded_guidance"] = True diff --git a/models/hyvideo/hunyuan_handler.py b/models/hyvideo/hunyuan_handler.py index dc9ba96..79e3083 100644 --- a/models/hyvideo/hunyuan_handler.py +++ b/models/hyvideo/hunyuan_handler.py @@ -42,7 +42,11 @@ class family_handler(): extra_model_def["frames_minimum"] = 5 extra_model_def["frames_steps"] = 4 extra_model_def["sliding_window"] = False - extra_model_def["embedded_guidance"] = base_model_type in ["hunyuan", "hunyuan_i2v"] + if base_model_type in ["hunyuan", "hunyuan_i2v"]: + extra_model_def["embedded_guidance"] = True + else: + extra_model_def["guidance_max_phases"] = 1 + extra_model_def["cfg_star"] = base_model_type in [ "hunyuan_avatar", "hunyuan_custom_audio", "hunyuan_custom_edit", "hunyuan_custom"] extra_model_def["tea_cache"] = True extra_model_def["mag_cache"] = True diff --git a/models/ltx_video/ltxv_handler.py b/models/ltx_video/ltxv_handler.py index cfdd069..d35bcd4 100644 --- a/models/ltx_video/ltxv_handler.py +++ b/models/ltx_video/ltxv_handler.py @@ -12,9 +12,7 @@ class family_handler(): def query_model_def(base_model_type, model_def): LTXV_config = model_def.get("LTXV_config", "") distilled= "distilled" in LTXV_config - extra_model_def = { - "no_guidance": True, - } + extra_model_def = {} if distilled: extra_model_def.update({ "lock_inference_steps": True, diff --git a/models/qwen/pipeline_qwenimage.py b/models/qwen/pipeline_qwenimage.py index ad39ae5..f9bc871 100644 --- a/models/qwen/pipeline_qwenimage.py +++ b/models/qwen/pipeline_qwenimage.py @@ -27,9 +27,30 @@ from diffusers.utils.torch_utils import randn_tensor from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, AutoTokenizer from .autoencoder_kl_qwenimage import AutoencoderKLQwenImage from diffusers import FlowMatchEulerDiscreteScheduler +from PIL import Image XLA_AVAILABLE = False +PREFERRED_QWENIMAGE_RESOLUTIONS = [ + (672, 1568), + (688, 1504), + (720, 1456), + (752, 1392), + (800, 1328), + (832, 1248), + (880, 1184), + (944, 1104), + (1024, 1024), + (1104, 944), + (1184, 880), + (1248, 832), + (1328, 800), + (1392, 752), + (1456, 720), + (1504, 688), + (1568, 672), +] + logger = logging.get_logger(__name__) # pylint: disable=invalid-name @@ -122,6 +143,18 @@ def retrieve_timesteps( timesteps = scheduler.timesteps return timesteps, num_inference_steps +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents +def retrieve_latents( + encoder_output: torch.Tensor, generator: Optional[torch.Generator] = None, sample_mode: str = "sample" +): + if hasattr(encoder_output, "latent_dist") and sample_mode == "sample": + return encoder_output.latent_dist.sample(generator) + elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax": + return encoder_output.latent_dist.mode() + elif hasattr(encoder_output, "latents"): + return encoder_output.latents + else: + raise AttributeError("Could not access latents of provided encoder_output") class QwenImagePipeline(): #DiffusionPipeline r""" @@ -151,20 +184,27 @@ class QwenImagePipeline(): #DiffusionPipeline text_encoder, tokenizer, transformer, + processor, ): self.vae=vae self.text_encoder=text_encoder self.tokenizer=tokenizer self.transformer=transformer + self.processor = processor + self.latent_channels = self.vae.config.z_dim if getattr(self, "vae", None) else 16 self.vae_scale_factor = 2 ** len(self.vae.temperal_downsample) if getattr(self, "vae", None) else 8 # QwenImage latents are turned into 2x2 patches and packed. This means the latent width and height has to be divisible # by the patch size. So the vae scale factor is multiplied by the patch size to account for this self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * 2) self.tokenizer_max_length = 1024 - self.prompt_template_encode = "<|im_start|>system\nDescribe the image by detailing the color, shape, size, texture, quantity, text, spatial relationships of the objects and background:<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n" - self.prompt_template_encode_start_idx = 34 + if processor is not None: + self.prompt_template_encode = "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n<|vision_start|><|image_pad|><|vision_end|>{}<|im_end|>\n<|im_start|>assistant\n" + self.prompt_template_encode_start_idx = 64 + else: + self.prompt_template_encode = "<|im_start|>system\nDescribe the image by detailing the color, shape, size, texture, quantity, text, spatial relationships of the objects and background:<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n" + self.prompt_template_encode_start_idx = 34 self.default_sample_size = 128 def _extract_masked_hidden(self, hidden_states: torch.Tensor, mask: torch.Tensor): @@ -178,6 +218,7 @@ class QwenImagePipeline(): #DiffusionPipeline def _get_qwen_prompt_embeds( self, prompt: Union[str, List[str]] = None, + image: Optional[torch.Tensor] = None, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None, ): @@ -189,16 +230,36 @@ class QwenImagePipeline(): #DiffusionPipeline template = self.prompt_template_encode drop_idx = self.prompt_template_encode_start_idx txt = [template.format(e) for e in prompt] - txt_tokens = self.tokenizer( - txt, max_length=self.tokenizer_max_length + drop_idx, padding=True, truncation=True, return_tensors="pt" - ).to(device) - encoder_hidden_states = self.text_encoder( - input_ids=txt_tokens.input_ids, - attention_mask=txt_tokens.attention_mask, - output_hidden_states=True, - ) - hidden_states = encoder_hidden_states.hidden_states[-1] - split_hidden_states = self._extract_masked_hidden(hidden_states, txt_tokens.attention_mask) + + if self.processor is not None and image is not None: + model_inputs = self.processor( + text=txt, + images=image, + padding=True, + return_tensors="pt", + ).to(device) + + outputs = self.text_encoder( + input_ids=model_inputs.input_ids, + attention_mask=model_inputs.attention_mask, + pixel_values=model_inputs.pixel_values, + image_grid_thw=model_inputs.image_grid_thw, + output_hidden_states=True, + ) + hidden_states = outputs.hidden_states[-1] + split_hidden_states = self._extract_masked_hidden(hidden_states, model_inputs.attention_mask) + else: + txt_tokens = self.tokenizer( + txt, max_length=self.tokenizer_max_length + drop_idx, padding=True, truncation=True, return_tensors="pt" + ).to(device) + encoder_hidden_states = self.text_encoder( + input_ids=txt_tokens.input_ids, + attention_mask=txt_tokens.attention_mask, + output_hidden_states=True, + ) + hidden_states = encoder_hidden_states.hidden_states[-1] + split_hidden_states = self._extract_masked_hidden(hidden_states, txt_tokens.attention_mask) + split_hidden_states = [e[drop_idx:] for e in split_hidden_states] attn_mask_list = [torch.ones(e.size(0), dtype=torch.long, device=e.device) for e in split_hidden_states] max_seq_len = max([e.size(0) for e in split_hidden_states]) @@ -216,6 +277,7 @@ class QwenImagePipeline(): #DiffusionPipeline def encode_prompt( self, prompt: Union[str, List[str]], + image: Optional[torch.Tensor] = None, device: Optional[torch.device] = None, num_images_per_prompt: int = 1, prompt_embeds: Optional[torch.Tensor] = None, @@ -227,6 +289,8 @@ class QwenImagePipeline(): #DiffusionPipeline Args: prompt (`str` or `List[str]`, *optional*): prompt to be encoded + image (`torch.Tensor`, *optional*): + image to be encoded device: (`torch.device`): torch device num_images_per_prompt (`int`): @@ -241,7 +305,7 @@ class QwenImagePipeline(): #DiffusionPipeline batch_size = len(prompt) if prompt_embeds is None else prompt_embeds.shape[0] if prompt_embeds is None: - prompt_embeds, prompt_embeds_mask = self._get_qwen_prompt_embeds(prompt, device) + prompt_embeds, prompt_embeds_mask = self._get_qwen_prompt_embeds(prompt, image, device) _, seq_len, _ = prompt_embeds.shape prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) @@ -251,6 +315,7 @@ class QwenImagePipeline(): #DiffusionPipeline return prompt_embeds, prompt_embeds_mask + def check_inputs( self, prompt, @@ -344,6 +409,29 @@ class QwenImagePipeline(): #DiffusionPipeline return latents + + def _encode_vae_image(self, image: torch.Tensor, generator: torch.Generator): + if isinstance(generator, list): + image_latents = [ + retrieve_latents(self.vae.encode(image[i : i + 1]), generator=generator[i], sample_mode="argmax") + for i in range(image.shape[0]) + ] + image_latents = torch.cat(image_latents, dim=0) + else: + image_latents = retrieve_latents(self.vae.encode(image), generator=generator, sample_mode="argmax") + latents_mean = ( + torch.tensor(self.vae.config.latents_mean) + .view(1, self.latent_channels, 1, 1, 1) + .to(image_latents.device, image_latents.dtype) + ) + latents_std = ( + torch.tensor(self.vae.config.latents_std) + .view(1, self.latent_channels, 1, 1, 1) + .to(image_latents.device, image_latents.dtype) + ) + image_latents = (image_latents - latents_mean) / latents_std + + return image_latents def enable_vae_slicing(self): r""" Enable sliced VAE decoding. When this option is enabled, the VAE will split the input tensor in slices to @@ -375,6 +463,7 @@ class QwenImagePipeline(): #DiffusionPipeline def prepare_latents( self, + image, batch_size, num_channels_latents, height, @@ -391,22 +480,41 @@ class QwenImagePipeline(): #DiffusionPipeline shape = (batch_size, 1, num_channels_latents, height, width) - if latents is not None: - latent_image_ids = self._prepare_latent_image_ids(batch_size, height // 2, width // 2, device, dtype) - return latents.to(device=device, dtype=dtype), latent_image_ids + image_latents = None + if image is not None: + image = image.to(device=device, dtype=dtype) + if image.shape[1] != self.latent_channels: + image_latents = self._encode_vae_image(image=image, generator=generator) + else: + image_latents = image + if batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] == 0: + # expand init_latents for batch_size + additional_image_per_prompt = batch_size // image_latents.shape[0] + image_latents = torch.cat([image_latents] * additional_image_per_prompt, dim=0) + elif batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] != 0: + raise ValueError( + f"Cannot duplicate `image` of batch size {image_latents.shape[0]} to {batch_size} text prompts." + ) + else: + image_latents = torch.cat([image_latents], dim=0) + + image_latent_height, image_latent_width = image_latents.shape[3:] + image_latents = self._pack_latents( + image_latents, batch_size, num_channels_latents, image_latent_height, image_latent_width + ) if isinstance(generator, list) and len(generator) != batch_size: raise ValueError( f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" f" size of {batch_size}. Make sure the batch size matches the length of the generators." ) + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + latents = self._pack_latents(latents, batch_size, num_channels_latents, height, width) + else: + latents = latents.to(device=device, dtype=dtype) - latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) - latents = self._pack_latents(latents, batch_size, num_channels_latents, height, width) - - latent_image_ids = self._prepare_latent_image_ids(batch_size, height // 2, width // 2, device, dtype) - - return latents, latent_image_ids + return latents, image_latents @property def guidance_scale(self): @@ -453,6 +561,7 @@ class QwenImagePipeline(): #DiffusionPipeline callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = None, callback_on_step_end_tensor_inputs: List[str] = ["latents"], max_sequence_length: int = 512, + image = None, callback=None, pipeline=None, loras_slists=None, @@ -540,6 +649,10 @@ class QwenImagePipeline(): #DiffusionPipeline height = height or self.default_sample_size * self.vae_scale_factor width = width or self.default_sample_size * self.vae_scale_factor + multiple_of = self.vae_scale_factor * 2 + width = width // multiple_of * multiple_of + height = height // multiple_of * multiple_of + # 1. Check inputs. Raise error if not correct self.check_inputs( prompt, @@ -567,13 +680,30 @@ class QwenImagePipeline(): #DiffusionPipeline else: batch_size = prompt_embeds.shape[0] device = "cuda" - # device = self._execution_device + + prompt_image = None + if image is not None and not (isinstance(image, torch.Tensor) and image.size(1) == self.latent_channels): + image = image[0] if isinstance(image, list) else image + image_height, image_width = self.image_processor.get_default_height_width(image) + aspect_ratio = image_width / image_height + if True : + _, image_width, image_height = min( + (abs(aspect_ratio - w / h), w, h) for w, h in PREFERRED_QWENIMAGE_RESOLUTIONS + ) + image_width = image_width // multiple_of * multiple_of + image_height = image_height // multiple_of * multiple_of + # image = self.image_processor.resize(image, image_height, image_width) + image = image.resize((image_width,image_height), resample=Image.Resampling.LANCZOS) + prompt_image = image + image = self.image_processor.preprocess(image, image_height, image_width) + image = image.unsqueeze(2) has_neg_prompt = negative_prompt is not None or ( negative_prompt_embeds is not None and negative_prompt_embeds_mask is not None ) do_true_cfg = true_cfg_scale > 1 and has_neg_prompt prompt_embeds, prompt_embeds_mask = self.encode_prompt( + image=prompt_image, prompt=prompt, prompt_embeds=prompt_embeds, prompt_embeds_mask=prompt_embeds_mask, @@ -583,6 +713,7 @@ class QwenImagePipeline(): #DiffusionPipeline ) if do_true_cfg: negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt( + image=prompt_image, prompt=negative_prompt, prompt_embeds=negative_prompt_embeds, prompt_embeds_mask=negative_prompt_embeds_mask, @@ -597,7 +728,8 @@ class QwenImagePipeline(): #DiffusionPipeline # 4. Prepare latent variables num_channels_latents = self.transformer.in_channels // 4 - latents, latent_image_ids = self.prepare_latents( + latents, image_latents = self.prepare_latents( + image, batch_size * num_images_per_prompt, num_channels_latents, height, @@ -607,7 +739,15 @@ class QwenImagePipeline(): #DiffusionPipeline generator, latents, ) - img_shapes = [(1, height // self.vae_scale_factor // 2, width // self.vae_scale_factor // 2)] * batch_size + if image is not None: + img_shapes = [ + [ + (1, height // self.vae_scale_factor // 2, width // self.vae_scale_factor // 2), + (1, image_height // self.vae_scale_factor // 2, image_width // self.vae_scale_factor // 2), + ] + ] * batch_size + else: + img_shapes = [(1, height // self.vae_scale_factor // 2, width // self.vae_scale_factor // 2)] * batch_size # 5. Prepare timesteps sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas @@ -639,6 +779,11 @@ class QwenImagePipeline(): #DiffusionPipeline if self.attention_kwargs is None: self._attention_kwargs = {} + txt_seq_lens = prompt_embeds_mask.sum(dim=1).tolist() if prompt_embeds_mask is not None else None + negative_txt_seq_lens = ( + negative_prompt_embeds_mask.sum(dim=1).tolist() if negative_prompt_embeds_mask is not None else None + ) + # 6. Denoising loop self.scheduler.set_begin_index(0) updated_num_steps= len(timesteps) @@ -655,46 +800,54 @@ class QwenImagePipeline(): #DiffusionPipeline # broadcast to batch dimension in a way that's compatible with ONNX/Core ML timestep = t.expand(latents.shape[0]).to(latents.dtype) + latent_model_input = latents + if image_latents is not None: + latent_model_input = torch.cat([latents, image_latents], dim=1) + if do_true_cfg and joint_pass: noise_pred, neg_noise_pred = self.transformer( - hidden_states=latents, + hidden_states=latent_model_input, timestep=timestep / 1000, guidance=guidance, encoder_hidden_states_mask_list=[prompt_embeds_mask,negative_prompt_embeds_mask], encoder_hidden_states_list=[prompt_embeds, negative_prompt_embeds], img_shapes=img_shapes, - txt_seq_lens_list=[prompt_embeds_mask.sum(dim=1).tolist(),negative_prompt_embeds_mask.sum(dim=1).tolist()], + txt_seq_lens_list=[txt_seq_lens, negative_txt_seq_lens], attention_kwargs=self.attention_kwargs, **kwargs ) if noise_pred == None: return None + noise_pred = noise_pred[:, : latents.size(1)] + neg_noise_pred = neg_noise_pred[:, : latents.size(1)] else: noise_pred = self.transformer( - hidden_states=latents, + hidden_states=latent_model_input, timestep=timestep / 1000, guidance=guidance, encoder_hidden_states_mask_list=[prompt_embeds_mask], encoder_hidden_states_list=[prompt_embeds], img_shapes=img_shapes, - txt_seq_lens_list=[prompt_embeds_mask.sum(dim=1).tolist()], + txt_seq_lens_list=[txt_seq_lens], attention_kwargs=self.attention_kwargs, **kwargs )[0] if noise_pred == None: return None + noise_pred = noise_pred[:, : latents.size(1)] if do_true_cfg: neg_noise_pred = self.transformer( - hidden_states=latents, + hidden_states=latent_model_input, timestep=timestep / 1000, guidance=guidance, encoder_hidden_states_mask_list=[negative_prompt_embeds_mask], encoder_hidden_states_list=[negative_prompt_embeds], img_shapes=img_shapes, - txt_seq_lens_list=[negative_prompt_embeds_mask.sum(dim=1).tolist()], + txt_seq_lens_list=[negative_txt_seq_lens], attention_kwargs=self.attention_kwargs, **kwargs )[0] if neg_noise_pred == None: return None + neg_noise_pred = neg_noise_pred[:, : latents.size(1)] if do_true_cfg: comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred) diff --git a/models/qwen/qwen_handler.py b/models/qwen/qwen_handler.py index 6db6a76..c6004e1 100644 --- a/models/qwen/qwen_handler.py +++ b/models/qwen/qwen_handler.py @@ -13,7 +13,8 @@ class family_handler(): "image_outputs" : True, "sample_solvers":[ ("Default", "default"), - ("Lightning", "lightning")] + ("Lightning", "lightning")], + "guidance_max_phases" : 1, } @@ -21,7 +22,7 @@ class family_handler(): @staticmethod def query_supported_types(): - return ["qwen_image_20B"] + return ["qwen_image_20B", "qwen_image_edit_20B"] @staticmethod def query_family_maps(): @@ -41,7 +42,7 @@ class family_handler(): return { "repoId" : "DeepBeepMeep/Qwen_image", "sourceFolderList" : ["", "Qwen2.5-VL-7B-Instruct"], - "fileList" : [ ["qwen_vae.safetensors", "qwen_vae_config.json"], ["merges.txt", "tokenizer_config.json", "config.json", "vocab.json"] + computeList(text_encoder_filename) ] + "fileList" : [ ["qwen_vae.safetensors", "qwen_vae_config.json"], ["merges.txt", "tokenizer_config.json", "config.json", "vocab.json", "video_preprocessor_config.json", "preprocessor_config.json"] + computeList(text_encoder_filename) ] } @staticmethod diff --git a/models/qwen/qwen_main.py b/models/qwen/qwen_main.py index ccaa758..8f38427 100644 --- a/models/qwen/qwen_main.py +++ b/models/qwen/qwen_main.py @@ -12,10 +12,24 @@ from .transformer_qwenimage import QwenImageTransformer2DModel from diffusers.utils import logging, replace_example_docstring from diffusers.utils.torch_utils import randn_tensor -from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, AutoTokenizer +from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, AutoTokenizer, Qwen2VLProcessor from .autoencoder_kl_qwenimage import AutoencoderKLQwenImage from diffusers import FlowMatchEulerDiscreteScheduler from .pipeline_qwenimage import QwenImagePipeline +from PIL import Image +from shared.utils.utils import calculate_new_dimensions + +def stitch_images(img1, img2): + # Resize img2 to match img1's height + width1, height1 = img1.size + width2, height2 = img2.size + new_width2 = int(width2 * height1 / height2) + img2_resized = img2.resize((new_width2, height1), Image.Resampling.LANCZOS) + + stitched = Image.new('RGB', (width1 + new_width2, height1)) + stitched.paste(img1, (0, 0)) + stitched.paste(img2_resized, (width1, 0)) + return stitched class model_factory(): def __init__( @@ -35,9 +49,16 @@ class model_factory(): transformer_filename = model_filename[0] - tokenizer = AutoTokenizer.from_pretrained(os.path.join(checkpoint_dir,"Qwen2.5-VL-7B-Instruct")) + processor = None + tokenizer = None + if base_model_type == "qwen_image_edit_20B": + processor = Qwen2VLProcessor.from_pretrained(os.path.join(checkpoint_dir,"Qwen2.5-VL-7B-Instruct")) + else: + tokenizer = AutoTokenizer.from_pretrained(os.path.join(checkpoint_dir,"Qwen2.5-VL-7B-Instruct")) - with open("configs/qwen_image_20B.json", 'r', encoding='utf-8') as f: + + base_config_file = "configs/qwen_image_20B.json" + with open(base_config_file, 'r', encoding='utf-8') as f: transformer_config = json.load(f) transformer_config.pop("_diffusers_version") transformer_config.pop("_class_name") @@ -46,9 +67,22 @@ class model_factory(): from accelerate import init_empty_weights with init_empty_weights(): transformer = QwenImageTransformer2DModel(**transformer_config) - offload.load_model_data(transformer, transformer_filename) + source = model_def.get("source", None) + + if source is not None: + offload.load_model_data(transformer, source) + else: + offload.load_model_data(transformer, transformer_filename) # transformer = offload.fast_load_transformers_model("transformer_quanto.safetensors", writable_tensors= True , modelClass=QwenImageTransformer2DModel, defaultConfigPath="transformer_config.json") + if not source is None: + from wgp import save_model + save_model(transformer, model_type, dtype, None) + + if save_quantized: + from wgp import save_quantized_model + save_quantized_model(transformer, model_type, model_filename[0], dtype, base_config_file) + text_encoder = offload.fast_load_transformers_model(text_encoder_filename, writable_tensors= True , modelClass=Qwen2_5_VLForConditionalGeneration, defaultConfigPath= os.path.join(checkpoint_dir, "Qwen2.5-VL-7B-Instruct", "config.json")) # text_encoder = offload.fast_load_transformers_model(text_encoder_filename, do_quantize=True, writable_tensors= True , modelClass=Qwen2_5_VLForConditionalGeneration, defaultConfigPath="text_encoder_config.json", verboseLevel=2) # text_encoder.to(torch.float16) @@ -56,11 +90,12 @@ class model_factory(): vae = offload.fast_load_transformers_model( os.path.join(checkpoint_dir,"qwen_vae.safetensors"), writable_tensors= True , modelClass=AutoencoderKLQwenImage, defaultConfigPath=os.path.join(checkpoint_dir,"qwen_vae_config.json")) - self.pipeline = QwenImagePipeline(vae, text_encoder, tokenizer, transformer) + self.pipeline = QwenImagePipeline(vae, text_encoder, tokenizer, transformer, processor) self.vae=vae self.text_encoder=text_encoder self.tokenizer=tokenizer self.transformer=transformer + self.processor = processor def generate( self, @@ -141,19 +176,31 @@ class model_factory(): if n_prompt is None or len(n_prompt) == 0: n_prompt= "text, watermark, copyright, blurry, low resolution" + if input_ref_images is not None: + # image stiching method + stiched = input_ref_images[0] + if "K" in video_prompt_type : + w, h = input_ref_images[0].size + height, width = calculate_new_dimensions(height, width, h, w, fit_into_canvas) + + for new_img in input_ref_images[1:]: + stiched = stitch_images(stiched, new_img) + input_ref_images = [stiched] + image = self.pipeline( - prompt=input_prompt, - negative_prompt=n_prompt, - width=width, - height=height, - num_inference_steps=sampling_steps, - num_images_per_prompt = batch_size, - true_cfg_scale=guide_scale, - callback = callback, - pipeline=self, - loras_slists=loras_slists, - joint_pass = joint_pass, - generator=torch.Generator(device="cuda").manual_seed(seed) + prompt=input_prompt, + negative_prompt=n_prompt, + image = input_ref_images, + width=width, + height=height, + num_inference_steps=sampling_steps, + num_images_per_prompt = batch_size, + true_cfg_scale=guide_scale, + callback = callback, + pipeline=self, + loras_slists=loras_slists, + joint_pass = joint_pass, + generator=torch.Generator(device="cuda").manual_seed(seed) ) if image is None: return None return image.transpose(0, 1) diff --git a/models/qwen/transformer_qwenimage.py b/models/qwen/transformer_qwenimage.py index 042032d..8751648 100644 --- a/models/qwen/transformer_qwenimage.py +++ b/models/qwen/transformer_qwenimage.py @@ -26,6 +26,7 @@ from diffusers.models.embeddings import TimestepEmbedding, Timesteps from diffusers.models.modeling_outputs import Transformer2DModelOutput from diffusers.models.normalization import AdaLayerNormContinuous, RMSNorm from shared.attention import pay_attention +import functools def get_timestep_embedding( timesteps: torch.Tensor, @@ -150,8 +151,8 @@ class QwenEmbedRope(nn.Module): super().__init__() self.theta = theta self.axes_dim = axes_dim - pos_index = torch.arange(1024) - neg_index = torch.arange(1024).flip(0) * -1 - 1 + pos_index = torch.arange(4096) + neg_index = torch.arange(4096).flip(0) * -1 - 1 self.pos_freqs = torch.cat( [ self.rope_params(pos_index, self.axes_dim[0], self.theta), @@ -170,7 +171,7 @@ class QwenEmbedRope(nn.Module): ) self.rope_cache = {} - # 是否使用 scale rope + # DO NOT USING REGISTER BUFFER HERE, IT WILL CAUSE COMPLEX NUMBERS LOSE ITS IMAGINARY PART self.scale_rope = scale_rope def rope_params(self, index, dim, theta=10000): @@ -194,38 +195,54 @@ class QwenEmbedRope(nn.Module): if isinstance(video_fhw, list): video_fhw = video_fhw[0] - frame, height, width = video_fhw - rope_key = f"{frame}_{height}_{width}" + if not isinstance(video_fhw, list): + video_fhw = [video_fhw] - if rope_key not in self.rope_cache: - seq_lens = frame * height * width - freqs_pos = self.pos_freqs.split([x // 2 for x in self.axes_dim], dim=1) - freqs_neg = self.neg_freqs.split([x // 2 for x in self.axes_dim], dim=1) - freqs_frame = freqs_pos[0][:frame].view(frame, 1, 1, -1).expand(frame, height, width, -1) - if self.scale_rope: - freqs_height = torch.cat([freqs_neg[1][-(height - height // 2) :], freqs_pos[1][: height // 2]], dim=0) - freqs_height = freqs_height.view(1, height, 1, -1).expand(frame, height, width, -1) - freqs_width = torch.cat([freqs_neg[2][-(width - width // 2) :], freqs_pos[2][: width // 2]], dim=0) - freqs_width = freqs_width.view(1, 1, width, -1).expand(frame, height, width, -1) + vid_freqs = [] + max_vid_index = 0 + for idx, fhw in enumerate(video_fhw): + frame, height, width = fhw + rope_key = f"{idx}_{height}_{width}" + if not torch.compiler.is_compiling(): + if rope_key not in self.rope_cache: + self.rope_cache[rope_key] = self._compute_video_freqs(frame, height, width, idx) + video_freq = self.rope_cache[rope_key] else: - freqs_height = freqs_pos[1][:height].view(1, height, 1, -1).expand(frame, height, width, -1) - freqs_width = freqs_pos[2][:width].view(1, 1, width, -1).expand(frame, height, width, -1) + video_freq = self._compute_video_freqs(frame, height, width, idx) + video_freq = video_freq.to(device) + vid_freqs.append(video_freq) - freqs = torch.cat([freqs_frame, freqs_height, freqs_width], dim=-1).reshape(seq_lens, -1) - self.rope_cache[rope_key] = freqs.clone().contiguous() - vid_freqs = self.rope_cache[rope_key] - - if self.scale_rope: - max_vid_index = max(height // 2, width // 2) - else: - max_vid_index = max(height, width) + if self.scale_rope: + max_vid_index = max(height // 2, width // 2, max_vid_index) + else: + max_vid_index = max(height, width, max_vid_index) max_len = max(txt_seq_lens) txt_freqs = self.pos_freqs[max_vid_index : max_vid_index + max_len, ...] + vid_freqs = torch.cat(vid_freqs, dim=0) return vid_freqs, txt_freqs + @functools.lru_cache(maxsize=None) + def _compute_video_freqs(self, frame, height, width, idx=0): + seq_lens = frame * height * width + freqs_pos = self.pos_freqs.split([x // 2 for x in self.axes_dim], dim=1) + freqs_neg = self.neg_freqs.split([x // 2 for x in self.axes_dim], dim=1) + + freqs_frame = freqs_pos[0][idx : idx + frame].view(frame, 1, 1, -1).expand(frame, height, width, -1) + if self.scale_rope: + freqs_height = torch.cat([freqs_neg[1][-(height - height // 2) :], freqs_pos[1][: height // 2]], dim=0) + freqs_height = freqs_height.view(1, height, 1, -1).expand(frame, height, width, -1) + freqs_width = torch.cat([freqs_neg[2][-(width - width // 2) :], freqs_pos[2][: width // 2]], dim=0) + freqs_width = freqs_width.view(1, 1, width, -1).expand(frame, height, width, -1) + else: + freqs_height = freqs_pos[1][:height].view(1, height, 1, -1).expand(frame, height, width, -1) + freqs_width = freqs_pos[2][:width].view(1, 1, width, -1).expand(frame, height, width, -1) + + freqs = torch.cat([freqs_frame, freqs_height, freqs_width], dim=-1).reshape(seq_lens, -1) + return freqs.clone().contiguous() + class QwenDoubleStreamAttnProcessor2_0: """ diff --git a/models/wan/any2video.py b/models/wan/any2video.py index c879701..c2493ab 100644 --- a/models/wan/any2video.py +++ b/models/wan/any2video.py @@ -393,7 +393,11 @@ class WanAny2V: sampling_steps=50, guide_scale=5.0, guide2_scale = 5.0, + guide3_scale = 5.0, switch_threshold = 0, + switch2_threshold = 0, + guide_phases= 1 , + model_switch_phase = 1, n_prompt="", seed=-1, callback = None, @@ -427,6 +431,7 @@ class WanAny2V: prefix_frames_count = 0, image_mode = 0, window_no = 0, + set_header_text = None, **bbargs ): @@ -745,16 +750,25 @@ class WanAny2V: # init denoising updated_num_steps= len(timesteps) - if callback != None: - from shared.utils.loras_mutipliers import update_loras_slists - model_switch_step = updated_num_steps - for i, t in enumerate(timesteps): - if t <= switch_threshold: - model_switch_step = i - break - update_loras_slists(self.model, loras_slists, updated_num_steps, model_switch_step= model_switch_step) - if self.model2 is not None: update_loras_slists(self.model2, loras_slists, updated_num_steps, model_switch_step= model_switch_step) - callback(-1, None, True, override_num_inference_steps = updated_num_steps) + + denoising_extra = "" + from shared.utils.loras_mutipliers import update_loras_slists, get_model_switch_steps + + phase_switch_step, phase_switch_step2, phases_description = get_model_switch_steps(timesteps, updated_num_steps, guide_phases, 0 if self.model2 is None else model_switch_phase, switch_threshold, switch2_threshold ) + if len(phases_description) > 0: set_header_text(phases_description) + guidance_switch_done = guidance_switch2_done = False + if guide_phases > 1: denoising_extra = f"Phase 1/{guide_phases} High Noise" if self.model2 is not None else f"Phase 1/{guide_phases}" + def update_guidance(step_no, t, guide_scale, new_guide_scale, guidance_switch_done, switch_threshold, trans, phase_no, denoising_extra): + if guide_phases >= phase_no and not guidance_switch_done and t <= switch_threshold: + if model_switch_phase == phase_no-1 and self.model2 is not None: trans = self.model2 + guide_scale, guidance_switch_done = new_guide_scale, True + denoising_extra = f"Phase {phase_no}/{guide_phases} {'Low Noise' if trans == self.model2 else 'High Noise'}" if self.model2 is not None else f"Phase {phase_no}/{guide_phases}" + callback(step_no-1, denoising_extra = denoising_extra) + return guide_scale, guidance_switch_done, trans, denoising_extra + update_loras_slists(self.model, loras_slists, updated_num_steps, phase_switch_step= phase_switch_step, phase_switch_step2= phase_switch_step2) + if self.model2 is not None: update_loras_slists(self.model2, loras_slists, updated_num_steps, phase_switch_step= phase_switch_step, phase_switch_step2= phase_switch_step2) + callback(-1, None, True, override_num_inference_steps = updated_num_steps, denoising_extra = denoising_extra) + if sample_scheduler != None: scheduler_kwargs = {} if isinstance(sample_scheduler, FlowMatchScheduler) else {"generator": seed_g} @@ -766,16 +780,12 @@ class WanAny2V: text_momentumbuffer = MomentumBuffer(apg_momentum) audio_momentumbuffer = MomentumBuffer(apg_momentum) - guidance_switch_done = False # denoising trans = self.model for i, t in enumerate(tqdm(timesteps)): - if not guidance_switch_done and t <= switch_threshold: - guide_scale = guide2_scale - if self.model2 is not None: trans = self.model2 - guidance_switch_done = True - + guide_scale, guidance_switch_done, trans, denoising_extra = update_guidance(i, t, guide_scale, guide2_scale, guidance_switch_done, switch_threshold, trans, 2, denoising_extra) + guide_scale, guidance_switch2_done, trans, denoising_extra = update_guidance(i, t, guide_scale, guide3_scale, guidance_switch2_done, switch2_threshold, trans, 3, denoising_extra) offload.set_step_no_for_lora(trans, i) timestep = torch.stack([t]) @@ -920,7 +930,7 @@ class WanAny2V: if trim_frames > 0: latents_preview= latents_preview[:, :,:-trim_frames] if image_outputs: latents_preview= latents_preview[:, :,:1] if len(latents_preview) > 1: latents_preview = latents_preview.transpose(0,2) - callback(i, latents_preview[0], False) + callback(i, latents_preview[0], False, denoising_extra =denoising_extra ) latents_preview = None if timestep_injection: diff --git a/models/wan/df_handler.py b/models/wan/df_handler.py index bf7c266..bc79e2e 100644 --- a/models/wan/df_handler.py +++ b/models/wan/df_handler.py @@ -24,6 +24,8 @@ class family_handler(): extra_model_def["sliding_window"] = True extra_model_def["skip_layer_guidance"] = True extra_model_def["tea_cache"] = True + extra_model_def["guidance_max_phases"] = 1 + return extra_model_def @staticmethod diff --git a/models/wan/modules/model.py b/models/wan/modules/model.py index d36efc2..8f9695b 100644 --- a/models/wan/modules/model.py +++ b/models/wan/modules/model.py @@ -767,6 +767,15 @@ class WanModel(ModelMixin, ConfigMixin): if first == None: return sd + new_sd = {} + + # for k,v in sd.items(): + # if k.endswith("modulation.diff"): + # pass + # else: + # new_sd[ k] = v + # sd = new_sd + # if first.startswith("blocks."): # new_sd = {} # for k,v in sd.items(): diff --git a/models/wan/wan_handler.py b/models/wan/wan_handler.py index 6d222a2..dc55a15 100644 --- a/models/wan/wan_handler.py +++ b/models/wan/wan_handler.py @@ -7,6 +7,9 @@ def test_class_i2v(base_model_type): def test_class_1_3B(base_model_type): return base_model_type in [ "vace_1.3B", "t2v_1.3B", "recam_1.3B","phantom_1.3B","fun_inp_1.3B"] +def test_multitalk(base_model_type): + return base_model_type in ["multitalk", "vace_multitalk_14B", "i2v_2_2_multitalk"] + class family_handler(): @staticmethod @@ -79,11 +82,11 @@ class family_handler(): extra_model_def["no_steps_skipping"] = True i2v = test_class_i2v(base_model_type) extra_model_def["i2v_class"] = i2v - extra_model_def["multitalk_class"] = base_model_type in ["multitalk", "vace_multitalk_14B", "i2v_2_2_multitalk"] + extra_model_def["multitalk_class"] = test_multitalk(base_model_type) vace_class = base_model_type in ["vace_14B", "vace_1.3B", "vace_multitalk_14B"] extra_model_def["vace_class"] = vace_class - if base_model_type in ["multitalk", "vace_multitalk_14B", "i2v_2_2_multitalk"]: + if test_multitalk(base_model_type): fps = 25 elif base_model_type in ["fantasy"]: fps = 23 @@ -92,7 +95,7 @@ class family_handler(): else: fps = 16 extra_model_def["fps"] =fps - + multiple_submodels = "URLs2" in model_def if vace_class: frames_minimum, frames_steps = 17, 4 else: @@ -101,12 +104,13 @@ class family_handler(): "frames_minimum" : frames_minimum, "frames_steps" : frames_steps, "sliding_window" : base_model_type in ["multitalk", "t2v", "fantasy"] or test_class_i2v(base_model_type) or vace_class, #"ti2v_2_2", - "guidance_max_phases" : 2, + "multiple_submodels" : multiple_submodels, + "guidance_max_phases" : 3, "skip_layer_guidance" : True, "cfg_zero" : True, "cfg_star" : True, "adaptive_projected_guidance" : True, - "tea_cache" : not (base_model_type in ["i2v_2_2", "ti2v_2_2" ] or "URLs2" in model_def), + "tea_cache" : not (base_model_type in ["i2v_2_2", "ti2v_2_2" ] or multiple_submodels), "mag_cache" : True, "sample_solvers":[ ("unipc", "unipc"), @@ -157,8 +161,7 @@ class family_handler(): @staticmethod def get_rgb_factors(base_model_type ): from shared.RGB_factors import get_rgb_factors - if base_model_type == "ti2v_2_2": return None, None - latent_rgb_factors, latent_rgb_factors_bias = get_rgb_factors("wan") + latent_rgb_factors, latent_rgb_factors_bias = get_rgb_factors("wan", base_model_type) return latent_rgb_factors, latent_rgb_factors_bias @staticmethod @@ -218,6 +221,10 @@ class family_handler(): if ui_defaults.get("sample_solver", "") == "": ui_defaults["sample_solver"] = "unipc" + if settings_version < 2.24: + if model_def.get("multiple_submodels", False) or ui_defaults.get("switch_threshold", 0) > 0: + ui_defaults["guidance_phases"] = 2 + @staticmethod def update_default_settings(base_model_type, model_def, ui_defaults): ui_defaults.update({ @@ -233,7 +240,6 @@ class family_handler(): ui_defaults.update({ "guidance_scale": 5.0, "flow_shift": 7, # 11 for 720p - "audio_guidance_scale": 4, "sliding_window_discard_last_frames" : 4, "sample_solver" : "euler", "adaptive_switch" : 1, @@ -258,4 +264,10 @@ class family_handler(): "image_prompt_type": "T", }) - \ No newline at end of file + if test_multitalk(base_model_type): + ui_defaults["audio_guidance_scale"] = 4 + + if model_def.get("multiple_submodels", False): + ui_defaults["guidance_phases"] = 2 + + \ No newline at end of file diff --git a/shared/RGB_factors.py b/shared/RGB_factors.py index 5ec1e59..6e865fa 100644 --- a/shared/RGB_factors.py +++ b/shared/RGB_factors.py @@ -1,26 +1,80 @@ -# thanks Comfyui for the rgb factors -def get_rgb_factors(model_family): +# thanks Comfyui for the rgb factors (https://github.com/comfyanonymous/ComfyUI/blob/master/comfy/latent_formats.py) +def get_rgb_factors(model_family, model_type = None): if model_family == "wan": - latent_channels = 16 - latent_dimensions = 3 - latent_rgb_factors = [ - [-0.1299, -0.1692, 0.2932], - [ 0.0671, 0.0406, 0.0442], - [ 0.3568, 0.2548, 0.1747], - [ 0.0372, 0.2344, 0.1420], - [ 0.0313, 0.0189, -0.0328], - [ 0.0296, -0.0956, -0.0665], - [-0.3477, -0.4059, -0.2925], - [ 0.0166, 0.1902, 0.1975], - [-0.0412, 0.0267, -0.1364], - [-0.1293, 0.0740, 0.1636], - [ 0.0680, 0.3019, 0.1128], - [ 0.0032, 0.0581, 0.0639], - [-0.1251, 0.0927, 0.1699], - [ 0.0060, -0.0633, 0.0005], - [ 0.3477, 0.2275, 0.2950], - [ 0.1984, 0.0913, 0.1861] - ] + if model_type =="ti2v_2_2": + latent_channels = 48 + latent_dimensions = 3 + latent_rgb_factors = [ + [ 0.0119, 0.0103, 0.0046], + [-0.1062, -0.0504, 0.0165], + [ 0.0140, 0.0409, 0.0491], + [-0.0813, -0.0677, 0.0607], + [ 0.0656, 0.0851, 0.0808], + [ 0.0264, 0.0463, 0.0912], + [ 0.0295, 0.0326, 0.0590], + [-0.0244, -0.0270, 0.0025], + [ 0.0443, -0.0102, 0.0288], + [-0.0465, -0.0090, -0.0205], + [ 0.0359, 0.0236, 0.0082], + [-0.0776, 0.0854, 0.1048], + [ 0.0564, 0.0264, 0.0561], + [ 0.0006, 0.0594, 0.0418], + [-0.0319, -0.0542, -0.0637], + [-0.0268, 0.0024, 0.0260], + [ 0.0539, 0.0265, 0.0358], + [-0.0359, -0.0312, -0.0287], + [-0.0285, -0.1032, -0.1237], + [ 0.1041, 0.0537, 0.0622], + [-0.0086, -0.0374, -0.0051], + [ 0.0390, 0.0670, 0.2863], + [ 0.0069, 0.0144, 0.0082], + [ 0.0006, -0.0167, 0.0079], + [ 0.0313, -0.0574, -0.0232], + [-0.1454, -0.0902, -0.0481], + [ 0.0714, 0.0827, 0.0447], + [-0.0304, -0.0574, -0.0196], + [ 0.0401, 0.0384, 0.0204], + [-0.0758, -0.0297, -0.0014], + [ 0.0568, 0.1307, 0.1372], + [-0.0055, -0.0310, -0.0380], + [ 0.0239, -0.0305, 0.0325], + [-0.0663, -0.0673, -0.0140], + [-0.0416, -0.0047, -0.0023], + [ 0.0166, 0.0112, -0.0093], + [-0.0211, 0.0011, 0.0331], + [ 0.1833, 0.1466, 0.2250], + [-0.0368, 0.0370, 0.0295], + [-0.3441, -0.3543, -0.2008], + [-0.0479, -0.0489, -0.0420], + [-0.0660, -0.0153, 0.0800], + [-0.0101, 0.0068, 0.0156], + [-0.0690, -0.0452, -0.0927], + [-0.0145, 0.0041, 0.0015], + [ 0.0421, 0.0451, 0.0373], + [ 0.0504, -0.0483, -0.0356], + [-0.0837, 0.0168, 0.0055] + ] + else: + latent_channels = 16 + latent_dimensions = 3 + latent_rgb_factors = [ + [-0.1299, -0.1692, 0.2932], + [ 0.0671, 0.0406, 0.0442], + [ 0.3568, 0.2548, 0.1747], + [ 0.0372, 0.2344, 0.1420], + [ 0.0313, 0.0189, -0.0328], + [ 0.0296, -0.0956, -0.0665], + [-0.3477, -0.4059, -0.2925], + [ 0.0166, 0.1902, 0.1975], + [-0.0412, 0.0267, -0.1364], + [-0.1293, 0.0740, 0.1636], + [ 0.0680, 0.3019, 0.1128], + [ 0.0032, 0.0581, 0.0639], + [-0.1251, 0.0927, 0.1699], + [ 0.0060, -0.0633, 0.0005], + [ 0.3477, 0.2275, 0.2950], + [ 0.1984, 0.0913, 0.1861] + ] latent_rgb_factors_bias = [-0.1835, -0.0868, -0.3360] diff --git a/shared/utils/loras_mutipliers.py b/shared/utils/loras_mutipliers.py index 6d2acca..58cc9a9 100644 --- a/shared/utils/loras_mutipliers.py +++ b/shared/utils/loras_mutipliers.py @@ -8,7 +8,7 @@ def preparse_loras_multipliers(loras_multipliers): loras_multipliers = " ".join(loras_mult_choices_list) return loras_multipliers.split(" ") -def expand_slist(slists_dict, mult_no, num_inference_steps, model_switch_step ): +def expand_slist(slists_dict, mult_no, num_inference_steps, model_switch_step, model_switch_step2 ): def expand_one(slist, num_inference_steps): if not isinstance(slist, list): slist = [slist] new_slist= [] @@ -23,13 +23,20 @@ def expand_slist(slists_dict, mult_no, num_inference_steps, model_switch_step ): phase1 = slists_dict["phase1"][mult_no] phase2 = slists_dict["phase2"][mult_no] - if isinstance(phase1, float) and isinstance(phase2, float) and phase1 == phase2: - return phase1 - return expand_one(phase1, model_switch_step) + expand_one(phase2, num_inference_steps - model_switch_step) + phase3 = slists_dict["phase3"][mult_no] + shared = slists_dict["shared"][mult_no] + if shared: + if isinstance(phase1, float): return phase1 + return expand_one(phase1, num_inference_steps) + else: + if isinstance(phase1, float) and isinstance(phase2, float) and isinstance(phase3, float) and phase1 == phase2 and phase2 == phase3: return phase1 + return expand_one(phase1, model_switch_step) + expand_one(phase2, model_switch_step2 - model_switch_step) + expand_one(phase3, num_inference_steps - model_switch_step2) -def parse_loras_multipliers(loras_multipliers, nb_loras, num_inference_steps, merge_slist = None, max_phases = 2, model_switch_step = None): +def parse_loras_multipliers(loras_multipliers, nb_loras, num_inference_steps, merge_slist = None, nb_phases = 2, model_switch_step = None, model_switch_step2 = None): if model_switch_step is None: model_switch_step = num_inference_steps + if model_switch_step2 is None: + model_switch_step2 = num_inference_steps def is_float(element: any) -> bool: if element is None: return False @@ -40,8 +47,11 @@ def parse_loras_multipliers(loras_multipliers, nb_loras, num_inference_steps, me return False loras_list_mult_choices_nums = [] slists_dict = { "model_switch_step": model_switch_step} + slists_dict = { "model_switch_step2": model_switch_step2} slists_dict["phase1"] = phase1 = [1.] * nb_loras slists_dict["phase2"] = phase2 = [1.] * nb_loras + slists_dict["phase3"] = phase3 = [1.] * nb_loras + slists_dict["shared"] = shared = [False] * nb_loras if isinstance(loras_multipliers, list) or len(loras_multipliers) > 0: list_mult_choices_list = preparse_loras_multipliers(loras_multipliers)[:nb_loras] @@ -51,41 +61,66 @@ def parse_loras_multipliers(loras_multipliers, nb_loras, num_inference_steps, me mult = mult.strip() phase_mult = mult.split(";") shared_phases = len(phase_mult) <=1 - if len(phase_mult) > max_phases: - return "", "", f"Loras can not be defined for more than {max_phases} Denoising phase{'s' if max_phases>1 else ''} for this model" + if not shared_phases and len(phase_mult) != nb_phases : + return "", "", f"if the ';' syntax is used for one Lora multiplier, the multipliers for its {nb_phases} denoising phases should be specified for this multiplier" for phase_no, mult in enumerate(phase_mult): - if phase_no > 0: current_phase = phase2 + if phase_no == 1: + current_phase = phase2 + elif phase_no == 2: + current_phase = phase3 if "," in mult: multlist = mult.split(",") slist = [] for smult in multlist: if not is_float(smult): - return "", "", f"Lora sub value no {i+1} ({smult}) in Multiplier definition '{multlist}' is invalid" + return "", "", f"Lora sub value no {i+1} ({smult}) in Multiplier definition '{multlist}' is invalid in Phase {phase_no+1}" slist.append(float(smult)) else: if not is_float(mult): return "", "", f"Lora Multiplier no {i+1} ({mult}) is invalid" slist = float(mult) if shared_phases: - phase1[i] = phase2[i] = slist + phase1[i] = phase2[i] = phase3[i] = slist + shared[i] = True else: current_phase[i] = slist else: - phase1[i] = phase2[i] = float(mult) + phase1[i] = phase2[i] = phase3[i] = float(mult) + shared[i] = True if merge_slist is not None: slists_dict["phase1"] = phase1 = merge_slist["phase1"] + phase1 slists_dict["phase2"] = phase2 = merge_slist["phase2"] + phase2 + slists_dict["phase3"] = phase3 = merge_slist["phase3"] + phase3 + slists_dict["shared"] = shared = merge_slist["shared"] + shared - loras_list_mult_choices_nums = [ expand_slist(slists_dict, i, num_inference_steps, model_switch_step ) for i in range(len(phase1)) ] + loras_list_mult_choices_nums = [ expand_slist(slists_dict, i, num_inference_steps, model_switch_step, model_switch_step2 ) for i in range(len(phase1)) ] loras_list_mult_choices_nums = [ slist[0] if isinstance(slist, list) else slist for slist in loras_list_mult_choices_nums ] return loras_list_mult_choices_nums, slists_dict, "" -def update_loras_slists(trans, slists_dict, num_inference_steps, model_switch_step = None ): +def update_loras_slists(trans, slists_dict, num_inference_steps, phase_switch_step = None, phase_switch_step2 = None ): from mmgp import offload sz = len(slists_dict["phase1"]) - slists = [ expand_slist(slists_dict, i, num_inference_steps, model_switch_step ) for i in range(sz) ] + slists = [ expand_slist(slists_dict, i, num_inference_steps, phase_switch_step, phase_switch_step2 ) for i in range(sz) ] nos = [str(l) for l in range(sz)] offload.activate_loras(trans, nos, slists ) + + +def get_model_switch_steps(timesteps, total_num_steps, guide_phases, model_switch_phase, switch_threshold, switch2_threshold ): + model_switch_step = model_switch_step2 = None + for i, t in enumerate(timesteps): + if guide_phases >=2 and model_switch_step is None and t <= switch_threshold: model_switch_step = i + if guide_phases >=3 and model_switch_step2 is None and t <= switch2_threshold: model_switch_step2 = i + if model_switch_step is None: model_switch_step = total_num_steps + if model_switch_step2 is None: model_switch_step2 = total_num_steps + phases_description = "" + if guide_phases > 1: + phases_description = "Denoising Steps: " + phases_description += f" Phase 1 = None" if model_switch_step == 0 else f" Phase 1 = 1:{ min(model_switch_step,total_num_steps) }" + if model_switch_step < total_num_steps: + phases_description += f", Phase 2 = None" if model_switch_step == model_switch_step2 else f", Phase 2 = {model_switch_step +1}:{ min(model_switch_step2,total_num_steps) }" + if guide_phases > 2 and model_switch_step2 < total_num_steps: + phases_description += f", Phase 3 = {model_switch_step2 +1}:{ total_num_steps}" + return model_switch_step, model_switch_step2, phases_description diff --git a/shared/utils/notification_sound.py b/shared/utils/notification_sound.py index 64ffd8f..26d1966 100644 --- a/shared/utils/notification_sound.py +++ b/shared/utils/notification_sound.py @@ -8,17 +8,22 @@ import sys import threading import time import numpy as np -import os + os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1" -def generate_notification_beep(volume=50, sample_rate=44100): +_cached_waveforms = {} +_sample_rate = 44100 +_mixer_initialized = False +_mixer_lock = threading.Lock() + +def _generate_notification_beep(volume=50, sample_rate=_sample_rate): """Generate pleasant C major chord notification sound""" if volume == 0: return np.array([]) - + volume = max(0, min(100, volume)) - - # Volume curve mapping: 25%->50%, 50%->75%, 75%->100%, 100%->105% + + # Volume curve mapping if volume <= 25: volume_mapped = (volume / 25.0) * 0.5 elif volume <= 50: @@ -26,232 +31,191 @@ def generate_notification_beep(volume=50, sample_rate=44100): elif volume <= 75: volume_mapped = 0.75 + ((volume - 50) / 25.0) * 0.25 else: - volume_mapped = 1.0 + ((volume - 75) / 25.0) * 0.05 # Only 5% boost instead of 15% - + volume_mapped = 1.0 + ((volume - 75) / 25.0) * 0.05 + volume = volume_mapped - + # C major chord frequencies - freq_c = 261.63 # C4 - freq_e = 329.63 # E4 - freq_g = 392.00 # G4 - + freq_c, freq_e, freq_g = 261.63, 329.63, 392.00 duration = 0.8 t = np.linspace(0, duration, int(sample_rate * duration), False) - + # Generate chord components - wave_c = np.sin(freq_c * 2 * np.pi * t) * 0.4 - wave_e = np.sin(freq_e * 2 * np.pi * t) * 0.3 - wave_g = np.sin(freq_g * 2 * np.pi * t) * 0.2 - - wave = wave_c + wave_e + wave_g - - # Prevent clipping + wave = ( + np.sin(freq_c * 2 * np.pi * t) * 0.4 + + np.sin(freq_e * 2 * np.pi * t) * 0.3 + + np.sin(freq_g * 2 * np.pi * t) * 0.2 + ) + + # Normalize max_amplitude = np.max(np.abs(wave)) if max_amplitude > 0: wave = wave / max_amplitude * 0.8 - + # ADSR envelope def apply_adsr_envelope(wave_data): length = len(wave_data) attack_time = int(0.2 * length) decay_time = int(0.1 * length) release_time = int(0.5 * length) - + envelope = np.ones(length) - + if attack_time > 0: envelope[:attack_time] = np.power(np.linspace(0, 1, attack_time), 3) - + if decay_time > 0: - start_idx = attack_time - end_idx = attack_time + decay_time + start_idx, end_idx = attack_time, attack_time + decay_time envelope[start_idx:end_idx] = np.linspace(1, 0.85, decay_time) - + if release_time > 0: start_idx = length - release_time envelope[start_idx:] = 0.85 * np.exp(-4 * np.linspace(0, 1, release_time)) - + return wave_data * envelope - + wave = apply_adsr_envelope(wave) - + # Simple low-pass filter def simple_lowpass_filter(signal, cutoff_ratio=0.8): window_size = max(3, int(len(signal) * 0.001)) if window_size % 2 == 0: window_size += 1 - + kernel = np.ones(window_size) / window_size - padded = np.pad(signal, window_size//2, mode='edge') - filtered = np.convolve(padded, kernel, mode='same') - return filtered[window_size//2:-window_size//2] - + padded = np.pad(signal, window_size // 2, mode="edge") + filtered = np.convolve(padded, kernel, mode="same") + return filtered[window_size // 2 : -window_size // 2] + wave = simple_lowpass_filter(wave) - - # Add reverb effect + + # Add reverb if len(wave) > sample_rate // 4: delay_samples = int(0.12 * sample_rate) reverb = np.zeros_like(wave) reverb[delay_samples:] = wave[:-delay_samples] * 0.08 wave = wave + reverb - - # Apply volume first, then normalize to prevent clipping - wave = wave * volume * 0.5 - - # Final normalization with safety margin - max_amplitude = np.max(np.abs(wave)) - if max_amplitude > 0.85: # If approaching clipping threshold - wave = wave / max_amplitude * 0.85 # More conservative normalization - - return wave -_mixer_lock = threading.Lock() -def play_audio_with_pygame(audio_data, sample_rate=44100): - """ - Play audio with clean stereo output - sounds like single notification from both speakers - """ + # Apply volume & final normalize + wave = wave * volume * 0.5 + max_amplitude = np.max(np.abs(wave)) + if max_amplitude > 0.85: + wave = wave / max_amplitude * 0.85 + + return wave + +def _get_cached_waveform(volume): + """Return cached waveform for volume""" + if volume not in _cached_waveforms: + _cached_waveforms[volume] = _generate_notification_beep(volume) + return _cached_waveforms[volume] + + +def play_audio_with_pygame(audio_data, sample_rate=_sample_rate): + """Play audio with pygame backend""" + global _mixer_initialized try: import pygame - + with _mixer_lock: - if len(audio_data) == 0: - return False - - # Clean mixer initialization - quit any existing mixer first - if pygame.mixer.get_init() is not None: - pygame.mixer.quit() - time.sleep(0.2) # Longer pause to ensure clean shutdown - - # Initialize fresh mixer - pygame.mixer.pre_init( - frequency=sample_rate, - size=-16, - channels=2, - buffer=512 # Smaller buffer to reduce latency/doubling - ) - pygame.mixer.init() - - # Verify clean initialization + if not _mixer_initialized: + pygame.mixer.pre_init(frequency=sample_rate, size=-16, channels=2, buffer=512) + pygame.mixer.init() + _mixer_initialized = True + mixer_info = pygame.mixer.get_init() if mixer_info is None or mixer_info[2] != 2: return False - - # Prepare audio - ensure clean conversion + audio_int16 = (audio_data * 32767).astype(np.int16) if len(audio_int16.shape) > 1: audio_int16 = audio_int16.flatten() - - # Create clean stereo with identical channels + stereo_data = np.zeros((len(audio_int16), 2), dtype=np.int16) - stereo_data[:, 0] = audio_int16 # Left channel - stereo_data[:, 1] = audio_int16 # Right channel - - # Create sound and play once + stereo_data[:, 0] = audio_int16 + stereo_data[:, 1] = audio_int16 + sound = pygame.sndarray.make_sound(stereo_data) - - # Ensure only one playback - pygame.mixer.stop() # Stop any previous sounds + pygame.mixer.stop() sound.play() - - # Wait for completion + duration_ms = int(len(audio_data) / sample_rate * 1000) + 50 pygame.time.wait(duration_ms) - + return True - + except ImportError: return False except Exception as e: - print(f"Pygame clean error: {e}") + print(f"Pygame error: {e}") return False - -def play_audio_with_sounddevice(audio_data, sample_rate=44100): + +def play_audio_with_sounddevice(audio_data, sample_rate=_sample_rate): """Play audio using sounddevice backend""" try: import sounddevice as sd sd.play(audio_data, sample_rate) sd.wait() return True - except ImportError: return False except Exception as e: print(f"Sounddevice error: {e}") return False - -def play_audio_with_winsound(audio_data, sample_rate=44100): +def play_audio_with_winsound(audio_data, sample_rate=_sample_rate): """Play audio using winsound backend (Windows only)""" if sys.platform != "win32": return False - try: - import winsound - import wave - import tempfile - import uuid - + import winsound, wave, tempfile, uuid + temp_dir = tempfile.gettempdir() temp_filename = os.path.join(temp_dir, f"notification_{uuid.uuid4().hex}.wav") - + try: - with wave.open(temp_filename, 'w') as wav_file: + with wave.open(temp_filename, "w") as wav_file: wav_file.setnchannels(1) wav_file.setsampwidth(2) wav_file.setframerate(sample_rate) - audio_int16 = (audio_data * 32767).astype(np.int16) wav_file.writeframes(audio_int16.tobytes()) - + winsound.PlaySound(temp_filename, winsound.SND_FILENAME) - + finally: - # Clean up temp file - for _ in range(3): - try: - if os.path.exists(temp_filename): - os.unlink(temp_filename) - break - except: - time.sleep(0.1) - + try: + if os.path.exists(temp_filename): + os.unlink(temp_filename) + except: + pass + return True - except ImportError: return False except Exception as e: print(f"Winsound error: {e}") return False - def play_notification_sound(volume=50): """Play notification sound with specified volume""" if volume == 0: return - - audio_data = generate_notification_beep(volume=volume) - + + audio_data = _get_cached_waveform(volume) if len(audio_data) == 0: return - - # Try audio backends in order - audio_backends = [ - play_audio_with_pygame, - play_audio_with_sounddevice, - play_audio_with_winsound, - ] - + + audio_backends = [play_audio_with_pygame, play_audio_with_sounddevice, play_audio_with_winsound] for backend in audio_backends: try: if backend(audio_data): return - except Exception as e: + except Exception: continue - - # Fallback: terminal beep - print(f"All audio backends failed, using terminal beep") - print('\a') + print("All audio backends failed, using terminal beep") + print("\a") def play_notification_async(volume=50): """Play notification sound asynchronously (non-blocking)""" @@ -260,24 +224,12 @@ def play_notification_async(volume=50): play_notification_sound(volume) except Exception as e: print(f"Error playing notification sound: {e}") - - sound_thread = threading.Thread(target=play_sound, daemon=True) - sound_thread.start() + threading.Thread(target=play_sound, daemon=True).start() def notify_video_completion(video_path=None, volume=50): """Notify about completed video generation""" play_notification_async(volume) - -if __name__ == "__main__": - print("Testing notification sounds with different volumes...") - print("Auto-detecting available audio backends...") - - volumes = [25, 50, 75, 100] - for vol in volumes: - print(f"Testing volume {vol}%:") - play_notification_sound(vol) - time.sleep(2) - - print("Test completed!") \ No newline at end of file +for vol in (25, 50, 75, 100): + _get_cached_waveform(vol) \ No newline at end of file diff --git a/shared/utils/prompt_parser.py b/shared/utils/prompt_parser.py index faaa1ca..46edec4 100644 --- a/shared/utils/prompt_parser.py +++ b/shared/utils/prompt_parser.py @@ -1,6 +1,6 @@ import re -def process_template(input_text): +def process_template(input_text, keep_comments = False): """ Process a text template with macro instructions and variable substitution. Supports multiple values for variables to generate multiple output versions. @@ -28,9 +28,12 @@ def process_template(input_text): line_number += 1 # Skip empty lines or comments - if not line or line.startswith('#'): + if not line: continue - + + if line.startswith('#') and not keep_comments: + continue + # Handle macro instructions if line.startswith('!'): # Process any accumulated template lines before starting a new macro @@ -106,13 +109,14 @@ def process_template(input_text): # Handle template lines else: - # Check for unknown variables in template line - var_references = re.findall(r'\{([^}]+)\}', line) - for var_ref in var_references: - if var_ref not in current_variables: - error_message = f"Unknown variable '{{{var_ref}}}' in template\nLine: '{orig_line}'" - return "", error_message - + if not line.startswith('#'): + # Check for unknown variables in template line + var_references = re.findall(r'\{([^}]+)\}', line) + for var_ref in var_references: + if var_ref not in current_variables: + error_message = f"Unknown variable '{{{var_ref}}}' in template\nLine: '{orig_line}'" + return "", error_message + # Add to current template lines current_template_lines.append(line) diff --git a/wgp.py b/wgp.py index c306383..c7e8bc2 100644 --- a/wgp.py +++ b/wgp.py @@ -55,8 +55,8 @@ AUTOSAVE_FILENAME = "queue.zip" PROMPT_VARS_MAX = 10 target_mmgp_version = "3.5.10" -WanGP_version = "7.7777" -settings_version = 2.23 +WanGP_version = "8.0" +settings_version = 2.24 max_source_video_frames = 3000 prompt_enhancer_image_caption_model, prompt_enhancer_image_caption_processor, prompt_enhancer_llm_model, prompt_enhancer_llm_tokenizer = None, None, None, None @@ -71,8 +71,8 @@ task_id = 0 vmc_event_handler = matanyone_app.get_vmc_event_handler() unique_id = 0 unique_id_lock = threading.Lock() -offloadobj = None -wan_model = None +gen_lock = threading.Lock() +offloadobj = enhancer_offloadobj = wan_model = None def get_unique_id(): global unique_id @@ -164,7 +164,6 @@ def process_prompt_and_add_tasks(state, model_choice): return state["validate_success"] = 0 - model_filename = state["model_filename"] model_type = state["model_type"] inputs = get_model_settings(state, model_type) @@ -299,13 +298,24 @@ def process_prompt_and_add_tasks(state, model_choice): switch_threshold = inputs["switch_threshold"] loras_multipliers = inputs["loras_multipliers"] activated_loras = inputs["activated_loras"] + guidance_phases= inputs["guidance_phases"] + model_switch_phase = inputs["model_switch_phase"] + switch_threshold = inputs["switch_threshold"] + switch_threshold2 = inputs["switch_threshold2"] + if len(loras_multipliers) > 0: - _, _, errors = parse_loras_multipliers(loras_multipliers, len(activated_loras), num_inference_steps, max_phases= 2 if get_model_family(model_type)=="wan" and model_type not in ["sky_df_1.3B", "sky_df_14B"] else 1) + _, _, errors = parse_loras_multipliers(loras_multipliers, len(activated_loras), num_inference_steps, nb_phases= guidance_phases) if len(errors) > 0: gr.Info(f"Error parsing Loras Multipliers: {errors}") return - + if guidance_phases == 3: + if switch_threshold < switch_threshold2: + gr.Info(f"Phase 1-2 Switch Noise Level ({switch_threshold}) should be Greater than Phase 2-3 Switch Noise Level ({switch_threshold2}). As a reminder, noise will gradually go down from 1000 to 0.") + return + else: + model_switch_phase = 1 + if not any_steps_skipping: skip_steps_cache_type = "" if not model_def.get("lock_inference_steps", False) and model_type in ["ltxv_13B"] and num_inference_steps < 20: gr.Info("The minimum number of steps should be 20") @@ -535,7 +545,8 @@ def process_prompt_and_add_tasks(state, model_choice): "image_prompt_type": image_prompt_type, "video_prompt_type": video_prompt_type, "audio_prompt_type": audio_prompt_type, - "skip_steps_cache_type": skip_steps_cache_type + "skip_steps_cache_type": skip_steps_cache_type, + "model_switch_phase": model_switch_phase, } if inputs["multi_prompts_gen_type"] == 0: @@ -2185,7 +2196,8 @@ if len(args.attention)> 0: else: raise Exception(f"Unknown attention mode '{args.attention}'") -profile = force_profile_no if force_profile_no >=0 else server_config["profile"] +default_profile = force_profile_no if force_profile_no >=0 else server_config["profile"] +loaded_profile = -1 compile = server_config.get("compile", "") boost = server_config.get("boost", 1) vae_config = server_config.get("vae_config", 0) @@ -2333,7 +2345,7 @@ def download_mmaudio(): } process_files_def(**enhancer_def) -def download_models(model_filename, model_type, module_type = None, submodel_no = 1): +def download_models(model_filename = None, model_type= None, module_type = None, submodel_no = 1): def computeList(filename): if filename == None: return [] @@ -2366,7 +2378,16 @@ def download_models(model_filename, model_type, module_type = None, submodel_no } process_files_def(**enhancer_def) + elif server_config.get("enhancer_enabled", 0) == 2: + enhancer_def = { + "repoId" : "DeepBeepMeep/LTX_Video", + "sourceFolderList" : [ "Florence2", "llama-joycaption-beta-one-hf-llava" ], + "fileList" : [ ["config.json", "configuration_florence2.py", "model.safetensors", "modeling_florence2.py", "preprocessor_config.json", "processing_florence2.py", "tokenizer.json", "tokenizer_config.json"],["config.json", "llama_joycaption_quanto_bf16_int8.safetensors", "special_tokens_map.json", "tokenizer.json", "tokenizer_config.json"] ] + } + process_files_def(**enhancer_def) + download_mmaudio() + if model_filename is None: return def download_file(url,filename): if url.startswith("https://huggingface.co/") and "/resolve/main/" in url: @@ -2554,12 +2575,76 @@ def get_transformer_model(model, submodel_no = 1): else: raise Exception("no transformer found") +def init_pipe(pipe, kwargs, override_profile): + preload =int(args.preload) + if preload == 0: + preload = server_config.get("preload_in_VRAM", 0) -def load_models(model_type): - global transformer_type + kwargs["extraModelsToQuantize"]= None + profile = override_profile if override_profile != -1 else default_profile + if profile in (2, 4, 5): + budgets = { "transformer" : 100 if preload == 0 else preload, "text_encoder" : 100 if preload == 0 else preload, "*" : max(1000 if profile==5 else 3000 , preload) } + if "transformer2" in pipe: + budgets["transformer2"] = 100 if preload == 0 else preload + kwargs["budgets"] = budgets + elif profile == 3: + kwargs["budgets"] = { "*" : "70%" } + + if "transformer2" in pipe: + if profile in [3,4]: + kwargs["pinnedMemory"] = ["transformer", "transformer2"] + + return profile + +def reset_prompt_enhancer(): + global prompt_enhancer_image_caption_model, prompt_enhancer_image_caption_processor, prompt_enhancer_llm_model, prompt_enhancer_llm_tokenizer, enhancer_offloadobj + prompt_enhancer_image_caption_model = None + prompt_enhancer_image_caption_processor = None + prompt_enhancer_llm_model = None + prompt_enhancer_llm_tokenizer = None + if enhancer_offloadobj is not None: + enhancer_offloadobj.release() + enhancer_offloadobj = None + +def setup_prompt_enhancer(pipe, kwargs): + global prompt_enhancer_image_caption_model, prompt_enhancer_image_caption_processor, prompt_enhancer_llm_model, prompt_enhancer_llm_tokenizer + model_no = server_config.get("enhancer_enabled", 0) + if model_no != 0: + from transformers import ( AutoModelForCausalLM, AutoProcessor, AutoTokenizer, LlamaForCausalLM ) + prompt_enhancer_image_caption_model = AutoModelForCausalLM.from_pretrained( "ckpts/Florence2", trust_remote_code=True) + prompt_enhancer_image_caption_processor = AutoProcessor.from_pretrained( "ckpts/Florence2", trust_remote_code=True) + pipe["prompt_enhancer_image_caption_model"] = prompt_enhancer_image_caption_model + prompt_enhancer_image_caption_model._model_dtype = torch.float + # def preprocess_sd(sd, map): + # new_sd ={} + # for k, v in sd.items(): + # k = "model." + k.replace(".model.", ".") + # if "lm_head.weight" in k: k = "lm_head.weight" + # new_sd[k] = v + # return new_sd, map + # prompt_enhancer_llm_model = offload.fast_load_transformers_model("c:/temp/joy/model-00001-of-00004.safetensors", modelClass= LlavaForConditionalGeneration, defaultConfigPath="ckpts/llama-joycaption-beta-one-hf-llava/config.json", preprocess_sd=preprocess_sd) + # offload.save_model(prompt_enhancer_llm_model, "joy_llava_quanto_int8.safetensors", do_quantize= True) + + if model_no == 1: + budget = 5000 + prompt_enhancer_llm_model = offload.fast_load_transformers_model("ckpts/Llama3_2/Llama3_2_quanto_bf16_int8.safetensors") + prompt_enhancer_llm_tokenizer = AutoTokenizer.from_pretrained("ckpts/Llama3_2") + else: + budget = 10000 + prompt_enhancer_llm_model = offload.fast_load_transformers_model("ckpts/llama-joycaption-beta-one-hf-llava/llama_joycaption_quanto_bf16_int8.safetensors") + prompt_enhancer_llm_tokenizer = AutoTokenizer.from_pretrained("ckpts/llama-joycaption-beta-one-hf-llava") + pipe["prompt_enhancer_llm_model"] = prompt_enhancer_llm_model + if not "budgets" in kwargs: kwargs["budgets"] = {} + kwargs["budgets"]["prompt_enhancer_llm_model"] = budget + else: + reset_prompt_enhancer() + + + +def load_models(model_type, override_profile = -1): + global transformer_type, loaded_profile base_model_type = get_base_model_type(model_type) model_def = get_model_def(model_type) - preload =int(args.preload) save_quantized = args.save_quantized and model_def != None model_filename = get_model_filename(model_type=model_type, quantization= "" if save_quantized else transformer_quantization, dtype_policy = transformer_dtype_policy) if "URLs2" in model_def: @@ -2583,8 +2668,6 @@ def load_models(model_type): transformer_dtype = torch.bfloat16 if "bf16" in model_filename or "BF16" in model_filename else transformer_dtype transformer_dtype = torch.float16 if "fp16" in model_filename or"FP16" in model_filename else transformer_dtype perc_reserved_mem_max = args.perc_reserved_mem_max - if preload == 0: - preload = server_config.get("preload_in_VRAM", 0) model_file_list = [model_filename] model_type_list = [model_type] module_type_list = [None] @@ -2614,44 +2697,18 @@ def load_models(model_type): model_file_list, model_type, base_model_type, model_def, quantizeTransformer = quantizeTransformer, text_encoder_quantization = text_encoder_quantization, dtype = transformer_dtype, VAE_dtype = VAE_dtype, mixed_precision_transformer = mixed_precision_transformer, save_quantized = save_quantized) - kwargs = { "extraModelsToQuantize": None } + kwargs = {} + profile = init_pipe(pipe, kwargs, override_profile) + if server_config.get("enhancer_mode", 0) == 0: + setup_prompt_enhancer(pipe, kwargs) loras_transformer = ["transformer"] - if profile in (2, 4, 5): - budgets = { "transformer" : 100 if preload == 0 else preload, "text_encoder" : 100 if preload == 0 else preload, "*" : max(1000 if profile==5 else 3000 , preload) } - if "transformer2" in pipe: - budgets["transformer2"] = 100 if preload == 0 else preload - kwargs["budgets"] = budgets - elif profile == 3: - kwargs["budgets"] = { "*" : "70%" } - if "transformer2" in pipe: loras_transformer += ["transformer2"] - if profile in [3,4]: - kwargs["pinnedMemory"] = ["transformer", "transformer2"] - - global prompt_enhancer_image_caption_model, prompt_enhancer_image_caption_processor, prompt_enhancer_llm_model, prompt_enhancer_llm_tokenizer - if server_config.get("enhancer_enabled", 0) == 1: - from transformers import ( AutoModelForCausalLM, AutoProcessor, AutoTokenizer, LlamaForCausalLM ) - prompt_enhancer_image_caption_model = AutoModelForCausalLM.from_pretrained( "ckpts/Florence2", trust_remote_code=True) - prompt_enhancer_image_caption_processor = AutoProcessor.from_pretrained( "ckpts/Florence2", trust_remote_code=True) - prompt_enhancer_llm_model = offload.fast_load_transformers_model("ckpts/Llama3_2/Llama3_2_quanto_bf16_int8.safetensors") #, configKwargs= {"_attn_implementation" :"XXXsdpa"} - prompt_enhancer_llm_tokenizer = AutoTokenizer.from_pretrained("ckpts/Llama3_2") - pipe["prompt_enhancer_image_caption_model"] = prompt_enhancer_image_caption_model - pipe["prompt_enhancer_llm_model"] = prompt_enhancer_llm_model - prompt_enhancer_image_caption_model._model_dtype = torch.float - if "budgets" in kwargs: - kwargs["budgets"]["prompt_enhancer_llm_model"] = 5000 - else: - prompt_enhancer_image_caption_model = None - prompt_enhancer_image_caption_processor = None - prompt_enhancer_llm_model = None - prompt_enhancer_llm_tokenizer = None - - offloadobj = offload.profile(pipe, profile_no= profile, compile = compile, quantizeTransformer = False, loras = loras_transformer, coTenantsMap= {}, perc_reserved_mem_max = perc_reserved_mem_max , convertWeightsFloatTo = transformer_dtype, **kwargs) if len(args.gpu) > 0: torch.set_default_device(args.gpu) transformer_type = model_type + loaded_profile = profile return wan_model, offloadobj if not "P" in preload_model_policy: @@ -2722,11 +2779,12 @@ def apply_changes( state, preload_model_policy_choice = 1, UI_theme_choice = "default", enhancer_enabled_choice = 0, + enhancer_mode_choice = 0, mmaudio_enabled_choice = 0, fit_canvas_choice = 0, preload_in_VRAM_choice = 0, depth_anything_v2_variant_choice = "vitl", - notification_sound_enabled_choice = 1, + notification_sound_enabled_choice = 0, notification_sound_volume_choice = 50, max_frames_multiplier_choice = 1, display_stats_choice = 0, @@ -2736,9 +2794,9 @@ def apply_changes( state, last_resolution_choice = None, ): if args.lock_config: - return + return "