音声の文字起こし、もっと楽に・高精度にできるかも?MFAの活用

マイクのイラスト

こんにちは。ソニー・ミュージックエンタテインメントでWebエンジニアをしている万次郎です。

みなさんは、音声の文字起こしをどのように行なっていますか?手作業での書き起こしは時間がかかりますし、自動文字起こしツールを使っても精度に悩まされることがあるかもしれません。

最近では、OpenAI の Whisper や、その改良版である WhisperX などが登場し、高精度な文字起こし技術が話題になっています。しかし、実は特定の条件下では Whisper や WhisperX よりも高精度な方法 があります。それが Montreal Forced Aligner(MFA) です。

この記事ではMFAのしくみから、実際にどのように使うのかまでを一通り解説します。記事の最後には生成された字幕と実際の音声を同期させた作例を掲載していますので、ぜひその精度の高さをご確認いただければと思います。

MFAとは?文字起こしとは何が違うのか

Whisperなどの自動文字起こしツールは、「音声を聞いて一からテキストを生成」しますが、MFAは「すでにあるテキストを音声と一致させる」ことに特化しています。そのため、スクリプトが事前にある場合には、Whisper よりも高精度に単語ごとのタイムスタンプを取得できる のが特徴です。つまり、WhisperWhisperXはイニシャルプロンプトの設定は可能なものの、基本的にはゼロから文字起こしできるのに対して、MFAはテキストデータとそれを読み上げた音声データのペアを用いて音声の整列(アラインメント)を行います

名称 方式 入力データ
Whisper 文字起こし 音声データ
WhisperX 文字起こし 音声データ
MFA 音声アライメント 音声データ and テキストデータ

音声データを扱う際に直面する大きな課題の一つが、音声とテキストの同期です。例えば、「この音声の中で、どの時点でどの単語が発声されているのか」を特定する作業は、音声合成や音声認識の研究開発において重要な基盤となります。この同期作業を自動化するツールの一つが、Montreal Forced Aligner(MFA)なのです。

MFAの仕組み

MFAは、音声ファイルとテキストを入力として、各単語や音素(言語を構成する最小の音の単位)が音声のどの時点で発声されているかを自動的に特定します。具体的には、録音された音声に「こんにちは」というテキストが対応している場合、「こ」「ん」「に」「ち」「は」それぞれの開始時刻と終了時刻を推定するわけです。

この作業を正確に行うために、MFAはいくつかの段階的なアプローチを採用しています。

ここでは音声アライメント(音声の整列)を行うために重要なプロセスを大まかに4つに分けて解説します。

1. 音素の初期位置の特定

第1段階は「モノフォンモデル」と呼ばれる初期アラインメントです。「こんにちは」という単語を例にとると、まず音声を細かい時間枠(フレーム)に分割し、各フレームがどの音素(「こ」「ん」「に」「ち」「は」)に対応するかを推定します。この段階では、各音素を完全に独立したものとして扱い、音声の特徴(音の高さ、大きさ、周波数特性など)から最も可能性の高い音素を割り当てていきます。言い換えるなら、音声という連続した流れの中で、音素をざっくばらんに整列させていくようなものです。

2. 精緻化

第2段階では「トライフォンモデル」という、より洗練された手法を使います。人間が話すとき、ある音は前後の音の影響を受けて微妙に変化します。例えば「こんにちは」の「ん」は、前の「こ」と後ろの「に」の影響で、単独の「ん」とは少し異なる音になります。この段階では、そうした音の変化のパターンをモデル化し、より正確な位置の特定を行います。これにより、自然な発話の流れをより正確に捉えることができるのです。

3. 特徴量変換による識別性能の向上

第3段階では、音声特徴量に対して「LDA(Linear Discriminant Analysis:線形判別分析)」「MLLT(Maximum Likelihood Linear Transform:最尤線形変換)」という2つの技術的な処理を行います。これらは音声データを、音素の違いがより明確になるように変換する技術です。例えると、写真の明るさやコントラストを調整して細部をより見やすくする作業に似ています。この処理により、似たような音(例えば「か」と「が」)の区別がより正確になり、アラインメントの精度が向上します。

4. 話者適応による最終調整

最後の第4段階は「話者適応」です。音声には話者固有の特徴があります。例えば、同じ「こんにちは」でも、話者によって声の高さ、スピード、抑揚のつけ方が異なります。この段階では、その話者特有の音声パターンを学習し、モデルを調整します。技術的には「fMLLR(feature-space Maximum Likelihood Linear Regression)」という手法を使用し、話者ごとに音声特徴量を補正します。これにより、個人の話し方の癖を考慮した、より正確なアラインメントが可能になります。

これらの段階的な手順を踏むことで、高精度な音声とテキストの同期を可能にしているのです。

MFAの使い方

MFAのインストールにはconda-forgeを利用します。conda-forgeはオープンソースコミュニティによって提供されているので、MFA自体は無料で利用することができます。

以降ではmac上のMiniconda環境でインストールする方法とDocker環境でインストールする方法の2通り紹介します。

詳細な使い方は公式レポジトリ公式ページをご参照ください。

Minicondaとconda-forgeを使用するインストール方法

前提: Miniconda※がインストール済みであること

※Anacondaでの利用も可能ですが、Anacondaは従業員 200 名以上の企業での商用利用には、Business または Enterprise ライセンスが必要ですし、MFAはMinicondaでも十分動作しますので、今回はMinicondaを利用します。

