Pulse Voice
Why your H3 character's mouth isn't moving — and the node that fixes it
- audio
- voice
An AUDIO socket in ComfyUI carries a waveform and absolutely nothing else. Not a role, not an owner, and - the kicker - not a position in time. For MiniMax H3 lip sync, that third one is the whole ballgame: a lip-sync clip gets cut to the exact seconds of the window it rides in, and if nothing says where the recording starts, the render succeeds, the report says nothing, and the mouth just doesn't move. People blamed H3 for that for a long time. It wasn't H3.
PulseVoice is Pulse Studio's answer: a small metadata wrapper - one recording, plus what it's for, whose it is, and where on the film clock it begins. Its PULSE_VOICE output goes into Pulse Slate's voices.voice_1..3 group (every window of a multi-window film) or into one Pulse Shot's voice input (that shot alone). It's the newest node in the pack.
What it actually is
Before this node, a narration covering a four-window film meant four identical wires from one LoadAudio into every shot's ref_audio - that socket was the only route to lip sync, and each wire re-trimmed the file at film-clock seconds. PulseVoice collapses it: one node on the slate, one wire, every window reached.
The real reason it exists is the failure it closes. A take recorded for one shot starts at that shot, not at the film - wired into ref_audio it gets cut past its own end, the window gets silence, no error, no warning, and a talking head with a dead mouth. PulseVoice is where a recording finally gets to say where it starts.
How it works
Understand one thing first: H3 always generates its own audio track. A reference recording changes what the model does alongside it, and it never reaches the language model - the tokenizer only emits a marker like <Audio 1>: . So the prompt text has to name the recording.
Two jobs, set by the role field:
lip_sync- the character's mouth matches the recording, and the clip is cut to each window's exact span so both describe the same seconds.voice_timbre- the model speaks the shot's own written dialogue and only borrows the character of the voice. No alignment, no trim.
The inputs that matter
Most are things you set once and forget:
name- the@handleyou cite in a shot's prompt (@Voice). Never an ordinal; those are computed at compile time, so renumbering can't desync.aligns_to-film_clock(default): the first sample is the film's second zero, for a narration spanning the timeline.shot_start: the first sample is the start of the shot it's wired to, for a per-shot take. The field the whole node exists for.speaker- the character's@Namefrom the Asset Bin. Gets them a stable(S1)speaker id across the whole film, which binds the voice to a face on a two-hander. Leave blank on a one-hander.offset_seconds- added on top ofaligns_to, for a take with two seconds of room tone in front (use-2.0).trim_start/trim_end- which part of the file to use;-1ontrim_endmeans "to the end".description- a few words that ride into the prompt'ssubject_definitionsbeside the reference.
The optional audio socket takes the recording. Leave it unwired and the node describes nothing and says so. Output is voice (PULSE_VOICE).
Installing it
ComfyUI Manager (search "comfyui-pulse-studio") or the old-fashioned way:
cd ComfyUI/custom_nodes
git clone https://github.com/Addis-Pulse-Studio/comfyui-pulse-studio
Restart. There's no pip install - the pack has zero dependencies of its own, deliberately. You do need ComfyUI 0.30.0+ carrying comfy_extras/nodes_minimax_h3.py, and the H3 weights from Comfy-Org/MiniMax-H3 (into models/diffusion_models/minimax/, models/text_encoders/minimax/, models/vae/minimax/). A plain reference-driven render only needs the ~20 GB ref2va DiT branch. example_workflows/PulseSlate_Voice.json shows the whole thing in one graph.
Where people get burned
- A
lip_syncrecording on a shot with no dialogue produces no mouth movement, and nothing warns you. There's no speech to match. Put the recording on the shot that speaks. - The report is your friend. It measures coverage: "
@Voicecovers 0.00-12.40s and window 3 covers 24.50-36.75s. The two do not meet." Dead mouth? Read the report before blaming the model. - A quote plus a lip-sync recording gives the model two answers to "what is she saying." If the quote isn't literally the recording's transcript, drop it.
- On Linux/macOS the model loader widgets load red - a Windows-authored path separator quirk; re-pick each file and the graph is unchanged.
- Keep
cfgat 1.0 - H3's native path has no negative conditioning, and anything above it runs an empty-negative CFG that was asked for rather than recommended.
And the boring legal bit: the weights carry the MiniMax H3 Community License, which excludes the US, EU, UK and South Korea; the pack itself is Apache-2.0.
Inputs (10)
| Name | Type | Default | Description |
|---|---|---|---|
| schema_version | STRING | 3.0.0 | Which widget layout this node was saved with. Written by the node, read at load time to restore values by name. Do not edit. |
| name | STRING | Voice | The @handle for this recording. Cite it in a shot as @Voice so the prompt names it; the ordinal is computed, never typed. |
| role | COMBO | lip_sync | 'lip_sync': the character's mouth matches this recording, and the clip is cut to each window's exact span so the two describe the same seconds. 'voice_timbre': the model speaks the shot's own written dialogue and borrows only the character of the voice. No alignment, no trim. |
| aligns_to | COMBO | film_clock | Where this recording's first sample sits. 'film_clock': at the start of the film -- what a single narration track covering the whole timeline does, and what every project written before this node existed means. 'shot_start': at the start of the shot it is connected to, which is what a per-shot take does. On PulseSlate there is no shot, so 'shot_start' is reported and read as 'film_clock'. |
| offset_seconds | FLOAT | 0.00-3600–3600 | Added on top of whatever `aligns_to` chose. Use it when the recording begins a little before or after that point -- a take with two seconds of room tone in front of it wants -2.0. |
| trim_start | FLOAT | 0.000–3600 | Skip this many seconds of the file before anything else. Applies to a file on disk; a recording arriving through the `audio` socket is already whatever the upstream node handed over. |
| trim_end | FLOAT | -1.00-1–3600 | Stop at this many seconds into the file. -1 means run to the end. |
| speaker | STRING | Whose voice this is, as an @Name from the Asset Bin. That character gets a speaker id -- (S1), (S2) -- stable across the whole film, and the prompt says this recording is theirs rather than 'this character'. Leave blank on a one-hander, where there is nothing to confuse. | |
| description | STRING | What this recording is, in a few words. Rides into the prompt's subject_definitions beside the reference itself. | |
| audioopt | AUDIO | The recording. Without it this node describes nothing and is ignored, with a note saying so. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| voice | PULSE_VOICE | — |