[AI] WhisperX:有限 GPU 下的繁中/英文轉錄實驗

在 GTX 1050 Ti 4GB 上最佳化 WhisperX:有限 GPU 下的繁中/英文轉錄實驗

這篇紀錄的是一次「小 GPU 榨汁」實驗:不是靠頂級顯卡硬輾,而是在 GTX 1050 Ti 4GB 上,透過模型選擇、參數調整、顯存釋放與 two-pass 說話人分類,把 WhisperX 調到實務可用。

TL;DR
  • GTX 1050 Ti 不適合跑 float16,這次以 int8 為主。
  • 品質優先設定:large-v2batch_size=3beam_size=8chunk_size=30
  • 英文 10 分鐘音檔約 113.5 秒完成,約 5.3 倍即時速度。
  • GPU peak 約 3813 MB,約吃到 4GB 顯存的 93%。
  • 說話人分類加入 two-pass,改善自動判斷人數錯誤的問題。

測試平台

CPU AMD Ryzen 5 5500X3D
GPU NVIDIA GTX 1050 Ti 4GB
系統 Windows
主要工具 WhisperX、faster-whisper / CTranslate2、pyannote、OpenCC

GTX 1050 Ti 只有 4GB 顯存,而且這張 Pascal 架構的卡在 CTranslate2 下不適合使用 float16。 所以這次優化方向不是硬上更大的模型或更高精度,而是找出在 int8 條件下,這張卡可以穩定承受的最佳參數。

品質優先的參數組合

目前品質優先的設定如下:

{
  "model": "large-v2",
  "compute_type": "int8",
  "batch_size": 3,
  "chunk_size": 30,
  "beam_size": 8
}
項目 實驗結論
compute_type int8 最穩;float16 不適合這張 GPU。
batch_size 3 是甜蜜點;4 顯存更滿但速度反而下降。
beam_size 8 品質與速度較平衡;12 沒看到明顯品質提升。
模型 繁中測試下,large-v2large-v3 / large-v3-turbo 更穩。

英文 10 分鐘音檔效能

音檔長度 約 600 秒
總處理時間 約 113.5 秒
即時倍率 約 5.3 倍 realtime
GPU peak 約 3813 MB
顯存使用 約 4GB 的 93%

這個結果對 GTX 1050 Ti 來說已經相當接近極限。重點不是把參數全部拉滿,而是在不讓顯存壓力拖慢流程的情況下,盡量吃滿 GPU。

簡化後的轉錄流程

完整專案有 benchmark、報告、speaker rename、two-pass diarization 等功能。不過核心概念可以簡化成下面這樣。

1. 選擇 device 與 compute type

def choose_device():
    import torch
    return "cuda" if torch.cuda.is_available() else "cpu"


def choose_compute_type(device):
    import ctranslate2
    supported = ctranslate2.get_supported_compute_types(device)

    for candidate in ["int8", "int8_float32", "float32"]:
        if candidate in supported:
            return candidate

    return "default"

2. 載入 WhisperX model

import whisperx

device = choose_device()
compute_type = choose_compute_type(device)

model = whisperx.load_model(
    "large-v2",
    device=device,
    compute_type=compute_type,
    language="zh",
    asr_options={
        "beam_size": 8,
        "best_of": 8,
        "condition_on_previous_text": False,
    },
    vad_method="silero",
)

3. 轉錄音檔

audio = whisperx.load_audio("meeting.m4a")

result = model.transcribe(
    audio,
    batch_size=3,
    chunk_size=30,
    language="zh",
)

4. ASR 與 alignment 分開載入

這是 4GB GPU 上很重要的一點:ASR model 用完後要釋放,再載入 alignment model。小顯存不適合讓兩個模型同時待在 GPU 裡。

import gc
import torch

del model
gc.collect()
torch.cuda.empty_cache()

align_model, align_meta = whisperx.load_align_model(
    language_code=result["language"],
    device=device,
)

result = whisperx.align(
    result["segments"],
    align_model,
    align_meta,
    audio,
    device=device,
)

del align_model
gc.collect()
torch.cuda.empty_cache()
小 GPU 的穩定性常常不是輸在模型,而是輸在顯存管理。ASR、alignment、diarization 分階段載入,對 4GB 顯存很有幫助。

繁體中文後處理

Whisper 輸出的中文有時候會混簡體、空白或標點格式,所以加入簡單的繁中後處理。

import re
from opencc import OpenCC

cc = OpenCC("s2twp")


def postprocess_zh(text):
    text = cc.convert(text)
    text = re.sub(r"\s*([,。!?;:、])\s*", r"\1", text)
    text = re.sub(r"(?<=[\u3400-\u9fff])\s+(?=[\u3400-\u9fff])", "", text)
    return text.strip()

例如:

這 是 測試 , OK !

可以整理成:

這是測試,OK!

不過繁中逐字稿不能只看字錯率。例如「徵才」被辨識成「身材」,從 CER 看只是兩個字錯,但語意上完全不一樣。 這種錯誤如果直接拿去做會議摘要或 Action Item,很容易出事。

說話人分類:自動判斷人數是最大風險

說話人分類使用 pyannote。實驗中發現最大問題不是分段,而是「自動判斷說話人數」。

條件 結果
指定 --num-speakers 2 DER 約 8.86%,turn accuracy 100%,confusion 0%。
自動判斷 speaker 數 原本 2 人被判成 3 人,DER 上升到約 42.41%,turn accuracy 下降到約 66.67%。

