~/blog/qwen3-tts-voice-clone-macbook

AI Workflow · part 21

[Dev Workflow] 你的聲音,24 秒就能複製:MacBook 上的 Qwen3-TTS 教學

cat --toc

TL;DR

預設 TTS 的聲音好歸好,就是不像你。要拿到自己的聲音只有一條路:錄 24 秒乾淨錄音,用 Qwen3-TTS-12Hz-1.7B-Base 的 8bit 版做 zero-shot clone,不用訓練也不用 GPU 機。MacBook 本機合成 83 秒旁白花 51 秒,比即時還快。逐段轉寫比對,字元錯誤率 7.6%,我自己聽過,過關。要注意的是它沒有固定 seed,同一句每次語調都不一樣,產線要用逐句重跑而不是拼寫字典。

首圖:巨大的數字 24,兩個字的筆畫都由聲音波形構成;2 是密而短促的白色波形,4 是舒展的青綠色波形,右下角站著一個很小的人影

前言

電話那頭是不是本人,你通常在對方講完第一句話之前就知道了。

我要幫自己的文章配旁白,第一個念頭當然是找現成的 TTS。中文語音的品質其實都不錯,問題不在品質——它們聽起來都不像我。旁白是要掛在我自己寫的東西上面的,那個聲音卻是別人的。

試了幾種現成的做法,都卡在同一個地方。macOS 內建的 say -v Meijia 慢,一秒只有 4.7 個字;預設語音庫我逐個聽過,結論是「還是 AI 感很重」。這條路沒有調得好的空間,因為缺的東西不在參數裡。

剩下唯一一條路是 clone 我自己的聲音。錄一段,讓模型照著這段的音色去唸別的字。

好消息是這件事比想像中簡單:錄 24 秒、十行 Python、MacBook 本機跑,不用訓練也不用 GPU 機。


zero-shot clone 是什麼:你只要準備兩樣東西

先講清楚 zero-shot voice clone 是什麼,免得你以為要自己訓練一顆模型。

你不用準備一小時的錄音,不用跑微調,不用等好幾個鐘頭。你給模型兩樣東西:一段你的錄音,和那段錄音的逐字稿。模型從這兩樣東西抓出你的音色跟腔調,然後拿去唸你給的新句子。整個過程完全不用更新權重,所以叫 zero-shot。

這也是為什麼參考錄音跟逐字稿必須完全對得上。模型是靠「這段聲音對應這些字」去學你怎麼發音的,對不上就學歪。

錄那 24 秒:一次錄完,吃螺絲就重錄

先錄一段自己的聲音。錄一次,之後可以一直用。

環境:安靜房間,冷氣出風口別對著你,關掉背景音樂。麥克風用 MacBook 內建的就夠了,離嘴 15 到 20 公分。

唸法:照你平常講話的速度跟音量唸,像在跟一個人解釋事情,不要用廣播腔

長度:20 到 30 秒。我實際用的那份是 23.9 秒。

內容:自己寫一段,但要涵蓋你之後真的會用到的詞。我這份塞了英文技術詞進去,因為旁白一定會唸到:

大家好,我是 ai-muninn 的作者。這個頻道談的是怎麼用便宜的硬體、開源的模型,把 AI 真正跑起來。我不太喜歡講理論,比較喜歡把東西實際跑一遍,量出數字,再告訴你哪裡會踩坑。今天這一堂會提到 GPU、token 跟 llama.cpp 這幾個詞,聽不懂沒關係,我會一個一個解釋。好,我們開始。

最重要的一條:一次錄完整段,中間唸錯就整段重錄。 不要剪。因為逐字稿要跟音檔一字不差,剪過的音檔跟你手上那份文字就對不起來了。

錄法最簡單的是 QuickTime Player 的「新增音訊錄製」,或手機語音備忘錄錄完 AirDrop 過來。要用命令列的話:

# 先看麥克風是哪一號
ffmpeg -f avfoundation -list_devices true -i ""
# → [1] MacBook Pro的麥克風

# 錄 30 秒(冒號開頭 = 只收音訊)
ffmpeg -f avfoundation -i ":1" -t 30 -ac 1 -ar 24000 ref_raw.wav

