Files
Live2D-Virtual-Girlfriend/tts.py
T
2025-08-06 18:51:24 +08:00

216 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import live2d.v3 as live2d
import sounddevice as sd
import numpy as np
import requests
import hashlib
import time
import re
import threading
from init import Global
from queue import Queue
import string
import random
class LipSyncHandler:
def __init__(self, model, sample_rate=32000):
self.model = model
self.sample_rate = sample_rate
self.smoothing_factor = 0.3
self.last_mouth_value = 0.0
self.silence_threshold = 0 # 静音阈值
self.max_rms_scale = Global.character["max_rms_scale"]
self.mouth_scale = 1.0 # 整体嘴巴开合缩放
def update_mouth_sync(self, audio_chunk):
"""更新口型同步"""
rms = np.sqrt(np.mean(audio_chunk.astype(np.float32) ** 2))
if rms < self.silence_threshold:
mouth_value = 0.0
else:
normalized_rms = (rms - self.silence_threshold) / self.max_rms_scale
mouth_value = min(normalized_rms ** 2.0, 1.0) * self.mouth_scale
smoothed_value = (self.smoothing_factor * mouth_value +
(1 - self.smoothing_factor) * self.last_mouth_value)
self.model.SetParameterValue("ParamMouthOpenY", smoothed_value)
self.last_mouth_value = smoothed_value
class AudioQueue:
def __init__(self, model):
self.q = {}
self.t = None
self.model = model
threading.Thread(target=self.process).start()
def process(self):
while True:
while not self.q:
time.sleep(0.01)
idx = list(self.q.keys())[0]
lip_sync = LipSyncHandler(self.model)
stream = sd.OutputStream(samplerate=32000, channels=1, dtype=np.int16)
stream.start()
while self.q:
audio_chunk, subtitle = self.q[idx].get()
if audio_chunk is None:
del self.q[idx]
break
if not Global.exp_queue.empty():
Global.expression_animator.add(Global.exp_params[Global.exp_queue.get()], 2)
if subtitle:
Global.func_queue2.add(Global.animator1.animate_subtitle, subtitle)
stream.write(audio_chunk)
live2d.clearBuffer()
self.model.Update()
lip_sync.update_mouth_sync(audio_chunk)
self.model.Draw()
stream.stop()
stream.close()
self.model.SetParameterValue("ParamMouthOpenY", 0.0)
self.model.Draw()
def add(self, idx, audio_chunk, subtitle):
if idx not in self.q:
self.q[idx] = Queue()
self.q[idx].put((audio_chunk, subtitle))
return None
def generate_auth_str(params):
sorted_params = sorted(list(params.items()) + [('apikey', Global.niutrans_api_key)], key=lambda x: x[0])
param_str = '&'.join([f'{key}={value}' for key, value in sorted_params])
md5 = hashlib.md5()
md5.update(param_str.encode('utf-8'))
auth_str = md5.hexdigest()
return auth_str
def translate(from_l, to_l, src_t, pre_text):
if Global.niutrans_app_id and Global.niutrans_api_key:
data = {
'from': from_l,
'to': to_l,
'appId': Global.niutrans_app_id,
'timestamp': int(time.time()),
'srcText': src_t
}
auth_str = generate_auth_str(data)
data['authStr'] = auth_str
response = requests.post("https://api.niutrans.com/v2/text/translate", data=data)
try:
pre_text[0] = response.json()['tgtText']
except:
print(from_l, to_l, [src_t], response.json())
else:
l = {'zh':'ZH_CN', 'en':'EN', 'ja':'JA'}
from_l = l[from_l]
to_l = l[to_l]
response = requests.get(f'https://api.pearktrue.cn/api/translate?text={src_t}&type={from_l}2{to_l}')
pre_text[0] = response.json()['data']['translate']
def text_process(text, text_lang, prompt_lang):
text = re.sub(r'\(.*?\)', '', text)
text = re.sub(r'(.*?)', '', text)
text = re.sub(r'{.*?}', '', text)
text = text.lstrip(string.punctuation + '……。,;:!?""''()【】')
text = text.replace('\n', '')
pre_text = ['']
if text_lang != prompt_lang and text and not text.isspace():
threading.Thread(target=translate, args=(prompt_lang, text_lang, text, pre_text)).start()
text_lang = prompt_lang
return text, text_lang, pre_text
def gptsovits_audio(pre_text, text, text_lang, ref_audio_path, prompt_text, prompt_lang, speed_factor):
flag = True
t = time.time()
idx = random.randint(1, 10000)
url = Global.tts_api2
data = {
"text": text,
"text_lang": text_lang,
"ref_audio_path": ref_audio_path,
"aux_ref_audio_paths": [],
"prompt_text": prompt_text,
"prompt_lang": prompt_lang,
"top_k": 5,
"top_p": 1,
"temperature": 1,
"text_split_method": "cut5",
"batch_size": 1,
"batch_threshold": 0.75,
"split_bucket": True,
"speed_factor": speed_factor,
"streaming_mode": True,
"seed": -1,
"parallel_infer": False,
"repetition_penalty": 1.35,
"sample_steps": 32,
"super_sampling": False,
}
response = requests.post(url, json=data, stream=True)
if response.status_code != 200:
raise Exception(f"API请求失败: {response.status_code}")
pre_i = 0
subtitle_text = ''
audio_length = 0
subtitle_speed = Global.character["subtitle_speed"]
buffer_size = 4096
fade_in_samples = 1600
samplerate = 32000
current_sample = 0
for chunk in response.iter_content(chunk_size=buffer_size):
if not chunk:
continue
audio_chunk = np.frombuffer(chunk, dtype=np.int16).copy()
# 淡入效果
chunk_length = len(audio_chunk)
for i in range(chunk_length):
if current_sample < fade_in_samples:
fade_factor = current_sample / fade_in_samples
audio_chunk[i] = int(audio_chunk[i] * fade_factor)
else:
break
current_sample += 1
audio_length += chunk_length / samplerate
if pre_text[0]:
i = int((int(audio_length / subtitle_speed) + 1) / len(text) * len(pre_text[0])) - 1
i = max(i, 0)
else:
i = 0
subtitle = None
add_text = pre_text[0][pre_i:i]
if add_text:
subtitle = (subtitle_text, add_text)
subtitle_text += add_text
pre_i = i
if flag:
print(f"\nTTS(耗时: {time.time()-t:.2f}s) {pre_text[0]}")
if Global.sign1:
Global.sign1 = False
Global.appearance_animator.reset(-1)
flag = False
t = time.time()
Global.audio_queue.add(idx, audio_chunk, subtitle)
Global.audio_queue.add(idx, None, None)