diff --git a/comfy/latent_formats.py b/comfy/latent_formats.py index ed516707577..34c6d700c91 100644 --- a/comfy/latent_formats.py +++ b/comfy/latent_formats.py @@ -577,6 +577,7 @@ class MiniMaxH3Video(LatentFormat): spacial_downscale_ratio = 16 temporal_downscale_ratio = 4 scale_factor = 1.0 + taesd_decoder_name = "taeh3" latent_rgb_factors = [ [-0.018555, 0.024344, -0.017536], diff --git a/comfy/ldm/minimax/model.py b/comfy/ldm/minimax/model.py index b6feb8860a2..765f219888f 100644 --- a/comfy/ldm/minimax/model.py +++ b/comfy/ldm/minimax/model.py @@ -74,6 +74,17 @@ def _axis_from_sqrt_area(dim, patch, sqrt_area): return (torch.arange(n, dtype=torch.float64) * (ratio / n) + (1.0 - ratio) / 2.0) * 32.0 +def mask_row_values(mask, latent_t, lat_h, lat_w): + # [T, H, W] denoise mask (1 = generate) -> per-2x2-patch-row float in [0, 1], + # None when every row fully generates + m = torch.nn.functional.pad(mask, (0, lat_w - mask.shape[-1], 0, lat_h - mask.shape[-2]), mode="replicate") + m = m.reshape(latent_t, lat_h // 2, 2, lat_w // 2, 2).amax(dim=(2, 4)) + values = m.reshape(-1) + if bool((values >= 1.0 - 1e-3).all()): + return None + return values + + def _frame_grid(h, w): # area-normalized (h, w) coordinates of one latent frame's 2x2-patch rows area = math.sqrt(h * w) @@ -212,17 +223,22 @@ def forward(self, t_emb): return x.chunk(self.expand, dim=-1) +def _mod_row(vecs, row, dtype): + # row is a mod-row index, or a per-token LongTensor of mod-row indices + return vecs[row].to(dtype) + + def _mod_scale_shift(h, shift, scale, segments): # segments: [(start, stop, mod_row)] covering h contiguously. for a, b, row in segments: - h[a:b].mul_(1.0 + scale[row].to(h.dtype)).add_(shift[row].to(h.dtype)) + h[a:b].mul_(1.0 + _mod_row(scale, row, h.dtype)).add_(_mod_row(shift, row, h.dtype)) return h def _mod_gate(x, gate, other, segments): # other is the fresh attn/mlp output: accumulate the gated residual into the stream in place, one fused kernel per segment for a, b, row in segments: - x[a:b].addcmul_(other[a:b], gate[row].to(x.dtype)) + x[a:b].addcmul_(other[a:b], _mod_row(gate, row, x.dtype)) return x @@ -288,13 +304,15 @@ def __init__(self, hidden, t_dim, video_dim, audio_dim, eps, apply_silu=True, ad self.audio_out = operations.Linear(hidden, audio_dim, bias=True, dtype=torch.float32, device=device) def forward(self, x, t_emb, video_seg, audio_seg): - # video_seg / audio_seg: (start, stop, timestep_row) of the target streams + # video_seg / audio_seg: (start, stop, row) of the target streams, where row + # is a mod-row index or a per-token blend (see _mod_row) shift, scale = self.adaln_proj(t_emb) - va, vb, vrow = video_seg - aa, ab, arow = audio_seg - hv = (self.norm(x[va:vb]) * (1.0 + scale[vrow]) + shift[vrow]).to(torch.float32) - ha = (self.norm(x[aa:ab]) * (1.0 + scale[arow]) + shift[arow]).to(torch.float32) - return self.video_out(hv), self.audio_out(ha) + + def mod(seg): + a, b, row = seg + return (self.norm(x[a:b]) * (1.0 + _mod_row(scale, row, scale.dtype)) + _mod_row(shift, row, shift.dtype)).to(torch.float32) + + return self.video_out(mod(video_seg)), self.audio_out(mod(audio_seg)) class PackedLayout: @@ -506,7 +524,7 @@ def _cond_audio_rows(self, payload, device): rows.append(r.to(device)) return torch.cat(rows, dim=0) if rows else None - def forward(self, x, timestep, context, transformer_options={}, minimax_payload=None, **kwargs): + def forward(self, x, timestep, context, transformer_options={}, minimax_payload=None, denoise_mask=None, audio_denoise_mask=None, **kwargs): # the sampler carries the audio as (sigma_v / sigma_a) * x_audio; undo it outside # the wrappers so they and the network see the stream's own latent and velocity scale = float((minimax_payload or {}).get("audio_scale", 1.0)) @@ -523,7 +541,8 @@ def forward(self, x, timestep, context, transformer_options={}, minimax_payload= self._forward, self, comfy.patcher_extension.get_all_wrappers(comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, transformer_options) - ).execute(x, timestep, context, transformer_options, minimax_payload=minimax_payload, **kwargs) + ).execute(x, timestep, context, transformer_options, minimax_payload=minimax_payload, + denoise_mask=denoise_mask, audio_denoise_mask=audio_denoise_mask, **kwargs) if scale != 1.0: # d/d(sigma_v) of the carried variable @@ -531,7 +550,7 @@ def forward(self, x, timestep, context, transformer_options={}, minimax_payload= + (1.0 + (scale - 1.0) * sigma_a).to(out[1].dtype) * out[1]) return out - def _forward(self, x, timestep, context, transformer_options={}, minimax_payload=None, **kwargs): + def _forward(self, x, timestep, context, transformer_options={}, minimax_payload=None, denoise_mask=None, audio_denoise_mask=None, **kwargs): video_x, audio_x = x[0], x[1] orig_t, orig_h, orig_w = video_x.shape[2], video_x.shape[3], video_x.shape[4] video_x = comfy.ldm.common_dit.pad_to_patch_size(video_x, self.patch_size) @@ -561,16 +580,47 @@ def _forward(self, x, timestep, context, transformer_options={}, minimax_payload # distinct timesteps are known analytically: text/pad follow video, cond rows pin near 1 vis_aug = float(payload.get("visual_cond_noise_aug", VISUAL_COND_TIMESTEP)) aud_aug = float(payload.get("audio_cond_noise_aug", AUDIO_COND_TIMESTEP)) - has_vis_cond = any(k in ("cond", "ref_img") for _, _, k in layout.segments) - has_aud_cond = any(k in ("cond_audio", "ref_audio") for _, _, k in layout.segments) seg_t = {"text": t_v, "video": t_v, "audio": t_a, "cond": max(t_v, vis_aug), "ref_img": max(t_v, vis_aug), "cond_audio": max(t_a, aud_aug), "ref_audio": max(t_a, aud_aug)} - unique_t = sorted({t_v, t_a} | ({seg_t["cond"]} if has_vis_cond else set()) - | ({seg_t["ref_audio"]} if has_aud_cond else set())) + + # masked rows run at their own strength: mask value m puts a row at sigma = m * sigma_stream, + # so its label is 1 - m * sigma, clamped at the cond timestep for fully preserved rows + t_pin_v = max(t_v, VISUAL_COND_TIMESTEP) + t_pin_a = max(t_a, AUDIO_COND_TIMESTEP) + video_rows_t = None + audio_rows_t = None + if denoise_mask is not None: + m = mask_row_values(denoise_mask[0, 0].to(torch.float32), latent_t, lat_h, lat_w) + if m is not None: + rows_t = (1.0 - m * sigma_v.to(m.device)).clamp(max=t_pin_v) + if rows_t.unique().numel() == 1: + seg_t["video"] = float(rows_t[0]) + else: + video_rows_t = rows_t + if audio_denoise_mask is not None: + m = audio_denoise_mask[0, 0].to(torch.float32).reshape(-1) + if not bool((m >= 1.0 - 1e-3).all()): + sigma_a = 1.0 - t_a + rows_t = (1.0 - m * sigma_a).clamp(max=t_pin_a) + if rows_t.unique().numel() == 1: + seg_t["audio"] = float(rows_t[0]) + else: + audio_rows_t = rows_t + + unique_t = sorted({t_v, t_a} | {seg_t[k] for _, _, k in layout.segments} + | (set(video_rows_t.unique().tolist()) if video_rows_t is not None else set()) + | (set(audio_rows_t.unique().tolist()) if audio_rows_t is not None else set())) t_row = {t: i for i, t in enumerate(unique_t)} seg_tag = {"text": 1, "video": 0, "audio": 2, "cond": 0, "ref_img": 0, "cond_audio": 2, "ref_audio": 2} + def rows_to_mod_index(rows_t, tag): + # per-row timestep values -> per-row mod-row indices into the t_emb table + levels = rows_t.unique() + base = torch.tensor([t_row[v] * 3 + tag for v in levels.tolist()], + dtype=torch.long, device=rows_t.device) + return base[torch.searchsorted(levels, rows_t)] + text_tags = payload.get("text_token_tags") mod_segments = [] for a, b, kind in layout.segments: @@ -583,6 +633,10 @@ def _forward(self, x, timestep, context, transformer_options={}, minimax_payload if i == b - a or tags[i] != tags[run_start]: mod_segments.append((a + run_start, a + i, row_base + int(tags[run_start]))) run_start = i + elif kind == "video" and video_rows_t is not None: + mod_segments.append((a, b, rows_to_mod_index(video_rows_t, seg_tag[kind]))) + elif kind == "audio" and audio_rows_t is not None: + mod_segments.append((a, b, rows_to_mod_index(audio_rows_t, seg_tag[kind]))) else: mod_segments.append((a, b, row_base + seg_tag[kind])) @@ -659,8 +713,16 @@ def block_wrap(args): comfy.model_prefetch.prefetch_queue_pop(prefetch_queue, device, None) # target streams are single contiguous segments (audio then video, last two) - video_seg = next((a, b, t_row[seg_t["video"]]) for a, b, k in layout.segments if k == "video") - audio_seg = next((a, b, t_row[seg_t["audio"]]) for a, b, k in layout.segments if k == "audio") + va, vb, _ = next(s for s in layout.segments if s[2] == "video") + aa, ab, _ = next(s for s in layout.segments if s[2] == "audio") + if video_rows_t is not None: + video_seg = (va, vb, rows_to_mod_index(video_rows_t, 0) // 3) + else: + video_seg = (va, vb, t_row[seg_t["video"]]) + if audio_rows_t is not None: + audio_seg = (aa, ab, rows_to_mod_index(audio_rows_t, 0) // 3) + else: + audio_seg = (aa, ab, t_row[seg_t["audio"]]) v, a = self.final_layer(h, t_emb, video_seg, audio_seg) video_out = unpatchify_video(v, latent_t, lat_h // 2, lat_w // 2, self.latents_dim, self.patch_size) diff --git a/comfy/model_base.py b/comfy/model_base.py index 6705eb6c39c..79f711e92e4 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -2179,6 +2179,11 @@ def extra_conds(self, **kwargs): payload["seed"] = kwargs.get("seed", 0) # same value process_latent_in/out used, so the model never undoes a scale that was not applied payload["audio_scale"] = self.audio_scale() + + denoise_mask = kwargs.get("denoise_mask", None) + if denoise_mask is not None: + out.update(self._denoise_mask_conds(denoise_mask, latent_shapes)) + if cross_attn is not None and latent_shapes is not None and len(latent_shapes) > 1: # packed layout built once per sampling run, h/w rounded up to the DiT's 2x2 patch vs = latent_shapes[0] @@ -2189,6 +2194,65 @@ def extra_conds(self, **kwargs): out['minimax_payload'] = comfy.conds.CONDConstant(payload) return out + def _pool_masks_to_token_grid(self, masks): + # pool the per-pixel masks to the label grid with amax: video per 2x2 DiT patch, audio per latent frame + video_mask = masks[0] + h, w = video_mask.shape[-2:] + ph, pw = self.diffusion_model.patch_size[1:] + lead = video_mask.shape[:-2] + video_mask = torch.nn.functional.pad(video_mask.reshape((-1,) + video_mask.shape[-3:]), (0, -w % pw, 0, -h % ph), mode="replicate") + video_mask = video_mask.reshape(lead + video_mask.shape[-2:]) + video_mask = video_mask.reshape(video_mask.shape[:-2] + (video_mask.shape[-2] // ph, ph, video_mask.shape[-1] // pw, pw)).amax(dim=(-3, -1)) + pooled = [video_mask.repeat_interleave(ph, dim=-2).repeat_interleave(pw, dim=-1)[..., :h, :w]] + if len(masks) > 1: + audio_mask = masks[1].amax(dim=1, keepdim=True) + pooled.append(audio_mask.expand_as(masks[1]).contiguous()) + return pooled + + def _token_grid_masks(self, denoise_mask, latent_shapes): + masks = utils.unpack_latents(denoise_mask, latent_shapes) + return [torch.ceil(mask * 256.0) / 256.0 for mask in self._pool_masks_to_token_grid(masks)] + + def _denoise_mask_values(self, denoise_mask, latent_shapes): + if latent_shapes is None or len(latent_shapes) < 2: + return {} + masks = self._token_grid_masks(denoise_mask, latent_shapes) + out = {} + if torch.amin(masks[0]).item() < 1.0 - 1e-3: + out['denoise_mask'] = masks[0][:1, :1].clone() + if torch.amin(masks[1]).item() < 1.0 - 1e-3: + out['audio_denoise_mask'] = masks[1][:1].amax(dim=1, keepdim=True) + return out + + def _denoise_mask_conds(self, denoise_mask, latent_shapes): + return {name: comfy.conds.CONDRegular(value) for name, value in self._denoise_mask_values(denoise_mask, latent_shapes).items()} + + def scale_latent_inpaint(self, sigma, noise, latent_image, x=None, denoise_mask=None, **kwargs): + # preserved regions run at the cond timestep, inject them at cond strength + shapes = self.latent_shapes + if shapes is None or len(shapes) < 2: + return super().scale_latent_inpaint(sigma=sigma, noise=noise, latent_image=latent_image, **kwargs) + cleans = utils.unpack_latents(latent_image, shapes) + noises = utils.unpack_latents(noise, shapes) + aug = comfy.ldm.minimax.model.VISUAL_COND_TIMESTEP # H3's video timestep is 0.999 by default + cleans[0] = aug * cleans[0] + (1.0 - aug) * noises[0] + scale = self.audio_scale() + if scale != 1.0: + # the sampler carries audio as (sigma_v / sigma_a) * x_audio and latent_image + # holds audio_scale * x_audio, so rescale for the model to see it clean + model_sampling = self.model_sampling + sigma_v = sigma.clamp(min=1e-6) + sigma_a = comfy.ldm.minimax.model.time_shift_sigma(sigma_v, model_sampling.shift, model_sampling.audio_shift) + factor = (sigma_v / sigma_a) / scale + cleans[1] = cleans[1] * factor.view(factor.shape[:1] + (1,) * (cleans[1].ndim - 1)).to(cleans[1].dtype) + injected = utils.pack_latents(cleans)[0] + if x is None or denoise_mask is None: + return injected + token_grid_mask = utils.pack_latents(self._token_grid_masks(denoise_mask, shapes))[0] + x_blend_weight = (token_grid_mask - denoise_mask) / (1.0 - denoise_mask).clamp(min=1e-6) + x_blend_weight = torch.where(denoise_mask < 1.0, x_blend_weight.clamp(0.0, 1.0), torch.zeros_like(x_blend_weight)) + return injected + x_blend_weight.to(injected.dtype) * (x - injected) + class TripoSplat(BaseModel): def __init__(self, model_config, model_type=ModelType.FLOW, device=None): super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.triposplat.model.LatentSeqMMFlowModel) diff --git a/comfy/samplers.py b/comfy/samplers.py index 1d6a4e10473..94307c1a70b 100755 --- a/comfy/samplers.py +++ b/comfy/samplers.py @@ -636,7 +636,7 @@ def __call__(self, x, sigma, denoise_mask, model_options={}, seed=None): if "denoise_mask_function" in model_options: denoise_mask = model_options["denoise_mask_function"](sigma, denoise_mask, extra_options={"model": self.inner_model, "sigmas": self.sigmas}) latent_mask = 1. - denoise_mask - x = x * denoise_mask + self.inner_model.inner_model.scale_latent_inpaint(x=x, sigma=sigma, noise=self.noise, latent_image=self.latent_image) * latent_mask + x = x * denoise_mask + self.inner_model.inner_model.scale_latent_inpaint(x=x, sigma=sigma, noise=self.noise, latent_image=self.latent_image, denoise_mask=denoise_mask) * latent_mask out = self.inner_model(x, sigma, model_options=model_options, seed=seed) if denoise_mask is not None: out = out * denoise_mask + self.latent_image * latent_mask diff --git a/comfy/sd.py b/comfy/sd.py index 4bdaa978c17..06679c6fb90 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -907,7 +907,14 @@ def estimate_memory(shape, dtype, num_layers = 16, kv_cache_multiplier = 2): self.upscale_index_formula = (4, 16, 16) self.downscale_ratio = (lambda a: max(0, math.floor((a + 3) / 4)), 16, 16) self.downscale_index_formula = (4, 16, 16) - if self.latent_channels in [48, 128]: # Wan 2.2 and LTX2 + if self.latent_channels == 24 and sd["decoder.22.bias"].shape[0] == 12: # MiniMax H3 + self.first_stage_model = comfy.taesd.taehv.TAEHV(latent_channels=self.latent_channels, latent_format=None) + self.process_input = self.process_output = lambda image: image + self.upscale_ratio = (lambda a: max(1, (a - 2) // 5 * 17 + 5), 16, 16) + self.downscale_ratio = (lambda a: max(1, (a - 1) // 17 * 5 + 2) if a > 1 else 1, 16, 16) + self.memory_used_encode = lambda shape, dtype: (400 * ((shape[-3] + 16) // 17) * shape[-2] * shape[-1] * model_management.dtype_size(dtype)) + self.memory_used_decode = lambda shape, dtype: ((260 * 16 * 16 + shape[1] * shape[-3]) * shape[-2] * shape[-1] * model_management.dtype_size(dtype)) + elif self.latent_channels in [48, 128]: # Wan 2.2 and LTX2 self.first_stage_model = comfy.taesd.taehv.TAEHV(latent_channels=self.latent_channels, latent_format=None) # taehv doesn't need scaling self.process_input = self.process_output = lambda image: image self.process_output = lambda image: image diff --git a/comfy/taesd/taehv.py b/comfy/taesd/taehv.py index 696013200be..ffa9f89d174 100644 --- a/comfy/taesd/taehv.py +++ b/comfy/taesd/taehv.py @@ -131,10 +131,11 @@ def __init__(self, latent_channels, parallel=False, encoder_time_downscale=(True self.latent_channels = latent_channels self.parallel = parallel self.latent_format = latent_format + self.is_h3 = self.latent_channels == 24 self.show_progress_bar = show_progress_bar self.process_in = latent_format().process_in if latent_format is not None else (lambda x: x) self.process_out = latent_format().process_out if latent_format is not None else (lambda x: x) - if self.latent_channels in [48, 32]: # Wan 2.2 and HunyuanVideo1.5 + if self.latent_channels in [48, 32, 24]: # Wan 2.2, HunyuanVideo1.5 and MiniMax H3 self.patch_size = 2 elif self.latent_channels == 128: # LTX2 self.patch_size, self.latent_channels, encoder_time_downscale, decoder_time_upscale = 4, 128, (True, True, True), (True, True, True) @@ -176,6 +177,21 @@ def show_progress_bar(self, value): def encode(self, x, **kwargs): x = x.movedim(2, 1) # [B, C, T, H, W] -> [B, T, C, H, W] + if self.is_h3: + single_frame = x.shape[1] == 1 + batch = x.shape[0] + x = torch.cat([x, x[:, -1:].expand(-1, -x.shape[1] % 17, -1, -1, -1)], dim=1) + x = F.pad(x.reshape(batch, -1, 17, *x.shape[2:]), (0, 0, 0, 0, 0, 0, 3, 0)) + if self.parallel: + x = apply_model_with_memblocks(self.encoder, x.flatten(0, 1), True, self.show_progress_bar, + patch_size=self.patch_size) + x = x.reshape(batch, -1, *x.shape[2:]) + else: + x = torch.cat([apply_model_with_memblocks(self.encoder, chunk, False, False, + patch_size=self.patch_size) + for chunk in tqdm(x.unbind(1), disable=not self.show_progress_bar)], dim=1) + x = x[:, :1] if single_frame else x[:, :-3] + return self.process_out(x.movedim(2, 1)) if x.shape[1] % self.t_downscale != 0: # pad at end to multiple of t_downscale n_pad = self.t_downscale - x.shape[1] % self.t_downscale @@ -189,7 +205,16 @@ def decode(self, x, **kwargs): x = x.unsqueeze(0) if x.ndim == 4 else x # [T, C, H, W] -> [1, T, C, H, W] x = x.movedim(1, 2) if x.shape[1] != self.latent_channels else x # [B, T, C, H, W] or [B, C, T, H, W] x = self.process_in(x).movedim(2, 1) # [B, C, T, H, W] -> [B, T, C, H, W] + if self.is_h3: + single_frame = x.shape[1] == 1 x = apply_model_with_memblocks(self.decoder, x, self.parallel, self.show_progress_bar, output_device=comfy.model_management.intermediate_device(), patch_size=self.patch_size, decode=True) + if self.is_h3: + x.clamp_(0, 1) + if not single_frame: + chunk_frames = 5 * self.t_upscale + x = F.pad(x, (0, 0, 0, 0, 0, 0, 0, -x.shape[1] % chunk_frames)) + x = x.unflatten(1, (-1, chunk_frames))[:, :, self.frames_to_trim:].flatten(1, 2) + return x[:, :-3 * self.t_upscale].movedim(2, 1) return x[:, self.frames_to_trim:].movedim(2, 1) diff --git a/comfy/text_encoders/minimax.py b/comfy/text_encoders/minimax.py index c2dc47f7f14..d79ccf0ea8e 100644 --- a/comfy/text_encoders/minimax.py +++ b/comfy/text_encoders/minimax.py @@ -127,10 +127,6 @@ def __init__(self, embedding_directory=None, tokenizer_data={}): tokenizer = lambda *a, **kw: Qwen3VLSDTokenizer(*a, **kw, embedding_size=5120, embedding_key="qwen3vl_32b") super().__init__(embedding_directory=embedding_directory, tokenizer_data=tokenizer_data, name="qwen3vl_32b", tokenizer=tokenizer) - def _text_ids(self, text): - tok = self.qwen3vl_32b.tokenizer - return tok(text, add_special_tokens=False)["input_ids"] - @staticmethod def _vision_entry(data, video_block=False): emb = {"type": "image", "data": data, "original_type": "image"} @@ -143,7 +139,16 @@ def tokenize_with_weights(self, text, return_word_ids=False, images=[], entries = [] def add_text(s): - entries.extend((tid, 1.0) for tid in self._text_ids(s)) + if not s: + return + token_batches = self.qwen3vl_32b.tokenize_with_weights( + s, + return_word_ids=False, + disable_weights=True, + ) + if len(token_batches) != 1: + raise ValueError("MiniMax H3 text segment exceeds the supported prompt length.") + entries.extend(token_batches[0]) def add_vision(data, video_block=False): entries.append((VISION_START, 1.0)) diff --git a/latent_preview.py b/latent_preview.py index 6bf2c186984..d98b70019af 100644 --- a/latent_preview.py +++ b/latent_preview.py @@ -11,7 +11,7 @@ default_preview_method = args.preview_method MAX_PREVIEW_RESOLUTION = args.preview_size -VIDEO_TAES = ["taehv", "lighttaew2_2", "lighttaew2_1", "lighttaehy1_5", "taeltx_2"] +VIDEO_TAES = ["taehv", "lighttaew2_2", "lighttaew2_1", "lighttaehy1_5", "taeltx_2", "taeh3"] def preview_to_image(latent_image, do_scale=True): if do_scale: @@ -136,4 +136,3 @@ def set_preview_method(override: str = None): args.preview_method = method return args.preview_method = default_preview_method - diff --git a/nodes.py b/nodes.py index 1a3dd3f484f..fa3a7794955 100644 --- a/nodes.py +++ b/nodes.py @@ -768,7 +768,7 @@ def load_lora_model_only(self, model, lora_name, strength_model): return (self.load_lora(model, None, lora_name, strength_model, 0)[0],) class VAELoader: - video_taes = ["taehv", "lighttaew2_2", "lighttaew2_1", "lighttaehy1_5", "taeltx_2"] + video_taes = ["taehv", "lighttaew2_2", "lighttaew2_1", "lighttaehy1_5", "taeltx_2", "taeh3"] image_taes = ["taesd", "taesdxl", "taesd3", "taef1", "taef2"] @staticmethod