所以實務上如果知道人數,應該直接指定。但很多真實會議一開始並不知道有幾個人,所以我又做了 two-pass diarization。

Two-pass diarization:先找長語音聲紋,再回頭分類

想法很直覺:

  1. 先跑一次 pyannote。
  2. 找出比較長、比較乾淨的說話段。
  3. 從長段建立聲紋。
  4. 合併相似聲紋。
  5. 再回頭分類整段音訊。
  6. 低信心片段標成 UNKNOWN,不要硬分。

聲紋分群

def cosine(a, b):
    return float(a @ b)


def group_voiceprints(seed_vectors, threshold=0.60):
    groups = []
    used = set()

    for i, vector in enumerate(seed_vectors):
        if i in used:
            continue

        group = [i]
        used.add(i)

        for j, other in enumerate(seed_vectors):
            if j in used:
                continue

            if cosine(vector, other) >= threshold:
                group.append(j)
                used.add(j)

        groups.append(group)

    return groups

建立 speaker center

import numpy as np


def make_voice_centers(embeddings, seed_groups):
    centers = []

    for group in seed_groups:
        center = np.mean([embeddings[i] for i in group], axis=0)
        center = center / np.linalg.norm(center)
        centers.append(center)

    return centers

回頭分類所有語音段

def assign_speaker(vector, centers, threshold=0.45):
    scores = [cosine(vector, center) for center in centers]
    best = int(np.argmax(scores))

    if scores[best] < threshold:
        return "UNKNOWN", scores[best]

    return f"VOICE_{best:02}", scores[best]

這個方法在繁中雙人測試上,把原本 auto 判成 3 人的結果修正成 2 人,最後 DER 約 8.86%,turn accuracy 100%。

英文 10 分鐘音檔上,two-pass 額外成本大約 3.3 秒。相對完整 ASR + alignment + diarization,大約只增加 2% 左右時間,這個交換很划算。

Speaker rename

自動產生的 speaker 名稱通常長這樣:

VOICE_00
VOICE_01
UNKNOWN

實際閱讀很不方便,所以加了 speaker rename。

def rename_speakers(rows, mapping):
    for row in rows:
        speaker = row.get("speaker")

        if speaker in mapping:
            row["original_speaker"] = speaker
            row["speaker"] = mapping[speaker]

    return rows

使用時可以這樣指定:

--speaker-rename "VOICE_00=主持人,VOICE_01=客戶"

輸出的逐字稿就會變成:

主持人: 目前整理到六月的進度……
客戶: 真的是電瓶問題……

Confidence report 與 review report

逐字稿最怕的是「看起來很完整,但其實有錯」。所以我額外輸出兩種報告:

  • confidence report:機器可讀 JSON。
  • review report:人工可讀 Markdown。
def segment_confidence(segment):
    scores = [
        word["score"]
        for word in segment.get("words", [])
        if "score" in word
    ]

    if not scores:
        return None

    return sum(scores) / len(scores)
def low_confidence_segments(segments, threshold=0.45):
    results = []

    for index, segment in enumerate(segments, 1):
        confidence = segment_confidence(segment)

        if confidence is not None and confidence < threshold:
            results.append({
                "index": index,
                "start": segment["start"],
                "end": segment["end"],
                "speaker": segment.get("speaker"),
                "confidence": confidence,
                "text": segment.get("text", ""),
            })

    return results

review report 會額外標記:

  • 低 confidence 片段
  • UNKNOWN speaker
  • 太短的 speaker turn
  • 空白逐字稿
  • 中英文語言混雜
  • 重複字元異常

這些不一定代表錯,但很適合丟給人工或下一階段 ChatGPT API 檢查。

v1.0 整合指令

目前整理成 v1.0,一個指令可以跑完整品質流程:

.\whisperx-env\Scripts\python.exe .\whisperx_optimized.py .\input\meeting.m4a `
  --profile quality `
  --language zh `
  --two-pass-diarize `
  --speaker-rename "VOICE_00=主持人,VOICE_01=客戶" `
  --confidence-report `
  --review-report

會輸出:

  • JSON 完整結果
  • SRT 字幕
  • TXT 逐字稿
  • two-pass diarization 診斷
  • confidence report
  • review report

最後心得

在有限 GPU 上,流程設計比盲目換大模型更重要。

GTX 1050 Ti 4GB 當然不是什麼強卡,但透過幾個策略,還是可以做出實用的本機逐字稿流程:

  • 使用 int8
  • 避免 float16
  • 控制 batch,不硬塞到爆
  • ASR / alignment 分開載入並釋放顯存
  • 繁中後處理
  • 說話人分類不要完全相信 auto
  • two-pass 聲紋回頭分類
  • 低信心片段獨立列出

目前的成果是:

  • 英文 10 分鐘音檔約 113 秒完成品質優先轉錄
  • GPU 使用峰值約 3.8GB
  • 約吃到 4GB 顯存的 93%
  • 繁中雙人對話可做到可用逐字稿與說話人分類
  • two-pass 方法能改善 speaker auto 判斷錯誤

下一步預計做 v1.1:

  • ChatGPT API 自動校稿
  • 會議摘要
  • Action Item 擷取

不過我會保留一個原則:原始逐字稿、校稿版、摘要、Action Items 要分開保存。 AI 可以協助整理,但不應該覆蓋原始紀錄。

備註:這篇文章中的程式碼是為了說明流程而簡化,完整版本包含 benchmark、報告輸出、two-pass 診斷與 speaker rename 等細節。

留言

這個網誌中的熱門文章

[旅遊]日本大阪部品(上) - 南海部品