錄完轉成模型要的格式,順便修掉頭尾的空白:

ffmpeg -i ref_raw.wav -ac 1 -ar 24000 \
  -af "silenceremove=start_periods=1:start_threshold=-45dB:stop_periods=-1:stop_threshold=-45dB" \
  ref.wav

檢查一下錄得能不能用:

ffmpeg -i ref.wav -af volumedetect -f null /dev/null 2>&1 | grep volume
# mean_volume: -29.6 dB
# max_volume: -10.7 dB

峰值落在 -6 到 -12 dB 之間就很好,代表音量夠大又沒有爆音。我這份是 -10.7 dB。

最後把逐字稿存成 ref_text.txt,內容跟你唸的一模一樣。

裝環境:兩個套件

python3 -m venv .venv && source .venv/bin/activate
pip install mlx-audio soundfile

mlx-audio 會把 MLX 一起帶進來,那是 Apple 自己的機器學習框架,直接吃 Apple Silicon 的 GPU。我這台上面是 mlx 0.31.1 + mlx-audio 0.5.1,Python 3.14 arm64。

模型第一次跑會自己從 Hugging Face 下載。

流程圖:ref.wav 24 秒錄音與 ref_text.txt 逐字稿一起進 Qwen3-TTS,輸出 out.wav,再往下接到 CER 7.6% 的驗收
整條線只有兩個東西是你準備的:一段 24 秒的錄音,跟那段錄音一字不差的逐字稿。模型完全不用訓練,權重完全沒改。下面那條分支是驗收,數字怎麼來的在後面那節。

十行 Python

from mlx_audio.tts.utils import load_model
import soundfile as sf

model = load_model("mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit")
ref_text = open("ref_text.txt", encoding="utf-8").read().strip()

results = list(model.generate(
    text="今天要講的是怎麼在自己的機器上跑大模型。",
    ref_audio="ref.wav",
    ref_text=ref_text,
    lang_code="auto",
    split_pattern="",
    verbose=False,
))
r = results[-1]
sf.write("out.wav", r.audio, r.sample_rate, subtype="PCM_16")

就這樣。ref_audioref_text 是那對必須對得上的東西,text 是你要它唸的新句子。

🔴 模型名字裡的 12Hz 不能漏。 完整是 Qwen3-TTS-12Hz-1.7B-Base。少打那三個字元會回 401,而 401 看起來像權限問題,你會跑去查登入而不是查名字。我就是這樣浪費過時間。

另外 Base 這個變體只做 clone,它沒有預設語音。 想要現成的音色要換 CustomVoice 那支,兩支的 API 也不一樣。細節在後面那節。

你會拿到什麼

速度比即時快。 我合成 83 秒的旁白花了 51 秒。這是 MacBook 本機的數字,沒有動用任何 GPU 機。

語速大約一秒 5.3 到 6.5 個字,換算下來 60 秒的旁白裝得下 320 到 390 字。寫腳本的時候拿這個抓長度很準。

品質:我把成品逐段丟給 whisper 轉寫,再跟原始腳本逐字比對,字元錯誤率 7.6%。這個數字的用途是抓「唸錯字」,不是抓「好不好聽」——好不好聽還是得自己聽。我聽過,過關。

授權:Base、1.7B-CustomVoice、0.6B-CustomVoice 三個都是 Apache-2.0 而且非 gated,商用沒問題。這點值得特別確認,因為同類的開源 TTS 有不少是不能商用的。


進階:四件開始用之後才會遇到的事

不讀這節不影響你把第一段旁白做出來。

三個變體分工不同,選錯 API 就對不上

Qwen3-TTS 有三個變體,名字很像,能做的事不一樣。我一開始照著網路上查到的說法選,結果跟實測對不上,以下是自己跑出來的:

變體做什麼呼叫方式
Base只做聲音複製,沒有預設語音model.generate(text=, ref_audio=, ref_text=)
CustomVoice有 9 個預設語音,支援情緒指令model.generate_custom_voice(text=, speaker=, language=, instruct=)
VoiceDesign用文字描述生一個新聲音model.generate_voice_design(text=, language=, instruct=)