ステップ1: 作業用ディレクトリの準備

お好きなディレクトリ内でmfaというディレクトリを作成し、作成したディレクトリ内に移動してください。

mkdir mfa
cd mfa

​ 最終的なディレクトリ/ファイルの構成はこちらのようになる想定です。

mfa/
├── input/
│   └── test/
│       ├── test.txt <= 入力ファイル
│       └── test.wav <= 入力ファイル
└── output/
        └── test.TextGrid <= 出力ファイル(実行後に生成されるファイルです)

ステップ2: 仮想環境の作成とアクティベート

プロジェクトごとに依存関係を分離するため、仮想環境を作成することをおすすめします。ここではPython 3.10を利用した環境を作成します。

conda create -n mfa_env python=3.10

作成した仮想環境を有効化するには、次のコマンドを実行します。

conda activate mfa_env

この環境内でMFAおよび関連パッケージが動作するため、他のプロジェクトとの依存関係の衝突を避けることができます。

ステップ3: Condaチャネルの設定

まず、Miniconda環境で以下のコマンドを実行してconda-forgeチャネルを追加します。

conda config --add channels conda-forge

ステップ4: MFAのインストール

conda-forgeチャネルが追加された状態で、MFAをインストールします。

conda install montreal-forced-aligner

ステップ5: 必要なモジュールのインストール

ステップ3でインストールはされないものの、MFAを動作させるためには必要なモジュールをインストールします。

conda install -c conda-forge spacy sudachipy sudachidict-core

ステップ6: MFAの動作確認

インストールが完了したら、以下のコマンドでMFAのバージョンやヘルプ情報を確認できます。

mfa --help

出力

Usage: mfa [OPTIONS] COMMAND [ARGS]...

 Montreal Forced Aligner is a command line utility for aligning speech and text.

╭─ Options ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --help      Show this message and exit.                                                                                                                                                                                                           │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ adapt                                 Adapt an acoustic model                                                                                                                                                                                     │
│ align                                 Align a corpus                                                                                                                                                                                              │
│ align_one                             Align a single file                                                                                                                                                                                         │
│ anchor                                Launch Anchor                                                                                                                                                                                               │
│ configure                             The configure command is used to set global defaults for MFA so you don't have to set them every time you call an MFA command.                                                                              │
│ diarize                               Diarize a corpus                                                                                                                                                                                            │
│ g2p                                   Generate pronunciations                                                                                                                                                                                     │
│ history                               Show previously run mfa commands                                                                                                                                                                            │
│ model                                 Download, inspect, and save models                                                                                                                                                                          │
│ models                                Download, inspect, and save models                                                                                                                                                                          │
│ segment                               Split long audio files into shorter segments                                                                                                                                                                │
│ segment_vad                           Split long audio files into shorter segments                                                                                                                                                                │
│ server                                Start, stop, and delete MFA database servers                                                                                                                                                                │
│ tokenize                              Tokenize utterances                                                                                                                                                                                         │
│ train                                 Train a new acoustic model                                                                                                                                                                                  │
│ train_dictionary                      Calculate pronunciation probabilities                                                                                                                                                                       │
│ train_g2p                             Train a G2P model                                                                                                                                                                                           │
│ train_ivector                         Train an ivector extractor                                                                                                                                                                                  │
│ train_lm                              Train a language model                                                                                                                                                                                      │
│ train_tokenizer                       Train a tokenizer model                                                                                                                                                                                     │
│ transcribe                            Transcribe audio files                                                                                                                                                                                      │
│ transcribe_speechbrain                Transcribe utterances using an ASR model trained by SpeechBrain                                                                                                                                             │
│ transcribe_whisper                    Transcribe utterances using a Whisper ASR model via faster-whisper                                                                                                                                          │
│ validate                              Validate corpus                                                                                                                                                                                             │
│ validate_dictionary                   Validate dictionary                                                                                                                                                                                         │
│ version                               Show version of MFA                                                                                                                                                                                         │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

以上で準備は完了です。

続いて実際に動かすための準備とコマンドをご紹介します。

ステップ7: 日本語辞書と日本語音響モデルのダウンロード

推論を行うために、日本語辞書と日本語の音響モデルをローカルにダウンロードします。

日本語辞書のダウンロード

mfa model download dictionary japanese_mfa

日本語音響モデルのダウンロード

mfa model download acoustic japanese_mfa

実行後、下記のようなパスにファイルが保存されます。

/Users/ユーザー名/Documents/MFA/pretrained_models/dictionary/japanese_mfa.dict
/Users/ユーザー名/Documents/MFA/pretrained_models/acoustic/japanese_mfa.zip

実行環境によってダウンロード先が異なることにご注意ください。

ステップ8: アライメント対象のデータの準備

日本語音響モデルを利用したアライメントを行うためには、以下の2種類のファイルが必要です。

  • 音声ファイル(.wav)
  • 対応する文字起こしや台本ファイル(.txt)

これらのファイルは、同じフォルダに配置し、かつファイル名(拡張子を除く)を同一にしてください。

※厳密には同一である必要はありませんが、実行のわかりやすさのため今回は同一にしています。

例えば、以下のように配置してください。

mfa/
└── input/
    └── test/
        ├── test.txt
        └── test.wav

