本記事は、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プロジェクト(どこでも良いが、管理しやすい場所に置く)
C:\work\asr-docker\kotoba_diar_asr
入力音声・出力・キャッシュ(別ドライブへ逃がす例)
- 入力/出力:
D:\asr\work\source - HFモデルキャッシュ:
D:\asr\hf_cache - 一時ファイル:
D:\asr\tmp
- 入力/出力:
補足: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.txtDockerfilediar_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 # quiet warnings 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) # Path Settings 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) # HF/Temp Cache redirection 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 # PyTorch 2.6+ workaround(ロード互換性のためのパッチ) 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) # Environment variables 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 # Convert to WAV 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)) # Diarization 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)") # VRAM Release del dia gc.collect() if device == 0: torch.cuda.empty_cache() # ASR 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()}) # Output 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 パッチを抑止(切り分け用)