第一次讓自己的電腦用一個不存在的人的聲音唸出一段中文,是一個很奇妙的晚上。我打了一句話,等了一分鐘,喇叭裡傳出一個聲音,語氣自然、斷句正確、還有一點點鼻音。我把那段音檔重播了大概十次,每一次都覺得有點不可思議,又有點毛。室友從房間探頭出來問我在跟誰講話,我說是電腦,她看了我一眼,把門關上。
那個模型是 VoxCPM2。OpenBMB 釋出的語音合成模型,Apache-2.0 明文可商用,二十億參數,輸出原生 48 kHz,支援用一段參考音加逐字稿複製音色。我在本機拿它做中文語音合成,取代了原本的引擎。以下是我認為任何想在自己機器上跑它的人該先知道的事,數字都來自一張 RTX 2070 SUPER,8 GB 顯存,一張在 2026 年已經算長輩的卡。
速度與顯存
四句合計約三十秒的中文語音,預設參數下牆鐘約六十秒,也就是每一秒語音要不到兩秒運算,顯存峰值約 6.2 GB。它的 Python API 一次只合成一段文字,沒有批次,卻仍然比我之前那個已經四句一批的引擎快了七成。這件事讓我對「批次一定比較快」這個直覺產生了懷疑,原來一個設計得好的循序引擎可以贏過一個硬塞批次的對手。
這張卡是 Turing 架構,沒有原生 bfloat16,而套件在 CUDA 上沒有讓使用者改 dtype 的路徑,所以是靠 PyTorch 模擬跑的,上面的數字已經包含這個劣勢。換句話說,你的卡如果比我的新,數字只會更好看。
擴散步數:預設十步,別砍到五
inference_timesteps 預設十步,砍到五步速度直接翻倍。翻倍耶,誰不想要。我當然試了。
然後我做了一件事:把同樣四句在兩種設定下的輸出,用語音辨識轉回文字比對。五步有兩句唸錯字,唸成了發音相近的另一個詞,同時 8 kHz 以上的能量每句都升高,那是擴散步數不足的粗糙雜訊,用耳朵聽是一種說不上來的「毛毛的」。音色沒有飄,問題不在音色。速度是真的,代價是唸錯字,維持十步。
這件事教我的是:任何「更快」的選項都要拿輸出去驗,不能只看碼表。快兩倍但唸錯字的語音合成,就像快兩倍但抄錯題目的考試。
音色複製
用同一段參考音生成不同的句子,兩句之間的音色指紋相似度 1.000,是穩定的同一個嗓音,不會這句像小明、下句像小華。跟參考音本身的長期頻譜相似度,和我之前的引擎在同一段參考音上拿到的數字相同,換引擎沒有讓聲音變得更像或更不像。
換引擎時記得一件事:一段參考音的核准或同意紀錄綁的是「這段音在這個引擎上的聲音」,換了引擎要重新聽過、重新記錄。同一個人的聲音經過兩個引擎,出來是兩個不太一樣的人,這件事在做任何跟聲音有關的東西之前都要想清楚,因為聲音是別人的,不是你的。
代價一:載入要幾分鐘
乾淨環境下載入約兩百二十秒,跟其他工作搶顯卡時量到三百七十秒。整批製作攤提得掉,「改一句重跑」會很痛,痛到你會開始在等待的時候泡茶、洗碗、回訊息,然後忘記自己在等什麼。第一句還有冷啟動的成本,之後就正常。
代價二:繁體的「著」
它把繁體的「著」讀成 zhù,「順著」變成「順住」,「看著你」變成「看住你」,聽起來像是在講某種很兇的話。修法是只在送進引擎的那一份文字裡把「著」換成「着」,其他地方維持原文。我只實測過這一個字,原本擔心「发」「干」「里」這類簡繁差異也會出事,實測沒有,所以不做整體轉換,也不把沒量過的字寫進替換表。沒量過的不算修正,只算猜測。
代價三:載入時偶爾當掉
它在建構 AudioVAE 時,會在 PyTorch 一個已棄用的 weight_norm 裡間歇性 segfault,我遇到的比例大約一半。一半。就是你按下執行,然後擲一枚硬幣。
對策是讓外層程式在原生崩潰時自動重啟 worker,最多幾次,並且只對原生崩潰重啟;程式自己拒絕工作的退出不重試,否則真正的原因會被重試次數埋掉。診斷這種問題先拿到沒被管線污染的退出碼,接了管線你看到的會是 cut 的退出碼,然後你會對著一個完全無關的數字研究半天。我研究過。
小劇場:這聲音會不會太真
「你確定這是電腦唸的?」
「確定,我剛打的字。」
「那你會不會哪天被自己電腦的聲音騙?」
「不會,它唸『著』的時候會露餡。」
「所以那個 bug 其實是防偽功能。」
「這個角度我沒想過,但我還是把它修掉了。」
代價四:兩個安靜的 API 陷阱
它沒有 seed 參數,確定性只能靠 torch.manual_seed。它的 generate() 宣告成 (*args, **kwargs) 再轉呼叫內部函式,對 generate 做參數自省會得到空集合。如果你的程式為了相容不同版本而「只傳這個版本接受的參數」,會把包含參考音在內的所有引數全部丟掉,模型改用預設嗓音講話,聽起來完全正常。
完全正常是最可怕的部分。它不會報錯,不會警告,就是用另一個人的聲音把你的句子唸得好好的。參數名要從內部的 _generate 讀,並且用測試釘住「參考音真的傳進去了」。這條測試是我寫過最像偏執狂的一條,也是我最慶幸有寫的一條。
OpenBMB 是誰
OpenBMB 是清華大學自然語言處理實驗室和面壁智能一起維護的開源社群,名字裡的 BMB 是 Big Model Base。他們最出名的是 MiniCPM 系列,主打「小模型也能很強」,在手機和邊緣裝置上跑的那種。VoxCPM 是這個家族的語音成員,CPM 三個字母在他們所有模型名字裡都會出現,Chinese Pretrained Models 的縮寫。知道這個背景之後,它對中文特別好這件事就不意外了,中文本來就是它的母語。
48 kHz 這個數字值得說一下。很多語音合成模型輸出 24 kHz 或更低,聽起來沒問題但頻寬有限,高頻的氣音和齒音會糊掉。48 kHz 是專業音訊的常見取樣率,跟影片的音軌對得上,不用重採樣。對要進影片的用途來說,這省了一道處理,也少了一次品質損失。
音色複製這件事現在很多模型都會,技術上不稀奇,稀奇的是穩定:同一段參考音,今天生成和明天生成、這句話和那句話,要是同一個人。VoxCPM2 在我的測試裡做到了這件事,指紋相似度 1.000 不是四捨五入,是真的一樣。
常見的三個誤會
第一個誤會是「音色複製只要一段音就好」。技術上是,但你還需要那段音的逐字稿,還需要說話的人同意。前兩個是模型的需求,第三個是做人的需求。沒有第三個,前兩個做得再好都不該用。
第二個誤會是「載入慢是因為模型大」。二十億參數在今天不算大,慢的是它要初始化好幾個子模組,還有第一次 CUDA 的延遲初始化。這是為什麼要用長時間存活的 worker,不是每句話重新載一次。
第三個誤會是「唸錯字是模型不好」。有些是,例如擴散步數不足;有些是文字本身的歧義,例如繁體的「著」有四個讀音,人也常唸錯。分清楚哪種錯是誰的問題,才知道要改模型參數還是改送進去的文字。
一個週末可以試的小練習
這個練習不需要跑模型。找一段中文,任何一段,把裡面所有的「著」圈出來,然後試著唸出來。你會發現你在唸的時候幾乎沒有想,但如果要你說出每一個「著」是哪個讀音,你會停下來。「順著」「著急」「穿著」「著作」,四個讀音,同一個字。
然後想像你是一個模型,只看到字,沒有上下文的語感。你會怎麼猜?這十分鐘會讓你對「語音合成唸錯字」這件事多一點同理心,也會讓你理解為什麼我只在送進引擎的那一份文字裡動手腳,而不是改原文。原文是作者的,讀音是引擎的問題,兩件事要分開。
後記
室友後來又聽到我在跟電腦講話,這次她沒有關門,她探頭問「這次是誰的聲音」。我說是一個不存在的人。她說聽起來蠻好聽的。我說對啊,然後我們都沉默了一下,大概是同時想到了同一件事:這個技術好聽是好聽,但它能做的事,有一半我不太想知道有人會拿去做什麼。所以參考音要有人同意這件事,我會一直堅持。
留言(0)
還沒有留言。讀完有想法,歡迎留下第一則。