Nodes/ComfyUI-ForcedAlignTimings/Forced Align Timings 🎯⏱️
ComfyUI Node

Forced Align Timings 🎯⏱️

Know exactly when each word starts in a TTS clip — no Whisper required

By veoreg·Created 3 months ago·Updated 3 months ago· 1
Forced Align Timings 🎯⏱️
  • audio
  • timings_json
  • audio_ms
  • word_count
text
device

If you've ever tried to burn word-by-word captions or karaoke-style subtitles onto a TTS clip, you know the usual path: run the audio through Whisper, get its guesses at timestamps, and hope it got the words right. This node does the opposite, and the name tells you exactly how. It forces the alignment.

ForcedAlignTimings takes an audio clip plus the exact text that was spoken, and returns a JSON blob with a start time, end time, and confidence score for every word. It's built on Meta's MMS forced-alignment model (torchaudio.pipelines.MMS_FA), not ASR. Since you synthesized the audio yourself, the text is known - so you align audio to that text instead of transcribing it and fuzzy-matching the transcript. That's a meaningfully more robust approach on the cases that wreck Whisper: repeated words ("test test test"), numbers, and languages it's shaky on. No API calls, no key, no upload - everything runs locally.

One honest caveat before you get excited: this is a personal-tool-grade node, shipped out of one author's own pipeline (a Synthy/iRead TTS provider feeding an n8n automation, per the README). Don't expect a polished ecosystem around it. It's one node, one job, and it does that job cleanly enough to serve as a working reference if you want to bolt word timestamps onto your own TTS workflow.

How it works

The node runs a short pipeline, all in-process:

  1. Prep the audio. Your AUDIO is converted to mono and resampled to 16 kHz, which is what MMS expects.
  2. Tokenize the text. Words are split out with their character offsets (char_start / char_end) counted in Python codepoints.
  3. Romanize. Words get romanized via uroman (handy for Russian), then mapped to the model's dictionary tokens.
  4. Align. The MMS wav2vec2 model produces an emission, torchaudio.functional.forced_align runs Viterbi alignment against your token sequence, and token spans get merged back into per-word times in milliseconds.

The one dependency worth calling out is the model itself: MMS_FA downloads roughly 1 GB on first use. In a plain desktop ComfyUI install that's just a patient first run. If you're running this in a Docker image on a remote box, bake the cache in or mount it - the README is explicit that a cold-start network fetch will bite you in production.

The inputs and outputs that matter

It's a small surface. Wire in an audio (AUDIO) from any TTS or audio-producing node, and paste the exact spoken text into text (multiline STRING). That's 90% of it. device (auto/cuda/cpu, default auto) is the only thing left to touch - let it pick CUDA if you have it, because the wav2vec2 model is far happier on GPU.

The outputs:

  • timings_json (STRING) - the payload, with sample_rate, audio_ms, align_text, and words[]. Each word carries value, char_start/char_end, time, end (ms), and score (0–1).
  • audio_ms (INT) - total clip length, handy for sanity checks.
  • word_count (INT) - how many words actually got aligned.

Because the node is marked an output node, its results also land in ComfyUI's /history API - which is exactly how the author's n8n workflow grabs timings_json without touching the UI. If you're driving ComfyUI headlessly, that's the pattern to copy.

Installing it

Easiest route is ComfyUI Manager - search "ComfyUI-ForcedAlignTimings". Or, the manual way:

cd ComfyUI/custom_nodes
git clone https://github.com/veoreg/ComfyUI-ForcedAlignTimings
# restart ComfyUI, then:
pip install uroman

torch and torchaudio are already required by ComfyUI, so uroman is effectively the only extra pip install. Note it's even semi-optional - without it the node falls back to a builtin Cyrillic→Latin table, though you'll get better Russian results with it installed.

Where people get burned

  • Feed the node the wrong text. Forced alignment is exact-match, not "close enough." If what you pass doesn't match what was actually synthesized, you'll get empty or garbage alignments. This is the #1 conceptual trap - the word "forced" is the whole contract.
  • The first-run model download. A ~1 GB pull on a fresh environment, or a crash if your Docker box has no internet at cold start. Pre-warm it (python nodes.py sample.wav "test" in the repo works as a smoke test).
  • Codepoint offsets aren't byte offsets. char_start/char_end are Python codepoint indices into the text you gave it, not UTF-16 or byte positions. The README's downstream code converts them to UTF-16 against the original request text - if you're wiring this into a captioning system, remember who owns that conversion.

The score per word is your friend for QA: low scores mean a weak alignment, worth eyeballing before you ship the captions. For the niche it's built for - turning already-known TTS text into word-level timing data - this node is a tidy, dependency-light solution that skips Whisper entirely.

Categoryaudio/tts

Inputs (3)

NameTypeDefaultDescription
audioAUDIO
textSTRING
deviceoptCOMBO3 options: auto, cuda, cpu

Outputs (3)

NameTypeDescription
timings_jsonSTRING
audio_msINT
word_countINT