0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

英単語リストから自動生成した多数の音声ファイルを1つに統合してみた

Posted at

1. はじめに

前回の記事の「英語学習のため、自作のExcel英単語・英例文リストから英日音声ファイルを生成してみた」では、英単語帳のExcelファイルから音声付きの語彙学習素材を作成し、ウォークマンなどのデバイスで再生できるようにしました。
ただ、この方法ですと、英単語の数が増えると 数千個のMP3ファイル が出力されてしまい、ウォークマンなど一部のプレイヤーでは再生可能数の上限を超えてしまいます。そこで今回は、これらの分割MP3ファイルを 1つの長い音声ファイルに統合するためにPythonでスクリプトを作成した、というのが今回の記事の内容です。

2. 実施環境

  • CPU: CORE i5 8th Gen
  • メモリ: 8GB
  • OS: Windows 11 22H2

3. 使用ライブラリ

必要なライブラリをインストールします。

pip install pydub

また、音声処理のために ffmpeg が必要です。

4. 作成したプログラム:merge_vocab_audio.py

以下が実際に作成したプログラムです。

merge_vocab_audio.py
from pathlib import Path
import re
import csv
import os
import subprocess
from pydub import AudioSegment
from pydub.utils import which

# === 設定 ======================================================
IN_DIR = Path("分割mp3のフォルダを指定")   # 分割mp3のフォルダ
OUT_WAV = Path("merged_vocab.wav")
OUT_MP3 = Path("merged_vocab.mp3")
TIMELINE_CSV = Path("merged_timeline.csv")

GAP_MS = 300          # 同一行の各パート間の無音
ROW_GAP_MS = 700      # 行と行の間の無音

PAT = re.compile(r"^(?P<num>\d{4})_.*_(?P<order>[1-4])_(?P<tag>en_word|mean|ja_example|en_example)\.mp3$", re.IGNORECASE)

# 必要なら ffmpeg のフルパスを指定(例:r"C:\ffmpeg\bin\ffmpeg.exe")
# 未設定なら PATH から自動検出
FFMPEG_PATH = os.environ.get("FFMPEG_PATH", "").strip()
# ===============================================================


def resolve_ffmpeg_path() -> str:

    if FFMPEG_PATH:
        if Path(FFMPEG_PATH).exists():
            return FFMPEG_PATH
        raise SystemExit(f"FFMPEG_PATH に指定した ffmpeg が見つかりません: {FFMPEG_PATH}")
    p = which("ffmpeg")
    if p is None:
        raise SystemExit("ffmpeg が見つかりません。インストールし PATH を通すか、FFMPEG_PATH を設定してください。")
    return p


def require_ffmpeg():
    ff = resolve_ffmpeg_path()
    AudioSegment.converter = ff 
    return ff


def export_mp3_with_lame(merged: AudioSegment, out_mp3: Path, tmp_wav: Path, ffmpeg_path: str):
    # まずは pydub で直接 MP3 出力(libmp3lame 明示)
    try:
        merged.export(
            out_mp3,
            format="mp3",
            codec="libmp3lame",      
            bitrate="48k",
            parameters=["-ac", "1", "-ar", "24000", "-write_xing", "0"]
        )
        return  # 成功
    except Exception as e:
        print(f"[WARN] pydub 直接MP3出力に失敗: {e}")

    # フォールバック:WAV に一旦書き出してから ffmpeg CLI で MP3 化
    print("フォールバック: WAV に書き出し → ffmpeg CLI で MP3 化 …")
    merged.export(tmp_wav, format="wav")

    cmd = [
        ffmpeg_path, "-y", "-hide_banner", "-loglevel", "error",
        "-i", str(tmp_wav),
        "-ac", "1", "-ar", "24000",
        "-b:a", "48k", "-codec:a", "libmp3lame",
        str(out_mp3)
    ]
    try:
        subprocess.run(cmd, check=True)
    except subprocess.CalledProcessError as ee:
        raise SystemExit(f"ffmpeg による MP3 変換に失敗しました。詳細: {ee}") from ee
    finally:
        try:
            tmp_wav.unlink()
        except Exception:
            pass