私はテストとして、4秒程度沈黙した後に「私の名前は万次郎です。最近はモンスターハンターにハマっています。」と発話したwavファイルと「私の名前は万次郎です。最近はモンスターハンターにハマっています。」と記載したtxtファイルを用意しました。

ステップ9: MFAによるアライメントの実行

準備が整ったら、ターミナルで次のコマンドを実行します。

この例では、入力フォルダ、音響モデル、および出力フォルダをそれぞれ指定しています。

mfa align ./input/test/ japanese_mfa ~/Documents/MFA/pretrained_models/acoustic/japanese_mfa.zip ./output/test/
  • ./input/test/: 音声ファイルと文字起こしが入ったフォルダのパス
  • japanese_mfa: 使用する音響モデルの名前(ダウンロードしたモデルの識別子)
  • ~/Documents/MFA/pretrained_models/acoustic/japanese_mfa.zip: ダウンロード済みの音響モデルのZIPファイルのパス
  • ./output/test: アラインメント結果を出力するフォルダのパス(任意の場所でOK)

私の場合は、入力ファイルがあるフォルダ直下にoutput/testというフォルダを作成し、そこに結果を出力しています。

ステップ10: 結果の確認

出力先フォルダ(今回の場合はoutput/test)には、入力と同名のTextGridファイルが生成され、各音素や単語の開始・終了時刻が記録されています。

item [1] には基本的には単語ベースの出力、 item [2]には音素ベースで出力されます。

test.TextGridの出力

