"""The small settings nodes of the MiniMax H3 Endless Video generator. Each one answers a single question and plugs into the generator (PromptForge H3 Endless Video): Modes ............ T2V / I2V / L2V / FL2V / REF2V, one clip or parts, extend a video, second stage on/off; shows which references are on Idea ............. your own text (the idea of the video) with saved ideas, the prompt template and the prompt writer Parts ............ optional: a box per part for a long video made part by part References ....... who is in it (character pictures and their descriptions, a voice) Video Format ..... how big and how long (aspect ratio, megapixels, seconds per part) Continuity ....... how the parts join (carry the ending forward, cuts, smoothing) Sampling ......... how the first stage is sampled (sampler, steps, cfg) Second Stage ..... optional: upscale every part and sample it again Live Preview ..... optional: what you see while it samples Speed and Memory . optional: attention backend and feed-forward chunking Every node is optional except the Idea or the Action node, whichever the Modes node picks: an unconnected one means its defaults. Connecting the Second Stage node is what turns the second stage on, so none of them has an on/off switch for a whole section. """ import importlib import comfy.samplers from . import h3_actions, h3_upscaler, preview_decoder from . import empty_latent CATEGORY = "🔮 PromptForge/Endless Video" FPS = 24 # how the parts join: what the user sees -> what the generator does JOIN_MODES = { "carry the last frames forward": "anchor tail", "give the ending as a reference video": "reference video", "independent parts (no carry-over)": "none", } STORY_STYLES = ("continue the same shot", "let the story decide", "cut to a new shot") CARRIED_FRAMES = ("1", "5", "22", "39", "56", "73") PROMPT_STYLES = ("standard", "NSFW") MODES = ("T2V - text to video", "I2V - first frame to video", "L2V - last frame to video", "FL2V - first and last frame to video", "REF2V - references (pictures, videos, sounds)") GENERATIONS = ("one clip", "several parts (endless video)") PROMPT_SOURCES = ("my idea (Idea node)", "action preset (Action node)", "parts (Parts node)") SUBJECT_DISABLED = "disabled (not in the video)" SUBJECT_SOURCES = (SUBJECT_DISABLED, "none (describe in words)", "first frame") + tuple(f"reference picture {number}" for number in range(1, 10)) AUTOMATIC_TEMPLATE = "automatic (by mode and style)" DIALOGUE_AUTO = "auto (from the prompts)" DIALOGUE_LANGUAGES = (DIALOGUE_AUTO, "Arabic", "Chinese", "English", "French", "German", "Italian", "Japanese", "Korean", "Portuguese", "Russian", "Spanish", "other (type below)") PREVIEW_MODES = ("live", "none", "ComfyUI's own") ATTENTION = ("comfy kitchen attention", "pytorch attention", "as connected") DEFAULTS = { "idea": {"general_prompt": "", "negative_prompt": "", "dialogue": "", "dialogue_language": DIALOGUE_AUTO, "dialogue_language_other": ""}, "action": {"action": next(iter(h3_actions.ACTIONS)), "penetration": "vaginal", "camera_angle": h3_actions.ANY_CAMERA, "notes": "", "negative_prompt": "", "dialogue": "", "dialogue_language": DIALOGUE_AUTO, "dialogue_language_other": "", **{f"subject_{number}_{part}": value for number in range(1, 4) for part, value in (("from", SUBJECT_SOURCES[1] if number < 3 else SUBJECT_DISABLED), ("look", ""))}}, "writer": {"use_prompt_writer": True, "prompt_template": AUTOMATIC_TEMPLATE, "prompt_style": "standard", "writer_creativity": 0.7, "writer_top_p": 0.8, "writer_top_k": 20, "writer_repetition_penalty": 1.0, "writer_presence_penalty": 1.0, "writer_max_tokens": 3072, "check_prompts": True, "review_prompts": False}, "parts": {"parts": 1, "part_prompts": "", "part_notes": "", "category": h3_actions.ACTION_CATEGORIES[0], "action": next(iter(h3_actions.ACTIONS)), "penetration": "vaginal", "camera_angle": h3_actions.ANY_CAMERA, "subject_1_from": "none (describe in words)", "subject_1_look": "", "subject_2_from": "none (describe in words)", "subject_2_look": "", "subject_3_from": "disabled (not in the video)", "subject_3_look": ""}, "references": {}, "modes": {"mode": MODES[0], "generation": GENERATIONS[0], "extend_existing_video": False, "two_stage_upscale": True, "prompt_source": PROMPT_SOURCES[0]}, "format": {"aspect_ratio": "16:9", "orientation": "landscape", "megapixels": 0.5, "seconds": 5.0}, "continuity": {"how_parts_join": next(iter(JOIN_MODES)), "story_style": STORY_STYLES[0], "carried_frames": "22", "smooth_joins": True, "check_joins": False, "join_retries": 1}, "sampling": {"sampler_name": "euler", "scheduler": "beta57", "steps": 8, "cfg": 1.0, "shift_video": 0.0, "shift_audio": 0.0}, "second_stage": {"upscale_model": h3_upscaler.default_model(), "size_mode": h3_upscaler.MODES[0], "scale": 2.0, "megapixels": 1.0, "width": 1280, "height": 704, "align": 32, "precision": "fp16", "steps": 3, "start_sigma": 0.9035, "sampler_name": "er_sde"}, "preview": {"mode": PREVIEW_MODES[0], "frames": 4, "fps": 8, "size": 512, "decoder": preview_decoder.default_decoder()}, "performance": {"attention": ATTENTION[0], "ffn_chunks": 3, "sparse_attention": True, "keep_percent": 10.0}, "continue_from": {}, } def settings(name, given): """The defaults of one settings node, overridden by what the connected node gave.""" return {**DEFAULTS[name], **(given or {})} class _Settings: """Base of the settings nodes: the widgets go into one dict, which is the node's only output.""" CATEGORY = CATEGORY FUNCTION = "build" KEY = "" def build(self, **values): return (values,) def _dialogue_inputs(): return { "negative_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Things the video must not contain."}), "dialogue": ("STRING", {"default": "", "multiline": True, "tooltip": "What the characters talk about and how it sounds. Empty = no dialogue instructions."}), "dialogue_language": (list(DIALOGUE_LANGUAGES), {"default": DIALOGUE_AUTO, "tooltip": "Language of the spoken lines. The list is what MiniMax H3 speaks well."}), "dialogue_language_other": ("STRING", {"default": "", "tooltip": "Used when the language is 'other', for example Polish. Languages outside the list work only partly."}), } class PROMPTFORGE_H3Idea(_Settings): KEY = "idea" RETURN_TYPES = ("PROMPTFORGE_H3_IDEA",) RETURN_NAMES = ("idea",) DESCRIPTION = ("Your own text: what the video is about. The prompt writer turns it into a full MiniMax H3 prompt (or, with the writer off, your text is the prompt as it is). " "Ideas you like can be saved and flipped through with the bar at the top. With a Parts node connected this is the general idea and each part has its own box. " "The Modes node chooses between this node and the Action node.") @classmethod def INPUT_TYPES(cls): return { "required": { "general_prompt": ("STRING", {"default": "", "multiline": True, "placeholder": "Your idea: what happens, who is in it, the look, the sound.", "tooltip": "What the video is about. With a Parts node: the context for every part; each part shows what its own box says."}), }, "optional": _dialogue_inputs(), } class PROMPTFORGE_H3Action(_Settings): KEY = "action" RETURN_TYPES = ("PROMPTFORGE_H3_ACTION",) RETURN_NAMES = ("action",) DESCRIPTION = ("Adult content from a preset instead of your own text: a solo act or a position with clearly defined roles, who is who, and a camera angle. Every role has its own picture picker, " "so the woman and the man each get their own reference picture (Picture 1, 2, 3 ...), the first frame, or none and only a description. The preset also recommends the frame shape: " "9:16 for standing, sitting, on-top and solo action, 16:9 for lying and side arrangements. The prompt writer writes the whole video prompt (NSFW template), for one clip or spread over the parts. " "Works with every mode. The Modes node chooses between this node and the Idea node.") @classmethod def INPUT_TYPES(cls): subjects = {} for number in range(1, 4): subjects[f"subject_{number}_from"] = (list(SUBJECT_SOURCES), {"default": SUBJECT_SOURCES[1] if number < 3 else SUBJECT_DISABLED, "tooltip": "Which picture shows this subject: the first frame, a reference picture by the number of its References slot, or none and only the description. The label above says which role it is (woman, man, partner)."}) subjects[f"subject_{number}_look"] = ("STRING", {"default": "", "multiline": True, "tooltip": "The person in words: hair, body, face, age. Used when no picture is chosen for this role, and added to the picture when one is."}) return { "required": { "category": (list(h3_actions.ACTION_CATEGORIES), {"default": h3_actions.ACTION_CATEGORIES[0], "tooltip": "Filters the action list; the act itself is chosen below."}), "action": (list(h3_actions.ACTIONS), {"default": next(iter(h3_actions.ACTIONS)), "tooltip": "The act or position. The roles of the subjects are shown below it."}), "camera_angle": ([h3_actions.ANY_CAMERA] + list(h3_actions.CAMERAS), {"default": h3_actions.ANY_CAMERA, "tooltip": "How it is filmed. POV (the viewer is B) is what subject 2 sees."}), }, "optional": { "penetration": (list(h3_actions.PENETRATION_MODES), {"default": "vaginal", "tooltip": "For penetrative sex: vagina or anus. Forces the named hole for the whole clip so the writer never swaps them."}), **subjects, "notes": ("STRING", {"default": "", "multiline": True, "placeholder": "Setting, looks, mood, sounds. Not the act: that is the action above.", "tooltip": "Anything else the video needs: the room, the lighting, the mood, the sounds."}), **_dialogue_inputs(), }, } class PROMPTFORGE_H3Writer(_Settings): KEY = "writer" RETURN_TYPES = ("PROMPTFORGE_H3_WRITER",) RETURN_NAMES = ("writer_settings",) DESCRIPTION = "How the prompt writer writes: template, sampling, retries and approval. It is the same for the Idea and the Action node. Not connected: the defaults." @classmethod def INPUT_TYPES(cls): templates = [AUTOMATIC_TEMPLATE] + [name for name in importlib.import_module(__package__).PRESET_OPTIONS if name != "auto"] return { "required": { "use_prompt_writer": ("BOOLEAN", {"default": True, "tooltip": "On: the language model turns your idea into a full H3 prompt. Off: your idea text is used as the finished prompt, as it is (an action preset needs the writer)."}), "prompt_template": (templates, {"default": AUTOMATIC_TEMPLATE, "tooltip": "The prompt writer's template, SFW and NSFW alike. A template you pick is used as it is, ignoring the style. Automatic picks the one of the mode (T2VA, I2VA, L2VA, FL2VA, Ref2VA), NSFW for an action preset or when the style below says so."}), "prompt_style": (list(PROMPT_STYLES), {"default": "standard", "tooltip": "Standard or NSFW templates, used only while the prompt template is automatic."}), "writer_creativity": ("FLOAT", {"default": 0.7, "min": 0.01, "max": 2.0, "step": 0.05, "tooltip": "The writer's temperature: how freely it words things. Lower sticks closer to your text."}), "writer_top_p": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "The writer's top_p."}), "writer_top_k": ("INT", {"default": 20, "min": 0, "max": 1000, "tooltip": "The writer's top_k."}), "writer_repetition_penalty": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 2.0, "step": 0.01, "tooltip": "The writer's repetition penalty. Qwen3-VL is tuned for 1.0; raise only if a model still loops."}), "writer_presence_penalty": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "Discourages already-seen tokens. 1.5 is the Qwen serving value, 0.5-1.0 is safer for long structured prompts, 0 = off."}), "writer_max_tokens": ("INT", {"default": 3072, "min": 256, "max": 8192, "tooltip": "The longest prompt the writer may write. A prompt that runs to this limit has usually lost track; its runaway tail is cut."}), "check_prompts": ("BOOLEAN", {"default": True, "tooltip": "Write the prompt again (new seed, up to 3 tries) when a check finds a problem: empty, repeated, a refusal, wrong dialogue tags, or a broken H3 output contract (missing section, extra shots, timestamps, wrong length). Off = the first answer is used. Every retry costs a full writing pass."}), "review_prompts": ("BOOLEAN", {"default": False, "tooltip": "Pause after each prompt is written: a panel lets you edit it, approve it, or have it written again. Needs the ComfyUI page open."}), }, } class PROMPTFORGE_H3Parts(_Settings): KEY = "parts" RETURN_TYPES = ("PROMPTFORGE_H3_PARTS",) RETURN_NAMES = ("parts",) DESCRIPTION = ("For a long video made part by part: a box for what happens in each part (press + / -). Connect it, and set the Modes node to several parts. " "Without this node the generator makes one clip. A box can start with (10s) for a part of its own length, or raw: to use it as the finished prompt.") @classmethod def INPUT_TYPES(cls): return { "required": { "parts": ("INT", {"default": 3, "min": 1, "max": 200, "tooltip": "Number of parts. Press + / - on the node."}), "part_prompts": ("STRING", {"default": "", "multiline": True, "tooltip": "The part boxes (kept by the node)."}), "part_notes": ("STRING", {"default": "", "multiline": True, "tooltip": "One line per part with what changes in that part: 1: speed up, 2: pause and talk, 3: pull out and come on her stomach. Lines are matched to parts in order."}), "category": (list(h3_actions.ACTION_CATEGORIES), {"default": h3_actions.ACTION_CATEGORIES[0]}), "action": (list(h3_actions.ACTIONS), {"default": next(iter(h3_actions.ACTIONS))}), }, "optional": { "penetration": (list(h3_actions.PENETRATION_MODES), {"default": "vaginal"}), "camera_angle": ([h3_actions.ANY_CAMERA] + list(h3_actions.CAMERAS), {"default": h3_actions.ANY_CAMERA}), "subject_1_from": (list(SUBJECT_SOURCES), {"default": "none (describe in words)"}), "subject_1_look": ("STRING", {"default": "", "multiline": True}), "subject_2_from": (list(SUBJECT_SOURCES), {"default": "none (describe in words)"}), "subject_2_look": ("STRING", {"default": "", "multiline": True}), "subject_3_from": (list(SUBJECT_SOURCES), {"default": SUBJECT_DISABLED}), "subject_3_look": ("STRING", {"default": "", "multiline": True}), }, } class PROMPTFORGE_H3Modes(_Settings): KEY = "modes" RETURN_TYPES = ("PROMPTFORGE_H3_MODES",) RETURN_NAMES = ("modes",) DESCRIPTION = ("What kind of video this is. The mode decides how the video is encoded, not only the prompt template: " "T2V text only (empty latent); I2V the first frame is encoded and anchored at frame 0; L2V the last frame is anchored at the end; " "FL2V both; REF2V pictures, videos and sounds are encoded as references. Also: one clip or several parts, extending a video you already have, and the second stage.") @classmethod def INPUT_TYPES(cls): return { "required": { "mode": (list(MODES), {"default": MODES[0], "tooltip": "T2V: text only. I2V: needs a first frame. L2V: needs a last frame. FL2V: needs both. REF2V: needs reference pictures or videos (sounds optional). The frames and references come from the References node. REF2V is best with a ref2va checkpoint."}), "generation": (list(GENERATIONS), {"default": GENERATIONS[0], "tooltip": "One clip: one video of the length set in Video Format. Several parts: a long video made part by part from the Parts node, joined with the Continuity settings."}), "extend_existing_video": ("BOOLEAN", {"default": False, "tooltip": "Continue the video from the Continue Existing Video node; the result starts with it."}), "two_stage_upscale": ("BOOLEAN", {"default": True, "tooltip": "Run the Second Stage (upscale and refine) when it is connected. Off = first pass only, much faster, for trying prompts."}), "prompt_source": (list(PROMPT_SOURCES), {"default": PROMPT_SOURCES[0], "tooltip": "What the prompt writer writes from: your own text (Idea node), the Action node, or the Parts node. The other nodes are ignored."}), }, } class PROMPTFORGE_H3References(_Settings): KEY = "references" RETURN_TYPES = ("PROMPTFORGE_H3_REFERENCES",) RETURN_NAMES = ("references",) DESCRIPTION = ("Everything the video is built from besides the idea: the first and last frame (I2V, L2V, FL2V) and the reference pictures, videos and sounds (REF2V), each with a description. " "They are numbered in the order of the slots: is the first connected picture,