以前も似たサンプルコード紹介していますが、結構使うコードですので、最新情報をアップデートしようと思います。
- Macユーザーの音楽家/エンジニア
- JupyterLabやconda環境でPythonを使っている人
- アルバムまとめ動画のYouTube概要欄にチャプターを一括生成したい人
この記事でできること
- フォルダ内の音源(
01 タイトル.wav
など)を番号順に走査 - 演奏時間を自動取得(
mutagen
があれば高精度/なければ macOSのmdls
で代用) - YouTubeのチャプター行(
0:00 タイトル (mm:ss)
)を自動生成し、コピペするだけでOK
このサンプルコードを使えば、Youtubeチャプターの編集時間をカットすることができます。
サンプルコード
解説はコメントアウトにて。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
YouTube概要欄のチャプター行を自動生成(フォルダパスはコード内で固定)。
目的:
- フォルダ内の音源ファイルを「先頭番号順(01,02,...)」に並べ、
各曲の演奏時間からチャプター行を生成します。
- mutagen が使えれば優先。無い場合は macOS の `mdls` を使って秒数を取得します。
使い方:
1) 下の TARGET_DIR をご自分のフォルダに書き換える(今回は既に指定済み)。
2) 端末で実行: python3 make_youtube_chapters_fixed.py
3) 出力されたテキストを YouTube の概要欄にコピペ。
Example:
出力例の1行: 0:00 01 オッペケペー (1:41)
"""
from __future__ import annotations
import re
import subprocess
from pathlib import Path
from typing import List, Optional, Tuple
# === ★ ここを書き換えるだけでOK(分析したいフォルダのパスを書いてください) ==================
TARGET_DIR = Path("ここにパスを貼り付け")
# ====================================================================================
# 対象とする拡張子(必要に応じて追加)
AUDIO_EXTS = {".wav", ".mp3", ".m4a", ".aac", ".aif", ".aiff", ".flac", ".ogg", ".oga", ".wma", ".mp4", ".mka"}
# ---------- 基本ユーティリティ ----------
def hhmmss_from_seconds(sec: float) -> str:
"""秒を h:mm:ss / m:ss に整形(例: 101→'1:41', 3661→'1:01:01')。"""
total = int(round(sec))
h, r = divmod(total, 3600)
m, s = divmod(r, 60)
return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"
def extract_leading_number(name: str) -> int:
"""ファイル名の先頭連番('01 ...' → 1)を抽出。無ければ 9999(末尾へ)。"""
m = re.match(r"^(\d+)", name)
return int(m.group(1)) if m else 9999
# ---------- 演奏時間の取得(mutagen 優先 / mdls フォールバック) ----------
def duration_seconds_mutagen(path: Path) -> Optional[float]:
"""mutagen で長さ(秒)を取得。mutagen無い/未対応なら None。"""
try:
from mutagen import File as MutagenFile # 遅延インポートで未導入でもOK
except Exception:
return None
try:
m = MutagenFile(str(path))
if m and getattr(m, "info", None) and getattr(m.info, "length", None):
return float(m.info.length)
except Exception:
pass
return None
def duration_seconds_mdls(path: Path) -> Optional[float]:
"""macOSの `mdls` から kMDItemDurationSeconds を取得。取れなければ None。"""
try:
out = subprocess.run(
["mdls", "-name", "kMDItemDurationSeconds", str(path)],
capture_output=True, text=True, check=False
).stdout
if "=" in out:
val = out.split("=", 1)[1].strip()
if val and val.lower() != "(null)":
return float(val)
except Exception:
pass
return None
# ---------- フォルダ走査とチャプター作成 ----------
def scan_folder(folder: Path) -> List[Tuple[str, float]]:
"""
フォルダ内の音源を「先頭番号順」に並べ、(タイトル, 秒) の配列を返す。
タイトルは拡張子を除いたファイル名(例: '01 オッペケペー')。
"""
files = [
p for p in folder.iterdir()
if p.is_file() and not p.name.startswith(".") and p.suffix.lower() in AUDIO_EXTS
]
files.sort(key=lambda p: extract_leading_number(p.name)) # 01, 02, 03 ... の順
items: List[Tuple[str, float]] = []
for p in files:
sec = duration_seconds_mutagen(p)
if sec is None:
sec = duration_seconds_mdls(p)
if sec is None:
# 取得できないファイルはスキップ(必要なら 0 秒扱いに変更可)
continue
items.append((p.stem, float(sec)))
return items
def make_chapters(items: List[Tuple[str, float]]) -> List[str]:
"""
(タイトル, 秒) → チャプター行の配列を返す。
例: ["0:00 01 タイトル (1:41)", "1:41 02 次曲 (2:51)", ...]
"""
chapters: List[str] = []
t = 0.0
for title, sec in items:
chapters.append(f"{hhmmss_from_seconds(t)} {title} ({hhmmss_from_seconds(sec)})")
t += sec
return chapters
def main() -> None:
# 1) フォルダ存在チェック
if not TARGET_DIR.is_dir():
print(f"[Error] Folder not found: {TARGET_DIR}")
return
# 2) 走査 → (タイトル, 秒) を得る
items = scan_folder(TARGET_DIR)
if not items:
print("[Info] 対象が見つかりませんでした。")
return
# 3) チャプター生成
chapters = make_chapters(items)
# 4) 画面に出力(YouTube概要欄へコピペ)
print("\n=== YouTube Chapters ===\n")
print("\n".join(chapters))
if __name__ == "__main__":
main()
mutagenを使う場合
mutagenを使えば以下のようなことが実現できます。
- 演奏時間(length) の取得
- サンプルレート(sample_rate)/チャンネル数(channels)/ビットレート(bitrate) の取得
- タグ読取:ID3(MP3)、MP4(M4A/MP4)、FLAC(VorbisComment)、OGG、ASF(WMA)など
- アートワークの抽出(ID3 APIC、MP4 “covr”、FLAC PICTURE など)
インストール方法ですが、(Mac/Anaconda/Jupyter 対応)conda推奨です。
# 新規環境(任意)
conda create -n audiochap python=3.11 -y
conda activate audiochap
# mutagen を conda-forge から
conda install -c conda-forge mutagen -y
pip / venvの場合
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install mutagen
JupyterLab の“今のカーネル”に入れる(セルで実行)場合
import sys, subprocess
print(sys.executable) # 参照中のPython確認
subprocess.check_call([sys.executable, "-m", "pip", "install", "mutagen"])
サンプルコード
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
YouTube概要欄のチャプター行を生成(再帰走査・mutagen→mdls フォールバック付き)
- サブフォルダも含めて音源を探索(rglob)
- mutagen で長さが取れない場合は macOS の mdls を使用
- 先頭番号(01, 02, ...)で数値ソート
- 最後に成功/失敗の集計を表示して原因を把握しやすくする
"""
from __future__ import annotations
from pathlib import Path
from typing import Optional, List, Tuple
import subprocess
import re
# === ★ フォルダパス(こうたろうさんのフォルダ) ===================================
TARGET_DIR = Path("ここにパス")
# ===================================================================================
# 対象拡張子(小文字想定)。空集合なら「全ファイル試す」でもOK
AUDIO_EXTS = {".wav", ".mp3", ".m4a", ".aac", ".aif", ".aiff", ".flac", ".ogg", ".oga", ".wma", ".mp4", ".mka"}
# ---- まず mutagen を試す(未導入でも動くように遅延import) -----------------------
def duration_seconds_mutagen(path: Path) -> Optional[float]:
try:
from mutagen import File as MutagenFile
except Exception:
return None
try:
m = MutagenFile(str(path))
if m and getattr(m, "info", None) and getattr(m.info, "length", None):
return float(m.info.length)
except Exception:
pass
return None
# ---- だめなら mdls(macOS標準) ---------------------------------------------------
def duration_seconds_mdls(path: Path) -> Optional[float]:
try:
out = subprocess.run(
["mdls", "-name", "kMDItemDurationSeconds", str(path)],
capture_output=True, text=True, check=False
).stdout
if "=" in out:
val = out.split("=", 1)[1].strip()
if val and val.lower() != "(null)":
return float(val)
except Exception:
pass
return None
# ---- ユーティリティ ---------------------------------------------------------------
def hhmmss_from_seconds(sec: float) -> str:
total = int(round(sec))
h, r = divmod(total, 3600)
m, s = divmod(r, 60)
return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"
def extract_leading_number(name: str) -> int:
m = re.match(r"^(\d+)", name)
return int(m.group(1)) if m else 9999
# ---- 走査&取得 -------------------------------------------------------------------
def scan_folder_recursive(folder: Path) -> List[Tuple[str, float]]:
files = []
for p in folder.rglob("*"):
if not p.is_file() or p.name.startswith("."):
continue
if AUDIO_EXTS and p.suffix.lower() not in AUDIO_EXTS:
continue
files.append(p)
# 01,02,... の先頭番号で安定ソート
files.sort(key=lambda p: extract_leading_number(p.name))
items: List[Tuple[str, float]] = []
stats = {"total": 0, "ok_mutagen": 0, "ok_mdls": 0, "fail": 0}
failed_samples: List[str] = []
for p in files:
stats["total"] += 1
sec = duration_seconds_mutagen(p)
if sec is not None:
stats["ok_mutagen"] += 1
else:
sec = duration_seconds_mdls(p)
if sec is not None:
stats["ok_mdls"] += 1
else:
stats["fail"] += 1
if len(failed_samples) < 5:
failed_samples.append(p.name)
continue
items.append((p.stem, float(sec)))
# 診断出力
print("\n--- Scan summary --------------------------------")
print(f"Scanned files : {stats['total']}")
print(f"✓ mutagen : {stats['ok_mutagen']}")
print(f"✓ mdls : {stats['ok_mdls']}")
print(f"✗ failed : {stats['fail']}")
if failed_samples:
print("Examples of failed files:", ", ".join(failed_samples))
print("-------------------------------------------------\n")
return items
def make_chapters(items: List[Tuple[str, float]]) -> List[str]:
chapters: List[str] = []
t = 0.0
for title, sec in items:
chapters.append(f"{hhmmss_from_seconds(t)} {title} ({hhmmss_from_seconds(sec)})")
t += sec
return chapters
def main() -> None:
if not TARGET_DIR.is_dir():
print(f"[Error] Folder not found: {TARGET_DIR}")
return
items = scan_folder_recursive(TARGET_DIR)
if not items:
print("[Info] 音源は見つかりましたが、長さが取得できませんでした。拡張子やメタデータをご確認ください。")
print(" ・WAVでも特殊コーデック/壊れたヘッダだと取得できない場合があります。")
print(" ・Spotlight未インデックス時は `mdimport \"<folder>\"` をお試しください。")
return
chapters = make_chapters(items)
print("=== YouTube Chapters ===\n")
print("\n".join(chapters))
if __name__ == "__main__":
main()

音楽家:朝比奈幸太郎
神戸生まれ。2025 年、40 年近く住んだ神戸を離れ北海道・十勝へ移住。
録音エンジニア五島昭彦氏より金田式バランス電流伝送 DC 録音技術を承継し、
ヴィンテージ機材で高品位録音を実践。
ヒーリング音響ブランド「Curanz Sounds」でソルフェジオ周波数音源を配信。
“音の文化を未来へ”届ける活動を展開中。