基于python的live2d模型AI对话交互Demo
以下是修改后的位于utils下的lipsync.py文件,通过加入pydub和wavfile库进行了拓展,当然pydub库是借用了ffmpeg,wavfile主要是解决audition转换后的wav文件不能被读取的问题。我个人环境的python版本是3.10,conda构建,在这个基础上,安装好了py-live2d针对py310的轮子文件,大家也可以参照我的情况安装。以下是参考作者提供的示例代码进
·
首先对live2d-py的作者表示感谢,是他让live2d模型得以通过python语言加载。
以下是live2d-py仓库:
我个人环境的python版本是3.10,conda构建,在这个基础上,安装好了py-live2d针对py310的轮子文件,大家也可以参照我的情况安装。
以下是演示视频:
【ChatBatV2对话Demo】python中运行live2d模型与口型同步
大模型用的是DEEPSEEK,语音生成模块是fish audio。
由于py-live2d库原生只支持wav文件进行口型同步,所以我对源码进行了修改。
以下是修改后的位于utils下的lipsync.py文件,通过加入pydub和wavfile库进行了拓展,当然pydub库是借用了ffmpeg,wavfile主要是解决audition转换后的wav文件不能被读取的问题
import wave
import wavfile
import numpy as np
import time
from pydub import AudioSegment
class WavHandler:
def __init__(self):
# 每个通道的采样帧数
self.numFrames: int = 0
# 采样率,帧/秒
self.sampleRate: int = 0
self.sampleWidth: int = 0
# 通道数
self.numChannels: int = 0
# 数据
self.pcmData: np.ndarray = None
# 已经读取的帧数
self.lastOffset: int = 0
# 当前rms值
self.currentRms: float = 0
# 开始读取的时间
self.startTime: float = -1
def Start(self, filePath: str) -> None:
self.ReleasePcmData()
if filePath.endswith('.wav'):
try:
with wave.open(filePath, "r") as wav:
self.numFrames = wav.getnframes()
self.sampleRate = wav.getframerate()
self.sampleWidth = wav.getsampwidth()
self.numChannels = wav.getnchannels()
byte_data = wav.readframes(self.numFrames)
self.pcmData = np.frombuffer(byte_data, dtype=np.int16)
print(self.pcmData.shape)
except Exception as e:
print(f'wav音频读取错误:{e}, 启用备用方案')
self.ReleasePcmData()
try:
with wavfile.open(filePath, "r") as wav_file:
self.numFrames = wav_file.num_frames
self.sampleRate = wav_file.sample_rate
self.sampleWidth = wav_file.bits_per_sample
self.numChannels = wav_file.num_channels
byte_data, _, _ = wavfile.read(filePath, fmt='float')
self.pcmData = np.array(byte_data)
print(self.pcmData.shape)
except Exception as e:
print(f'wav音频读取错误:{e}')
self.ReleasePcmData()
else:
mp3_file = AudioSegment.from_mp3(filePath)
self.numFrames = int(mp3_file.frame_count(ms=len(mp3_file)))
self.sampleRate = mp3_file.frame_rate
self.sampleWidth = mp3_file.sample_width
self.numChannels = mp3_file.channels
byte_data = mp3_file.raw_data
self.pcmData = np.frombuffer(byte_data, dtype=np.int16)
print(self.pcmData.shape)
# 标准化
self.pcmData = self.pcmData / np.max(np.abs(self.pcmData))
# 拆分通道
self.pcmData = self.pcmData.reshape(-1, self.numChannels).T
# print(self.pcmData)
self.startTime = time.time()
self.lastOffset = 0
def ReleasePcmData(self):
if self.pcmData is not None:
del self.pcmData
self.pcmData = None
def GetRms(self) -> float:
"""
获取当前音频响度
"""
return self.currentRms
def Update(self) -> bool:
"""
更新位置
"""
# 数据未加载或者数据已经读取完毕
if self.pcmData is None or self.lastOffset >= self.numFrames:
return False
currentTime = time.time() - self.startTime
currentOffset = int(currentTime * self.sampleRate)
# 时间太短
if currentOffset == self.lastOffset:
return True
currentOffset = min(currentOffset, self.numFrames)
dataFragment = self.pcmData[:, self.lastOffset:currentOffset].astype(np.float32)
self.currentRms = np.sqrt(np.mean(np.square(dataFragment)))
self.lastOffset = currentOffset
return True
整个项目的源码并不考虑公开。
以下是参考作者提供的示例代码进行修改后的代码
# -*- coding: utf-8 -*-
# -*- file: live2d_wrapper.py -*-
import math
import pygame
from pygame.locals import *
from OpenGL.GL import *
import live2d.v3 as live2d
from live2d.utils.lipsync import WavHandler
from live2d.utils import log
from live2d.v3 import StandardParams
from live2d.v3 import MotionPriority
class Live2DModel:
"""封装 Live2D 模型的加载、渲染和交互逻辑。"""
def __init__(self, model_path: str, canvas_width: int = 1200, canvas_height: int = 800):
"""
初始化 Live2D 模型。
Args:
model_path: Live2D 模型文件的路径(.model3.json)。
canvas_width: 画布宽度。
canvas_height: 画布高度。
"""
self.model = live2d.LAppModel()
self.model.LoadModelJson(model_path)
self.model.Resize(canvas_width, canvas_height)
self.model.SetAutoBlinkEnable(True)
self.model.SetAutoBreathEnable(True)
self.model.StartRandomMotion()
self.canvas_width = canvas_width
self.canvas_height = canvas_height
self.dx = 0.0
self.dy = 0.0
self.scale = 1.0
self.wav_handler = WavHandler()
self.lip_sync_n = 3
self.audio_played = False
def start_callback(self, group, no):
audio_path = 'haru_normal_01.wav'
log.Info("start motion: [%s_%d]" % (group, no))
pygame.mixer.music.load(audio_path)
pygame.mixer.music.play()
log.Info("start lipSync")
self.wav_handler.Start(audio_path)
@staticmethod
def on_finish_motion_callback():
log.Info("motion finished")
def update(self, delta_time: float):
"""更新模型状态(如旋转、位置、缩放等)。"""
progress = delta_time * math.pi * 10 / 1000 * 0.5
deg = math.sin(progress) * 5 # 最大旋转角度为5度
self.model.Rotate(deg)
self.model.Update()
def update_setoff(self):
self.set_offset(self.dx, self.dy)
self.set_scale(self.scale)
live2d.clearBuffer()
def draw(self):
"""绘制模型到当前 OpenGL 上下文。"""
self.model.Draw()
def handle_event(self, event):
"""处理用户输入事件(鼠标、键盘)。"""
if event.type == pygame.MOUSEBUTTONDOWN:
x, y = pygame.mouse.get_pos()
self.model.SetRandomExpression()
self.model.StartRandomMotion(priority=3, onFinishMotionHandler=self.on_finish_motion_callback)
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
self.dx -= 0.1
elif event.key == pygame.K_RIGHT:
self.dx += 0.1
elif event.key == pygame.K_UP:
self.dy += 0.1
elif event.key == pygame.K_DOWN:
self.dy -= 0.1
elif event.key == pygame.K_i:
self.scale += 0.1
elif event.key == pygame.K_u:
self.scale -= 0.1
elif event.key == pygame.K_r:
self.model.StopAllMotions()
self.model.ResetPose()
elif event.key == pygame.K_e:
self.model.ResetExpression()
elif event.type == pygame.MOUSEMOTION:
self.model.Drag(*pygame.mouse.get_pos())
def set_offset(self, dx: float, dy: float):
"""设置模型偏移量。"""
self.dx = dx
self.dy = dy
self.model.SetOffset(dx, dy)
def set_scale(self, scale: float):
"""设置模型缩放比例。"""
self.scale = scale
self.model.SetScale(scale)
class Live2DRenderer:
"""封装 OpenGL 和 Pygame 的渲染逻辑。"""
@staticmethod
def render_texture(surface: pygame.Surface) -> int:
"""将 Pygame Surface 转换为 OpenGL 纹理并返回纹理 ID。"""
texture_data = pygame.image.tobytes(surface, "RGBA", True)
texture_id = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, texture_id)
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGBA,
surface.get_width(),
surface.get_height(),
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
texture_data
)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
print(f'=========={texture_id}==============')
return texture_id
@staticmethod
def draw_texture(texture_id: object, *args: tuple[tuple[3], tuple[3], tuple[3], tuple[3]]) -> None:
"""绘制 OpenGL 纹理。"""
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, texture_id)
glBegin(GL_QUADS)
glTexCoord2f(0, 0) # 左上角纹理坐标
glVertex3f(*args[3]) # 对应左下角顶点
glTexCoord2f(1, 0) # 右上角纹理坐标
glVertex3f(*args[2]) # 对应右下角顶点
glTexCoord2f(1, 1) # 右下角纹理坐标
glVertex3f(*args[1]) # 对应右上角顶点
glTexCoord2f(0, 1) # 左下角纹理坐标
glVertex3f(*args[0]) # 对应左上角顶点
glEnd()
@staticmethod
def draw_black_box():
glDisable(GL_TEXTURE_2D) # 禁用纹理
glColor3f(0, 0, 0) # 设置黑色
glLineWidth(2) # 边框宽度
# 定义方框的标准化坐标(OpenGL 坐标系范围为 [-1, 1])
x_min = -0.5
x_max = 0.5
y_min = -0.5
y_max = 0.5
# 绘制方框
glBegin(GL_LINE_LOOP)
glVertex2f(x_min, y_max) # 左上角
glVertex2f(x_max, y_max) # 右上角
glVertex2f(x_max, y_min) # 右下角
glVertex2f(x_min, y_min) # 左下角
glEnd()
glEnable(GL_TEXTURE_2D) # 恢复纹理
# 动作开始播放前调用该函数
def onStartCallback(group: str, no: int):
print(f"touched and motion [{group}_{no}] is started")
# 动作播放结束后会调用该函数
def onFinishCallback():
print("motion finished")
def main():
"""主函数:初始化并运行 Live2D 演示。"""
# 初始化 Pygame 和 OpenGL
pygame.init()
live2d.init()
live2d.setLogEnable(True)
screen = pygame.display.set_mode((1200, 800), DOUBLEBUF | OPENGL, vsync=1)
if live2d.LIVE2D_VERSION == 3:
print('Live2D V3 模式')
live2d.glInit()
# 加载背景和模型
img_alpha = pygame.image.load("山_川沿いA.png").convert_alpha()
background = Live2DRenderer.render_texture(img_alpha)
background_vertices = (
(-1.0, 1.0, 0), # 左上
(1.0, 1.0, 0), # 右上
(1.0, -1.0, 0), # 右下
(-1.0, -1.0, 0) # 左下
)
model = Live2DModel(r"Resources\BRAIN\BRAIN.model3.json")
clock = pygame.time.Clock()
running = True
while running:
delta_time = clock.tick(60) / 1000.0 # 转换为秒
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.MOUSEWHEEL:
model.model.StartMotion("Flick", 0, MotionPriority.FORCE, onStartCallback, onFinishCallback)
model.handle_event(event)
if not model.wav_handler.Update():
# 更新模型状态
model.update(delta_time)
if model.wav_handler.Update():
# 利用 wav 响度更新 嘴部张合
model.model.SetParameterValue(
StandardParams.ParamMouthOpenY, model.wav_handler.GetRms() * model.lip_sync_n
)
if not model.audio_played:
# 播放一个不存在的动作
model.model.StartMotion(
"",
0,
live2d.MotionPriority.FORCE,
model.start_callback,
model.on_finish_motion_callback,
)
model.audio_played = True
model.update_setoff()
# 清除缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# 渲染
Live2DRenderer.draw_texture(background, *background_vertices)
# Live2DRenderer.draw_black_box()
model.draw()
pygame.display.flip()
# 清理资源
live2d.dispose()
pygame.quit()
if __name__ == "__main__":
main()
更多推荐
所有评论(0)