def main():
    ffmpeg_path = require_ffmpeg()

    if not IN_DIR.exists():
        raise SystemExit(f"入力フォルダがありません: {IN_DIR.resolve()}")

    files = list(IN_DIR.glob("*.mp3"))
    if not files:
        raise SystemExit(f"{IN_DIR.resolve()} に mp3 が見つかりません。")

    # 行ごとにグループ化
    rows = {}  # num -> [(order, tag, Path)]
    skipped = []
    for p in files:
        m = PAT.match(p.name)
        if not m:
            skipped.append(p.name)
            continue
        num = m.group("num")
        order = int(m.group("order"))
        tag = m.group("tag").lower()
        rows.setdefault(num, []).append((order, tag, p))

    if not rows:
        raise SystemExit("命名規則に合致するファイルがありません。ファイル名のフォーマットを確認してください。")

    # 行番号で昇順
    sorted_nums = sorted(rows.keys())

    merged = AudioSegment.silent(duration=0)
    timeline = []
    current_ms = 0

    print(f"対象行数: {len(sorted_nums)}")
    if skipped:
        print(f"※命名規則に合わずスキップ: {len(skipped)} 件(例:{skipped[:5]}")

    for idx, num in enumerate(sorted_nums, start=1):
        items = sorted(rows[num], key=lambda x: x[0]) 
        parts = []
        print(f"[{idx}/{len(sorted_nums)}] 行 {num} を結合中...", end="")

        for order, tag, path in items:
            try:
                seg = AudioSegment.from_file(path, format="mp3")
                parts.append(seg)
            except Exception as e:
                print(f"\n  [WARN] 読み込み失敗: {path.name} ({e})")

        if not parts:
            print(" スキップ(有効なセグメントなし)")
            continue

        # 行の開始時刻を記録
        timeline.append((num, current_ms))

        row_audio = parts[0]
        for s in parts[1:]:
            row_audio += AudioSegment.silent(duration=GAP_MS)
            row_audio += s

        merged += row_audio
        current_ms += len(row_audio)

        # 行間の無音
        if idx < len(sorted_nums):
            merged += AudioSegment.silent(duration=ROW_GAP_MS)
            current_ms += ROW_GAP_MS

        print(f" 完了({len(row_audio)} ms)")

    if len(merged) == 0:
        raise SystemExit("統合結果が空でした。")

    # WAV も必要なら出力
    print("WAV に書き出し中…")
    merged.export(OUT_WAV, format="wav")

    # MP3 へ(libmp3lame 明示+フォールバック付き)
    print("MP3 に変換中…")
    tmp_wav = OUT_WAV.with_suffix(".tmp.wav")
    export_mp3_with_lame(merged, OUT_MP3, tmp_wav, ffmpeg_path)

    print(f"OK: {OUT_MP3.resolve()}  総時間: {len(merged)} ms")

    # 目次CSV
    with open(TIMELINE_CSV, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(["row_num(先頭4桁)", "start_ms"])
        for row_num, start_ms in timeline:
            w.writerow([row_num, start_ms])
    print(f"目次: {TIMELINE_CSV.resolve()}")


if __name__ == "__main__":
    main()

5. 実行方法

以下のコマンドで実行します。

python merge_vocab_audio.py

6. 実行結果

スクリプトを実行すると、指定フォルダ内のMP3ファイルが自動的に読み込まれ、
1つのファイルにまとめられます。

生成されるファイルは次の3つです。

ファイル 内容
merged_vocab.wav 中間生成物(WAV形式)
merged_vocab.mp3 完成した音声ファイル(ウォークマン対応)
merged_timeline.csv 各英単語(行)の再生開始時刻(ms)を記録

7. 結果とまとめ

このスクリプトを使うことで、数百〜数千に分割されていた英単語音声ファイルを1つの長いMP3としてまとめることができました。ウォークマンに転送すると、リスト全体をスムーズに再生できることも確認できました。現在は、ExcelからKindle対応の英語単語帳PDFを自動生成してみたの記事で作成したPDFを見つつ、この英単語音声ファイルを聞いて勉強を進めています。 ちなみに問題点としては、やはり発音やイントネーションにやや難があるところです。これの解決に今は取り組むつもりはありませんが、何か新しい手法を見つけたら取り組みたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?