Whisper 是 OpenAI 在 2022 年推出來的通用語音辨識模型(GitHub)。他基本上支援多語言的偵測、也還有內建翻譯成英文的功能,基本上算是一個相當好的語音辨識框架。
而由於目前深度學習領域主流的開發環境還是 Python,所以不意外地,OpenAI 提供的開發環境也是以 Python 為主。
不過,目前 Georgi Gerganov 也有開發出 C API 的 whisper.cpp 專案(GitHub),它提供了可以在不需要額外的函示庫的狀況下使用的 whisper API,對於 C / C++ 的開發者來說,應該是相當地友善的~(注意,他只支援推論、不支援訓練)
這篇文章就先不管 Whisper 的原理和運作方式,單純就以 C++ 程式開發者為出發點,來記錄一下要怎麼透過 whisper.cpp 來撰寫一個語音辨識的程式吧!
建置 whisper.cpp
由於他標榜著無相依性、所以要建置 whisper.cpp 在最基本的狀況下、算是相當地簡單的!
在最單純的狀況下,只要把開發環境準備好(在 Windows 下要安裝 Visual Studio 和 CMake),只要把專案 clone 下來,執行下面的指令就可以完成建置了!
mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX:PATH="c:/whispercpp" ..
cmake --build . --config Release
cmake --install .
而建置完之後,必要的檔案(h、lib、dll)則會放在 c:\whispercpp
這個資料夾下。
不過要注意的是,這樣建置的方法是最基本的建置,其實會少很多東西!
比如說在範例的部分會只剩下「main」這個範例,如果要其他範例還會需要 SDL 這個函式庫才行(比如說要存取電腦的麥克風)。
而在計算的部分,也會是最基本的純 CPU 計算,沒有用到 GPU 加速之類的。如果想要透過 GPU 來加速的話,則就需要使用第三方函示庫了~
他有支援 Intel OpenVINO、NVIDIA CUDA、CLBlast 之類的框架;而如果只是要使用 CPU 來運作,透過 OpenBLAS 似乎也是可以做到一定程度的加速。如果要使用這些函示庫來加速的話,在透過 CMake 進行組態設定的時候,也都需要加上特定的參數,不過在這邊就先不提了。
模型
whisper.cpp 支援的模型是量化成整數的 ggml 格式模型。這部分他有提供工具可以轉換,不過要簡單的話,就是先到 Hugging Face 上去下載已經轉好的了~
他的頁面是 https://huggingface.co/ggerganov/whisper.cpp/tree/main,裡面有不同大小、不同版本的檔案,可以根據自己的需要來選擇。
下面是官方提供的使用記憶體的需求表:
Model
|
磁碟空間
|
記憶體空間
|
---|---|---|
tiny
|
75 MiB
|
~273 MB
|
base
|
142 MiB
|
~388 MB
|
small
|
466 MiB
|
~852 MB
|
medium
|
1.5 GiB
|
~2.1 GB
|
large
|
2.9 GiB
|
~3.9 GB
|
不過,基本上模型越大效果應該還是會越好,但是相對地所需要的資源就更多。所以要怎麼取捨,就是看自己的需求了。
這邊接下來基本上是拿「ggml-medium.bin」這個檔案來作範例。
使用範例測試
都準備好了之後,接下來就是先測試自己建置出來的程式能不能正確運作吧。
理論上,在 c:\whispercpp\bin
下,會有 whisper.dll
和 main.exe
這兩個檔案,其中 main.exe
就是官方提供的範例。
要執行範例,最簡單的指令是:
main.exe -m c:\whisperpp\ggml-medium.bin d:\whisper.cpp\samples\jfk.wav
這邊就是要指定前面下載好的模型檔、以及拿一個 .wav 檔案來做輸入的音源了;而這邊使用的檔案,是官方提供的 jfk.wav
。
執行結果大致上會輸出下面的內容:
whisper_init_from_file_with_params_no_state: loading model from
'c:\whisperpp\ggml-medium.bin' whisper_model_load: loading model whisper_model_load: n_vocab = 51865 whisper_model_load: n_audio_ctx = 1500 whisper_model_load: n_audio_state = 1024 whisper_model_load: n_audio_head = 16 whisper_model_load: n_audio_layer = 24 whisper_model_load: n_text_ctx = 448 whisper_model_load: n_text_state = 1024 whisper_model_load: n_text_head = 16 whisper_model_load: n_text_layer = 24 whisper_model_load: n_mels = 80 whisper_model_load: ftype = 1 whisper_model_load: qntvr = 0 whisper_model_load: type = 4 (medium) whisper_model_load: adding 1608 extra tokens whisper_model_load: n_langs = 99 ggml_init_cublas: GGML_CUDA_FORCE_MMQ: no ggml_init_cublas: CUDA_USE_TENSOR_CORES: yes ggml_init_cublas: found 1 CUDA devices: Device 0: NVIDIA GeForce RTX 4090, compute capability 8.9, VMM: yes whisper_backend_init: using CUDA backend whisper_model_load: CUDA0 total size = 1533.52 MB (2 buffers) whisper_model_load: model size = 1533.14 MB whisper_backend_init: using CUDA backend whisper_init_state: kv self size = 132.12 MB whisper_init_state: kv cross size = 147.46 MB whisper_init_state: compute buffer (conv) = 25.61 MB whisper_init_state: compute buffer (encode) = 170.28 MB whisper_init_state: compute buffer (cross) = 7.85 MB whisper_init_state: compute buffer (decode) = 98.32 MB system_info: n_threads = 4 / 32 | AVX = 1 | AVX2 = 1 | AVX512 = 0
| FMA = 1 | NEON = 0 | ARM_FMA = 0 | METAL = 0 | F16C = 1
| FP16_VA = 0 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 1 | SSSE3 = 0
| VSX = 0 | CUDA = 1 | COREML = 0 | OPENVINO = 0 | main: processing '..\..\samples\jfk.wav' (176000 samples, 11.0 sec),
4 threads, 1 processors, 5 beams + best of 5, lang = en,
task = transcribe, timestamps = 1 ... [00:00:00.000 --> 00:00:11.000] And so my fellow Americans,
ask not what your country can do for you,
ask what you can do for your country. whisper_print_timings: load time = 1053.01 ms whisper_print_timings: fallbacks = 0 p / 0 h whisper_print_timings: mel time = 6.69 ms whisper_print_timings: sample time = 68.48 ms / 143 runs ( 0.48 ms per run) whisper_print_timings: encode time = 73.72 ms / 1 runs (73.72 ms per run) whisper_print_timings: decode time = 0.00 ms / 1 runs ( 0.00 ms per run) whisper_print_timings: batchd time = 456.64 ms / 141 runs ( 3.24 ms per run) whisper_print_timings: prompt time = 0.00 ms / 1 runs ( 0.00 ms per run) whisper_print_timings: total time = 1665.32 ms
其中,黃字的部分就是辨識的結果(本來只有一行)了~
實際上,Heresy 這邊在建置的時候其實有加上 NVIDIA CUDA,所以速度會快很多、而且顯示出來的資訊也會不太一樣。
另外,如果想要玩別的聲音檔案的話,由於目前這個範例只支援採樣頻率是 16k 的單聲道 WAV,所以會需要先轉換成這樣的格式才能給它處理。如果是使用 ffmpeg 轉換的話,指令如下:
ffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le output.wav
理論上這樣轉換後的 WAV 檔是可以支援的。
C API 基本範例
雖然 whisper.cpp 的名字有 C++,但是實際上他提供的是 C 風格的 API。
而他最基本的使用形式,大概會是像下面的樣子:
#include "whisper.h" #include <vector> #include <iostream> int main() { whisper_context_params cparams = whisper_context_default_params(); whisper_context* ctx = whisper_init_from_file_with_params(
"/path/to/ggml-base.bin", cparams); std::vector<float> pcmf32; // get audio whisper_full_params wparams = whisper_full_default_params(WHISPER_SAMPLING_GREEDY); if (whisper_full(ctx, wparams, pcmf32.data(), pcmf32.size()) == 0) { const int n_segments = whisper_full_n_segments(ctx); for (int i = 0; i < n_segments; ++i) { const char* text = whisper_full_get_segment_text(ctx, i); std::cout << text << "\n"; } } whisper_free(ctx); }
不過要注意,這段程式碼沒有真正的聲音資訊(pcmf32
),所以實際上是不會真的有東西輸出的。
context
基本上,要使用的時候要先建立一個對應到模型的 context,他的型別是 whisper_context
,最簡單的方法就是透過 whisper_init_from_file_with_params()
這個函式來讀取模型並建立 context。
除了給一個路徑讓他從檔案去讀取外,他也可以直接去使用已經讀到記憶體裡面的資料、或是自定義讀取方式來做初始化。
而在初始化的時候,還要指定 whisper_context_params
這個參數;在目前的版本它的內容很簡單,就是要不要使用 GPU 而已。
在不需要使用的時候,則是要透過 whisper_free()
來釋放記憶體。
推論
初始化完成之後,接下來就可以進行推論(inference)、辨識輸入的語音了。
基本上,它內部的處理流程會分成好幾的階段,但是對於一般只希望取得最後的辨識結果的人還說,只需要使用 whisper_full()
這個函式就可以了!
他的第一個參數是 context、就是前面初始化好的 whisper_context
的指標。
第二個參數則是要用來推論的設定、格式是 whisper_full_params
,裡面很複雜,能調的東西很多,全部用預設值基本上就是拿來測試的方法了。
第三個參數則是一個 const float*
、代表要拿來辨識的聲音資訊;第四個參數則是他的大小。
而如果這個函式回傳 0,就是推論成功了!
讀取結果
whisper.cpp 在推論完之後,結果會分成好一個或多個 segment、每個 segment 裡面會有很多 token。
一個 segment 可能就幾個字、也有可能是句子或段落;而 token 則是他在處理時的最小單位、但是基於設計的原理,token 並不是對應到字元、單字這類的東西,以中文字來說,一個中文字甚至可能會被拆成兩個 token。
而要讀取結果,基本上就是先透過 whisper_full_n_segments()
來取得推論結果的 segment 的數量,這邊是 n_segments
。
之後,則就可以透過 whisper_full_get_segment_text()
來把想要的 segment 代表的文字讀取出來了。
而如果想要這段文字的時間點的話,則可以透過 whisper_full_get_segment_t0()
和 whisper_full_get_segment_t1()
這兩個函式來取得起始、結束的時間。
基本上,最簡單的使用方法大概就是這樣了~
某方面來說,比較麻煩的可能反而是要處理音訊資料了。 XD
處理 token
如同前面所提到的,whisper 最小的處理單元是 token、而上面用來得到文字的 segment 基本上也是 token 的組成。所以如果有必要的話,也是可以針對 token 來做操作,取得更進一步的細部資訊的。
這邊的程式可以寫成:
const int n_segments = whisper_full_n_segments(ctx); for (int i = 0; i < n_segments; ++i) { const int n_tokens = whisper_full_n_tokens(ctx, i); for (int t = 0; t < n_tokens; ++t) { const char* text = whisper_full_get_token_text(ctx, i, t); float p = whisper_full_get_token_p(ctx, i, t); } }
基本上,就是先透過 whisper_full_n_tokens()
來取得第 i
個 segment 裡面有幾個 token,然後再透過迴圈去讀取個別 token 的資訊了。
這邊是透過 whisper_full_get_token_text()
來讀取 token 的文字、而 whisper_full_get_token_p()
則是用來讀取他的可靠性。
如果要更細節的資料,也可以透過 whisper_full_get_token_data()
來取得 whisper_token_data
這個結構的資料,裡面會包含時間資訊、更細節的機率等資訊。
不過如果是要辨識中文或其他非英文的語言的話,在輸出文字的時候應該不適合用 token 做輸出;因為這邊得到的資料可能不是代表完整的中文字,直接輸出會出問題。
而如果以官方的 main 這個範例說,如果加上「-pc
」這個參數後,他就會將每個 token 都根據可能性加上顏色,有興趣的話可以自己玩看看。
基本參數
前面有提到,要推論的時候除了聲音的資料外,還要給他一個 whisper_full_params
來作為推論時的設定參數。
而這個結構裡面定義的東西非常多,很多東西大概也得自己去測試、慢慢調整才會知道怎樣設定比較合適了。
不過,這邊先列一些比較基本的參數出來講一下:
n_threads
:要使用幾個執行序來進行推論language
:預設的語言,可以給nullptr
、""
、"auto"
讓他自動偵測,或是給"en"
/"english"
代表英文、"zh"
/"chinese"
代表中文。translate
:設定成true
的話,他會把結果翻譯成英文。
此外,他也可以設定提示詞(prompt)或 callback 這類的東西,不過這邊就先不講了。
這篇大概就這樣了。
基本上,whisper.cpp 基本使用比想像的簡單不少,花比較多時間的反而是在研究怎麼透過 Qt6 讀取麥克風的資料(苦笑),這部分之後有機會在整理了。
不過老實說,真正的問題是在抓到文字之後到底要怎麼做後續的處理了?如果要拿來做簡單的單字比對問題應該不大,但是如果要能做語意分析的話,頭就會很大了。(眼神死
而這個作者另外還有一個 llama.cpp 的專案(GitHub),是讓開發者可以透過 C API 來使用大語言模型(例如 ChatGPT)的,或許之後可以考慮串起來吧~
(其實 whisper.cpp 也有一個範例 talk-llama 是串好的)
附註:
使用 main 這個範例辨識中文語音內容的時候,預設會自動翻譯成英文,如果要保留中文輸出,需要加上 -l zh
這個參數、指定內容是中文。
不過由於 PowerShell 預設編碼不是 UTF-8,所以可能會看到輸出一堆看不懂的亂碼。這時候建議先執行下面的指令:
[System.Console]::OutputEncoding = [System.Console]::InputEncoding = [System.Text.Encoding]::UTF8
額外參考:
想問一下,
“理論上,在 c:\whispercpp\bin 下,會有 whisper.dll 和 main.exe 這兩個檔案,其中 main.exe 就是官方提供的範例。”
但我下載下來只有whisper.dll ,並沒有main.exe這個檔案
不知道你下載的是哪個?
1.5.5 版的 whisper-bin-XXX 應該都包含執行檔。
https://github.com/ggerganov/whisper.cpp/releases/tag/v1.5.4
作者大大,
我直接把C:\whisper.cpp\build\bin\Debug\main.exe,
移到c:\whispercpp\bin裡面也可以跑得動,
我自己是透過git clone https://github.com/ggerganov/whisper.cpp指令來下載,
假設有人要運用這篇文章碰到這問題,
希望這則留言會有幫助
另外,方便問大大C API 基本範例的檔案室放在哪個資料夾呢?
如果是 git clone 下來的話,就是要自己建置了。
你找到的 main.exe 就是你建置出來的程式了。不過看來你是建置偵錯(debug)版本,執行的效率可能會差一點就是。
另外,本文的「C API 基本範例」是 Heresy 這邊寫的,不是官方的;要放哪都可以,只要專案設定好就可。
了解,謝謝作者大大