![]()
📝 原文(日文)發表於 forge.workstyle.tech。
當你想把錄下的聲音轉換成另一個人的聲音時,第一個浮現的問題是:
「聲音」究竟是什麼?
即使說出相同的詞句,不同的說話者會產生不同的聲音。即使是同一位說話者,根據所說內容的不同也會產生不同的聲音。再加上語調變化,就會更加不同。換句話說,語音是一種由多個獨立資訊混合而成的訊號——至少包括:誰在說話(說話者身分)、說了什麼(內容)、以及怎麼說的(韻律)。
語音轉換(VC)的核心挑戰就在這裡。如果你單純地處理語音來替換說話者身分,內容和語調往往也會跟著改變。在這篇文章中,我們將說明我們採用作為「聲音設計」應用程式後端的 Seed-VC 如何解決這個問題——透過使用四個模組分離並處理資訊,同時逐步講解實際的模型載入與推論程式碼。
Seed-VC 的核心思想是:「不要讓單一大型模型包辦一切。」而是按類型分解語音的組成要素,將每個要素分配給專用模組,最後再將它們重新組合起來。啟動時的模型載入流程如下:
model, semantic_fn, f0_fn, vocoder_fn, campplus_model, mel_fn, mel_fn_args = load_models(a)
進入全螢幕模式
退出全螢幕模式
雖然回傳了七個元件,但可以歸納為四個功能層:
semantic_fn) — 提取內容(說了什麼)作為語意特徵campplus_model) — 將說話者身分(誰在說話)編碼成單一向量嵌入model.cfm) — 以擴散模型根據上述兩者生成梅爾頻譜圖vocoder_fn) — 將梅爾頻譜圖轉換回可聽見的波形每個角色——「提取語意」、「提取說話者」、「繪製頻譜圖」、「轉換成聲音」——都乾淨地分離開來。這種分工就是我們能夠只替換說話者身分,同時保持其他一切不變的原因。讓我們依序來看每個步驟。
在內容提取上,我們使用語音辨識模型 Whisper 的編碼器。我們不使用解碼器(轉錄)。而是直接取用其中間表示(一序列語意特徵)作為特徵向量。
s_alt = _semantic(torchaudio.functional.resample(src_t, _S["sr"], 16000))
進入全螢幕模式
退出全螢幕模式
一個關鍵點:輸入在處理前會重新取樣到16kHz。由於 Whisper 是為 16kHz 音訊所設計,即使主要轉換模型以 44.1kHz 運作,我們在語意提取前總是會降採樣到 16kHz。產生的序列幾乎不包含音高或音色資訊——它代表的是與所說內容對齊的表示。這是我們之後能夠自由替換說話者身分的先決條件。
注意:Whisper 有一個限制——一次只能處理最多 30 秒的音訊。較長的音訊需要進行分塊處理。這個限制本身曾是個重大陷阱(詳見我們的另一篇文章:「語音轉換中『說話變慢』臭蟲的元兇是 Whisper 的 30 秒限制」)。
說話者身分提取由CAMPPlus這個說話者嵌入模型負責。它接收音訊,計算 fbank 特徵,並輸出一個單一的 192 維向量。
feat = torchaudio.compliance.kaldi.fbank(w16, num_mel_bins=80, dither=0, sample_frequency=16000)
feat = feat - feat.mean(dim=0, keepdim=True)
return _S["campplus_model"](feat.unsqueeze(0)).squeeze().detach().cpu().numpy()
進入全螢幕模式
退出全螢幕模式
「說話者身分 = 單一向量」這件事對我們的「聲音設計」應用程式至關重要。因為向量可以相加、混合或內插,我們會預先從 18 位說話者的乾淨錄音計算嵌入作為「錨點」,然後根據滑桿輸入使用加權平均來混合它們。
w = design_weights(slider_values, bank, **kw)
emb = (w[:, None] * bank.embeddings).sum(axis=0)
進入全螢幕模式
退出全螢幕模式
我們為「年齡」、「音高」、「沙啞度」等可解釋軸定義滑桿,計算權重 w,然後對錨點嵌入進行加權平均。這種「透過混合聲音來設計」的操作之所以可行,正是因為說話者身分被模組化為獨立的向量。如果說話者身分與內容糾纏在一起,混合就會破壞所說的詞句。
一旦我們取得語意特徵(內容)和說話者嵌入(身分),就會進入合成階段。這就是擴散模型(CFM:Conditional Flow Matching;以 DiT 實作)登場的地方。它從隨機噪聲開始,逐步生成以輸入為條件的梅爾頻譜圖。
vt = _S["model"].cfm.inference(cat, torch.LongTensor([cat.size(1)]).to(dev),
mel2, style, None, STEPS, inference_cfg_rate=CFG)
vt = vt[:, :, mel2.size(-1):]
進入全螢幕模式
退出全螢幕模式
觀察引數可以看出這個階段如何整合先前模組的輸出:
cat — 語意特徵(內容)。提示音訊(pc)與目標條件(cond)的串接style — 來自步驟 2 的說話者嵌入(= 混合後的設計嵌入)mel2 — 提示音訊的梅爾頻譜圖。生成後,我們會裁剪前 mel2.size(-1) 幀,只保留新內容STEPS / inference_cfg_rate — 擴散步數與無分類器引導的強度模型以分離的引數接收內容(cat)與說話者身分(style)。它將它們解釋為指令:「用這個說話者的聲音繪製這個內容。」如果你固定其中一個並改變另一個,只有預期的屬性會改變。這種分離設計直接反映在推論介面上。
length_regulator 與 F0「怎麼說的」——韻律(速度與音高變化)則有些不同。它無法完全與內容解耦,因此不使用專用模組,而是作為內容與說話者身分之間的連接點來處理。
首先是速度(持續時間)。來自 Whisper 的語意序列需要被拉伸以符合所需的梅爾幀數。這由 length_regulator 負責。
cond, *_ = _S["model"].length_regulator(s_alt, ylens=torch.LongTensor([mel.size(2)]).to(dev),
n_quantizers=3, f0=None)
進入全螢幕模式
退出全螢幕模式
透過傳入目標幀長(ylens),語意序列會被調整到所需的持續時間。接著是音高(F0)。在 44.1kHz F0 條件模型中,length_regulator 可以接受 f0 引數,得以進行韻律控制,例如將載音的音高範圍向目標說話者的範圍偏移。
lf[F0_alt > 1] = lf[F0_alt > 1] - m_alt + m_ori # 將載音 F0 偏移至目標中位數
shifted = torch.exp(lf)
cond, *_ = _S["model"].length_regulator(S_alt, ylens=tgt_len, n_quantizers=3, f0=shifted)
進入全螢幕模式
退出全螢幕模式
這是一個簡單的操作:在對數域中依中位數差進行偏移。韻律並未被視為獨立模組,而是作為注入的參數。這反映了一種務實的妥協——嘗試完全解耦往往會導致不自然的結果。
擴散模型生成的是梅爾頻譜圖——一種聲音的「藍圖」。最後一步,將它轉換成人類能聽見的波形,由聲碼器 BigVGAN 負責。
wav = _S["vocoder_fn"](vt.float()).squeeze().detach().cpu().float().numpy().reshape(-1)
進入全螢幕模式
退出全螢幕模式
雖然只是一行程式碼,但這一步對音質影響很大。即使使用相同的梅爾頻譜圖,不同的聲碼器也會產生不同的聲音。我們的 44.1kHz 模型能夠輸出「真正的寬頻」,正是因為 BigVGAN 是以高取樣率訓練的(詳見我們的另一篇文章:「22.05kHz 與 44.1kHz」)。
以下是實作與操作這個四層架構時的一些重要心得:
length_regulator 注入的條件參數是務實的解決方案。強行完全解耦往往會產生不自然的偽影。whisper(語意)+ campplus(說話者)+ CFM/DiT(擴散梅爾生成)+ BigVGAN(聲碼器)
cat)與說話者身分(style)——這種分離設計直接反映在推論介面上length_regulator 注入