```
File type = "ooTextFile"
Object class = "TextGrid"

xmin = 0 
xmax = 9.636281 
tiers? <exists> 
size = 2 
item []: 
    item [1]:
        class = "IntervalTier" 
        name = "words" 
        xmin = 0 
        xmax = 9.636281 
        intervals: size = 18 
        intervals [1]:
            xmin = 0.0 
            xmax = 2.37 
            text = "" 
        intervals [2]:
            xmin = 2.37 
            xmax = 2.81 
            text = "私" 
        intervals [3]:
            xmin = 2.81 
            xmax = 2.93 
            text = "の" 
        intervals [4]:
            xmin = 2.93 
            xmax = 3.26 
            text = "名前" 
        intervals [5]:
            xmin = 3.26 
            xmax = 3.52 
            text = "は" 
        intervals [6]:
            xmin = 3.52 
            xmax = 3.62 
            text = "" 
        intervals [7]:
            xmin = 3.62 
            xmax = 4.2 
            text = "万次郎" 
        intervals [8]:
            xmin = 4.2 
            xmax = 4.62 
            text = "です" 
        intervals [9]:
            xmin = 4.62 
            xmax = 5.04 
            text = "" 
        intervals [10]:
            xmin = 5.04 
            xmax = 5.59 
            text = "最近" 
        intervals [11]:
            xmin = 5.59 
            xmax = 5.82 
            text = "は" 
        intervals [12]:
            xmin = 5.82 
            xmax = 5.96 
            text = "" 
        intervals [13]:
            xmin = 5.96 
            xmax = 6.48 
            text = "モンスター" 
        intervals [14]:
            xmin = 6.48 
            xmax = 6.9 
            text = "ハンター" 
        intervals [15]:
            xmin = 6.9 
            xmax = 7.02 
            text = "に" 
        intervals [16]:
            xmin = 7.02 
            xmax = 7.48 
            text = "ハマって" 
        intervals [17]:
            xmin = 7.48 
            xmax = 8.01 
            text = "います" 
        intervals [18]:
            xmin = 8.01 
            xmax = 9.636281 
            text = "" 
    item [2]:
        class = "IntervalTier" 
        name = "phones" 
        xmin = 0 
        xmax = 9.636281 
        intervals: size = 63 
        intervals [1]:
            xmin = 0.0 
            xmax = 2.37 
            text = "" 
        intervals [2]:
            xmin = 2.37 
            xmax = 2.48 
            text = "w" 
        intervals [3]:
            xmin = 2.48 
            xmax = 2.53 
            text = "a" 
        intervals [4]:
            xmin = 2.53 
            xmax = 2.6 
            text = "t" 
        intervals [5]:
            xmin = 2.6 
            xmax = 2.65 
            text = "a" 
        intervals [6]:
            xmin = 2.65 
            xmax = 2.77 
            text = "ɕ" 
        intervals [7]:
            xmin = 2.77 
            xmax = 2.81 
            text = "i" 
        intervals [8]:
            xmin = 2.81 
            xmax = 2.85 
            text = "n" 
        intervals [9]:
            xmin = 2.85 
            xmax = 2.93 
            text = "o" 
        intervals [10]:
            xmin = 2.93 
            xmax = 2.99 
            text = "n" 
        intervals [11]:
            xmin = 2.99 
            xmax = 3.04 
            text = "a" 
        intervals [12]:
            xmin = 3.04 
            xmax = 3.12 
            text = "m" 
        intervals [13]:
            xmin = 3.12 
            xmax = 3.2 
            text = "a" 
        intervals [14]:
            xmin = 3.2 
            xmax = 3.26 
            text = "e" 
        intervals [15]:
            xmin = 3.26 
            xmax = 3.34 
            text = "w" 
        intervals [16]:
            xmin = 3.34 
            xmax = 3.52 
            text = "a" 
        intervals [17]:
            xmin = 3.52 
            xmax = 3.62 
            text = "" 
        intervals [18]:
            xmin = 3.62 
            xmax = 3.72 
            text = "m" 
        intervals [19]:
            xmin = 3.72 
            xmax = 3.8 
            text = "a" 
        intervals [20]:
            xmin = 3.8 
            xmax = 3.88 
            text = "ɲ" 
        intervals [21]:
            xmin = 3.88 
            xmax = 3.97 
            text = "dʑ" 
        intervals [22]:
            xmin = 3.97 
            xmax = 4.03 
            text = "i" 
        intervals [23]:
            xmin = 4.03 
            xmax = 4.09 
            text = "ɾ" 
        intervals [24]:
            xmin = 4.09 
            xmax = 4.2 
            text = "oː" 
        intervals [25]:
            xmin = 4.2 
            xmax = 4.26 
            text = "d" 
        intervals [26]:
            xmin = 4.26 
            xmax = 4.35 
            text = "e" 
        intervals [27]:
            xmin = 4.35 
            xmax = 4.56 
            text = "s" 
        intervals [28]:
            xmin = 4.56 
            xmax = 4.62 
            text = "ɨ̥" 
        intervals [29]:
            xmin = 4.62 
            xmax = 5.04 
            text = "" 
        intervals [30]:
            xmin = 5.04 
            xmax = 5.19 
            text = "s" 
        intervals [31]:
            xmin = 5.19 
            xmax = 5.25 
            text = "a" 
        intervals [32]:
            xmin = 5.25 
            xmax = 5.35 
            text = "i" 
        intervals [33]:
            xmin = 5.35 
            xmax = 5.44 
            text = "c" 
        intervals [34]:
            xmin = 5.44 
            xmax = 5.53 
            text = "i" 
        intervals [35]:
            xmin = 5.53 
            xmax = 5.59 
            text = "ɴ" 
        intervals [36]:
            xmin = 5.59 
            xmax = 5.67 
            text = "w" 
        intervals [37]:
            xmin = 5.67 
            xmax = 5.82 
            text = "a" 
        intervals [38]:
            xmin = 5.82 
            xmax = 5.96 
            text = "" 
        intervals [39]:
            xmin = 5.96 
            xmax = 6.02 
            text = "m" 
        intervals [40]:
            xmin = 6.02 
            xmax = 6.1 
            text = "o" 
        intervals [41]:
            xmin = 6.1 
            xmax = 6.16 
            text = "ɰ̃" 
        intervals [42]:
            xmin = 6.16 
            xmax = 6.23 
            text = "s" 
        intervals [43]:
            xmin = 6.23 
            xmax = 6.31 
            text = "t" 
        intervals [44]:
            xmin = 6.31 
            xmax = 6.48 
            text = "aː" 
        intervals [45]:
            xmin = 6.48 
            xmax = 6.54 
            text = "h" 
        intervals [46]:
            xmin = 6.54 
            xmax = 6.62 
            text = "a" 
        intervals [47]:
            xmin = 6.62 
            xmax = 6.7 
            text = "n" 
        intervals [48]:
            xmin = 6.7 
            xmax = 6.79 
            text = "t" 
        intervals [49]:
            xmin = 6.79 
            xmax = 6.9 
            text = "aː" 
        intervals [50]:
            xmin = 6.9 
            xmax = 6.96 
            text = "ɲ" 
        intervals [51]:
            xmin = 6.96 
            xmax = 7.02 
            text = "i" 
        intervals [52]:
            xmin = 7.02 
            xmax = 7.08 
            text = "h" 
        intervals [53]:
            xmin = 7.08 
            xmax = 7.12 
            text = "a" 
        intervals [54]:
            xmin = 7.12 
            xmax = 7.21 
            text = "m" 
        intervals [55]:
            xmin = 7.21 
            xmax = 7.27 
            text = "a" 
        intervals [56]:
            xmin = 7.27 
            xmax = 7.43 
            text = "tː" 
        intervals [57]:
            xmin = 7.43 
            xmax = 7.48 
            text = "e" 
        intervals [58]:
            xmin = 7.48 
            xmax = 7.54 
            text = "i" 
        intervals [59]:
            xmin = 7.54 
            xmax = 7.62 
            text = "m" 
        intervals [60]:
            xmin = 7.62 
            xmax = 7.7 
            text = "a" 
        intervals [61]:
            xmin = 7.7 
            xmax = 7.93 
            text = "s" 
        intervals [62]:
            xmin = 7.93 
            xmax = 8.01 
            text = "ɨ̥" 
        intervals [63]:
            xmin = 8.01 
            xmax = 9.636281 
            text = "" 
```

次にMFAのDocker環境での構築方法を紹介します。

Docker環境でのMFAの利用方法

この手順を実行するには、以下のツールがインストールされている必要があります:

  • Docker (動作確認済みバージョン: 27.3.1)
  • docker compose (動作確認済みバージョン: v2.30.3)

ステップ1: プロジェクト構成

まずは以下のようなディレクトリ構造でプロジェクトを構成します:

.
├──mfa/
│   ├── docker/
│   │    └── Dockerfile
│   └── environment.yml
├── common/
│       └── test/
│             ├── test.wav <= 入力ファイル
│             ├── test.txt <= 入力ファイル
│             └── test.TextGrid <= 出力(実行後に生成されることに注意してください)
└── docker-compose.yaml

