diff --git a/models/wan/any2video.py b/models/wan/any2video.py index 5cf1e712a..5c8158bf7 100644 --- a/models/wan/any2video.py +++ b/models/wan/any2video.py @@ -27,7 +27,8 @@ from .modules.clip import CLIPModel from shared.utils.fm_solvers import (FlowDPMSolverMultistepScheduler, - get_sampling_sigmas, retrieve_timesteps) + get_sampling_sigmas, retrieve_timesteps) +from shared.utils.seed_management import create_generator, create_subseed_generator, apply_subseed_variation from shared.utils.fm_solvers_unipc import FlowUniPCMultistepScheduler from .modules.posemb_layers import get_rotary_pos_embed, get_nd_rotary_pos_embed from shared.utils.vace_preprocessor import VaceVideoProcessor @@ -344,6 +345,8 @@ def generate(self, model_switch_phase = 1, n_prompt="", seed=-1, + subseed=-1, + subseed_strength=0.0, callback = None, enable_RIFLEx = None, VAE_tile_size = 0, @@ -428,8 +431,9 @@ def generate(self, raise NotImplementedError(f"Unsupported Scheduler {sample_solver}") original_timesteps = timesteps - seed_g = torch.Generator(device=self.device) - seed_g.manual_seed(seed) + seed_g = create_generator(seed, self.device) + subseed_g = create_subseed_generator(subseed, subseed_strength, self.device) + image_outputs = image_mode == 1 kwargs = {'pipeline': self, 'callback': callback} color_reference_frame = None @@ -857,6 +861,8 @@ def clear(): scheduler_kwargs = {} if isinstance(sample_scheduler, FlowMatchScheduler) else {"generator": seed_g} # b, c, lat_f, lat_h, lat_w latents = torch.randn(batch_size, *target_shape, dtype=torch.float32, device=self.device, generator=seed_g) + latents = apply_subseed_variation(latents, subseed_g, subseed_strength) + if "G" in video_prompt_type: randn = latents if apg_switch != 0: apg_momentum = -0.75 diff --git a/shared/radial_attention/utils.py b/shared/radial_attention/utils.py index 92b3c3e13..e7e5ff9f6 100644 --- a/shared/radial_attention/utils.py +++ b/shared/radial_attention/utils.py @@ -1,16 +1 @@ -import os -import random -import numpy as np -import torch - -def set_seed(seed): - """ - Set the random seed for reproducibility. - """ - random.seed(seed) - os.environ['PYTHONHASHSEED'] = str(seed) - np.random.seed(seed) - torch.manual_seed(seed) - torch.cuda.manual_seed(seed) - torch.backends.cudnn.deterministic = True - torch.backends.cudnn.benchmark = False \ No newline at end of file +from shared.utils.seed_management import set_seed \ No newline at end of file diff --git a/shared/utils/seed_management.py b/shared/utils/seed_management.py new file mode 100644 index 000000000..03c89ccfe --- /dev/null +++ b/shared/utils/seed_management.py @@ -0,0 +1,146 @@ +""" +Seed Management Utilities + +Centralized seed management for reproducible generation with support for: +- Main seed randomization and setting +- Subseed variation (seed interpolation) +- PyTorch Generator creation +- Cross-platform reproducibility +""" + +import random +import os +import torch +import numpy as np + + +def set_seed(seed): + """ + Set all random seeds for reproducible results across Python, NumPy, and PyTorch. + + Args: + seed: Integer seed value, or -1/None to generate random seed + + Returns: + The seed value that was set (generated if input was -1/None) + """ + seed = random.randint(0, 99999999) if seed is None or seed < 0 else seed + + # Set all random number generators + random.seed(seed) + os.environ['PYTHONHASHSEED'] = str(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + + # Ensure deterministic behavior + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + return seed + + +def initialize_subseed(subseed, subseed_strength): + """ + Initialize subseed value for variation generation. + Only randomizes if subseed=-1 AND subseed_strength > 0. + + Args: + subseed: Integer subseed value, or -1 for random + subseed_strength: Float 0.0-1.0, strength of variation + + Returns: + Tuple of (subseed, original_subseed) where: + - subseed: The actual subseed to use (randomized if needed) + - original_subseed: The input subseed value (for tracking -1) + """ + original_subseed = subseed + + # Only randomize if it will actually be used + if subseed < 0 and subseed_strength > 0: + subseed = random.randint(0, 99999999) + + return subseed, original_subseed + + +def regenerate_subseed(original_subseed, subseed_strength): + """ + Generate a new random subseed for repeat generations. + Only generates if original_subseed was -1 AND subseed_strength > 0. + + Args: + original_subseed: The original subseed value (before any randomization) + subseed_strength: Float 0.0-1.0, strength of variation + + Returns: + New random subseed, or original_subseed if not applicable + """ + if original_subseed < 0 and subseed_strength > 0: + return random.randint(0, 99999999) + return original_subseed + + +def create_generator(seed, device): + """ + Create a PyTorch Generator with the specified seed. + + Args: + seed: Integer seed value + device: torch.device or string ('cpu', 'cuda', etc.) + + Returns: + torch.Generator initialized with the seed + """ + generator = torch.Generator(device=device) + generator.manual_seed(seed) + return generator + + +def create_subseed_generator(subseed, subseed_strength, device): + """ + Create a PyTorch Generator for subseed variation if enabled. + + Args: + subseed: Integer subseed value + subseed_strength: Float 0.0-1.0, strength of variation + device: torch.device or string ('cpu', 'cuda', etc.) + + Returns: + torch.Generator if variation is enabled (subseed_strength > 0 and subseed >= 0), + None otherwise + """ + if subseed_strength > 0 and subseed >= 0: + return create_generator(subseed, device) + return None + + +def apply_subseed_variation(latents, subseed_generator, subseed_strength): + """ + Apply subseed variation to latents using linear interpolation. + + Generates a second set of latents using the subseed generator and blends them: + result = latents * (1 - strength) + sub_latents * strength + + Args: + latents: torch.Tensor - base latents from main seed + subseed_generator: torch.Generator or None - generator for variation + subseed_strength: Float 0.0-1.0, strength of variation + + Returns: + torch.Tensor - latents with variation applied, or original latents if + subseed_generator is None or subseed_strength is 0 + """ + if subseed_generator is not None and subseed_strength > 0: + # Generate variation latents + sub_latents = torch.randn( + *latents.shape, + dtype=latents.dtype, + device=latents.device, + generator=subseed_generator + ) + + # Linear interpolation between main and variation + latents = latents * (1.0 - subseed_strength) + sub_latents * subseed_strength + + return latents + diff --git a/wgp.py b/wgp.py index 74223a6c9..f6a4752e6 100644 --- a/wgp.py +++ b/wgp.py @@ -2205,6 +2205,8 @@ def get_default_prompt(i2v): "video_length": 81, "num_inference_steps": 30, "seed": -1, + "subseed": -1, + "subseed_strength": 0.0, "repeat_generation": 1, "multi_images_gen_type": 0, "guidance_scale": 5.0, @@ -3480,6 +3482,19 @@ def check(src, cond): labels += ["Sampler Solver"] values += [video_resolution, video_length_summary, video_seed, video_guidance_scale, video_audio_guidance_scale, video_flow_shift, video_num_inference_steps] labels += [ "Resolution", video_length_label, "Seed", video_guidance_label, "Audio Guidance Scale", "Shift Scale", "Num Inference steps"] + video_subseed = configs.get("subseed", -1) + video_subseed_strength = configs.get("subseed_strength", 0.0) + if video_subseed_strength > 0: + # Show the actually used subseed if available, otherwise show the subseed value + subseed_used = configs.get("subseed_used", None) + if subseed_used is not None: + subseed_display = subseed_used + elif video_subseed >= 0: + subseed_display = video_subseed + else: + subseed_display = "random" + values += [f"{subseed_display} (strength: {video_subseed_strength})"] + labels += ["Variation Seed"] video_negative_prompt = configs.get("negative_prompt", "") if len(video_negative_prompt) > 0: values += [video_negative_prompt] @@ -4118,15 +4133,7 @@ def get_available_filename(target_path, video_source, suffix = "", force_extensi return full_path counter += 1 -def set_seed(seed): - import random - seed = random.randint(0, 99999999) if seed == None or seed < 0 else seed - torch.manual_seed(seed) - torch.cuda.manual_seed_all(seed) - np.random.seed(seed) - random.seed(seed) - torch.backends.cudnn.deterministic = True - return seed +from shared.utils.seed_management import set_seed, initialize_subseed, regenerate_subseed def edit_video( send_cmd, @@ -4490,6 +4497,8 @@ def generate_video( video_length, batch_size, seed, + subseed, + subseed_strength, force_fps, num_inference_steps, guidance_scale, @@ -4854,8 +4863,14 @@ def remove_temp_filenames(temp_filenames_list): current_video_length = min(current_video_length, length) + # Initialize subseed BEFORE setting main seed to ensure independent randomization + # (set_seed will seed the random module, making subsequent random calls deterministic) + subseed, original_subseed = initialize_subseed(subseed, subseed_strength) + print(f"[Subseed Init] Input: subseed={original_subseed}, strength={subseed_strength} → Using: subseed={subseed}") + + # Now set the main seed (this seeds random module for reproducibility) seed = set_seed(seed) - + torch.set_grad_enabled(False) os.makedirs(save_path, exist_ok=True) os.makedirs(image_save_path, exist_ok=True) @@ -5227,6 +5242,8 @@ def set_header_text(txt): embedded_guidance_scale=embedded_guidance_scale, n_prompt=negative_prompt, seed=seed, + subseed=subseed, + subseed_strength=subseed_strength, callback=callback, enable_RIFLEx = enable_RIFLEx, VAE_tile_size = VAE_tile_size, @@ -5462,8 +5479,21 @@ def set_header_text(txt): inputs.update({ "transformer_loras_filenames" : transformer_loras_filenames, "transformer_loras_multipliers" : transformer_loras_multipliers - }) + }) + # Preserve original subseed value (-1) in metadata if it was random + # This ensures next generation will also randomize instead of reusing + # But also save the actually used subseed for display purposes (only if strength > 0) + print(f"[Subseed Metadata] original_subseed={original_subseed}, inputs['subseed'] before={inputs.get('subseed', 'MISSING')}, inputs['subseed_strength']={inputs.get('subseed_strength', 'MISSING')}") + if original_subseed < 0 and subseed_strength > 0: + actual_subseed_used = inputs["subseed"] # Save the actual randomized value + print(f"[Subseed Metadata] Preserving original random flag: changing {inputs['subseed']} → {original_subseed}, actual used: {actual_subseed_used}") + inputs["subseed"] = original_subseed + inputs["subseed_used"] = actual_subseed_used # Store actual value for display + elif original_subseed < 0: + # If strength is 0, just set subseed to -1 without storing subseed_used + inputs["subseed"] = original_subseed configs = prepare_inputs_dict("metadata", inputs, model_type) + print(f"[Subseed Metadata] After prepare_inputs_dict: configs has subseed={configs.get('subseed', 'MISSING')}, subseed_strength={configs.get('subseed_strength', 'MISSING')}") if sliding_window: configs["window_no"] = window_no configs["prompt"] = "\n".join(original_prompts) if prompt_enhancer_image_caption_model != None and prompt_enhancer !=None and len(prompt_enhancer)>0: @@ -5505,6 +5535,8 @@ def set_header_text(txt): send_cmd("output") + # Regenerate subseed BEFORE setting new seed to ensure independent randomization + subseed = regenerate_subseed(original_subseed, subseed_strength) seed = set_seed(-1) clear_status(state) trans.cache = None @@ -5627,7 +5659,7 @@ def release_gen(): gen["prompt_no"] = prompt_no task = queue[0] task_id = task["id"] - params = task['params'] + params = task['params'].copy() # Make a copy to avoid param pollution between queue tasks com_stream = AsyncStream() send_cmd = com_stream.output_queue.push @@ -6529,6 +6561,9 @@ def use_video_settings(state, input_file_list, choice): defaults = get_model_settings(state, model_type) defaults = get_default_settings(model_type) if defaults == None else defaults defaults.update(configs) + # For reproducibility: use the actual subseed that was used, not the random flag + if "subseed_used" in configs and configs.get("subseed_strength", 0.0) > 0: + defaults["subseed"] = configs["subseed_used"] prompt = configs.get("prompt", "") set_model_settings(state, model_type, defaults) if has_image_file_extension(file_name): @@ -6597,6 +6632,9 @@ def get_settings_from_file(state, file_path, allow_json, merge_with_defaults, sw defaults = get_model_settings(state, model_type) defaults = get_default_settings(model_type) if defaults == None else defaults defaults.update(configs) + # For reproducibility: use the actual subseed that was used, not the random flag + if "subseed_used" in configs and configs.get("subseed_strength", 0.0) > 0: + defaults["subseed"] = configs["subseed_used"] configs = defaults configs["model_type"] = model_type @@ -6664,6 +6702,8 @@ def save_inputs( video_length, batch_size, seed, + subseed, + subseed_strength, force_fps, num_inference_steps, guidance_scale, @@ -7900,7 +7940,9 @@ def get_image_gallery(label ="", value = None, single_image_mode = False, visibl with gr.Tab("General"): with gr.Column(): with gr.Row(): - seed = gr.Slider(-1, 999999999, value=ui_defaults.get("seed",-1), step=1, label="Seed (-1 for random)", scale=2) + seed = gr.Slider(-1, 999999999, value=ui_defaults.get("seed",-1), step=1, label="Seed (-1 for random)", scale=2) + random_seed_btn = gr.Button("🎲", size="sm", min_width=40, scale=0) + reuse_seed_btn = gr.Button("♻️", size="sm", min_width=40, scale=0) guidance_phases_value = ui_defaults.get("guidance_phases", 1) guidance_phases = gr.Dropdown( choices=[ @@ -7912,6 +7954,15 @@ def get_image_gallery(label ="", value = None, single_image_mode = False, visibl visible= guidance_max_phases >=2, interactive = not model_def.get("lock_guidance_phases", False) ) + # Auto-check Extra checkbox if loading settings with subseed data + # Only check if subseed_strength > 0, since that indicates variation was actually used + has_subseed_data = ui_defaults.get("subseed_strength", 0.0) > 0 + seed_extras_checkbox = gr.Checkbox(label="Extra Seed Options", value=has_subseed_data) + with gr.Row(visible=has_subseed_data) as seed_extras_row: + subseed = gr.Slider(-1, 999999999, value=ui_defaults.get("subseed", -1), step=1, label="Variation Seed (-1 for random)", scale=2) + random_subseed_btn = gr.Button("🎲", size="sm", min_width=40, scale=0) + reuse_subseed_btn = gr.Button("♻️", size="sm", min_width=40, scale=0) + subseed_strength = gr.Slider(0.0, 1.0, value=ui_defaults.get("subseed_strength", 0.0), step=0.01, label="Variation Strength", scale=1) with gr.Row(visible = guidance_phases_value >=2 ) as guidance_phases_row: multiple_submodels = model_def.get("multiple_submodels", False) model_switch_phase = gr.Dropdown( @@ -8380,7 +8431,7 @@ def gen_upsampling_dropdowns(temporal_upsampling, spatial_upsampling , film_grai video_buttons_row, image_buttons_row, video_postprocessing_tab, audio_remuxing_tab, PP_MMAudio_row, PP_custom_audio_row, video_info_to_start_image_btn, video_info_to_end_image_btn, video_info_to_reference_image_btn, video_info_to_image_guide_btn, video_info_to_image_mask_btn, NAG_col, remove_background_sound , speakers_locations_row, embedded_guidance_row, guidance_phases_row, guidance_row, resolution_group, cfg_free_guidance_col, control_net_weights_row, guide_selection_row, image_mode_tabs, - min_frames_if_references_col, video_prompt_type_alignment, prompt_enhancer_btn, tab_inpaint, tab_t2v] + image_start_extra + image_end_extra + image_refs_extra # presets_column, + min_frames_if_references_col, video_prompt_type_alignment, prompt_enhancer_btn, tab_inpaint, tab_t2v, seed_extras_checkbox] + image_start_extra + image_end_extra + image_refs_extra # presets_column, if update_form: locals_dict = locals() gen_inputs = [state_dict if k=="state" else locals_dict[k] for k in inputs_names] + [state_dict] + extra_inputs @@ -8397,6 +8448,37 @@ def gen_upsampling_dropdowns(temporal_upsampling, spatial_upsampling , film_grai # video_length.release(fn=refresh_video_length_label, inputs=[state, video_length ], outputs = video_length, trigger_mode="always_last" ) gr.on(triggers=[video_length.release, force_fps.change, video_guide.change, video_source.change], fn=refresh_video_length_label, inputs=[state, video_length, force_fps, video_guide, video_source] , outputs = video_length, trigger_mode="always_last", show_progress="hidden" ) guidance_phases.change(fn=change_guidance_phases, inputs= [state, guidance_phases], outputs =[model_switch_phase, guidance_phases_row, switch_threshold, switch_threshold2, guidance2_scale, guidance3_scale ]) + + def toggle_seed_extras(checked): + """Toggle seed extras visibility and reset subseed_strength when unchecked""" + if checked: + return gr.update(visible=True), gr.update() + else: + # Reset subseed_strength to 0 when unchecking to disable variation + return gr.update(visible=False), gr.update(value=0.0) + + seed_extras_checkbox.change(fn=toggle_seed_extras, inputs=[seed_extras_checkbox], outputs=[seed_extras_row, subseed_strength], show_progress=False) + + # Seed button handlers + random_seed_btn.click(fn=lambda: -1, inputs=[], outputs=[seed], show_progress=False) + random_subseed_btn.click(fn=lambda: -1, inputs=[], outputs=[subseed], show_progress=False) + + def get_last_seed_from_state(state, is_subseed=False): + gen = get_gen_info(state) + file_list = gen.get("file_list", []) + if not file_list: + return -1 + # Get the most recent file + last_file = file_list[-1] + file_settings = gen.get("file_settings_list", [{}])[-1] if gen.get("file_settings_list") else {} + if is_subseed: + return file_settings.get("subseed", -1) + else: + return file_settings.get("seed", -1) + + reuse_seed_btn.click(fn=lambda s: get_last_seed_from_state(s, False), inputs=[state], outputs=[seed], show_progress=False) + reuse_subseed_btn.click(fn=lambda s: get_last_seed_from_state(s, True), inputs=[state], outputs=[subseed], show_progress=False) + audio_prompt_type_remux.change(fn=refresh_audio_prompt_type_remux, inputs=[state, audio_prompt_type, audio_prompt_type_remux], outputs=[audio_prompt_type]) remove_background_sound.change(fn=refresh_remove_background_sound, inputs=[state, audio_prompt_type, remove_background_sound], outputs=[audio_prompt_type]) audio_prompt_type_sources.change(fn=refresh_audio_prompt_type_sources, inputs=[state, audio_prompt_type, audio_prompt_type_sources], outputs=[audio_prompt_type, audio_guide, audio_guide2, speakers_locations_row, remove_background_sound])