Forced Align (QwenASR)
Word-Level Timestamps for an Hour of Audio, Without Whisper's Chunk Drift
- audio
- WORD_TIMESTAMPS
The problem this node exists to dodge
Transcription is the easy half. Every ASR model writes down what was said. Getting when each word was said is what quietly ruins subtitle jobs, and it gets worse the longer the audio runs: timestamps drift, and once you hit the model's duration ceiling you have to chop somewhere - a chop that lands mid-sentence, so the end of chunk one and the start of chunk two get timed by a model that never heard the other half.
AILab_Qwen3ForcedAlign is 1038lab's answer to that, and the specialist node in a three-node pack: audio in, optional transcript in, word-level times out, however long the file is. Captioning a podcast or a generated clip and needing per-word rather than per-sentence timing? This is the node the pack was built around. Audio is still the thin, bolted-on layer of the ComfyUI stack (audio-generation.md), and word timing is what a lip-sync or on-screen-text workflow needs as input.
How the iterative aligner actually works
For audio under chunk_audio_sec the node calls the aligner once and formats the result. Everything clever is in the long path. The trick, stated in the source comments: feed the aligner a big audio window containing fewer words than it can hold. Estimate the script's speaking rate (total_words / total_duration), then send in only as many words as fit in chunk_audio_sec - min_tail_sec. That leaves a deliberate tail of empty audio, keeping the aligner in the "too few words" regime - the safe direction where every word gets an accurate timestamp instead of being smeared across the end of the window. The last kept word's end time becomes the anchor for the next window's start, and it repeats.
backoff_words (15 default) is how many words to throw away at the end of each window; those are the ones most likely to be mistimed, so they get re-aligned at the head of the next chunk. The cost is real - it's iterative, and each iteration runs the aligner on up to 240 seconds of audio.
The inputs you actually touch
- audio (required) - resampled to 16 kHz mono internally, so feed it whatever your audio loader gives you.
- text - the known transcript. Leave it blank and the node transcribes first, then aligns: one node instead of a chain. That ASR model isn't selectable here; it uses the default
repo_idfromconfig.json, which ships as the 0.6B. - language - 31 options including
auto. Auto-detect reads the audio when it transcribes and otherwise guesses from your transcript's characters. The trap: the list is shared with the ASR node, so it offers languages the forced aligner doesn't actually support. Hindi is the documented example - ASR handles it, the aligner returns partial timings. - chunk_audio_sec / min_tail_sec - the two knobs that matter. 300 seconds is the model's ceiling, so leave headroom. Raising
min_tail_secabove 60 is safer and slower. - precision, attention, normalize_text, unload_models - defaults are fine.
autoattention already falls back to fp16/SDPA on Apple Silicon;normalize_textrewrites digits and acronym spacing inside the emitted words.
What comes out
One output: WORD_TIMESTAMPS (STRING) - one line per word, start-end: word, so it wires into ShowText, a preview node, or anything downstream that parses text. It is not an SRT and there's no subtitle output here; for that you want the pack's Subtitle (QwenASR) node. This one is for when you're consuming the timings yourself.
Install
Via Manager, search the pack title - the README's tip is that typing ASR into ComfyUI's node search finds all three nodes. Manually:
cd ComfyUI/custom_nodes
git clone https://github.com/1038lab/ComfyUI-QwenASR.git
cd ComfyUI-QwenASR
pip install -r requirements.txt
Restart. The aligner (Qwen/Qwen3-ForcedAligner-0.6B-hf) downloads on first run into ComfyUI/models/Qwen3-ASR/. Official -hf checkpoints are required - the old un-suffixed ones are refused by the loader. In mainland China set "source": "ModelScope" in config.json; models on another drive are found via extra_model_paths.yaml.
Where people get burned
The dependency stack. requirements.txt pins transformers>=5.13.0 - the exact conflict this ecosystem fights constantly, because custom nodes share one Python environment with no isolation (comfyui-ecosystem.md). If you run LLM or captioning nodes, read the pip output before restarting. The same file pulls nagisa and soynlp, which bring DyNet38 - a C++ extension that compiles on install, and the step most likely to fail on Windows.
Blank transcript costs you a second model in VRAM. Auto-transcribe loads the ASR model and the aligner, so on a 12GB card with a video model already resident, budget for the juggle.
A name error at the finish line. In the v1.1.0 code I read, AILab_IterativeForcedAlign.py ends with if all_items and transcript_text: - a variable that's never defined, so a successful alignment raises instead of returning your string. The fix is two renames:
sed -i 's/if all_items and transcript_text:/if all_items and text:/; s/_restore_punctuation(all_items, transcript_text)/_restore_punctuation(all_items, text)/' \
ComfyUI/custom_nodes/ComfyUI-QwenASR/AILab_IterativeForcedAlign.py
The flip side: when alignment fails, all_items is empty, the buggy line never runs, and you get a silent empty string. Empty output and a crash are two different problems here.
Language support isn't symmetric. Early testing found the aligner lagging the ASR model on coverage. Patchy timings for only some words usually means the aligner doesn't handle that language - not that your chunk settings are wrong.
Inputs (11)
| Name | Type | Default | Description |
|---|---|---|---|
| audio | AUDIO | Audio input to align. | |
| textopt | STRING | Known transcript text to force-align against the audio. If left blank, speech is auto-transcribed first. | |
| languageopt | COMBO | auto | Language of the transcript. 'auto' detects from audio/text. |
| forced_aligneropt | COMBO | Qwen/Qwen3-ForcedAligner-0.6B-hf | Forced aligner model. |
| precisionopt | COMBO | bf16 | Inference precision. |
| attentionopt | COMBO | auto | Attention backend override. |
| chunk_audio_secopt | INT | 24060–300 | Audio window size per iteration (seconds). Must be under the model's 300s limit. |
| min_tail_secopt | INT | 6010–120 | Minimum seconds of empty audio after the last word. Larger = safer but more iterations. |
| backoff_wordsopt | INT | 153–50 | Words to back off from the end of each chunk to avoid edge effects. |
| normalize_textopt | BOOLEAN | true | Normalize numbers ('一百二十八' -> '128') and acronym spacing ('A S R' -> 'ASR') in output timestamps. |
| unload_modelsopt | BOOLEAN | true | Unload cached models after inference. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| WORD_TIMESTAMPS | STRING | — |