The Montreal Forced Aligner (MFA) aligns spoken audio with its transcript at word and phoneme level. It is a prerequisite for speech editing (to locate edit boundaries), dataset preparation, and TTS training.

pyfoal: Github MFA: Github MFA docs: readthedocs

Installation

conda install -c conda-forge montreal-forced-aligner
pip install pyfoal

Inference Script

import os
import torch
import librosa
import pyfoal

os.environ['MFA_ROOT_DIR'] = '/path/to/mfa_temp'
os.makedirs(os.environ['MFA_ROOT_DIR'], exist_ok=True)


def align_audio_text(audio_path, text):
    """
    Align audio with text using MFA.
    Returns a list of dicts with word, phonemes, start, end, is_silence.
    """
    audio_np, sr = librosa.load(audio_path, sr=16000)
    audio = torch.FloatTensor(audio_np).unsqueeze(0)

    alignment = pyfoal.from_text_and_audio(
        text, audio, 16000, aligner='mfa', gpu=0
    )

    results = []
    for mfa_word in alignment.words():
        word_text = mfa_word.word
        phonemes = [p.phoneme for p in mfa_word.phonemes]

        if mfa_word.phonemes:
            start = mfa_word.phonemes[0]._start
            end = mfa_word.phonemes[-1]._end
        else:
            start = end = 0.0

        is_silence = (word_text.strip() == '' or len(phonemes) == 0)

        results.append({
            'word': word_text,
            'phonemes': phonemes,
            'start': start,
            'end': end,
            'is_silence': is_silence
        })

    return results


if __name__ == "__main__":
    audio_path = "test_audio.wav"
    text = "This is a transcript of the test audio."

    results = align_audio_text(audio_path, text)

    for i, w in enumerate(results):
        if w['is_silence']:
            print(f"{i:2d}. [SILENCE] [{w['start']:.3f}s - {w['end']:.3f}s]")
        else:
            print(f"{i:2d}. '{w['word']:10s}' [{w['start']:.3f}s - {w['end']:.3f}s]  {w['phonemes']}")

Common Problem: GitHub Rate Limit

Problem: pyfoal checks GitHub for MFA model files on every call, hitting the 60 requests/hour rate limit quickly during bulk processing.

Fix: Edit the pyfoal source to check for local files first.

File: path/to/conda_env/lib/python3.10/site-packages/pyfoal/baselines/mfa.py

Backup first:

cp path/to/pyfoal/baselines/mfa.py path/to/pyfoal/baselines/mfa.py.backup

Change lines 64–67 from:

manager = mfa.models.ModelManager()
manager.download_model('dictionary', 'english_mfa')
manager.download_model('acoustic', 'english_mfa')

To:

import os
manager = mfa.models.ModelManager()
mfa_root = os.environ.get('MFA_ROOT_DIR', os.path.expanduser('~/Documents/MFA'))
dict_path = os.path.join(mfa_root, 'pretrained_models/dictionary/english_mfa.dict')
acoustic_path = os.path.join(mfa_root, 'pretrained_models/acoustic/english_mfa.zip')

if os.path.exists(dict_path):
    print(f"[MFA] Dictionary found locally")
else:
    print(f"[MFA] Downloading dictionary from GitHub")
    manager.download_model('dictionary', 'english_mfa')

if os.path.exists(acoustic_path):
    print(f"[MFA] Acoustic model found locally")
else:
    print(f"[MFA] Downloading acoustic model from GitHub")
    manager.download_model('acoustic', 'english_mfa')

This has been tested and eliminates the rate limit problem entirely.