Publish current ComfyUI O1Key code baseline
Replace the prior release tree with the current plugin, frontend, tests, and documentation. Document retired node IDs and the public Gitea update source.
This commit is contained in:
@@ -0,0 +1,579 @@
|
||||
"""MiniMax-H3 video generation through a New API gateway."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Optional
|
||||
|
||||
import folder_paths
|
||||
from comfy_api.latest import InputImpl, io
|
||||
|
||||
from ..clients.minimax_h3_client import MiniMaxH3Client
|
||||
from ..utils.config import get_base_url_by_route
|
||||
from ..utils.image_utils import tensor_to_pil
|
||||
from ..utils.minimax_h3_media import (
|
||||
MAX_REFERENCE_AUDIOS,
|
||||
MAX_REFERENCE_IMAGES,
|
||||
MAX_REFERENCE_VIDEOS,
|
||||
validate_image,
|
||||
validate_reference_audios,
|
||||
validate_reference_videos,
|
||||
)
|
||||
from ..utils.r2_uploader import upload_audio, upload_image, upload_video
|
||||
|
||||
|
||||
MODEL_ID = "MiniMax-H3"
|
||||
MODEL_MAX_ID = "MiniMax-H3-MAX"
|
||||
MODEL_OPTIONS = [MODEL_ID, MODEL_MAX_ID]
|
||||
MODE_TEXT = "文生视频"
|
||||
MODE_FIRST = "首帧图生视频"
|
||||
MODE_LAST = "尾帧图生视频"
|
||||
MODE_FIRST_LAST = "首尾帧生视频"
|
||||
MODE_REFERENCE = "参考素材生视频"
|
||||
|
||||
RESOLUTION_OPTIONS = ["2K", "768P", "480P"]
|
||||
MODEL_RESOLUTIONS = {
|
||||
MODEL_ID: {"768P", "2K"},
|
||||
MODEL_MAX_ID: {"480P", "768P"},
|
||||
}
|
||||
MODEL_DURATION_RANGES = {
|
||||
MODEL_ID: (4, 15),
|
||||
MODEL_MAX_ID: (5, 15),
|
||||
}
|
||||
RATIO_OPTIONS = ["16:9", "21:9", "4:3", "1:1", "3:4", "9:16"]
|
||||
REFERENCE_RATIO_OPTIONS = ["adaptive", *RATIO_OPTIONS]
|
||||
MAX_PROMPT_CHARS = 7000
|
||||
MAX_REQUEST_BODY_BYTES = 64 * 1024 * 1024
|
||||
MAX_REFERENCE_MATERIALS = 12
|
||||
MAX_SEED = 2**31 - 1
|
||||
|
||||
|
||||
def _validate_prompt(prompt: str) -> str:
|
||||
prompt = (prompt or "").strip()
|
||||
if not prompt:
|
||||
raise ValueError("MiniMax H3 提示词不能为空。")
|
||||
if len(prompt) > MAX_PROMPT_CHARS:
|
||||
raise ValueError(
|
||||
f"MiniMax H3 提示词最多 {MAX_PROMPT_CHARS} 字符,当前为 {len(prompt)} 字符。"
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
||||
def _validate_seed(seed: int) -> int:
|
||||
if isinstance(seed, bool) or not isinstance(seed, int) or not 0 <= seed <= MAX_SEED:
|
||||
raise ValueError(f"MiniMax H3 seed 必须是 0~{MAX_SEED} 的整数。")
|
||||
return seed
|
||||
|
||||
|
||||
def _validate_generation_options(
|
||||
model: str,
|
||||
resolution: str,
|
||||
duration: int,
|
||||
mode: Optional[str] = None,
|
||||
) -> None:
|
||||
if model not in MODEL_OPTIONS:
|
||||
raise ValueError(f"不支持的 MiniMax 模型:{model}")
|
||||
allowed_resolutions = MODEL_RESOLUTIONS[model]
|
||||
if resolution not in allowed_resolutions:
|
||||
allowed_text = "、".join(
|
||||
value for value in RESOLUTION_OPTIONS if value in allowed_resolutions
|
||||
)
|
||||
raise ValueError(f"{model} 分辨率仅支持 {allowed_text}。")
|
||||
minimum_duration, maximum_duration = MODEL_DURATION_RANGES[model]
|
||||
if (
|
||||
isinstance(duration, bool)
|
||||
or not isinstance(duration, int)
|
||||
or not minimum_duration <= duration <= maximum_duration
|
||||
):
|
||||
raise ValueError(
|
||||
f"{model} 时长必须是 {minimum_duration}~{maximum_duration} 的整数。"
|
||||
)
|
||||
if model == MODEL_MAX_ID and mode == MODE_REFERENCE:
|
||||
raise ValueError("MiniMax-H3-MAX 不支持图片、视频或音频参考素材模式。")
|
||||
|
||||
|
||||
def _validate_reference_counts(
|
||||
reference_images,
|
||||
reference_videos,
|
||||
reference_audios,
|
||||
) -> None:
|
||||
if len(reference_images) > MAX_REFERENCE_IMAGES:
|
||||
raise ValueError(f"参考图片最多 {MAX_REFERENCE_IMAGES} 张。")
|
||||
if len(reference_videos) > MAX_REFERENCE_VIDEOS:
|
||||
raise ValueError(f"参考视频最多 {MAX_REFERENCE_VIDEOS} 个。")
|
||||
if len(reference_audios) > MAX_REFERENCE_AUDIOS:
|
||||
raise ValueError(f"参考音频最多 {MAX_REFERENCE_AUDIOS} 个。")
|
||||
total = len(reference_images) + len(reference_videos) + len(reference_audios)
|
||||
if total > MAX_REFERENCE_MATERIALS:
|
||||
raise ValueError(
|
||||
f"参考图片、视频和音频合计最多 {MAX_REFERENCE_MATERIALS} 个,当前为 {total} 个。"
|
||||
)
|
||||
|
||||
|
||||
def _validate_image_tensor(image, label: str):
|
||||
if image is None:
|
||||
raise ValueError(f"{label}不能为空。")
|
||||
ndim = getattr(image, "dim", lambda: None)()
|
||||
if ndim == 4 and int(image.shape[0]) != 1:
|
||||
raise ValueError(f"{label}仅支持单张图片,当前批次包含 {int(image.shape[0])} 张。")
|
||||
|
||||
|
||||
def _image_to_pil(image, label: str):
|
||||
_validate_image_tensor(image, label)
|
||||
images = tensor_to_pil(image)
|
||||
if not images:
|
||||
raise ValueError(f"无法读取{label}。")
|
||||
result = images[0]
|
||||
if result.mode not in ("RGB", "RGBA"):
|
||||
result = result.convert("RGB")
|
||||
validate_image(result, label)
|
||||
return result
|
||||
|
||||
|
||||
def _validate_body_size(body: dict) -> None:
|
||||
body_size = len(
|
||||
json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||
)
|
||||
if body_size > MAX_REQUEST_BODY_BYTES:
|
||||
raise ValueError(
|
||||
f"MiniMax H3 请求体大小 {body_size / 1024 / 1024:.2f} MB 超过 64 MB 限制。"
|
||||
)
|
||||
|
||||
|
||||
def build_request_body(
|
||||
*,
|
||||
prompt: str,
|
||||
resolution: str,
|
||||
duration: int,
|
||||
mode: str,
|
||||
model: str = MODEL_ID,
|
||||
seed: int = 0,
|
||||
ratio: Optional[str] = None,
|
||||
first_url: Optional[str] = None,
|
||||
last_url: Optional[str] = None,
|
||||
reference_image_urls: Optional[list[str]] = None,
|
||||
reference_video_urls: Optional[list[str]] = None,
|
||||
reference_audio_urls: Optional[list[str]] = None,
|
||||
) -> dict:
|
||||
"""Build and validate the documented MiniMax H3 / H3 Max request body."""
|
||||
prompt = _validate_prompt(prompt)
|
||||
seed = _validate_seed(seed)
|
||||
_validate_generation_options(model, resolution, duration, mode)
|
||||
|
||||
reference_image_urls = [url for url in (reference_image_urls or []) if url]
|
||||
reference_video_urls = [url for url in (reference_video_urls or []) if url]
|
||||
reference_audio_urls = [url for url in (reference_audio_urls or []) if url]
|
||||
_validate_reference_counts(
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
)
|
||||
|
||||
has_first_last_material = bool(first_url or last_url)
|
||||
has_reference_material = bool(
|
||||
reference_image_urls or reference_video_urls or reference_audio_urls
|
||||
)
|
||||
|
||||
content = [{"type": "text", "text": prompt}]
|
||||
|
||||
if mode == MODE_TEXT:
|
||||
if has_first_last_material or has_reference_material:
|
||||
raise ValueError("文生视频不能同时使用首尾帧或参考素材。")
|
||||
if ratio not in RATIO_OPTIONS:
|
||||
raise ValueError("MiniMax H3 文生视频必须选择具体宽高比,不能使用 adaptive。")
|
||||
request_ratio = ratio
|
||||
elif mode == MODE_FIRST:
|
||||
if last_url or has_reference_material:
|
||||
raise ValueError("首帧图生视频不能同时使用尾帧或参考素材。")
|
||||
if not first_url:
|
||||
raise ValueError("首帧图生视频必须连接首帧图片。")
|
||||
content.append({
|
||||
"type": "image_url",
|
||||
"image_url": {"url": first_url},
|
||||
"role": "first_frame",
|
||||
})
|
||||
request_ratio = "adaptive"
|
||||
elif mode == MODE_LAST:
|
||||
if first_url or has_reference_material:
|
||||
raise ValueError("尾帧图生视频不能同时使用首帧或参考素材。")
|
||||
if not last_url:
|
||||
raise ValueError("尾帧图生视频必须连接尾帧图片。")
|
||||
content.append({
|
||||
"type": "image_url",
|
||||
"image_url": {"url": last_url},
|
||||
"role": "last_frame",
|
||||
})
|
||||
request_ratio = "adaptive"
|
||||
elif mode == MODE_FIRST_LAST:
|
||||
if has_reference_material:
|
||||
raise ValueError("首尾帧模式和参考素材模式不能混用。")
|
||||
if not first_url or not last_url:
|
||||
raise ValueError("首尾帧生视频必须同时连接首帧和尾帧图片。")
|
||||
content.extend([
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": first_url},
|
||||
"role": "first_frame",
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": last_url},
|
||||
"role": "last_frame",
|
||||
},
|
||||
])
|
||||
request_ratio = "adaptive"
|
||||
elif mode == MODE_REFERENCE:
|
||||
if has_first_last_material:
|
||||
raise ValueError("参考素材模式和首尾帧模式不能混用。")
|
||||
if not has_reference_material:
|
||||
raise ValueError("参考素材生视频至少需要一张图片、一个视频或一段音频。")
|
||||
content.extend(
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": url},
|
||||
"role": "reference_image",
|
||||
}
|
||||
for url in reference_image_urls
|
||||
)
|
||||
content.extend(
|
||||
{
|
||||
"type": "video_url",
|
||||
"video_url": {"url": url},
|
||||
"role": "reference_video",
|
||||
}
|
||||
for url in reference_video_urls
|
||||
)
|
||||
content.extend(
|
||||
{
|
||||
"type": "audio_url",
|
||||
"audio_url": {"url": url},
|
||||
"role": "reference_audio",
|
||||
}
|
||||
for url in reference_audio_urls
|
||||
)
|
||||
request_ratio = ratio or "adaptive"
|
||||
if request_ratio not in REFERENCE_RATIO_OPTIONS:
|
||||
raise ValueError(f"MiniMax H3 参考素材模式不支持宽高比:{request_ratio}")
|
||||
else:
|
||||
raise ValueError(f"不支持的 MiniMax H3 生成模式:{mode}")
|
||||
|
||||
body = {
|
||||
"model": model,
|
||||
"content": content,
|
||||
"resolution": resolution,
|
||||
"duration": duration,
|
||||
"ratio": request_ratio,
|
||||
"seed": seed,
|
||||
}
|
||||
_validate_body_size(body)
|
||||
return body
|
||||
|
||||
|
||||
def _build_mode_input():
|
||||
reference_images = io.Autogrow.Input(
|
||||
"参考图片组",
|
||||
template=io.Autogrow.TemplateNames(
|
||||
input=io.Image.Input("参考图片"),
|
||||
names=[f"参考图片{i}" for i in range(1, MAX_REFERENCE_IMAGES + 1)],
|
||||
min=0,
|
||||
),
|
||||
tooltip="可动态连接,最多 9 张。",
|
||||
)
|
||||
reference_videos = io.Autogrow.Input(
|
||||
"参考视频组",
|
||||
template=io.Autogrow.TemplateNames(
|
||||
input=io.Video.Input("参考视频"),
|
||||
names=[f"参考视频{i}" for i in range(1, MAX_REFERENCE_VIDEOS + 1)],
|
||||
min=0,
|
||||
),
|
||||
tooltip="可动态连接,最多 3 个;每段 2~15 秒,总时长不超过 15 秒。",
|
||||
)
|
||||
reference_audios = io.Autogrow.Input(
|
||||
"参考音频组",
|
||||
template=io.Autogrow.TemplateNames(
|
||||
input=io.Audio.Input("参考音频"),
|
||||
names=[f"参考音频{i}" for i in range(1, MAX_REFERENCE_AUDIOS + 1)],
|
||||
min=0,
|
||||
),
|
||||
tooltip="可动态连接,最多 3 段;每段 2~15 秒,总时长不超过 15 秒。",
|
||||
)
|
||||
return io.DynamicCombo.Input(
|
||||
"生成模式",
|
||||
options=[
|
||||
io.DynamicCombo.Option(
|
||||
MODE_TEXT,
|
||||
[
|
||||
io.Combo.Input(
|
||||
"宽高比",
|
||||
options=RATIO_OPTIONS,
|
||||
default="16:9",
|
||||
tooltip="文生视频必须使用具体比例,不能使用 adaptive。",
|
||||
)
|
||||
],
|
||||
),
|
||||
io.DynamicCombo.Option(
|
||||
MODE_FIRST,
|
||||
[io.Image.Input("首帧图片", tooltip="输入图片决定视频比例。")],
|
||||
),
|
||||
io.DynamicCombo.Option(
|
||||
MODE_LAST,
|
||||
[io.Image.Input("尾帧图片", tooltip="输入图片决定视频比例。")],
|
||||
),
|
||||
io.DynamicCombo.Option(
|
||||
MODE_FIRST_LAST,
|
||||
[
|
||||
io.Image.Input("首帧图片"),
|
||||
io.Image.Input("尾帧图片"),
|
||||
],
|
||||
),
|
||||
io.DynamicCombo.Option(
|
||||
MODE_REFERENCE,
|
||||
[
|
||||
reference_images,
|
||||
reference_videos,
|
||||
reference_audios,
|
||||
io.Combo.Input(
|
||||
"宽高比",
|
||||
options=REFERENCE_RATIO_OPTIONS,
|
||||
default="adaptive",
|
||||
tooltip="参考素材模式默认 adaptive,也可指定具体比例。",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
tooltip="首尾帧模式与参考素材模式互斥。",
|
||||
)
|
||||
|
||||
|
||||
def _collect_autogrow(value) -> list:
|
||||
"""Collect connected values while tolerating a legacy single input value."""
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, dict):
|
||||
return [item for item in value.values() if item is not None]
|
||||
return [value]
|
||||
|
||||
|
||||
def _make_progress_callbacks():
|
||||
try:
|
||||
from comfy.utils import ProgressBar
|
||||
|
||||
pbar = ProgressBar(100)
|
||||
except Exception:
|
||||
pbar = None
|
||||
|
||||
last_progress = -1
|
||||
|
||||
def set_progress(value: int):
|
||||
nonlocal last_progress
|
||||
value = max(0, min(100, int(value)))
|
||||
if value <= last_progress:
|
||||
return
|
||||
last_progress = value
|
||||
if pbar:
|
||||
pbar.update_absolute(value, 100)
|
||||
|
||||
def on_stage(stage: str):
|
||||
if stage == "submitting":
|
||||
print("[MiniMax H3] 正在创建任务...")
|
||||
set_progress(0)
|
||||
elif stage.startswith("submitted:"):
|
||||
print(f"[MiniMax H3] 任务已进入队列:{stage.split(':', 1)[1]}")
|
||||
elif stage == "downloading":
|
||||
print("[MiniMax H3] 生成成功,正在立即下载临时 CDN 视频...")
|
||||
elif stage == "done":
|
||||
set_progress(100)
|
||||
|
||||
def on_progress(progress: int):
|
||||
# New API returns the authoritative task percentage in data.progress.
|
||||
# Mirror it directly in ComfyUI while preventing stale poll responses
|
||||
# from moving the node progress bar backwards.
|
||||
set_progress(progress)
|
||||
|
||||
return on_stage, on_progress
|
||||
|
||||
|
||||
class MiniMaxH3Video(io.ComfyNode):
|
||||
"""MiniMax H3 / H3 Max text, frame, and reference video generation."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="MiniMaxH3Video",
|
||||
display_name="MiniMax H3 / H3 Max 视频生成",
|
||||
category="comfyui_o1key/Video",
|
||||
description=(
|
||||
"通过 New API 网关调用 MiniMax-H3 或 MiniMax-H3-MAX;H3 支持多模态参考,H3 Max 支持文生和首尾帧模式。"
|
||||
),
|
||||
inputs=[
|
||||
io.String.Input(
|
||||
"提示词",
|
||||
multiline=True,
|
||||
default="",
|
||||
placeholder="描述画面、动作、镜头与声音...",
|
||||
tooltip="必填,最多 7000 字符。",
|
||||
),
|
||||
_build_mode_input(),
|
||||
io.Combo.Input(
|
||||
"分辨率",
|
||||
options=RESOLUTION_OPTIONS,
|
||||
default="2K",
|
||||
),
|
||||
io.Int.Input(
|
||||
"时长",
|
||||
default=5,
|
||||
min=4,
|
||||
max=15,
|
||||
step=1,
|
||||
display_mode=io.NumberDisplay.slider,
|
||||
tooltip="H3 支持 4~15 秒;H3 Max 支持 5~15 秒。",
|
||||
),
|
||||
io.Combo.Input(
|
||||
"模型",
|
||||
options=MODEL_OPTIONS,
|
||||
default=MODEL_ID,
|
||||
tooltip="H3 支持 768P/2K 和多模态参考;H3 Max 支持 480P/768P,不支持参考素材模式。",
|
||||
),
|
||||
io.Int.Input(
|
||||
"seed",
|
||||
default=0,
|
||||
min=0,
|
||||
max=MAX_SEED,
|
||||
step=1,
|
||||
display_mode=io.NumberDisplay.number,
|
||||
control_after_generate=io.ControlAfterGenerate.randomize,
|
||||
tooltip="原生随机种子;相同参数与 seed 可用于复现结果。",
|
||||
),
|
||||
],
|
||||
outputs=[io.Video.Output(display_name="视频")],
|
||||
not_idempotent=True,
|
||||
search_aliases=["MiniMax H3", "MiniMax H3 Max", "海螺视频", "H3 视频"],
|
||||
accept_all_inputs=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
提示词,
|
||||
生成模式,
|
||||
分辨率,
|
||||
时长,
|
||||
模型=MODEL_ID,
|
||||
seed=0,
|
||||
**_kwargs,
|
||||
) -> io.NodeOutput:
|
||||
prompt = _validate_prompt(提示词)
|
||||
if not isinstance(生成模式, dict):
|
||||
raise ValueError("MiniMax H3 生成模式参数无效。")
|
||||
mode = 生成模式.get("生成模式")
|
||||
_validate_generation_options(模型, 分辨率, int(时长), mode)
|
||||
base_url = get_base_url_by_route()
|
||||
|
||||
first_url = None
|
||||
last_url = None
|
||||
reference_image_urls = []
|
||||
reference_video_urls = []
|
||||
reference_audio_urls = []
|
||||
|
||||
if mode == MODE_FIRST:
|
||||
first_image = 生成模式.get("首帧图片")
|
||||
first_url = await upload_image(
|
||||
_image_to_pil(first_image, "首帧图片"),
|
||||
base_url=base_url,
|
||||
)
|
||||
elif mode == MODE_LAST:
|
||||
last_image = 生成模式.get("尾帧图片")
|
||||
last_url = await upload_image(
|
||||
_image_to_pil(last_image, "尾帧图片"),
|
||||
base_url=base_url,
|
||||
)
|
||||
elif mode == MODE_FIRST_LAST:
|
||||
first_image = 生成模式.get("首帧图片")
|
||||
last_image = 生成模式.get("尾帧图片")
|
||||
first_url = await upload_image(
|
||||
_image_to_pil(first_image, "首帧图片"),
|
||||
base_url=base_url,
|
||||
)
|
||||
last_url = await upload_image(
|
||||
_image_to_pil(last_image, "尾帧图片"),
|
||||
base_url=base_url,
|
||||
)
|
||||
elif mode == MODE_REFERENCE:
|
||||
reference_images = _collect_autogrow(
|
||||
生成模式.get("参考图片组", 生成模式.get("参考图片"))
|
||||
)
|
||||
reference_videos = _collect_autogrow(
|
||||
生成模式.get("参考视频组", 生成模式.get("参考视频"))
|
||||
)
|
||||
reference_audios = _collect_autogrow(
|
||||
生成模式.get("参考音频组", 生成模式.get("参考音频"))
|
||||
)
|
||||
_validate_reference_counts(
|
||||
reference_images,
|
||||
reference_videos,
|
||||
reference_audios,
|
||||
)
|
||||
if not reference_images and not reference_videos and not reference_audios:
|
||||
raise ValueError("参考素材生视频至少需要连接一种参考素材。")
|
||||
|
||||
reference_pil_images = [
|
||||
_image_to_pil(image, f"参考图片{index}")
|
||||
for index, image in enumerate(reference_images, start=1)
|
||||
]
|
||||
validate_reference_videos(reference_videos)
|
||||
validate_reference_audios(reference_audios)
|
||||
|
||||
for image in reference_pil_images:
|
||||
reference_image_urls.append(await upload_image(image, base_url=base_url))
|
||||
for video in reference_videos:
|
||||
reference_video_urls.append(await upload_video(video, base_url=base_url))
|
||||
for audio in reference_audios:
|
||||
reference_audio_urls.append(await upload_audio(audio, base_url=base_url))
|
||||
|
||||
body = build_request_body(
|
||||
prompt=prompt,
|
||||
resolution=分辨率,
|
||||
duration=int(时长),
|
||||
mode=mode,
|
||||
model=模型,
|
||||
seed=int(seed),
|
||||
ratio=生成模式.get("宽高比"),
|
||||
first_url=first_url,
|
||||
last_url=last_url,
|
||||
reference_image_urls=reference_image_urls,
|
||||
reference_video_urls=reference_video_urls,
|
||||
reference_audio_urls=reference_audio_urls,
|
||||
)
|
||||
|
||||
temp_dir = folder_paths.get_temp_directory()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
fd, save_path = tempfile.mkstemp(
|
||||
suffix=".mp4",
|
||||
prefix="minimax_h3_",
|
||||
dir=temp_dir,
|
||||
)
|
||||
os.close(fd)
|
||||
|
||||
on_stage, on_progress = _make_progress_callbacks()
|
||||
client = MiniMaxH3Client(base_url=base_url)
|
||||
|
||||
try:
|
||||
result_path, _task_id = await client.generate_async(
|
||||
body=body,
|
||||
save_path=save_path,
|
||||
on_stage=on_stage,
|
||||
on_progress=on_progress,
|
||||
)
|
||||
return io.NodeOutput(InputImpl.VideoFromFile(result_path))
|
||||
except BaseException:
|
||||
try:
|
||||
if os.path.isfile(save_path):
|
||||
os.remove(save_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"MiniMaxH3Video": MiniMaxH3Video}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"MiniMaxH3Video": "MiniMax H3 / H3 Max 视频生成"}
|
||||
Reference in New Issue
Block a user