概览
LyraType Tide 凭语音实时带动虚拟角色的神情与上半身动作。LyraType Moon 让角色在两句话之间持续收听,并自主决定何时开口。两者可单独使用,也可配合:Moon 决定开口时机,Tide 让神情提前一步跟上。
Tide:语音进,动作出
Tide 接在语音和形象之间。它听着对话,持续带动形象,角色在说话、在听、在等待时都是如此。
- 输入
- 语音,随产生随发送
- 输出
- 角色的神情与上半身动作
- 口型
- 交给你自己的口型同步,由同一段语音驱动
- 形象
- 沿用现有的 Live2D 模型
Tide 通过你已经在用的形象软件带动角色,所以能用面部捕捉的模型就能用 Tide。
Moon:听到的话进来,要说的话出去
Moon 与你的语音识别、语音合成并行运转。两句话之间它也一直在听,所以接话快,也能在没人发问时自己开口。
- 输入
- 角色听到的话,以文字形式随到随送
- 输出
- 要说的话
- 搭配
- 声音交给你的语音合成,面容交给 Tide
在 VTube Studio 中使用 Tide
研究预览阶段,Tide 通过 VTube Studio 公开的插件 API 驱动形象。
- 在 VTube Studio 的设置里开启插件 API(默认监听 8001 端口)。
- 启动 Tide 客户端。VTube Studio 会询问一次是否允许插件连接,选择允许。
- 随后由 Tide 带动模型。带有身体参数的模型,上半身也会一起动。
- 嘴部仍由你原来的口型同步驱动,Tide 不接管。
使用 LyraType Studio
在 lyratype.com/studio 打开 LyraType Studio,就能用我们的声音和动作驱动你自己在 VTube Studio 里的形象。请在运行 VTube Studio 的电脑上用 Chrome 或 Edge 打开,不用安装任何东西。
- 在运行 VTube Studio 的电脑上,用 Chrome 或 Edge 打开 lyratype.com/studio。
- 粘贴 LyraType 邮件里的工作室令牌,它以 lt_ 开头。
- 在 VTube Studio 里打开设置,找到 VTube Studio Plugins,开启 Start API(允许插件)。端口默认为 8001,除非你改过。
- 浏览器询问是否允许 lyratype.com 访问这台电脑上的应用时,选择允许。如果之前拒绝了,点地址栏左侧的图标打开网站设置,把“设备上的应用”(Apps on device)设为允许。
- VTube Studio 会弹出插件“LyraType Studio”的授权请求。只在你刚在 lyratype.com/studio 上点过之后,才按“允许”(Allow)。
- 等模型检查跑完。缺少某个输入时,可以在 VTube Studio 的模型设置里把它连到模型的参数上,也可以先跳过,接着往下走。
- 安装一个虚拟声卡,例如 VB-Audio Virtual Cable,将 TTS 输出设为 CABLE Input。在 OBS 中添加“音频输入采集”,选择 CABLE Output,并开启音频监听以便自己监听。
- 点“打个招呼”(Say hello),看看你的模型和 OBS。如果 OBS 中动作比声音快,请在设置中调大“动作延迟”(Motion delay)。
- 在“角色”页填写角色设定、选择声音并调整动作参数;然后在“直播”页开始对话。
获取方式
来信写几句你在做什么、用哪种形象软件,以及打算在云端还是在自己的设备上运行模型。我们会回信给你。
- 云端 API
- 使用 Tide 与 Moon 的主要方式。Tide 的实时 API 开放免费试用:来信后我们以邮件发放令牌,也请留下宝贵意见。
- 本地授权
- 模型运行在你自己的设备上,离线可用。
- 定制合作
- 针对特定角色或场景的调校与集成。
实时 API:一个角色一条连接
打开一条 WebSocket,把角色自己的声音随合成随送、把用户的声音随听到随送,之后每三分之一秒收到一块十帧的动作,每帧以序号标记时刻:第 f 帧属于会话时间 f / 30 秒。声音和动作按同一个时钟播放,显示比时钟晚十分之一秒左右,好让每块动作在第一帧到期之前已经在手。
- GET /v1/session
- WebSocket 本身。一条连接一个会话。
- POST /v1/sessions
- 可选的准入接口,供自己管理用户的后端使用:返回会话号和一张 60 秒内有效、只能用一次的票据,凭票开一条连接,无需令牌。
- GET /healthz
- 主机负载:会话数、上限、服务端每拍(三分之一秒的动作)的计算耗时。无需令牌。
- 令牌
- Authorization: Bearer <token>,或连接地址上的 ?token=(浏览器无法给 WebSocket 加请求头)。票据放在 ?ticket= 或 Sec-WebSocket-Protocol 里。
第一条消息给出角色的风格参数(preset)和一个种子,回复列出每帧的通道。风格参数在会话内固定,换一组就是另开会话。相同的声音、风格参数和种子,两次会话的动作完全相同。
{"type": "start", "preset": {"smile_rest": 0.6, "head_activity": 0.5}, "seed": 0}
{"type": "started", "session": "9f2c…", "fps": 30, "commit": 10,
"channels": ["head_yaw", "head_pitch", "head_roll", "gaze_x", "gaze_y",
"eye_open_left", "eye_open_right", "brow_left", "brow_right", "smile",
"chest_yaw", "chest_pitch", "chest_roll", "sway_x",
"mouth_press", "mouth_pucker", "smile_skew"]}- smile_rest
- 0 静止时嘴放松 … 1 静止时带笑
- brow_rest
- 0 眉低 … 1 眉扬
- head_activity
- 0 头部安静 … 1 头部活跃
- blink_rate
- 0 少眨眼 … 1 多眨眼
- gaze_activity
- 0 目光稳定 … 1 目光游移
- listen_activity
- 0 听时安静 … 1 听时多动
- listen_nods
- 0 听时少点头 … 1 听时多点头
没给出的风格参数取中间值。会话时间的零点就是 started 消息发出的那一刻,声音的时间戳和帧序号都从这里起算。
送入声音
声音以二进制消息传送:一个字节标明是谁的声音(0 角色自己,1 对方),八个字节小端 float64 给出第一个采样播放或被听到的会话时间,其后是 16 kHz 单声道小端 16 位采样。每块长短不限,通常 50 到 200 毫秒一块。
- 合成器一交出角色的声音就立刻转发,并标上它将要播放的时刻。不必为 Tide 推迟任何东西;声音知道得越早,神情跟得越准。
- 想多早播就多早播。要不要延后播放由你定:对话不必延后,视频或独白可以延后到半秒,动作会跟声音更紧。
- 对方的声音没法提前,听到多少就送多少。
- 被打断时,撤回没播出去的部分:从那个时刻起的声音作废。
- 告诉会话谁在说话:每路声音已知的说话区间。短于一秒的区间按应声处理。
- spans
- {"type":"spans","voice":"own"|"partner","spans":[[start_s,end_s],…]},该路声音迄今已知的全部区间,整体替换上一次。尚未结束的区间到当前为止。
- withdraw
- {"type":"withdraw","at":seconds},从该时刻起角色的声音没有播出,相应区间在此截断。
- ping
- {"type":"ping"},回复 {"type":"pong","t":seconds}。六十秒没有声音也没有 ping 的连接会被关闭。
- close
- {"type":"close"},结束会话,与直接关闭连接相同。
收到什么
每拍一条二进制消息:32 位无符号首帧序号,32 位无符号帧数(10),然后是帧数 × 通道数的小端 32 位浮点,逐帧排列,通道顺序按 started 所列。每块在首帧到期的那一刻生成,再经一拍的计算送到你手里:主机空闲时 40 到 70 毫秒,坐满时最多三分之一秒。
每帧十七个数,用追踪器的单位。显示端为每个模型做一次映射,用这个模型的角色都共用它。静止值一栏给出会话起始的姿态。
- head_yaw, head_pitch, head_roll
- 弧度,向左、向上为正。明显的点头约 0.1。静止约 0。
- gaze_x, gaze_y
- 眼球行程的 −1 到 1。静止 0 与 0.03。
- eye_open_left, eye_open_right
- 0 闭到 1 睁大;放松睁开 0.65;眨眼落到 0。静止 0.65。
- brow_left, brow_right
- 0 低到 1 扬,绝对值。静止 0.33 与 0.28。
- smile
- 0 到 1,绝对值:0.37 为中性的嘴,0.7 为日常的满笑。静止 0.37。
- chest_yaw, chest_pitch, chest_roll
- 上半身的弧度。静止 0。
- sway_x
- 左右倾身,−1 到 1。静止 0。
- mouth_press
- 0 到 1,双唇抿紧的程度。静止 0.1。
- mouth_pucker
- 0 到 1,噘嘴。静止 0.1。
- smile_skew
- 左嘴角减右嘴角,−1 到 1;歪笑约 0.3。静止 0。
说话时的张嘴不在通道里:由你的口型同步从声音里驱动,模型的 smile、mouth_press、mouth_pucker 和 smile_skew 叠在它上面。
import asyncio, json, struct
import numpy as np, soundfile, websockets
URL, TOKEN = "wss://<host>/v1/session", "<token>"
async def main():
own, _ = soundfile.read("own.wav", dtype="int16") # 单声道,16 kHz
headers = {"Authorization": f"Bearer {TOKEN}"}
async with websockets.connect(URL, additional_headers=headers) as ws:
await ws.send(json.dumps({"type": "start", "preset": {"smile_rest": 0.6}, "seed": 0}))
channels = json.loads(await ws.recv())["channels"]
# 角色的声音,标为 0.5 秒开始播放,先于播放交出
await ws.send(bytes([0]) + struct.pack("<d", 0.5) + own.tobytes())
end = 0.5 + len(own) / 16000
await ws.send(json.dumps({"type": "spans", "voice": "own", "spans": [[0.5, end]]}))
while True:
message = await ws.recv()
if isinstance(message, bytes):
first, count = struct.unpack_from("<II", message)
rows = np.frombuffer(message, dtype="<f4", offset=8).reshape(count, len(channels))
print(f"frames {first}-{first + count - 1} at {first / 30:.2f} s", dict(zip(channels, rows[0].round(3))))
asyncio.run(main())限额与错误
- 消息大小
- 最多 8 MB。角色的声音可以比播放提前任意多;服务只留它需要的部分。
- 未读的帧
- 积压约三十秒的帧不读取,连接会被断开(关闭码 1013)。
- 容量
- 一台主机带 16 到 20 个会话。超出之后,或主机的计算超过实时的 85 %,新会话以 capacity 拒绝;已有会话不会被拖慢。
- limit
- 令牌各有上限,随令牌一并告知:同时开着的会话数,和每天(UTC)可用的分钟数。超过任一项,新会话以 limit 拒绝(申请票据得到 HTTP 429);用完当天分钟数的会话以 limit 关闭,关闭码 1008。
- 第一次连接
- 主机只在有人连接时运行。空闲一阵之后,第一次连接要等主机启动,约 10 秒;之后的连接即时。用户一到(打开页面、选定角色)就请求 GET /healthz,等第一句声音出来时,启动已经完成。
- unauthorized
- 没有令牌、令牌错误,或票据重复使用、过期。报错后连接关闭。
- bad_request
- 消息格式不对;连接保持。start 消息不对则关闭。
- internal
- 会话在主机上失败;连接关闭,请另开一条。
- 版本
- 路径为 /v1。在列表末尾新增通道、新增消息类型或字段都不改版本;既有通道或字段的含义、顺序变了才改。从 started 读通道表,就不会受影响。
常见问题
需要摄像头或动作捕捉设备吗?
不需要。Tide 只凭语音工作。
支持哪些形象?
目前支持通过 VTube Studio 使用的 Live2D 模型。其它运行环境会随 API 一并说明。
预览模型可以用于正式产品吗?
预览版供评估使用。正式使用的条款请来信商议。