最近有些錄音需要轉成文字,但手動整理非常耗時。需求很單純:聽得懂台灣口音與中英夾雜、輸出是正體中文、成本越低越好。因此,我決定嘗試用 Colab 免費 GPU 搭配 Breeze ASR 25 來自動化這個流程。
這篇記錄我用 Google Colab 的免費 GPU,搭配聯發創新基地開源的 Breeze ASR 25,做出一個可批次、可續跑的轉錄流程。
方案比較:本機、雲端 API,還是借 GPU
一次性的批次轉錄,使用 Colab 免費 GPU 跑開源模型最划算。三種做法的取捨如下:
| 做法 | 優點 | 缺點 | 適合情境 |
|---|---|---|---|
| 本機執行 | 錄音不離開電腦 | 需要夠力的硬體,環境要自己裝 | 內容敏感,且有 Apple Silicon 或獨立顯卡 |
| 雲端 API | 幾行程式就能用,速度快 | 按量計費,錄音要上傳第三方 | 想最快拿到結果 |
| Colab 加開源模型 | 免費,可選台灣在地模型 | 工作階段會中斷,流程要自己寫 | 批次、非即時的需求 |
模型選 Breeze ASR 25 的理由有三個:它以 Whisper-large-v2 微調,專門針對台灣華語與中英夾雜;採 Apache 2.0 授權可免費使用;模型約 3 GB,免費的 T4 顯卡就跑得動。
整體架構
- 錄音檔(m4a)放進 Google Drive 的
transcribe/audio - ffmpeg 把音檔解碼成 16 kHz 單聲道的浮點數陣列
- Breeze ASR 25 把陣列轉成帶時間戳的文字段落
- 每個音檔寫出兩份 txt 到
transcribe/output
Colab 是用完即丟的虛擬機,所以輸入和輸出都放在雲端空間,就算斷線,已經存好的成果也不會遺失。
整份 notebook 只有六段程式碼,由上到下依序執行。前四段是準備,後兩段才真正轉錄:
| 段 | 目的 | 執行後得到什麼 |
|---|---|---|
| 1 確認 GPU | 檢查有沒有拿到顯卡 | 印出 GPU 名稱 |
| 2 掛載 Google Drive | 連上雲端硬碟,設定輸入與輸出資料夾 | list_audio() 函式與音檔清單 |
| 3 載入模型 | 下載權重,組成轉錄管線 | 可直接呼叫的 asr 物件 |
| 4 定義函式 | 音檔前處理、時間格式化、轉錄 | load_audio、fmt、transcribe |
| 5 試跑 | 只轉第一個檔案的前 5 分鐘 | 畫面上印出帶時間的逐字稿 |
| 6 整批轉錄 | 處理資料夾內全部音檔 | 每個音檔兩份 txt |
後面的段落會用到前面定義的東西,所以順序不能跳。例如第 6 段呼叫第 4 段的 transcribe,而 transcribe 又用到第 3 段的 asr。
逐段實作
執行前準備
- 開啟 Google Colab,新增一份筆記本。
- 選單「執行階段 → 變更執行階段類型」,硬體加速器選 T4 GPU。
- 之後把每一段程式碼貼上,按執行鍵即可。
Colab 已預先裝好 PyTorch、Transformers 和 ffmpeg,所以全文沒有任何安裝指令。
第 1 段:確認 GPU
1 | import torch |
這段只做一件事:確認上一步的 GPU 設定有生效。torch.cuda 是 PyTorch 管理 NVIDIA 顯卡的模組。assert 在條件不成立時會丟出錯誤並停下,後面的程式就不會在沒有 GPU 的狀態下白跑。
放在第一段的理由是「越早失敗越好」。模型要下載 3 GB,如果等到載入完才發現沒開 GPU,就浪費了好幾分鐘。
第 2 段:掛載 Google Drive
1 | from pathlib import Path |
這段做三件事:
- 掛載:
drive.mount把 Google Drive 掛成虛擬機裡的一個資料夾,之後裡面的檔案就能像一般檔案一樣讀寫。執行時會跳出授權視窗。 - 建資料夾:
mkdir的parents=True會連上層資料夾一起建立,exist_ok=True則讓資料夾已存在時不報錯,所以不管執行第幾次都能順利通過。 - 列出音檔:
list_audio()會掃描資料夾,只留下副檔名符合的檔案,並依檔名排序。
第一次執行時資料夾還是空的,所以會看到提示訊息。把錄音放進 Drive 的 transcribe/audio 後再執行一次,確認檔案都有列出來。Mac「語音備忘錄」匯出的 m4a 可以直接用,不必轉檔。
所有路徑都從 BASE 往下組,換環境時只要改這一處。
第 3 段:載入模型並組成管線
1 | from transformers import ( |
這段是整份 notebook 的核心:先準備三個元件,再把它們組成一條管線。
| 元件 | 來源 | 負責什麼 |
|---|---|---|
| feature_extractor | processor |
把聲音波形轉成模型看得懂的頻譜圖 |
| model | model |
神經網路本體,從頻譜圖推論出文字代碼 |
| tokenizer | processor |
把文字代碼轉回人看得懂的字 |
from_pretrained 會依模型代號到 Hugging Face 下載需要的檔案。processor 那行只抓幾個小設定檔,model 那行抓的則是約 3 GB 的權重,所以第一次執行要等幾分鐘。
model 那行串了三個方法:.to("cuda") 把模型搬上 GPU;.half() 把數值精度減半,省記憶體也跑得更快;.eval() 則切換成推論模式。
最後組出來的 asr 是一個可以像函式一樣呼叫的物件:把音訊陣列丟進去,它就會自動跑完「前處理、推論、解碼」三個步驟。
兩個實務提醒:
- 執行時出現的
HF_TOKEN警告可以忽略。它只是提醒你沒有登入 Hugging Face,而公開模型不需要登入。 - 在同一個工作階段內重新執行這段,不會重新下載,因為檔案已經有快取。不過工作階段結束後快取就會清空,隔天得再下載一次。
第 4 段:定義三個函式
這段只定義函式,執行後不會有任何輸出。三個函式的關係是:transcribe 是主角,它先呼叫 load_audio 讀音檔,再把結果交給第 3 段建好的 asr;fmt 則是獨立的小工具。
1 | import subprocess |
load_audio:把音檔變成數字陣列
聲音在電腦裡就是一長串數字。這個函式會呼叫 ffmpeg,把任何格式的音檔解碼成「每秒 16000 個數字」的陣列。那幾行 cmd 是在拼一道終端機指令,以只讀前 5 分鐘為例,等同於:
1 | ffmpeg -i 錄音.m4a -t 300 -f f32le -ac 1 -ar 16000 - |
為什麼一定要轉成 16 kHz 單聲道?
因為模型訓練時只看過這種規格。手機和電腦的錄音通常是 44.1 或 48 kHz,直接餵進去的話,模型會以為聲音被放慢了約三倍。而人聲的資訊集中在 8000 Hz 以下,16 kHz 的取樣率剛好能涵蓋,所以轉換並不會讓辨識變差。
這個函式的價值在於統一入口:不管進來的是 m4a 還是 mp3,出去的都是同一種陣列,後面的程式只需要面對一種輸入。如果有給 max_minutes,指令會多加一個 -t,只讀取前幾分鐘;第 5 段的試跑就是靠它。
fmt:秒數轉成時間格式
用整數除法 // 和取餘數 % 拆出時、分、秒,:02d 則表示不足兩位時補零。例如 3725 秒會變成 01:02:05。
transcribe:主函式
流程分四步:讀音檔、丟進 asr、整理結果、回傳。asr 回傳的資料長這樣:
1 | { |
後面的迴圈會把它整理成更單純的 [(0.0, "大家好"), (3.2, "今天要討論"), ...],並略過空白的段落。
有兩個設計值得一提:
GENERATE_KWARGS是解碼設定,用來減少兩種常見毛病:同一句話不斷重複,以及在靜音段落憑空編字。其中temperature那一串數字的意思是:先用最保守的方式解碼,結果不對勁再逐級放寬重試。try / except是保險。這些進階設定在不同版本的套件上可能不相容,一旦報錯就改用預設值重跑,避免整批工作中斷。
第 5 段:試跑前 5 分鐘
1 | TEST_MINUTES = 5 |
這段的目的是用最小成本驗證整條流程。它只轉第一個檔案的前 5 分鐘,結果直接印在畫面上,不寫入檔案。
先試跑再跑整批的好處有兩個:流程有問題可以馬上發現,不必等一整個檔案跑完;也能先看辨識品質,再決定要不要調整設定或換模型。
transcribe 回傳的每個元素是一對「秒數、文字」,for start, text in ... 直接把它拆成兩個變數。輸出會像這樣:
1 | 試跑:01_開場.m4a(前 5 分鐘) |
執行時畫面會先安靜一陣子,然後所有句子一次出現。因為 transcribe 是整段轉完才回傳,不是邊轉邊印。
第 6 段:整批轉錄
1 | files = list_audio() |
骨架很單純:對每個音檔,先檢查是否已經做過;還沒做過就轉錄,再存成兩份 txt。
| 輸出檔 | 內容 | 用途 |
|---|---|---|
檔名.txt |
純文字,一段一行 | 交給語言模型後製,或直接閱讀 |
檔名_timestamps.txt |
每行前面帶 [時:分:秒] |
需要回頭對照錄音時使用 |
輸出檔名取自 path.stem,也就是去掉副檔名後的音檔名稱。例如 01_開場.m4a 會產生 01_開場.txt 和 01_開場_timestamps.txt。
這段最重要的設計是那個 continue。程式用「兩個輸出檔是否都存在」來判斷音檔有沒有做過,做過就跳過。免費版 Colab 隨時可能斷線,有了這個判斷,重新執行時就會從還沒完成的那個音檔接著做,不必整批重來。這個做法的好處是簡單:輸出檔本身就是進度紀錄,不必另外寫程式去記做到哪裡。
日後使用的三個情境
流程跑通之後,實際使用時最常遇到的是這三種狀況:
| 情境 | 做法 | 原因 |
|---|---|---|
| 之後又新增幾個音檔 | 直接放進 audio,重新執行第 6 段 |
舊檔案的輸出已存在,會自動跳過 |
| 某個檔案想重新轉錄 | 刪掉 output 裡它對應的兩個 txt,再執行第 6 段 |
程式找不到輸出檔,就會當成沒做過 |
| 隔天重開 Colab | 依序重跑第 1 到 4 段,再執行第 6 段 | 虛擬機已重置,模型和函式都要重新載入 |
要留意兩件事:不要移動或刪除 output 裡的舊 txt,也不要更改已轉錄音檔的檔名。這兩者都會讓程式判斷成「還沒做過」而重新轉錄。
測試與實測觀察
用 assert 驗證小函式
第 4 段的 fmt 是純函式,輸入相同就得到相同輸出,最適合用 assert 快速驗證。在 notebook 新增一段程式碼貼上即可,全部通過就不會有任何錯誤:
1 | assert fmt(0) == "00:00:00" |
transcribe 裡整理結果的那段迴圈也能用同樣方式測試,而且不需要 GPU。做法是自己造一份長得像 asr 回傳值的假資料,就像前端測試時模擬 API 回應:
1 | fake_result = {"chunks": [ |
中間那筆只有空白的段落被正確略過,文字前後的空白也被去掉了。
這裡有個最佳實踐值得分清楚。assert 適合寫測試和 notebook 裡的快速檢查,但正式程式的執行期檢查應該用 if ... raise。原因是 Python 以 -O 模式執行時會略過所有 assert,靠它把關的檢查會悄悄消失。
試跑結果與改善方向
我先只轉第一個檔案的前 5 分鐘,確認流程和品質後才跑全部。流程順利跑通,但輸出有三個明顯問題:
- 部分中文詞彙辨識不到位,會被換成發音相近的常見字
- 幾乎沒有標點符號
- 沒有分段
後兩者是 Whisper 系列模型的共同特性,換同類模型也一樣。改善要分兩層:轉錄時提供專有名詞詞彙表,引導模型選對字;轉錄後再用語言模型補標點、依語意分段並修正同音字。
後製用哪種語言模型也有取捨。在同一份 notebook 載入開源模型可以全自動,文字也不必再交給第三方,但免費 GPU 裝得下的小模型較容易擅自改寫內容。大型模型比較忠於原文,代價是多一道手動步驟或 API 費用。後續建議嘗試拿同一段試跑結果讓兩種方式各處理一次,比較後再決定。
小結
這次實作帶走三個重點:
- 依情境選方案:一次性批次工作用 Colab 加開源模型就夠,不必一開始就上付費 API 或架推論伺服器。
- 流程要能續跑:以輸出檔是否存在來判斷進度,中斷後重新執行就能接續,不必另外記錄。
- 語音辨識只是第一步:模型負責「聽」,標點、分段和錯字修正要靠後製補上。
未來可以加入詞彙表提示,並實測開源語言模型的後製效果。這份流程在 Mac 本機也能跑,要改的只有檔案路徑和運算裝置,之後有機會再另外整理。