ディレクトリ/ファイル構成を参考に、Dockerfile, environment.yml, docker-compose.yamlを配置してください。

それぞれのファイルの中身は下記のとおりです。

mfa/docker/Dockerfileの中身

FROM condaforge/miniforge3:latest

# 作業ディレクトリを作成
WORKDIR /opt/mfa
    
# environment.yml をコピーして conda 環境を作成
COPY environment.yml /tmp/environment.yml
RUN conda env create -f /tmp/environment.yml -n mfa_env && \
    conda clean -afy
    
# requirements.txt をコピーして pip インストール(必要に応じて)
COPY requirements.txt /tmp/requirements.txt
RUN conda run -n mfa_env pip install -r /tmp/requirements.txt
    
# Montreal Forced Aligner (MFA) をインストール
RUN conda run -n mfa_env conda install -c conda-forge montreal-forced-aligner -y
    
# コンテナ起動時に mfa_env が自動で有効化されるように設定
RUN echo "conda activate mfa_env" >> /root/.bashrc
    
# MFA がパスで使えるように設定
ENV PATH /opt/conda/envs/mfa_env/bin:$PATH
    
# dictionary (辞書) と acoustic (アコースティックモデル) をダウンロード
RUN mfa model download dictionary japanese_mfa
RUN mfa model download acoustic japanese_mfa
    
# 最終的な作業ディレクトリ
WORKDIR /app
    
# 対話的にコンテナを使うために bash を起動
CMD ["/bin/bash"]

このDockerfileでは以下の処理を行っています:

  • Condaベースイメージからスタート
  • MFA実行環境の構築
  • 日本語用の辞書と音響モデルをダウンロード
  • 対話的に使えるようにbashをエントリーポイントに設定

mfa/environment.ymlには、MFAの実行に必要な依存パッケージを記述します:

channels:
  - conda-forge
  - pytorch
  - nvidia
  - anaconda
dependencies:
  - python>=3.8
  - numpy
  - librosa
  - pysoundfile
  - tqdm
  - requests
  - pyyaml
  - dataclassy
  - kaldi=*=*cpu*
  - scipy
  - pynini
  - openfst=1.8.3
  - scikit-learn
  - hdbscan
  - baumwelch
  - ngram
  - praatio=6.0.0
  - biopython=1.79
  - sqlalchemy>=2.0
  - pgvector
  - pgvector-python
  - sqlite
  - postgresql
  - psycopg2
  - click
  - setuptools_scm
  - pytest
  - pytest-mypy
  - pytest-cov
  - pytest-timeout
  - mock
  - coverage
  - coveralls
  - interrogate
  - kneed
  - matplotlib
  - seaborn
  - pip
  - rich
  - rich-click
  - kalpy
    # Tokenization dependencies
  - spacy
  - sudachipy
  - sudachidict-core
  - spacy-pkuseg
  - pip:
      - build
      - twine
      # Tokenization dependencies
      - python-mecab-ko
      - jamo
      - pythainlp
      - hanziconv
      - dragonmapper

この設定ファイルは公式GitHubリポジトリから取得したものですが、一部不要な依存関係が含まれている可能性があります。プロジェクトの要件に応じて最適化することもできます。

docker-compose.yamlの中身

version: "3.9"
services:
  mfa:
    build:
      context: ./mfa
      dockerfile: docker/Dockerfile
    stdin_open: true
    tty: true
    volumes:
      - ./mfa:/app
      - ./common:/app/common
      - ./mfa/cache:/root/.cache
    restart: "no"

このファイルではボリュームマウントを設定し、ホスト側のファイルシステムとコンテナ内のファイルシステムを連携させています。

これで必要なファイルの準備は完了です。

ステップ2: アライメント対象のデータの準備

日本語音響モデルを利用したアライメントを行うためには、以下の2種類のファイルが必要です。

  • 音声ファイル(.wav)
  • 対応する文字起こしや台本ファイル(.txt)

これらのファイルは、同じフォルダに配置し、かつファイル名(拡張子を除く)が同一である必要があります。

例えば、以下のように配置してください。

└── common/
     └── test/
           ├── test.wav
           └── test.txt

私はテストとして、4秒程度沈黙した後に「私の名前は万次郎です。最近はモンスターハンターにハマっています。」と発話したwavファイル「私の名前は万次郎です。最近はモンスターハンターにハマっています。」と記載したtxtファイルを用意しました。

ステップ3: コンテナのビルド & ラン

それではターミナルからコンテナをビルドして実行しましょう。

まずコンテナをビルドしてください。

docker compose build mfa

ビルドが完了したら、コンテナを立ち上げてその中に入ります。

docker compose run --rm mfa

コンテナに接続すると、以下のようなプロンプトが表示されます:

(mfa_env) root@hogehoge:/app#

このプロンプトは、すでにMFA用のconda環境(mfa_env)がアクティブになっていることを示しています。

続いてMFAを実行しましょう。

ステップ4: MFAの実行

コンテナ内で以下のコマンドを実行して、音声とテキストのアライメントを行います:

mfa align ./common/test/ japanese_mfa /root/Documents/MFA/pretrained_models/acoustic/japanese_mfa.zip ./common/test/

