Initial commit
This commit is contained in:
commit
e5a6694aa4
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.venv/
|
||||||
|
model_cache/
|
||||||
|
docker/__pycache__/
|
||||||
|
|
||||||
99
README.md
Normal file
99
README.md
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# whisper-test (faster-whisper on DGX Spark/ThinkStation PGX)
|
||||||
|
|
||||||
|
NVIDIA DGX Spark 相当環境(Lenovo ThinkStation PGX)で、`faster-whisper` を CUDA で確実に動かすための最小構成をまとめたリポジトリです。ホスト側 CUDA 13 が既に入っており変更しづらい研究用マシンでも、Docker 上で CUDA 12.4 + cuDNN 9 でビルドした CTranslate2 を用いて GPU 推論を再現できます。
|
||||||
|
|
||||||
|
公式 faster-whisper リポジトリ: https://github.com/SYSTRAN/faster-whisper
|
||||||
|
|
||||||
|
## なぜこの構成が必要か
|
||||||
|
- `faster-whisper` は CTranslate2 を利用し、ビルド時の CUDA/cuDNN と実行環境が一致していないと GPU 版が動作しない。
|
||||||
|
- ThinkStation PGX ではホストに CUDA 13 が入っており、`pip install ctranslate2` すると CPU 版や CUDA 非対応ビルドが入ることがある。
|
||||||
|
- 公式手順だけでは CUDA 版が入らず、`ValueError: This CTranslate2 package was not compiled with CUDA support` となるケースがあった。
|
||||||
|
- そこで CUDA 12.4 + cuDNN 9 を明示した Docker で CTranslate2 をソースビルドし、Python バインディングを明示的に再インストールすることで再現性を確保。
|
||||||
|
|
||||||
|
## 提供物
|
||||||
|
- `docker-compose.yml` と `docker/Dockerfile` による GPU 対応コンテナ構成
|
||||||
|
- ベース: `nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04`
|
||||||
|
- CTranslate2 v4.6.2 を CUDA 有効でソースビルド
|
||||||
|
- `pip` で入る CPU 版 `ctranslate2` を一度アンインストールし、ビルド済み Python バインディングを再インストール
|
||||||
|
- FastAPI + Uvicorn の簡易サーバ (`docker/server.py`)
|
||||||
|
- `POST /transcribe` で音声を受け取り、Whisper 推論結果を返却
|
||||||
|
- `GET /health` でモデル名・デバイス・compute_type を返却
|
||||||
|
- サンプル音声とテストスクリプト(`test/`)
|
||||||
|
|
||||||
|
## 仕組みのポイント
|
||||||
|
- CUDA 12.x + cuDNN 9 で CTranslate2 をビルドし、実行時も同一バージョンを保証
|
||||||
|
- `entrypoint.sh` で `LD_LIBRARY_PATH` を明示設定し、`nvidia.cublas.lib` / `nvidia.cudnn.lib` のパスを動的に追加
|
||||||
|
- ホスト CUDA 13 と切り離すため Docker を必須とし、NVIDIA Container Toolkit を前提に GPU を渡す
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
- NVIDIA Container Toolkit 導入済み
|
||||||
|
- GPU ドライバがホストに正しく入っていること
|
||||||
|
- ホスト側 CUDA は 13 系のままで可(変更不要)
|
||||||
|
|
||||||
|
### ローカルCPU実行(任意)
|
||||||
|
Docker を使わず手元の Python (.venv) で CPU 推論だけ試す場合の簡易手順:
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r docker/requirements.txt
|
||||||
|
|
||||||
|
# CPU でサンプル音声を実行
|
||||||
|
FW_DEVICE=cpu FW_COMPUTE_TYPE=int8 python3 test/transcribe_cpu.py
|
||||||
|
```
|
||||||
|
※ CPU のみの確認用。GPU を使う場合は Docker 構成を推奨。
|
||||||
|
|
||||||
|
## セットアップ
|
||||||
|
```bash
|
||||||
|
# ビルド
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
# 起動(GPU 必須)
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# ヘルスチェック
|
||||||
|
curl http://localhost:8002/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## 推論APIの使い方
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8002/transcribe \
|
||||||
|
-F "file=@test/003.wav" \
|
||||||
|
-F "language=ja" \
|
||||||
|
-F "beam_size=5" \
|
||||||
|
-F "vad_filter=true"
|
||||||
|
```
|
||||||
|
レスポンスには `text` と `segments`(start/end/text)、`language`, `language_probability` が含まれます。
|
||||||
|
|
||||||
|
環境変数でモデルや実行設定を変更できます:
|
||||||
|
- `FW_MODEL` (デフォルト `large-v3`)
|
||||||
|
- `FW_DEVICE` (デフォルト `cuda`)
|
||||||
|
- `FW_COMPUTE_TYPE` (デフォルト `float16`)
|
||||||
|
|
||||||
|
### Docker コンテナ内で `test/` のスクリプトを実行する例
|
||||||
|
ホストに Python を入れずに検証したい場合、起動済みコンテナ内でテストスクリプトを実行できます。
|
||||||
|
```bash
|
||||||
|
# 1) コンテナに入る(compose が起動している前提)
|
||||||
|
docker compose exec whisper bash
|
||||||
|
|
||||||
|
# 2) GPU 版サンプル(デフォルトの large-v3 / float16)
|
||||||
|
python3 test/transcribe.py
|
||||||
|
|
||||||
|
# 3) CPU で試したい場合(compute_type を切り替え)
|
||||||
|
FW_DEVICE=cpu FW_COMPUTE_TYPE=int8 python3 test/transcribe_cpu.py
|
||||||
|
```
|
||||||
|
いずれも `test/003.wav` を入力にし、標準出力へテキストと各セグメントを表示します。自分の音声で試す場合は `python3 test/transcribe.py /path/to/your.wav` のようにパスを指定してください。
|
||||||
|
|
||||||
|
## ディレクトリ構成(抜粋)
|
||||||
|
- `docker-compose.yml` — サービス定義(GPU リクエスト含む)
|
||||||
|
- `docker/Dockerfile` — CUDA 12.4 + cuDNN 9 ベースで CTranslate2 をソースビルド
|
||||||
|
- `docker/entrypoint.sh` — CUDA ライブラリパスを動的設定し uvicorn 起動
|
||||||
|
- `docker/server.py` — FastAPI サーバ(/health, /transcribe)
|
||||||
|
- `docker/requirements.txt` — 最小依存: `faster-whisper`, `fastapi`, `uvicorn[standard]`, `python-multipart`
|
||||||
|
- `test/` — サンプル音声とローカル実行スクリプト
|
||||||
|
|
||||||
|
## 再現のための注意点
|
||||||
|
- `pip install faster-whisper` だけでは CUDA 版 `ctranslate2` は保証されない
|
||||||
|
- CUDA バージョン不一致だとインストール成功しても実行時に GPU を掴まない
|
||||||
|
- 本 Dockerfile は一度 CPU 版をアンインストールし、ビルド済み CUDA 版を再インストールして整合性を取る
|
||||||
|
|
||||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
whisper:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
image: whisper-test-whisper
|
||||||
|
command: uvicorn docker.server:app --host 0.0.0.0 --port 8000
|
||||||
|
entrypoint: ["/app/docker/entrypoint.sh"]
|
||||||
|
ports:
|
||||||
|
- "8002:8000"
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- capabilities: [gpu]
|
||||||
48
docker/Dockerfile
Normal file
48
docker/Dockerfile
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
FROM nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04
|
||||||
|
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# build deps
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
python3 python3-pip python3-dev \
|
||||||
|
git cmake ninja-build build-essential pkg-config \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# python basics
|
||||||
|
RUN python3 -m pip install -U pip setuptools wheel
|
||||||
|
|
||||||
|
# Build CTranslate2 from source with CUDA ON (and submodules!)
|
||||||
|
ARG CT2_VERSION=v4.6.2
|
||||||
|
RUN git clone --depth 1 --branch v4.6.2 --recurse-submodules \
|
||||||
|
https://github.com/OpenNMT/CTranslate2.git /tmp/ctranslate2 && \
|
||||||
|
cd /tmp/ctranslate2 && \
|
||||||
|
mkdir build && cd build && \
|
||||||
|
cmake .. \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DWITH_CUDA=ON \
|
||||||
|
-DWITH_CUDNN=ON \
|
||||||
|
-DWITH_MKL=OFF \
|
||||||
|
-DOPENMP_RUNTIME=COMP \
|
||||||
|
-DCMAKE_CUDA_ARCHITECTURES=native \
|
||||||
|
-GNinja && \
|
||||||
|
ninja -j$(nproc) && \
|
||||||
|
ninja install && \
|
||||||
|
ldconfig
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# app deps
|
||||||
|
COPY docker/requirements.txt /app/requirements.txt
|
||||||
|
RUN python3 -m pip install -r /app/requirements.txt
|
||||||
|
RUN python3 -m pip uninstall -y ctranslate2 && \
|
||||||
|
cd /tmp/ctranslate2/python && \
|
||||||
|
python3 -m pip install .
|
||||||
|
|
||||||
|
COPY docker/server.py /app/server.py
|
||||||
|
COPY docker/entrypoint.sh /app/entrypoint.sh
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
48
docker/entrypoint.sh
Executable file
48
docker/entrypoint.sh
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "[entrypoint] Python:" "$(python3 --version || true)"
|
||||||
|
echo "[entrypoint] pip packages (nvidia-*):"
|
||||||
|
python3 - <<'PY' || true
|
||||||
|
import pkgutil
|
||||||
|
names = sorted({m.name for m in pkgutil.iter_modules() if m.name.startswith("nvidia")})
|
||||||
|
print("\n".join(names) if names else "(none)")
|
||||||
|
PY
|
||||||
|
|
||||||
|
NVPATHS="$(python3 - <<'PY'
|
||||||
|
import os, sys
|
||||||
|
|
||||||
|
def try_mod(modname):
|
||||||
|
try:
|
||||||
|
m = __import__(modname, fromlist=["*"])
|
||||||
|
f = getattr(m, "__file__", None)
|
||||||
|
print(f"[entrypoint] {modname}.__file__ = {f}")
|
||||||
|
if not f:
|
||||||
|
return None
|
||||||
|
return os.path.dirname(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[entrypoint] import failed: {modname}: {e}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
paths = []
|
||||||
|
for mod in ("nvidia.cublas.lib", "nvidia.cudnn.lib"):
|
||||||
|
d = try_mod(mod)
|
||||||
|
if d:
|
||||||
|
paths.append(d)
|
||||||
|
|
||||||
|
print(":".join(paths))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
BASE="/usr/local/cuda/lib64:/usr/local/lib"
|
||||||
|
if [[ -n "${NVPATHS}" ]]; then
|
||||||
|
export LD_LIBRARY_PATH="${BASE}:${NVPATHS}:${LD_LIBRARY_PATH:-}"
|
||||||
|
echo "[entrypoint] LD_LIBRARY_PATH set with NVPATHS"
|
||||||
|
else
|
||||||
|
export LD_LIBRARY_PATH="${BASE}:${LD_LIBRARY_PATH:-}"
|
||||||
|
echo "[entrypoint] NVPATHS empty; using CUDA base paths only"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[entrypoint] LD_LIBRARY_PATH=${LD_LIBRARY_PATH}"
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
5
docker/requirements.txt
Normal file
5
docker/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
faster-whisper
|
||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
python-multipart
|
||||||
|
|
||||||
61
docker/server.py
Normal file
61
docker/server.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import FastAPI, UploadFile, File, Form
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from faster_whisper import WhisperModel
|
||||||
|
|
||||||
|
MODEL_NAME = os.getenv("FW_MODEL", "large-v3")
|
||||||
|
DEVICE = os.getenv("FW_DEVICE", "cuda")
|
||||||
|
COMPUTE_TYPE = os.getenv("FW_COMPUTE_TYPE", "float16")
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# 起動時にモデルをロードして常駐(ここが重要:毎回ロードしない)
|
||||||
|
model = WhisperModel(MODEL_NAME, device=DEVICE, compute_type=COMPUTE_TYPE)
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health():
|
||||||
|
return {"status": "ok", "model": MODEL_NAME, "device": DEVICE, "compute_type": COMPUTE_TYPE}
|
||||||
|
|
||||||
|
@app.post("/transcribe")
|
||||||
|
async def transcribe(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
language: Optional[str] = Form(default=None), # "ja"など
|
||||||
|
beam_size: int = Form(default=5),
|
||||||
|
vad_filter: bool = Form(default=True),
|
||||||
|
):
|
||||||
|
# アップロードを一時ファイルに保存(PyAVがファイルパスを扱いやすい)
|
||||||
|
suffix = os.path.splitext(file.filename or "audio.wav")[1] or ".wav"
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
|
||||||
|
tmp_path = tmp.name
|
||||||
|
tmp.write(await file.read())
|
||||||
|
|
||||||
|
try:
|
||||||
|
segments, info = model.transcribe(
|
||||||
|
tmp_path,
|
||||||
|
language=language,
|
||||||
|
beam_size=beam_size,
|
||||||
|
vad_filter=vad_filter,
|
||||||
|
)
|
||||||
|
|
||||||
|
out_segments = []
|
||||||
|
text_all = []
|
||||||
|
for s in segments:
|
||||||
|
out_segments.append({"start": s.start, "end": s.end, "text": s.text})
|
||||||
|
text_all.append(s.text)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"language": info.language,
|
||||||
|
"language_probability": info.language_probability,
|
||||||
|
"text": "".join(text_all).strip(),
|
||||||
|
"segments": out_segments,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.remove(tmp_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
BIN
test/003.wav
Normal file
BIN
test/003.wav
Normal file
Binary file not shown.
72
test/transcribe.py
Normal file
72
test/transcribe.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# test/transcribe_test.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from faster_whisper import WhisperModel
|
||||||
|
|
||||||
|
|
||||||
|
# === ここを自分のwavに合わせて書き換え ===
|
||||||
|
WAV_PATH = Path(__file__).resolve().parent / "003.wav"
|
||||||
|
MODEL_NAME = "large-v3"
|
||||||
|
LANGUAGE = "ja"
|
||||||
|
BEAM_SIZE = 5
|
||||||
|
COMPUTE_TYPE = "float16" # GPU想定
|
||||||
|
# =======================================
|
||||||
|
|
||||||
|
|
||||||
|
def pick_device():
|
||||||
|
return "cuda", COMPUTE_TYPE
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
wav_path = WAV_PATH
|
||||||
|
if len(sys.argv) >= 2:
|
||||||
|
wav_path = Path(sys.argv[1]).expanduser().resolve()
|
||||||
|
|
||||||
|
if not wav_path.exists():
|
||||||
|
print(f"[ERROR] wav not found: {wav_path}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
device, compute_type = pick_device()
|
||||||
|
print(f"[INFO] model={MODEL_NAME} device={device} compute_type={compute_type}")
|
||||||
|
print(f"[INFO] input={wav_path}")
|
||||||
|
|
||||||
|
model = WhisperModel(
|
||||||
|
MODEL_NAME,
|
||||||
|
device=device,
|
||||||
|
compute_type=compute_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==== 計測開始 ====
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
|
||||||
|
segments, info = model.transcribe(
|
||||||
|
str(wav_path),
|
||||||
|
language=LANGUAGE,
|
||||||
|
beam_size=BEAM_SIZE,
|
||||||
|
# vad_filter=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
segments = list(segments) # ここで実際に推論が走る
|
||||||
|
|
||||||
|
# ==== 計測終了 ====
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
elapsed = t1 - t0
|
||||||
|
|
||||||
|
print(f"\n[INFO] detected_language={info.language} (p={info.language_probability:.3f})")
|
||||||
|
print(f"[INFO] transcription_time={elapsed:.3f} sec")
|
||||||
|
print("[RESULT] --- transcript ---")
|
||||||
|
for seg in segments:
|
||||||
|
print(f"[{seg.start:7.2f}-{seg.end:7.2f}] {seg.text}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
72
test/transcribe_cpu.py
Normal file
72
test/transcribe_cpu.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# test/transcribe_test.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from faster_whisper import WhisperModel
|
||||||
|
|
||||||
|
|
||||||
|
# === ここを自分のwavに合わせて書き換え ===
|
||||||
|
WAV_PATH = Path(__file__).resolve().parent / "003.wav"
|
||||||
|
MODEL_NAME = "large-v3"
|
||||||
|
LANGUAGE = "ja"
|
||||||
|
BEAM_SIZE = 5
|
||||||
|
COMPUTE_TYPE = "float32" # GPU想定
|
||||||
|
# =======================================
|
||||||
|
|
||||||
|
|
||||||
|
def pick_device():
|
||||||
|
return "cpu", COMPUTE_TYPE
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
wav_path = WAV_PATH
|
||||||
|
if len(sys.argv) >= 2:
|
||||||
|
wav_path = Path(sys.argv[1]).expanduser().resolve()
|
||||||
|
|
||||||
|
if not wav_path.exists():
|
||||||
|
print(f"[ERROR] wav not found: {wav_path}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
device, compute_type = pick_device()
|
||||||
|
print(f"[INFO] model={MODEL_NAME} device={device} compute_type={compute_type}")
|
||||||
|
print(f"[INFO] input={wav_path}")
|
||||||
|
|
||||||
|
model = WhisperModel(
|
||||||
|
MODEL_NAME,
|
||||||
|
device=device,
|
||||||
|
compute_type=compute_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==== 計測開始 ====
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
|
||||||
|
segments, info = model.transcribe(
|
||||||
|
str(wav_path),
|
||||||
|
language=LANGUAGE,
|
||||||
|
beam_size=BEAM_SIZE,
|
||||||
|
# vad_filter=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
segments = list(segments) # ここで実際に推論が走る
|
||||||
|
|
||||||
|
# ==== 計測終了 ====
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
elapsed = t1 - t0
|
||||||
|
|
||||||
|
print(f"\n[INFO] detected_language={info.language} (p={info.language_probability:.3f})")
|
||||||
|
print(f"[INFO] transcription_time={elapsed:.3f} sec")
|
||||||
|
print("[RESULT] --- transcript ---")
|
||||||
|
for seg in segments:
|
||||||
|
print(f"[{seg.start:7.2f}-{seg.end:7.2f}] {seg.text}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Loading…
Reference in New Issue
Block a user