pymouth 是基于Python的Live2D口型同步库. 你可以用音频文件, 甚至是AI模型输出的ndarray,
就能轻松的让你的Live2D形象开口.
效果演示视频.
Demo video
-
以Python API的形式提供能力,用作和其他项目的集成,把宝贵的计算资源留给皮套的大脑,而不是给音频捕获软件和虚拟声卡。
-
采用动态时间规划算法(DTW)匹配音频中的元音,并以元音置信度(softmax)的方式输出,而不是使用AI模型,即使是移动端CPU也绰绰有余。
-
VTubeStudio对
pymouth来说只是可选项,只是一个Adapter,你可以使用Low Level API和你想要皮套引擎结合,只使用音频播放和音频分析能力。 -
1.3.0版本之后API已固定,请以本文档为准。
- Python>=3.10
- VTubeStudio>=1.28.0 (可选)
pip install pymouth-
你需要确定自己Live2D口型同步的支持参数.
请注意:下面提供一种简单的判断方式,但这种方式会修改(重置)Live2D模型口型部分参数,使用前请备份好自己的模型。
如果你对自己的模型了如指掌,可以跳过这步。

-
下面是两种基于不同方式的Demo.
你可以找一个音频文件替换some.wav.
samplerate:音频数据的采样率.
output_device:输出设备Index. 这里很重要,如果不告诉插件播放设备是哪个,那么插件不会正常工作。 可以参考audio_devices_utils.py-
基于分贝的口型同步import time from pymouth import VTSAdapter, DBAnalyser def main(): with VTSAdapter(DBAnalyser()) as a: a.action(audio='some.wav', samplerate=44100, output_device=2) time.sleep(100000) # do something if __name__ == "__main__": main()
-
基于元音的口型同步import time from pymouth import VTSAdapter, VowelAnalyser def main(): with VTSAdapter(VowelAnalyser()) as a: a.action(audio='some.wav', samplerate=44100, output_device=2) time.sleep(100000) # do something if __name__ == "__main__": main()
第一次运行程序时,
VTubeStudio会弹出插件授权界面, 通过授权后, 插件会在runtime路径下生成pymouth_vts_token.txt文件, 之后运行不会重复授权, 除非token文件丢失或在VTubeStudio移除授权.
-
下面是一个比较完整的使用pymouth作为AI TTS消费者的例子。
import queue
import threading
import time
from fish_speech import tts
from pymouth import VTSAdapter, DBAnalyser, VTSPluginInfo
class SpeakMsg:
def __init__(self, msg: str, required: bool):
self.msg = msg
self.required = required
self.create_timestamp = time.time()
self.create_datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.create_timestamp))
class Speaker:
def __init__(self):
self.queue = queue.Queue(1)
def start(self):
plugin_info = VTSPluginInfo(plugin_name='kanojyo2',
developer='organics',
authentication_token_path='./pymouth_vts_token.txt',
plugin_icon=None)
with VTSAdapter(DBAnalyser(temperature=10), plugin_info=plugin_info) as a:
while True:
msg: SpeakMsg = self.queue.get()
t0 = time.time()
audio, rate = tts.tts_ndarray(msg.msg)
print(f'speak time:{time.time() - t0:.02f}')
a.action(audio=audio, samplerate=rate, output_device=2)
def speak(self, msg: str, required=True):
if required:
self.queue.put(SpeakMsg(msg, required))
else:
try:
self.queue.put_nowait(SpeakMsg(msg, required))
except queue.Full:
print("Queue Full")
if __name__ == "__main__":
speakers = Speaker()
# 这里的实现只作为参考而不是建议。对于AI等CPU密集型场景,使用线程而不是协程可能会更好。
threading.Thread(target=speakers.start).start()关键的代码只有两行:
with VTSAdapter(DBAnalyser(temperature=10)) as a:
a.action(audio='some.wav', samplerate=44100, output_device=2) # no-block
# a.action_block(audio='aiueo.wav', samplerate=44100, output_device=2) # blocktemperature=10温度(softmax)
,有别于LLM中对下一个token的概率分布,这里的温度指的是:音频的每个窗帧FFT,与元音相似度的概率分布。值越大,概率越平均,口型会变得平滑。反之亦然。默认为10,不可
<=0,可以随意调整这个值观察同步效果,并确定理想值。
a.action()非阻塞,会立即返回,由程序内部维护线程池和队列。
a.action_block()阻塞,直到音频播放和处理完毕才会返回,纯同步代码无线程,线程由调用者维护。
VTSAdapter以下是详细的参数说明:
| param | required | default | describe |
|---|---|---|---|
analyser |
Y | 分析仪,必须是 Analyser 的子类,目前支持DBAnalyser和VowelAnalyser |
|
db_vts_mouth_param |
'MouthOpen' |
仅作用于DBAnalyser, VTS中控制mouth_input的参数, 如果不是默认值请自行修改. |
|
vowel_vts_mouth_param |
dict[str,str] |
仅作用于VowelAnalyser, VTS中控制mouth_input的参数, 如果不是默认值请自行修改. |
|
ws_uri |
str |
websocket uri 默认:ws://localhost:8001 | |
plugin_info |
VTSPluginInfo |
插件信息,可以自定义 |
a.action() 会开始处理音频数据. 以下是详细的参数说明:
| param | required | default | describe |
|---|---|---|---|
audio |
Y | 音频数据, 可以是文件path, 可以是SoundFile对象, 也可以是ndarray | |
samplerate |
Y | 采样率, 这取决与音频数据的采样率, 如果你无法获取到音频数据的采样率, 可以尝试输出设备的采样率. | |
output_device |
Y | 输出设备Index, 这取决与硬件或虚拟设备. 可用 audio_devices_utils.py 打印当前系统音频设备信息. | |
finished_callback |
None |
音频处理完成会回调这个方法. | |
auto_play |
True |
是否自动播放音频,默认为True,会播放音频(自动将audio写入指定output_device) |
Get Started 演示了一种High Level API 如果你不使用 VTubeStudio 或者想更加灵活的使用, 可以尝试Low Level API. 下面是一个Demo.
import time
from pymouth import DBAnalyser
def callback(y: float, data):
# Y is the Y coordinate of the model's mouth.
# Like is 0.4212883452
print(y) # do something
with DBAnalyser() as a:
a.action_noblock('zh.wav', 44100, output_device=2, callback=callback) # no block
# a.action_block() # block
print("end")
time.sleep(1000000)import time
from pymouth import VowelAnalyser
def callback(md: dict[str, float], data):
"""
md like is:
{
'VoiceSilence': 0,
'VoiceA': 0.6547555255,
'VoiceI': 0.2872873444,
'VoiceU': 0.1034789232,
'VoiceE': 0.3927834533,
'VoiceO': 0.1927834548,
}
"""
print(md) # do something
with VowelAnalyser() as a:
a.action_noblock('zh.wav', 44100, output_device=2, callback=callback) # no block
# a.action_block() # block
print("end")
time.sleep(1000000)- Test case


