虛擬女友初步調(diào)研總結(jié)2024-0212(轉(zhuǎn)載)
發(fā)布日期:2024/2/19 18:58:15 瀏覽量:
虛擬女友初步調(diào)研總結(jié)2024-0212(轉(zhuǎn)載)
轉(zhuǎn)自:https://zhuanlan.zhihu.com/p/682012849
整個(gè)AI生成領(lǐng)域的變化,已經(jīng)非??植懒?,呈現(xiàn)出一副勃勃生機(jī),萬(wàn)物競(jìng)發(fā)的狀態(tài)。
- 文本生成和對(duì)話領(lǐng)域,有Gemini-PRO,GPT4為代表的大語(yǔ)言模型,相當(dāng)于有了大腦;
- 語(yǔ)音識(shí)別領(lǐng)域,有openai的whisper、阿里的funasr為代表的(Speech Recontion)語(yǔ)音識(shí)別模型,有了聽覺。
- 視覺識(shí)別這塊,有Gemini-PRO的免費(fèi)api可以白嫖,可以直接處理文本+圖片信息,可以說(shuō)是 開天眼了。
- 再然后就是前段時(shí)間看到花兒不哭大佬的新作品:GPT-sovits,這東西可以只用一分鐘的音頻,做few-shot訓(xùn)練,然后就能直接拿到參考音頻的音色,毫不夸張的說(shuō),只需要一分鐘的錄音,就能偷掉別人的嗓子!霍金來(lái)了都得點(diǎn)贊。
- 最后是數(shù)字人視頻生成,這個(gè)對(duì)我來(lái)說(shuō)是一個(gè)全新的領(lǐng)域,我也只是淺嘗了一下,快速做一個(gè)記錄,和大家一起學(xué)習(xí)。我主要使用的是Linly-Talker。
- demo視頻:【賽博鷹醬給您拜年了】 賽博鷹醬給您拜年了_嗶哩嗶哩_bilibili
趁著過(guò)年這段時(shí)間比較浮躁,快速體驗(yàn)了一下這個(gè)功能,希望為后面的智能小車,以及最終的虛擬女友,做一個(gè)技術(shù)鋪墊。
由于文本生成和視覺識(shí)別都是調(diào)用api,所以接下來(lái)主要介紹語(yǔ)音識(shí)別、聲音復(fù)刻和數(shù)字人生成這三個(gè)部分。
1. 語(yǔ)音識(shí)別:whisper or FunASR?
openai的whisper
后面使用的是openai 開源的whisper,網(wǎng)上大家都說(shuō)它是最強(qiáng)語(yǔ)音識(shí)別模型。whisper有三種調(diào)用方式,做一下總結(jié):
- api調(diào)用:對(duì)于臨時(shí)使用、長(zhǎng)文本、不用即時(shí)對(duì)話的場(chǎng)景,用免費(fèi)的3.5api,就可以直接調(diào)用,國(guó)內(nèi)延遲比較高,差不多得3-18秒,才能完成一次識(shí)別,非常不穩(wěn)定,不適合實(shí)時(shí)對(duì)話。另外中文識(shí)別效果一言難盡,得字正腔圓的普通話才行,甚至于有時(shí)候還會(huì)生成繁體字。不知道氪金用戶,或者用azure的api會(huì)不會(huì)好些。
- 官網(wǎng)開源的whisper:官網(wǎng)的whisper本地部署,這個(gè)要求你得有至少8Gb的顯存,我自己用的4060顯卡,勉強(qiáng)可以用medium,推理速度比較穩(wěn)定,大概是2秒左右,識(shí)別2秒的音頻。缺點(diǎn)是一樣的,中文識(shí)別效果不是很好,錯(cuò)字、標(biāo)點(diǎn)亂打、繁體。
- fast-whisper:這個(gè)項(xiàng)目我得貼一下鏈接:https://github.com/SYSTRAN/faster-whisper, 它大大降低了對(duì)顯存的要求!
另外,需要提一句的是,large-v2的識(shí)別效果一般會(huì)比v3更好,所以如果大家要是用whisper的格式,推薦使用fast-whisper:large-v2。
1.1 關(guān)于語(yǔ)音識(shí)別的自動(dòng)分段:
在連續(xù)對(duì)話的邏輯中,不得不提的是一個(gè)技術(shù)是"語(yǔ)音活性檢測(cè) (Voice activity detection,VAD)",即當(dāng)一個(gè)正在錄音的循環(huán)中,怎么判斷有人聲,怎么判斷,這段話說(shuō)完了,開始進(jìn)入對(duì)話的邏輯?
好消息是fast whisper倉(cāng)庫(kù)自帶vad檢測(cè)模塊,因此,只需要做一個(gè)集成封裝就好。我的主要工作集中在:
- 采集音頻信號(hào)
- 判斷是否有人聲,調(diào)用vad模塊,可以直接實(shí)時(shí)判斷當(dāng)前音頻是否有人聲。
- 判斷是否是一個(gè)完整的對(duì)話,這里的思路比較抽象
- 提取完整輸入音頻之后,保存到本地,然后調(diào)用本地的whisper模型,進(jìn)行語(yǔ)音識(shí)別,獲取文本;
- 將文本+Prompt,輸入給GPT3.5,拿到對(duì)話文本;
- 將對(duì)話文本輸入給GPT3.5的tts模塊,拿到回答音頻文件;
- 播放回復(fù)音頻。
這里貼一下我的調(diào)用示例代碼,也算是開源了,如果有幫助,懇請(qǐng)幫忙給帖子點(diǎn)個(gè)贊吧:
# 需要在fast-whipsper工作目錄中運(yùn)行。
import pyaudio
import wave
import time
import tenacity
import openai
import random
import re
from openai import OpenAI
import openai
import numpy as np
from faster_whisper import WhisperModel
import pyaudio # 導(dǎo)入PyAudio庫(kù),用于音頻錄制和播放
import pygame
from faster_whisper.vad import get_vad_model
chatpaper_keys = """
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
"""
chatpaper_keys = chatpaper_keys.strip()
chatpaper_keys_list = re.findall(r’sk-\w+’, chatpaper_keys)
def play_mp3(file_path):
pygame.mixer.init()
pygame.mixer.music.load(file_path)
pygame.mixer.music.play()
while pygame.mixer.music.get_busy(): # Wait for music to finish playing
pygame.time.Clock().tick(10)
# 停止混音器
pygame.mixer.music.stop()
# 卸載當(dāng)前加載的音樂(lè)流,釋放資源
pygame.mixer.music.unload()
# 退出混音器
pygame.mixer.quit()
def get_tts(client, input_text, output_path="output.mp3"):
response = client.audio.speech.create(
model="tts-1",
voice="nova",
input=input_text,
)
st = time.time()
response.stream_to_file(output_path)
print("save audio time:", time.time()-st)
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, min=4, max=10),
stop=tenacity.stop_after_attempt(8),
reraise=True)
def get_response(client, robot_state=’’, user_task=’’):
openai.api_key = random.choice(chatpaper_keys_list)
st = time.time()
# 限制info和prompts的長(zhǎng)度
robot_state = robot_state[:400]
user_task = str(user_task)[:600]
messages = [
{"role": "system", "content": "你是一個(gè)對(duì)話機(jī)器人,你需要根據(jù)用戶的問(wèn)題回答."},
{"role": "assistant", "content": ""},
{"role": "user", "content": f"""
根據(jù)用戶的命令,簡(jiǎn)潔回答問(wèn)題。
robot_state: {robot_state},
user_task: {user_task}.
output format:
你好主人:xxx(具體回復(fù)).
"""},
]
api = "gpt-3.5-turbo"
response = client.chat.completions.create(
model=api,
messages=messages,
temperature=0.0,
)
result = ’’
for choice in response.choices:
result += choice.message.content
return result
class VoiceRecorder:
def __init__(self, audio_path=’mic_output.wav’):
# 音頻參數(shù)
self.format = pyaudio.paInt16
self.channels = 1
self.audio = pyaudio.PyAudio()
self.frames = []
self.audio_path = audio_path
# Run on GPU with FP16
model_size = "large-v2"
self.model = WhisperModel(model_size, device="cuda", compute_type="float16")
self.history = []
def start_recording(self):
# 打開錄音流
self.stream = self.audio.open(format=self.format, channels=self.channels,
rate=self.rate, input=True,
frames_per_buffer=self.chunk)
self.frames = []
print("Recording...")
def stop_recording(self):
# 停止錄音
self.stream.stop_stream()
self.stream.close()
print("Finished recording.")
def play_txt(self,):
OPENAI_API_KEY = np.random.choice(chatpaper_keys_list)
client = OpenAI(api_key=OPENAI_API_KEY)
# 語(yǔ)音識(shí)別:
st = time.time()
segments, info = self.model.transcribe(self.audio_path, beam_size=5,
initial_prompt="你是一個(gè)對(duì)話機(jī)器人,你需要根據(jù)用戶的問(wèn)題,回答對(duì)應(yīng)的問(wèn)題.",)
print("Detected language ’%s’ with probability %f" % (info.language, info.language_probability))
for segment in segments:
print("[%.2fs -> %.2fs] %s" % (segment.start, segment.end, segment.text))
print("time:", time.time()-st)
st = time.time()
robot_state = ’默認(rèn)狀態(tài)’
user_task = segment.text
robot_response = get_response(client, robot_state, user_task)
print("robot_response: ", robot_response)
print("robot_response Time taken: ", time.time() - st)
# 語(yǔ)音合成
st = time.time()
output_path = ’output.mp3’
get_tts(client, robot_response, output_path=output_path)
print("get_tts Time taken: ", time.time() - st)
# 播放MP3文件
st = time.time()
play_mp3(output_path)
print("Time taken: ", time.time() - st)
def save_recording(self, file_name):
# 保存錄音到文件
wave_file = wave.open(file_name, ’wb’)
wave_file.setnchannels(self.channels)
wave_file.setsampwidth(self.audio.get_sample_size(self.format))
wave_file.setframerate(self.rate)
wave_file.writeframes(b’’.join(self.frames))
wave_file.close()
print(f"Recording saved as {file_name}")
def find_indices(self, speech_list, time_num=2):
# 初始化索引
first_index = None
last_index = None
lst = [0 if sp < 0.5 else 1 for sp in speech_list]
# 遍歷列表,找到第一個(gè)0到1的索引
for i in range(len(lst) - 1):
if lst[i] == 0 and lst[i + 1] == 1:
first_index = i + 1
break
# 如果沒有找到0到1的轉(zhuǎn)變,返回None
if first_index is None:
return None
# 從找到的第一個(gè)1開始,遍歷列表,找到符合條件的最后一個(gè)1到0的轉(zhuǎn)變
for i in range(first_index, len(lst) - 1):
if lst[i] == 1 and lst[i + 1] == 0:
# 檢查是否有至少time_num個(gè)連續(xù)的0
zero_count = 0
for j in range(i + 1, len(lst)):
if lst[j] == 0:
zero_count += 1
else:
break
if zero_count >= time_num:
last_index = i
break # 可以提前結(jié)束循環(huán),因?yàn)槲覀冎恍枰詈笠粋€(gè)符合條件的索引
# 如果沒有找到符合條件的1到0的轉(zhuǎn)變,或者只有0沒有1,返回None
if last_index is None:
return None
# 返回第一個(gè)0到1的索引和最后一個(gè)符合條件的1到0的索引
return (first_index, last_index)
def run(self):
# 開始運(yùn)行錄音程序
temp = []
self.audio_rate = 16000
self.rate = 16000
self.chunk = 1024
self.stream = self.audio.open(format=self.format, channels=self.channels,
rate=self.rate, input=True,
frames_per_buffer=self.chunk)
self.vad_model = get_vad_model()
self.vad_state = self.vad_model.get_initial_state(batch_size=1)
self.frames = []
self.speech_list = []
time_sum = 0
time_count = 0
time_th = 1.0
print("Recording...")
while True:
# 進(jìn)入無(wú)限錄音模式:
st = time.time()
# 獲取當(dāng)前幀的數(shù)據(jù),chunk=1024
data = self.stream.read(self.chunk)
audio_int16 = np.frombuffer(data, np.int16)
audio_float32 = np.array(audio_int16).astype(np.float32)/32768.0 #(num_samples,)
# 利用vad檢測(cè)是否有人聲的概率
speech_prob, self.vad_state = self.vad_model(audio_float32, self.vad_state, self.audio_rate)
temp.append(audio_float32)
# 判斷當(dāng)前幀是否有人聲
is_speech = speech_prob[0][0] > 0.5
# 把所有的數(shù)據(jù)和人聲判斷都存到列表中。
self.frames.append(data)
self.speech_list.append(is_speech)
time_sum += time.time() - st
time_count += 1
time_mean = time_sum / time_count
time_num = time_th/(time_mean+0.0001)
# 根據(jù)人聲判斷來(lái)送篩選是否有人聲,且人聲的索引。
res = self.find_indices(self.speech_list, time_num=time_num)
if res is not None:
self.frames = self.frames[res[0]:res[1]]
file_name = self.audio_path
# 將人聲對(duì)應(yīng)的數(shù)據(jù)存到本地
self.save_recording(file_name)
# 和gpt對(duì)話,并且調(diào)用tts,最后播放
self.play_txt()
self.frames = []
self.speech_list = []
# 清理工作
self.audio.terminate()
print("Press and hold space to record, release to stop and save the audio. Press Esc to exit.")
recorder = VoiceRecorder()
recorder.run()
阿里的Funasr:
中間寫了一個(gè)插曲,繼續(xù)介紹第二個(gè)語(yǔ)音識(shí)別方案:阿里的funasr。
不得不說(shuō),我當(dāng)初小瞧了它。但現(xiàn)在使用下來(lái),funasr的優(yōu)勢(shì)還是很明顯的:部署簡(jiǎn)單,模型小,推理速度快,對(duì)顯存要求低。
最最關(guān)鍵的是,它對(duì)中文更友好!除非是方言和特殊的術(shù)語(yǔ),基本上都能識(shí)別出來(lái)。
另外,它應(yīng)該還是可以和阿里自家的emotion2vec結(jié)合起來(lái),用于音頻的情感識(shí)別,對(duì)后面的聲音復(fù)刻很有用。
OK,但funasr的使用我就不多介紹了,大家直接看后面的內(nèi)容就好了。(這個(gè)帖子今天晚上要寫完,明天不能碰這塊了,所以越寫越急躁~)
聲音復(fù)刻-gpt-sovits:
音色復(fù)刻,我的了解一樣很少,印象中只是看過(guò)“AI孫燕姿”和“郭德綱說(shuō)英文相聲”,知道AI現(xiàn)在能做到比較恐怖的程度。但我看完花佬的新項(xiàng)目之后,才被深刻的震撼到,原來(lái)只需要花一兩個(gè)小時(shí)的配置,加一分鐘的錄音,就能把別人的嗓子復(fù)刻出來(lái),簡(jiǎn)直是太恐怖了。
我不知道之前的什么bert-sovits,甚至也不知道GPT-sovits的技術(shù)原理,我只是最簡(jiǎn)單的使用整合包,跑數(shù)據(jù)、微調(diào)模型+推理整活。
我之前玩的時(shí)候,官方倉(cāng)庫(kù)還有不少bug,現(xiàn)在大家閉眼沖官方倉(cāng)庫(kù)的整合包就行,
If you are a Windows user (tested with win>=10) you can install directly via the prezip. Just download the prezip, unzip it and double-click go-webui.bat to start GPT-SoVITS-WebUI.
點(diǎn)擊prezip下載就好了,文件比較大,需要點(diǎn)魔法+好的網(wǎng)速。最好是谷歌瀏覽器下載,這樣比較穩(wěn)定。
數(shù)字人倉(cāng)庫(kù):Linly-Talker
GitHub - Kedreamix/Linly-Talker
大家自己玩就好,我不介紹了,沒時(shí)間了。
現(xiàn)在最大的問(wèn)題是口型和眼神不太對(duì)。
數(shù)字人進(jìn)階方案:ER-NeRF和geneface++
參考視頻:【AI 數(shù)字人制作(方案六)-嗶哩嗶哩】 https://b23.tv/vOk1IMg
點(diǎn)評(píng):這兩個(gè)是目前比較完善的開源方案,都是需要對(duì)特定個(gè)人微調(diào),但好像存在長(zhǎng)發(fā)亂飄的問(wèn)題。
另外還有一個(gè)wav2lip的方案,值得探索。
馬上咨詢: 如果您有業(yè)務(wù)方面的問(wèn)題或者需求,歡迎您咨詢!我們帶來(lái)的不僅僅是技術(shù),還有行業(yè)經(jīng)驗(yàn)積累。
QQ: 39764417/308460098 Phone: 13 9800 1 9844 / 135 6887 9550 聯(lián)系人:石先生/雷先生