CustomVoice 那 9 個預設語音是 serena / vivian / uncle_fu / ryan / aiden / ono_anna / sohee / eric / dylan,而且它吃 instruct 參數,可以下「平穩、清楚」這種情緒指令。Base 沒有這個參數,因為它的語氣是從你的錄音裡抄的。

支援 10 種語言,外加一個 auto 自動選:auto / chinese / english / german / italian / portuguese / spanish / japanese / korean / french / russian

沒有固定 seed,同一句每次語調都不一樣

這件事會直接決定你的產線怎麼設計。

同一段文字跑兩次,出來的語調不一樣。字都對,但抑揚頓挫會飄,英文字母每次唸出來也不太一樣。這不是 bug,是取樣本來就有隨機性,而這支模型沒有給你固定 seed 的入口。

我原本想的做法是建一份拼寫字典,把會唸歪的詞預先寫死。那個方向是錯的,因為問題不在「這個詞它不會唸」,是「這個詞它這次唸成這樣、下次唸成那樣」。

實際可行的做法是逐句重跑:整段旁白切成句子,一句一個檔,聽到哪句不順就只重跑那一句。反正一句只要一兩秒,重跑的成本低到可以當免費。這也順便解掉另一個問題——改一句話不用重合成整段。

驗收要逐段轉寫,別整檔丟進去

上面那個 7.6% 是逐段算的。我第一次量的時候是把 83 秒的拼接檔整個丟給 whisper,拿到 0.35,差點以為這條路不行。

去翻逐字稿才發現錯的全部擠在檔案結尾——whisper 在長音檔的尾巴會開始自己編,那是已知行為。前面每一句都對得漂漂亮亮。

改成逐段轉寫再算,實際只有 0.076。差 4.6 倍,而且我差點就照那個數字把一份沒問題的成品丟掉。

這裡的教訓跟 TTS 本身無關:壞掉的是量測程序,不是被量的東西,而這兩件事在數字上長得一模一樣。

同一台機器上,另一個選擇不能用

我同時測了 BreezyVoice,那是聯發科出的,明寫支援中文與中英混講,聽起來剛好就是我要的。

在 MacBook 上它比即時慢 24 倍。合成一分鐘旁白要跑二十四分鐘。這不是慢一點,是產線做不起來。

它不是壞模型,是它需要 GPU 而我這條線的前提是本機跑。要用它得整條搬到 GPU 機上,那就變成另一個題目了。

收工

錄 24 秒、裝兩個套件、十行 Python,你就有一個會用你自己聲音講話的 TTS,跑在自己的筆電上,商用授權沒問題。

三件事別忘了:錄音跟逐字稿要一字不差、模型名字裡有 12Hz、驗收要逐段轉寫。

至於旁白要拿來做什麼,那是下一篇的事。


同系列其他文章:Skill 裡的檢查完全寫對了,但它一次都沒跑過 · AI 味不在破折號,在結構

常見問題

MacBook 本機跑得動語音合成嗎?需要 GPU 機嗎?
跑得動,而且比即時還快。Qwen3-TTS-12Hz-1.7B 的 8bit 版透過 MLX 在 Apple Silicon 上跑,我實測合成 83 秒的旁白花 51 秒。同一台機器上 BreezyVoice 慢到即時的 24 倍,那個就不能用了。
為什麼不用內建的預設語音,一定要 clone 自己的聲音?
因為預設語音沒有一個是你。旁白掛在自己的文章或課程上時,聲音本身就是識別度的一部分。另外 Base 這個變體本來就沒有預設語音,它只做 clone;要現成音色得換 CustomVoice 那支。
參考錄音要多長、要多乾淨?
20 到 30 秒就夠,zero-shot 不用訓練。我用的那份是 23.9 秒、24 kHz 單聲道、峰值 -10.7 dB。安靜房間、離嘴 15 到 20 公分、用平常講話的速度念完一整段,中間吃螺絲就整段重錄,因為文字稿要跟音檔逐字對得上。

接著讀

不想錯過新文章?

訂閱我確保不漏接!

隨時一鍵退訂。