このコマンドの構造は以下の通りです:

  • mfa align - アライメント実行コマンド
  • ./common/test/ - 入力ファイル(音声とテキスト)のディレクトリ
  • japanese_mfa - 使用する辞書
  • /root/Documents/MFA/pretrained_models/acoustic/japanese_mfa.zip - 音響モデルへのパス
  • ./common/test/ - 出力先ディレクトリ

実行が完了すると、指定した出力先ディレクトリにtest.TextGridファイルが生成されます。このファイルには、テキストの各単語や音素が音声データのどの時間位置に対応しているかという情報が含まれています。

ステップ5: 実行内容の確認

では、結果を確認していきましょう。今回の場合はcommon/test/に出力されています。

common/test内のtest.TextGridの出力はこのとおりです。

common/test.TextGridの出力

File type = "ooTextFile"
Object class = "TextGrid"

xmin = 0 
xmax = 9.636281 
tiers? <exists> 
size = 2 
item []: 
    item [1]:
        class = "IntervalTier" 
        name = "words" 
        xmin = 0 
        xmax = 9.636281 
        intervals: size = 18 
        intervals [1]:
            xmin = 0.0 
            xmax = 2.37 
            text = "" 
        intervals [2]:
            xmin = 2.37 
            xmax = 2.81 
            text = "私" 
        intervals [3]:
            xmin = 2.81 
            xmax = 2.93 
            text = "の" 
        intervals [4]:
            xmin = 2.93 
            xmax = 3.26 
            text = "名前" 
        intervals [5]:
            xmin = 3.26 
            xmax = 3.52 
            text = "は" 
        intervals [6]:
            xmin = 3.52 
            xmax = 3.62 
            text = "" 
        intervals [7]:
            xmin = 3.62 
            xmax = 4.2 
            text = "万次郎" 
        intervals [8]:
            xmin = 4.2 
            xmax = 4.62 
            text = "です" 
        intervals [9]:
            xmin = 4.62 
            xmax = 5.04 
            text = "" 
        intervals [10]:
            xmin = 5.04 
            xmax = 5.59 
            text = "最近" 
        intervals [11]:
            xmin = 5.59 
            xmax = 5.82 
            text = "は" 
        intervals [12]:
            xmin = 5.82 
            xmax = 5.96 
            text = "" 
        intervals [13]:
            xmin = 5.96 
            xmax = 6.48 
            text = "モンスター" 
        intervals [14]:
            xmin = 6.48 
            xmax = 6.9 
            text = "ハンター" 
        intervals [15]:
            xmin = 6.9 
            xmax = 7.02 
            text = "に" 
        intervals [16]:
            xmin = 7.02 
            xmax = 7.48 
            text = "ハマって" 
        intervals [17]:
            xmin = 7.48 
            xmax = 8.01 
            text = "います" 
        intervals [18]:
            xmin = 8.01 
            xmax = 9.636281 
            text = "" 
    item [2]:
        class = "IntervalTier" 
        name = "phones" 
        xmin = 0 
        xmax = 9.636281 
        intervals: size = 63 
        intervals [1]:
            xmin = 0.0 
            xmax = 2.37 
            text = "" 
        intervals [2]:
            xmin = 2.37 
            xmax = 2.48 
            text = "w" 
        intervals [3]:
            xmin = 2.48 
            xmax = 2.53 
            text = "a" 
        intervals [4]:
            xmin = 2.53 
            xmax = 2.6 
            text = "t" 
        intervals [5]:
            xmin = 2.6 
            xmax = 2.65 
            text = "a" 
        intervals [6]:
            xmin = 2.65 
            xmax = 2.77 
            text = "ɕ" 
        intervals [7]:
            xmin = 2.77 
            xmax = 2.81 
            text = "i" 
        intervals [8]:
            xmin = 2.81 
            xmax = 2.85 
            text = "n" 
        intervals [9]:
            xmin = 2.85 
            xmax = 2.93 
            text = "o" 
        intervals [10]:
            xmin = 2.93 
            xmax = 2.99 
            text = "n" 
        intervals [11]:
            xmin = 2.99 
            xmax = 3.04 
            text = "a" 
        intervals [12]:
            xmin = 3.04 
            xmax = 3.12 
            text = "m" 
        intervals [13]:
            xmin = 3.12 
            xmax = 3.2 
            text = "a" 
        intervals [14]:
            xmin = 3.2 
            xmax = 3.26 
            text = "e" 
        intervals [15]:
            xmin = 3.26 
            xmax = 3.34 
            text = "w" 
        intervals [16]:
            xmin = 3.34 
            xmax = 3.52 
            text = "a" 
        intervals [17]:
            xmin = 3.52 
            xmax = 3.62 
            text = "" 
        intervals [18]:
            xmin = 3.62 
            xmax = 3.72 
            text = "m" 
        intervals [19]:
            xmin = 3.72 
            xmax = 3.8 
            text = "a" 
        intervals [20]:
            xmin = 3.8 
            xmax = 3.88 
            text = "ɲ" 
        intervals [21]:
            xmin = 3.88 
            xmax = 3.97 
            text = "dʑ" 
        intervals [22]:
            xmin = 3.97 
            xmax = 4.03 
            text = "i" 
        intervals [23]:
            xmin = 4.03 
            xmax = 4.09 
            text = "ɾ" 
        intervals [24]:
            xmin = 4.09 
            xmax = 4.2 
            text = "oː" 
        intervals [25]:
            xmin = 4.2 
            xmax = 4.26 
            text = "d" 
        intervals [26]:
            xmin = 4.26 
            xmax = 4.35 
            text = "e" 
        intervals [27]:
            xmin = 4.35 
            xmax = 4.56 
            text = "s" 
        intervals [28]:
            xmin = 4.56 
            xmax = 4.62 
            text = "ɨ̥" 
        intervals [29]:
            xmin = 4.62 
            xmax = 5.04 
            text = "" 
        intervals [30]:
            xmin = 5.04 
            xmax = 5.19 
            text = "s" 
        intervals [31]:
            xmin = 5.19 
            xmax = 5.25 
            text = "a" 
        intervals [32]:
            xmin = 5.25 
            xmax = 5.35 
            text = "i" 
        intervals [33]:
            xmin = 5.35 
            xmax = 5.44 
            text = "c" 
        intervals [34]:
            xmin = 5.44 
            xmax = 5.53 
            text = "i" 
        intervals [35]:
            xmin = 5.53 
            xmax = 5.59 
            text = "ɴ" 
        intervals [36]:
            xmin = 5.59 
            xmax = 5.67 
            text = "w" 
        intervals [37]:
            xmin = 5.67 
            xmax = 5.82 
            text = "a" 
        intervals [38]:
            xmin = 5.82 
            xmax = 5.96 
            text = "" 
        intervals [39]:
            xmin = 5.96 
            xmax = 6.02 
            text = "m" 
        intervals [40]:
            xmin = 6.02 
            xmax = 6.1 
            text = "o" 
        intervals [41]:
            xmin = 6.1 
            xmax = 6.16 
            text = "ɰ̃" 
        intervals [42]:
            xmin = 6.16 
            xmax = 6.23 
            text = "s" 
        intervals [43]:
            xmin = 6.23 
            xmax = 6.31 
            text = "t" 
        intervals [44]:
            xmin = 6.31 
            xmax = 6.48 
            text = "aː" 
        intervals [45]:
            xmin = 6.48 
            xmax = 6.54 
            text = "h" 
        intervals [46]:
            xmin = 6.54 
            xmax = 6.62 
            text = "a" 
        intervals [47]:
            xmin = 6.62 
            xmax = 6.7 
            text = "n" 
        intervals [48]:
            xmin = 6.7 
            xmax = 6.79 
            text = "t" 
        intervals [49]:
            xmin = 6.79 
            xmax = 6.9 
            text = "aː" 
        intervals [50]:
            xmin = 6.9 
            xmax = 6.96 
            text = "ɲ" 
        intervals [51]:
            xmin = 6.96 
            xmax = 7.02 
            text = "i" 
        intervals [52]:
            xmin = 7.02 
            xmax = 7.08 
            text = "h" 
        intervals [53]:
            xmin = 7.08 
            xmax = 7.12 
            text = "a" 
        intervals [54]:
            xmin = 7.12 
            xmax = 7.21 
            text = "m" 
        intervals [55]:
            xmin = 7.21 
            xmax = 7.27 
            text = "a" 
        intervals [56]:
            xmin = 7.27 
            xmax = 7.43 
            text = "tː" 
        intervals [57]:
            xmin = 7.43 
            xmax = 7.48 
            text = "e" 
        intervals [58]:
            xmin = 7.48 
            xmax = 7.54 
            text = "i" 
        intervals [59]:
            xmin = 7.54 
            xmax = 7.62 
            text = "m" 
        intervals [60]:
            xmin = 7.62 
            xmax = 7.7 
            text = "a" 
        intervals [61]:
            xmin = 7.7 
            xmax = 7.93 
            text = "s" 
        intervals [62]:
            xmin = 7.93 
            xmax = 8.01 
            text = "ɨ̥" 
        intervals [63]:
            xmin = 8.01 
            xmax = 9.636281 
            text = "" 

