github.com
本記事は、Windows 11 上で Docker Desktop(WSL2)と NVIDIA GPU を用い、話者分離(pyannote)→ セグメントごとの文字起こし(Kotoba Whisper)を一気通貫で実行するためのメモである。
- 音声の「誰が話したか」を推定(話者分離)し、区間ごとに話者ラベルを付与する
- 区間ごとにASR(自動文字起こし)し、話者名つきの
txt と srt を生成する
- Windows 11 環境でありがちな Cドライブ逼迫や キャッシュ肥大化を避けるため、モデルキャッシュや一時ファイルの置き場を工夫する
- PyTorch側の仕様変更に由来する ロード時エラーを回避できるようにする(後述のパッチ)
1. 前提(GPU / WSL / Docker)
本手順は以下を前提とする。
- Windows 11
- Docker Desktop(WSL2 backend)
- NVIDIA Driver 導入済み(CUDA対応)
nvidia-smi が Windows 側で動く
- Docker から GPU が見える(
--gpus all が使える)
前提チェック(PowerShell)
docker context show
wsl -l -v
nvidia-smi
# Docker上でGPUが認識されるか確認
docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu22.04 nvidia-smi
ここで最後の nvidia-smi がコンテナ内で表示されれば、GPUパススルーは概ね問題ない。
2. ストレージ設計(どこに置くと速いか)
本構成では大きく3種類の「置き場」が登場する。
- Dockerプロジェクト(Dockerfileなどのコード)
- 入力音声・出力テキスト
- Hugging Face モデルキャッシュ、分割WAVなどの一時ファイル
結論(おすすめ)
- モデルキャッシュ(Hugging Face)と一時ファイルはSSD推奨である。
ダウンロード自体も重いが、推論中も小さなファイルI/Oが発生しやすく、HDDだと体感で待ち時間が伸びる。
- 入力音声・出力は、速度要求がそこまで厳しくないなら HDDでも成立する。
ただし「長時間音声を大量に回す」「分割WAVを大量生成する」用途だと、ここもSSDが効いてくる。
置き場の指針
SSD必須寄り
hf_cache(モデルキャッシュ)
tmp(分割WAVや中間生成物が集まる)
HDDでも可(ただし遅くなる可能性あり)
work/source(入力音声)
work/source/output(SRT/TXTなど出力)
3. ディレクトリ構成(例)
ここではファイル名・パスを「任意のもの」に差し替えて例示する。読み替えやすいように、以下のような構成を採用する。
例:作業フォルダ
補足:Docker Desktop(WSL2)環境では、C: でも D: でも動く。だが「Cドライブ(システムドライブ)の残量を守る」「キャッシュ肥大化を隔離する」という意味で別ドライブ運用が便利である。
Dドライブ側のディレクトリ作成(PowerShell)
$base = "D:\asr"
New-Item -ItemType Directory -Force -Path "$base\work\source\output" | Out-Null
New-Item -ItemType Directory -Force -Path "$base\hf_cache" | Out-Null
New-Item -ItemType Directory -Force -Path "$base\tmp" | Out-Null
4. Dockerプロジェクトを作る
以下の3ファイルを、プロジェクトディレクトリ(例:C:\work\asr-docker\kotoba_diar_asr)に置く。
requirements.txt
Dockerfile
diar_asr.py(実行スクリプト)
4.1 requirements.txt
requirements.txt 例:
--extra-index-url https://download.pytorch.org/whl/cu126
transformers>=4.48.0
huggingface_hub>=0.23.0
accelerate>=0.26.0
sentencepiece
pyannote.audio==3.3.2
pytorch-lightning
speechbrain
pydub
tqdm
4.2 Dockerfile
CUDA 12.6 / cuDNN runtime ベースの例:
FROM nvidia/cuda:12.6.0-cudnn-runtime-ubuntu22.04
ENV DEBIAN_FRONTEND=noninteractive
# 必要パッケージのインストール
RUN apt-get update && apt-get install -y --no-install-recommends \
python3.11 python3.11-venv python3-pip \
ffmpeg git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN python3.11 -m pip install -U pip setuptools wheel
WORKDIR /app
# Torch系を先行インストール(cu126指定)
RUN python3.11 -m pip install --no-cache-dir --index-url https://download.pytorch.org/whl/cu126 \
torch==2.8.0+cu126 torchaudio==2.8.0+cu126
COPY requirements.txt .
RUN python3.11 -m pip install --no-cache-dir -r requirements.txt
COPY diar_asr.py .
CMD ["python3.11", "diar_asr.py"]
4.3 diar_asr.py(話者分離→ASR→出力)
- Hugging Face キャッシュと一時フォルダを 環境変数でリダイレクトする
- PyTorchの
weights_only 周りでコケるケースを想定し、torch.load をパッチする(安全性・互換性重視の妥協策)
- diarization と ASR の間で
gc / empty_cache を挟み、VRAMをなるべく解放する
ffmpeg を subprocess で叩き、失敗時に追跡しやすい形に寄せる
import os
import time
import datetime
import tempfile
import shutil
import warnings
import subprocess
import gc
from pathlib import Path
warnings.filterwarnings("ignore", message=".*torchaudio._backend.list_audio_backends.*")
warnings.filterwarnings("ignore", category=UserWarning)
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
def log(msg: str):
print(msg, flush=True)
WORK_SOURCE = Path(os.getenv("WORK_SOURCE", "/work/source"))
WORK_OUTPUT = WORK_SOURCE / "output"
WORK_HF = Path(os.getenv("WORK_HF_CACHE", "/work/hf_cache"))
WORK_TMP = Path(os.getenv("WORK_TMP", "/work/tmp"))
WORK_OUTPUT.mkdir(parents=True, exist_ok=True)
WORK_HF.mkdir(parents=True, exist_ok=True)
WORK_TMP.mkdir(parents=True, exist_ok=True)
os.environ.update({
"HF_HOME": str(WORK_HF),
"HF_HUB_CACHE": str(WORK_HF / "hub"),
"TRANSFORMERS_CACHE": str(WORK_HF / "transformers"),
"XDG_CACHE_HOME": str(WORK_HF),
"TMPDIR": str(WORK_TMP), "TEMP": str(WORK_TMP), "TMP": str(WORK_TMP)
})
import torch
try:
from torch.serialization import add_safe_globals
from pyannote.audio.core.task import Specifications, Problem
add_safe_globals([torch.torch_version.TorchVersion, Specifications, Problem])
except Exception:
pass
if os.getenv("TORCH_LOAD_WEIGHTS_ONLY", "0").lower() in ("0", "false"):
_orig_torch_load = torch.load
torch.load = lambda *a, **k: _orig_torch_load(*a, **{**k, "weights_only": False})
log("[INFO] Patched torch.load(weights_only=False)")
from huggingface_hub import login
hf_token = os.getenv("HF_TOKEN")
if hf_token:
login(token=hf_token)
INPUT_FILENAME = os.getenv("INPUT_FILENAME", "input.m4a")
INPUT_FILE = WORK_SOURCE / INPUT_FILENAME
NUM_SPEAKERS_ENV = os.getenv("NUM_SPEAKERS", "").strip()
NUM_SPEAKERS = None if NUM_SPEAKERS_ENV in ("", "none", "auto") else int(NUM_SPEAKERS_ENV)
MODEL_DIAR = os.getenv("MODEL_DIAR", "pyannote/speaker-diarization-3.1")
MODEL_ASR = os.getenv("MODEL_ASR", "kotoba-tech/kotoba-whisper-v2.2")
device = 0 if torch.cuda.is_available() else -1
torch_dtype = torch.float16 if device == 0 else torch.float32
if not INPUT_FILE.exists():
raise FileNotFoundError(f"Input not found: {INPUT_FILE}")
from transformers import pipeline
from pyannote.audio import Pipeline
from pydub import AudioSegment
from tqdm.auto import tqdm
log("[STEP] Converting input to WAV(16kHz mono)...")
tmp_in_dir = tempfile.mkdtemp(prefix="wav_", dir=str(WORK_TMP))
wav16 = Path(tmp_in_dir) / "input_16k_mono.wav"
subprocess.run(
["ffmpeg", "-nostdin", "-y", "-i", str(INPUT_FILE), "-ac", "1", "-ar", "16000", str(wav16)],
check=True
)
audio16 = AudioSegment.from_file(str(wav16))
log("[STEP] Loading Diarization model...")
dia = Pipeline.from_pretrained(MODEL_DIAR, use_auth_token=hf_token)
if device == 0:
dia.to(torch.device("cuda"))
log("[STEP] Running Diarization...")
t0 = time.time()
params = {"num_speakers": NUM_SPEAKERS, "min_speakers": NUM_SPEAKERS, "max_speakers": NUM_SPEAKERS} if NUM_SPEAKERS else {}
diar = dia(str(wav16), **params)
segments = list(diar.itertracks(yield_label=True))
log(f"[OK] Diarization finished: {len(segments)} segments ({time.time()-t0:.2f}s)")
del dia
gc.collect()
if device == 0:
torch.cuda.empty_cache()
log("[STEP] Loading Whisper...")
asr = pipeline(
"automatic-speech-recognition",
model=MODEL_ASR,
device=device,
trust_remote_code=True,
return_timestamps=True,
torch_dtype=torch_dtype
)
log("[STEP] Running ASR per segment...")
tmp_seg_dir = tempfile.mkdtemp(prefix="seg_", dir=str(WORK_TMP))
results = []
for idx, (turn, _, spk) in enumerate(tqdm(segments, desc="ASR")):
st, ed = float(turn.start), float(turn.end)
if ed <= st:
continue
seg_path = Path(tmp_seg_dir) / f"seg_{idx:04d}.wav"
audio16[int(st*1000):int(ed*1000)].export(str(seg_path), format="wav")
out = asr(str(seg_path), generate_kwargs={"language": "japanese", "task": "transcribe"})
results.append({"speaker": spk, "start": st, "end": ed, "text": out.get("text", "").strip()})
base = INPUT_FILE.stem
def fmt(t):
td = datetime.timedelta(seconds=float(t))
return f"{td.seconds//3600:02}:{(td.seconds//60)%60:02}:{td.seconds%60:02},{td.microseconds//1000:03}"
srt_body = [
f"{i}\n{fmt(r['start'])} --> {fmt(r['end'])}\n{r['speaker']}: {r['text']}"
for i, r in enumerate(results, 1)
]
(WORK_OUTPUT / f"{base}.srt").write_text("\n\n".join(srt_body), encoding="utf-8")
(WORK_OUTPUT / f"{base}.txt").write_text("\n".join([f"[{r['speaker']}] {r['text']}" for r in results]), encoding="utf-8")
shutil.rmtree(tmp_seg_dir, ignore_errors=True)
shutil.rmtree(tmp_in_dir, ignore_errors=True)
log(f"[DONE] Outputs saved to {WORK_OUTPUT}")
5. ビルドと実行
5.1 ビルド
cd C:\work\asr-docker\kotoba_diar_asr
docker build -t kotoba-diar-asr:cu126 .
5.2 実行
例として、入力音声を D:\asr\work\source\meeting_2026-02-11.m4a に置いた場合を示す。
docker run --rm --gpus all `
-e HF_TOKEN="$env:HF_TOKEN" `
-e INPUT_FILENAME="meeting_2026-02-11.m4a" `
-e NUM_SPEAKERS="2" `
-v D:\asr\work\source:/work/source `
-v D:\asr\hf_cache:/work/hf_cache `
-v D:\asr\tmp:/work/tmp `
kotoba-diar-asr:cu126
実行後、D:\asr\work\source\output に以下が出力される。
meeting_2026-02-11.txt(話者ラベルつきテキスト)
meeting_2026-02-11.srt(字幕形式)
6. 運用上の注意点
6.1 Hugging Face 認証(pyannoteの利用規約)
pyannote/speaker-diarization-3.1 は Hugging Face 上での利用規約同意が必要になることが多い。先にブラウザで Hugging Face にログインし、対象モデルのページで規約に同意しておくこと。
そのうえで、コンテナ実行時に HF_TOKEN を渡せるよう、Windows 側にトークンを環境変数として登録しておく。
手順A(推奨):PowerShellで「ユーザー環境変数」として保存する
以下は 現在ログインしているユーザー の環境変数として HF_TOKEN を保存する例である。1回設定すれば、次回以降は docker run のたびに手入力しなくてよい。
重要:ここで貼り付けるトークンは秘密情報である。画面共有やログ採取に混ざらないよう注意すること。
# 1) まずは一時的に変数へ入れる(この行の右側に自分のトークンを入れる)
$token = "hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 2) ユーザー環境変数として永続化(次回ログイン後も有効)
[Environment]::SetEnvironmentVariable("HF_TOKEN", $token, "User")
# 3) いったん新しいPowerShellを開き直してから、反映確認
$env:HF_TOKEN
反映が確認できたら、以後はコンテナ実行時に次のように渡せる。
docker run --rm --gpus all `
-e HF_TOKEN="$env:HF_TOKEN" `
-e INPUT_FILENAME="meeting_2026-02-11.m4a" `
-e NUM_SPEAKERS="2" `
-v D:\asr\work\source:/work/source `
-v D:\asr\hf_cache:/work/hf_cache `
-v D:\asr\tmp:/work/tmp `
kotoba-diar-asr:cu126
手順B:その場限りで一時的に渡す(永続化しない)
「環境に保存したくない」場合は、実行するPowerShellのセッション内だけで変数を作って渡す。
$env:HF_TOKEN = "hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
docker run --rm --gpus all `
-e HF_TOKEN="$env:HF_TOKEN" `
-e INPUT_FILENAME="meeting_2026-02-11.m4a" `
-e NUM_SPEAKERS="2" `
-v D:\asr\work\source:/work/source `
-v D:\asr\hf_cache:/work/hf_cache `
-v D:\asr\tmp:/work/tmp `
kotoba-diar-asr:cu126
PowerShellを閉じれば HF_TOKEN は消える。
手順C:GUIで設定(Windowsの環境変数画面)
コードを使わずGUIで設定する場合は、Windowsの「環境変数」設定で HF_TOKEN を追加してもよい。どちらにせよ、設定後は新しいターミナルを開き直して echo $env:HF_TOKEN(PowerShell)で反映確認するのが確実である。
6.2 VRAMと安定性
話者分離モデルとASRモデルを同一プロセスで扱うため、VRAM消費が重なりやすい。スクリプト中で gc.collect() と torch.cuda.empty_cache() を挟んでいるが、それでもGPUメモリに余裕がないとロード時に落ちることがある。少なくとも 8GB以上、できれば余裕のあるVRAMが望ましい。
6.3 I/Oと体感速度(SSDかHDDか)
hf_cache と tmp を HDD に置くと「動くが遅い」状態になりやすい。
特に初回ダウンロード後も、小さな読み書きが積み重なって待ち時間になる。
- 入力音声だけ HDD、キャッシュとtmpはSSD、という分離は効果が出やすい。
6.4 SRT改行
SRTはセグメント間を空行で区切る形式が一般的であるため、"\n\n" で区切る形に寄せている。テキストエディタやプレイヤーでの互換性が上がる。
7. 付録:環境変数で調整できる項目
INPUT_FILENAME:入力音声ファイル名(/work/source からの相対)
NUM_SPEAKERS:話者数(例:2、未指定なら推定寄り)
MODEL_DIAR:話者分離モデル(デフォルト:pyannote/speaker-diarization-3.1)
MODEL_ASR:ASRモデル(デフォルト:kotoba-tech/kotoba-whisper-v2.2)
HF_TOKEN:Hugging Face トークン
TORCH_LOAD_WEIGHTS_ONLY:1 にすると torch.load パッチを抑止(切り分け用)