出力結果を動画に合成

TextGridファイルは複数の層(item)で構成され、通常、単語情報はitem[1]に格納されています。この情報を抽出してSRT形式に変換することで、ほとんどの動画プレイヤーやエディタで使用できる字幕ファイルが生成できます。

具体的には、以下のプロセスで変換を行います:

  1. TextGridファイルからitem[1]の情報(開始時間、終了時間、テキスト)を抽出
  2. 時間情報をSRT互換形式(HH:MM:SS,mmm)に変換
  3. 連番と書式を適用してSRTファイルを生成
  4. 生成したSRTファイルを動画に適用

1-3までのプロセスを行ってくれるスクリプトもご紹介します。

test.TextGridをパースするためのpythonスクリプト

def format_timestamp(seconds):
    hours = int(seconds // 3600)
    minutes = int((seconds % 3600) // 60)
    seconds = seconds % 60
    milliseconds = int((seconds % 1) * 1000)
    seconds = int(seconds)
    return f"{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d}"

def parse_textgrid(file_path):
    intervals = []
    with open(file_path, 'r', encoding='utf-8') as f:
        lines = f.readlines()
    
    item1_start = -1
    for i, line in enumerate(lines):
        if line.strip() == "item [1]:":
            item1_start = i
            break
    
    if item1_start == -1:
        raise ValueError("Could not find item [1] in TextGrid file")
    
    intervals_start = -1
    for i in range(item1_start, len(lines)):
        if lines[i].strip().startswith("intervals ["):
            intervals_start = i
            break
    
    if intervals_start == -1:
        raise ValueError("Could not find intervals in TextGrid file")
    
    current_interval = {}
    for line in lines[intervals_start:]:
        line = line.strip()
        
        # Check if we've reached item [2]
        if line == "item [2]:":
            break
            
        if line.startswith("intervals ["):
            if current_interval and 'xmin' in current_interval and 'xmax' in current_interval and 'text' in current_interval:
                intervals.append(current_interval)
            current_interval = {}
        elif line.startswith("xmin = "):
            current_interval['xmin'] = float(line.split("=")[1].strip())
        elif line.startswith("xmax = "):
            current_interval['xmax'] = float(line.split("=")[1].strip())
        elif line.startswith("text = "):
            text = line.split("=")[1].strip()
            # Remove quotes if present
            if text.startswith('"') and text.endswith('"'):
                text = text[1:-1]
            current_interval['text'] = text
    
    if current_interval and 'xmin' in current_interval and 'xmax' in current_interval and 'text' in current_interval:
        intervals.append(current_interval)
    
    return intervals

def create_srt(intervals):
    srt_entries = []
    counter = 1
    
    for interval in intervals:
        # Skip empty text entries
        if not interval['text']:
            continue
            
        start_time = format_timestamp(interval['xmin'])
        end_time = format_timestamp(interval['xmax'])
        
        srt_entry = f"{counter}\n{start_time} --> {end_time}\n{interval['text']}\n"
        srt_entries.append(srt_entry)
        counter += 1
    
    return "\n".join(srt_entries)

def textgrid_to_srt(input_file, output_file):
    try:
        intervals = parse_textgrid(input_file)
        
        srt_content = create_srt(intervals)
        
        with open(output_file, 'w', encoding='utf-8') as f:
            f.write(srt_content)
            
        print(f"Successfully converted {input_file} to {output_file}")
        
    except Exception as e:
        print(f"Error converting file: {str(e)}")

if __name__ == "__main__":
    input_file = "test.TextGrid"
    output_file = "test.srt"
    textgrid_to_srt(input_file, output_file)

出力されたSRT(test.srt)ファイル

1
00:00:02,370 --> 00:00:02,810
私

2
00:00:02,810 --> 00:00:02,930
の

3
00:00:02,930 --> 00:00:03,259
名前

4
00:00:03,259 --> 00:00:03,520
は

5
00:00:03,620 --> 00:00:04,200
万次郎

6
00:00:04,200 --> 00:00:04,620
です

7
00:00:05,040 --> 00:00:05,589
最近

8
00:00:05,589 --> 00:00:05,820
は

9
00:00:05,960 --> 00:00:06,480
モンスター

10
00:00:06,480 --> 00:00:06,900
ハンター

11
00:00:06,900 --> 00:00:07,019
に

12
00:00:07,019 --> 00:00:07,480
ハマって

13
00:00:07,480 --> 00:00:08,009
います

生成されたSRTファイルを実際に活用するフェーズに入ります。字幕の流し込みにはさまざまなツールが利用可能です。代表的なものとしてffmpegを使用する方法や、Adobe Premiere Proのような業界標準の動画編集ソフトウェアを利用する方法があります。 今回の検証では、処理の一貫性を保つためにffmpegを選択し、アライメントのために用意したwavファイルの音声を合成した動画に対し、字幕の流し込みをコマンドラインで処理しました。以下が実行したコマンドの例です:

ffmpeg -i 元動画.mp4 -vf subtitles=生成したSRTファイル.srt -c:a copy test_with_cc_with_audio.mp4

処理後の動画であるtest_with_cc_with_audio.mp4を確認したところ、音声と字幕のタイミングが非常に高精度で同期していることが確認できました。特に単語レベルでの同期精度は、従来の自動字幕生成ツールと比較しても遜色ない結果となりました。

このように、MFAを活用することで、高品質な字幕付き動画を効率的に作成することが可能です。以上、Montreal Forced Aligner (MFA) の概要と実践的な使い方についての解説でした。

最後に

MFA(Montreal Forced Aligner)は特定の条件下において、OpenAIのWhisperやその改良版であるWhisperXと比較しても、より高精度にテキストと音声の同期を実現できることが分かりました。

しかし、このような便利なツールにも限界があります。例えば、台本上にエクスクラメーションマーク(!)やクエスチョンマーク(?)が含まれている場合でも、MFAは音声のアライメントのみに焦点を当てるため、出力されるTextGridファイルからこれらの記号は除外されてしまいます。また、TextGrid形式のファイルをそのままパースしてSRTファイルに変換した場合、単語の区切りのみで字幕が追加されることになり、読みやすさや自然な区切りが損なわれる可能性があります。

こうした課題に対して、近年注目を集めているLLM(大規模言語モデル)を活用することで解決策が見えてきます。LLMを組み合わせることで、MFAの技術的限界をある程度克服できる可能性があります。次回以降の記事では、「LLMを活用してMFAの弱点をどのように克服するか」というテーマについて詳しく掘り下げていく予定です。

長文になりましたが、MFAについての解説は以上となります。最後までお読みいただき、誠にありがとうございました。

次回の記事もどうぞお楽しみに!