ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

低成本AI数字人实战:从语音交互到游戏控制

低成本AI数字人实战:从语音交互到游戏控制

「AIRI」是我最近抽空折腾出来的一个AI数字人项目:用大约两周的下班时间,搭了一个能听、能看、能说、还能动手玩游戏的虚拟形象。它不贵、不复杂,没有去接那些动辄大几万的商业数字人平台,核心思路就一句话——把现有开源模型和免费服务串起来,用最低成本获得一个有真实行动力的交互数字人。

这个项目最戳我的点在于"交互"这两个字。市面上大多数数字人只停留在"能聊天"阶段,一问一答就完事;AIRI不一样,它后面接了键盘鼠标控制,能截屏看游戏画面,再让大模型判断该做什么操作,真正做到"你动嘴、它动手"。如果你也是自己折腾型选手,想入门AI数字人开发,或者想给直播加个数字人陪玩人设,又或者是做无障碍辅助方向,这套方案都值得参考。我尽量把每一步拆开讲清楚,包括踩过的坑、延迟优化的参数、以及怎么量化模块间的通信开销。

1. 项目缘起:为什么非要做一个能"动手"的数字人

1.1 从"虚拟主播"到"数字人打工人"

先说个背景。我之前做过一阵虚拟主播工具,纯Live2D模型加语音驱动口型,效果不错,但总觉得缺了点什么。聊天机器人只会回话,虚拟主播只会"演",两者拼在一起也只是一个高级点儿的聊天界面。真正让我想动手做AIRI的契机,是有次看朋友玩策略游戏,同一关反复刷资源,手都酸了。我心想,既然大模型能看图文、能写代码,为什么不能让它看游戏画面、操作键盘鼠标?把数字人从"陪聊"升级成"代聊代玩",这不才是真正的"替你把事办了"吗?

于是AIRI的定位就这么定了:一个低成本、可交互、能执行实际电脑操作的AI数字人。它不只存在于聊天窗口,而是能接管一部分桌面操作,像一个坐在你电脑前的"数字人助理兼陪玩"。

1.2 谁适合抄这套方案,需要做到什么程度

说实话,AIRI这个项目对读者的技术门槛不算高,但需要一些Python基础。我建议至少会写简单的循环、调用函数、以及能看懂JSON数据结构,否则后面调模块会有点吃力。如果你是零基础,也完全可以先跑通、再研究原理,我用到了大量现成库,说白就是把"积木"拼起来。

那到底要做到什么程度才算"能用"?我给自己定的及格线是三条:

  • 端到端交互延迟在3秒以内,让人等得不烦躁;
  • 数字人说话时有真实口型联动,而不是只会眨眼发呆;
  • 游戏控制能看懂当前画面并执行动作,不靠人工预设坐标。

三条都做到,靠的全是免费或低成本的组件。后续每一条的优化,我都会给出具体方案和量化数据。

1.3 低成本选型逻辑:为什么不用商业平台

这里给个对比。商业AI数字人平台按年订阅通常几千到几万,定制形象另收费,开放API之后想接游戏控制基本不可能,因为人家做的是"客服/导购"场景,压根不给键鼠权限。AIRI方案的花费是多少?本地功能全部走开源项目,语音识别用FunASR、TTS用Edge-TTS、数字人用免费Live2D模型、对话走本地Qwen或云端API按量付费,整套成本接近于零。如果你的电脑能跑动开源大模型,几乎一分钱不花;如果显卡不行用云API,一个月十几块也够了。

这就是AIRI的核心逻辑:它不是一个产品,而是一条链路。把最合适的免费组件用胶水代码串起来,效果未必比商业平台差,关键是你能完全掌控它,想加"游戏操作"这种奇怪功能,也没人拦你。

2. 核心链路拆解:AIRI的交互架构与代玩原理

2.1 完整交互链路:听、想、说、演、动

AIRI把整个人机交互拆成5个环节,分别是听、想、说、演、动。这五个词是我自己总结的,方便理解整个系统在做什么:

环节作用使用的方案延迟预算
听语音识别,把用户说的话转成文本FunASR离线语音识别0.2~0.5秒
想大模型理解文本、决定回复/动作本地Qwen或云端API0.5~2秒
说语音合成,把回复变为音频Edge-TTS,流式播放0.3~1秒
演数字人根据音频做口型、表情Live2D + WebSocket联动实时
动执行实际电脑操作mss截屏 + 多模态模型 + pyautogui0.5~2秒

这五个环节不是串行等完再走下一个,那样延迟会爆炸。我是这样设计的:用户说话时,ASR开始识别;识别完,LLM在思考的同时,TTS先合成一段暖场短句(比如"让我看看画面");游戏控制模块则在用户下达明确指令后异步执行,不等语音播报完成。这种"流水线并行"的设计,能把体感延迟压缩到1~2秒,效果比傻等强很多。

2.2 "替你玩游戏"的实现原理与合规边界

AIRI"替你玩游戏"不是用游戏外挂,而是走了一条更通用的链路:截屏感知、模型决策、键鼠执行。也就是把屏幕当作图片输入给多模态大模型,让模型输出一个结构化操作指令,然后由脚本模拟鼠标键盘动作。这种方式的好处是,它不需要逆向来读游戏内存,任何游戏都能"看"着玩,理论上是通用的。

必须得说明白了:这套链路我用的是网页版2048和一个开源的单机小游戏做演示,目的是验证"数字人看得懂画面、做得出动作"。请不要把它用在任何有用户协议明确禁止脚本的联机或竞技游戏上,一是容易被封号,二是对别的玩家不公平。AIRI的本意是辅助学习、本地娱乐和无障碍使用,比如手部不便的人通过语音让数字人帮忙操作单机游戏,这个价值比"帮你刷资源"大得多。

2.3 数字人形象:Live2D、2D卡通还是视频合成

数字人形象我试过三种,分别说一下利弊。

  • 视频合成方案(如SadTalker、Wav2Lip):一张图片配音频生成说话视频,效果挺惊艳,但实时性太差,在我这帧率个位数,不适合做现场交互。
  • 纯HTML Canvas 2D卡通头:简单、性能好,但表情变化少,看起来像个"会张嘴的图标",适合早期验证。
  • Live2D模型 + 口型驱动:最终选定的路线。Live2D是免费SDK,社区有大量开放模型,动捕参数丰富,浏览器里跑还很流畅。

如果你的目标是直播陪玩而不是精细表演,选Live2D就够了。官方之前有公开模型,也可以去免费素材站淘。AIRI用Live2D的最大价值在于:让数字人有"活气",说话时头身会微晃、嘴型对得上、表情能切换。用户不会因为画质一般出戏,反而会因为"它真的在看屏幕、帮你点鼠标"觉得这个形象很真实。

3. AIRI实操全记录:从零把一个能交互的数字人搭起来

3.1 环境准备与目录规划

先说下我跑AIRI的机器配置:Windows 11,i5-12400,32GB内存,无独立显卡。是的,你不用惊讶,这套方案在集显或无CUDA机器上也能跑,只不过语音识别和本地大模型会慢一点。如果你的显卡有6GB以上显存,能跑得更舒服,但纯CPU也不是不行。

我建议用Python 3.10环境,装依赖的时候记得用虚拟环境,避免跟系统Python打架。下面是我的目录结构,麻雀虽小五脏俱全:

airi/ ├── asr/ # 语音识别模块 ├── llm/ # 对话大脑 ├── tts/ # 语音合成 ├── avatar/ # Live2D前端页面 ├── control/ # 游戏控制、截屏 ├── core/ # 主调度器 └── assets/ # 音频缓存、临时图片

这种按模块分目录的好处是,后面想替换任何一个组件(比如把Edge-TTS换成CosyVoice)都只动对应的包,不会把主逻辑搞得一团糟。我在一开始图省事把所有代码堆在一个main.py里,最后改一个模块就心惊胆战,那是这项目最大的坑,千万别学。

3.2 语音识别:让数字人先"听"懂你

语音识别这块,我用的是FunASR,阿里开源的语音识别模型,中文效果在开源里数一数二。最关键的是它支持流式识别,边说边出文字,不用等人把整句话说粉了再反应过来。

安装方式很简单:

pip install funasr modelscope

调用逻辑里,我最常用的是它的离线文件接口。麦克风录音用sounddevice或pyaudio,录成16kHz单声道WAV,再丢给FunASR:

from funasr import AutoModel model = AutoModel(model="paraformer-zh", model_revision="v2.0.4", vad_model="fsmn-vad", punc_model="ct-punc") def recognize(wav_path: str) -> str: result = model.generate(input=wav_path, cache={}) return result[0]["text"]

实测下来,CPU模式下10秒左右的音频识别大约需要0.3秒,这在整条链路里完全可以接受。但我踩了一个坑:默认的VAD模型会把过短的语音截断,导致用户说"开始"两个字偶尔识别不出来。解决办法是设置vad_duration参数,或者在检测到语音能量超过一定阈值后再录音,避免把空环境音也算进去。

3.3 语音合成与口型联动:让数字人"有嘴巴"

语音合成我用的是Edge-TTS,微软的免费接口,音色自然、稳定,而且不需要申请密钥。代码量很轻:

import edge_tts import asyncio async def synthesize(text: str, out_path: str): tts = edge_tts.Communicate(text, voice="zh-CN-XiaoxiaoNeural") await tts.save(out_path)

但单纯合成了音频还不够,数字人必须跟着音频张闭嘴。这里我引入了一个免费工具Rhubarb Lip Sync。它可以根据音频文件输出口型时间戳,每个时间点对应一个口型状态(A、B、C、D、E等),然后把这些状态映射到Live2D的mouthOpenY参数上:

Rhubarb.exe -f json -o mouth.json assets/tts_output.wav

生成的口型JSON是这个格式:

[ {"start": 0.0, "value": "X"}, {"start": 0.2, "value": "C"}, {"start": 0.5, "value": "A"} ]

前端拿到这些时间戳,在音频播放的同时定时切换嘴型状态。这一步是AIRI"演"的核心,能做到音画同步。如果你不管口型,D数字人就会出现"只出声音不张嘴"或者"乱张嘴"的怪象,体验瞬间跌到地板上。

3.4 对话大脑:本地LLM与云端API的选择

"想"这个环节是数字人的大脑。我测试了两条路线:

  • 云端API:比如DeepSeek、Kimi、豆包,响应速度快,几毫秒到一秒,但按量付费,并且把音频/画面传上去有些隐私顾虑。
  • 本地LLM:用Ollama跑Qwen2.5-7B或GLM-4-9B,我32GB内存跑7B的Q4量化版,生成速度大约每秒11个token。一句简短回复(30字左右)大概要3秒,稍慢但完全可控。

考虑到AIRI的核心是廉价和可控,我默认用了本地Qwen2.5-7B。关键是让大模型明白它可以调用"游戏操作"能力,所以需要在提示词里给一个结构化指令schema。我用的Prompt骨架大致是:

你是AIRI数字人助手。你可以通过返回JSON指令来操作电脑玩游戏。 当用户要求移动/点击/按键时,返回如下格式: {"intent": "game_action", "action": "click", "x": 100, "y": 200} 否则正常聊天回复,intent设为"chat"。

这段提示词决定了数字人会不会主动产生"动手"动作。一开始我没写schema,模型总把操作意图藏在聊天话术里,根本没法解析。写清楚JSON格式后,解析成功率从60%左右提升到了接近100%。这个细节比换模型拥有更值钱。

3.5 游戏控制:截图感知与键鼠执行

"动"的环节分三步:截图、看懂、执行。

截图我用mss库,它比pyautogui自带的截图方法快很多,单次截图几毫秒。为了让模型聚焦游戏区域,需要手动指定窗口坐标。我一般是先用一个工具脚本把鼠标移到目标窗口左上角和右下角,记录坐标,再写进配置文件。

from mss import mss import numpy as np def capture(region: dict) -> np.ndarray: with mss() as sct: img = sct.grab(region) return np.array(img)

"看懂"这一步,如果电脑够劲,可以本地跑一个多模态模型;如果没显卡,就调用云端多模态API。我的做法是把截图缩放至640x480、转成JPEG后Base64编码,丢给大模型:

import base64, json, requests def ask_model_for_action(frame_b64: str, user_cmd: str) -> dict: # 调用云端或本地VLM服务 response = requests.post("http://localhost:11434/api/chat", json={"model": "qwen2.5-vl-7b", "messages": [ {"role": "user", "content": f"{user_cmd}, 画面:{frame_b64}", "images": [frame_b64]} ]}).text return json.loads(response)

最后"执行"用pyautogui。这里有个重要提醒:pyautogui默认移动鼠标太快,而且没有前置失败保护。一旦坐标错误,它可能把用户正在打字的内容搅乱。所以我做了一个安全阀:执行动作前强制延迟0.3秒,并且把pyautogui.FAILSAFE设为True(鼠标甩到左上角会中止)。这个习惯帮我躲过至少三次误操作事故。

import pyautogui pyautogui.FAILSAFE = True def execute_action(act: dict): if act["action"] == "move": pyautogui.moveTo(act["x"], act["y"], duration=0.15) elif act["action"] == "click": pyautogui.click() elif act["action"] == "press": pyautogui.press(act["key"])

3.6 主控编排:串起所有模块的调度器

到现在,听、想、说、演、动五个模块都齐了,最后需要一个"胶水"把它们粘起来。AIRI的主循环大概长这样:

while True: audio = record_until_silence() # 录到用户说完话 text = recognize(audio) # ASR if wake_word in text: # 唤醒词过滤 text = text.replace(wake_word, "") response = llm_chat(text) # LLM if response.intent == "game_action": screen = capture(game_region) # 截屏 act = ask_model_for_action(screen, response) execute_action(act) # 操作键鼠 audio_out = synthesize(response.reply) play_and_drive_avatar(audio_out) # TTS + Live2D口型

主循环的坑在于阻塞。录音、识别、合成都是耗时的,如果写成同步代码,用户说一句话要等半天才回应。所以我用threading把TTS播放和游戏操作放到独立线程,保证数字人在"动手"的同时还能继续跟用户闲聊,这种多线程并行设计把AIRI的交互体验拉升了一个档次。

4. 延迟、调参与问题排查实录

4.1 端到端延迟的量化与优化

AIRI做交互,最关键的是体感延迟。我把一次完整交互拆成时间线实测:

环节耗时
录音+端点检测0.8秒(含用户停顿)
FunASR识别0.3秒
LLM生成2.5~3秒
Edge-TTS合成0.5秒
播放+口型0.1秒

这样算下来,从用户说完话到数字人开始回话,大约要3.5秒,勉强可用,但不算好。我做了三个优化,把体感压缩到了约1.5秒:

  • 流式播放:Edge-TTS支持边合成边播放,不必等整段音频生成完;
  • 首字先行:LLM生成的回复先播前半句,同时后台把后半句合成完,听感上几乎无缝;
  • 动作提前:当识别出指令是"游戏操作"时,先并发截屏,再等LLM完整回复,省掉一轮截图等待。

这三个优化里效果最立竿见影的是"首字先行"。实测时用户几乎感受不到后台排队,因为它往往是刚说完最后一个字,数字人已经开始"嗯嗯"表示回应了。这就是让用户觉得"它听懂了"的关键,哪怕大模型还在瞎琢磨,它嘴上先给你反馈。

4.2 语音唤醒与误识别问题

AIRI的唤醒词是"嗨IRI",但最初一直不太灵敏,原因有三:

  • 麦克风收音质量差,房间回声大。我用的是笔记本内置麦,建议换成带降噪的USB麦,效果立竿见影;
  • 唤醒词与TTS音量不平衡。数字人在说话时,唤醒词识别会被自己的声音干扰。我加了麦克风静音逻辑:数字人说话期间暂停ASR,等播报完再恢复;
  • 误唤醒严重。后来我加了一级置信度过滤,只接受ASR得分高于0.75的唤醒结果,并且连续两次命中才触发操作。

误识别最大的教训是"不要信任一次结果"。像我喊"开始"这种只有两个字的指令,ASR很容易漏。现在我的做法是:如果文本长度小于2个字,就让数字人反问一句"你刚才说什么?";如果用户连说三遍还没识别出来,就自动进入文本输入兜底。这个兜底机制让我在演示时避免了翻车。

4.3 模块间通信与"信息量"怎么量化

AIRI里有大量的模块间通信,比如Python后端到Live2D前端走WebSocket,Python到大模型走HTTP,偶尔还有进程间管道。调试时我最头疼的是"我刚才到底发了多少数据、收到多少数据",也就是信息量。

这里先说一个通用的方法:在任何TCP或WebSocket客户端里,统计收发字节数是基本功。我在Python侧写了一个很简单的包装器:

total_sent = 0 total_recv = 0 def send_packet(sock, data: bytes): global total_sent sock.sendall(data) total_sent += len(data) def recv_packet(sock, bufsize=4096): global total_recv data = sock.recv(bufsize) total_recv += len(data) return data

这种做法虽然粗糙,但能立刻看出是不是有数据堆积:如果发送了100MB但对方只收到10MB,说明缓冲区在积压,就该检查是否忘记刷新或接收端处理太慢。

这让我想起之前在工控项目里被问过的一个高频问题:LabVIEW上位机与NI实时机之间做TCP交互,信息量怎么查。方法跟上面完全同源,常见有三种:

  • 代码埋点统计:在LabVIEW的TCP读写下分别累加读取/写入字节数,可以用移位寄存器保存累计值,这是最可靠的。
  • 属性节点监控:LabVIEW的TCP节点提供了"Bytes to Read"等属性,可以在每次读取前检查缓冲区待读字节数,有疑似粘包时很方便。
  • 第三方抓包:用Wireshark抓回环或指定端口的流量,按照TCP流统计包长与总字节数,适合做协议分析但配置略重。

AIRI调游戏控制时,我确实靠"统计收发字节数"定位了一个WebSocket粘包问题:前端收到了两个口型状态合并成一个JSON,导致嘴型跳变。解决办法是每次发送口型事件时锁定长度,或者改用JSON Lines格式、每个事件一行并以换行分隔。这个经验同样适用于LabVIEW上位机场景:如果TCP交互的信息量不对劲,先确认报文边界和接收缓冲状态,再去看带宽。

4.4 Live2D口型"卡脸"的三大原因

口型同步做完之后,有时数字人会出现"卡脸"——嘴巴张一下停在某个角度,过一会才复位。排查后我知道了三个主要原因:

  • 帧率不足:Live2D在浏览器里渲染占用GPU,老核显会掉帧。解决方法是把画布分辨率降到720p,关闭不必要的粒子特效;
  • 口型状态映射不连续:Rhubarb输出的口型等级(A/B/C/D/E)如果映射到mouthOpenY时没有做平滑,就会生硬跳变。我在前端加了一段线性插值,每次状态切换用50ms过渡;
  • WebSocket消息乱序:后端同时发来表情和口型两条消息,前端处理顺序如果错乱,嘴型就停顿。我统一改成单通道消息队列,按时间戳处理,不再执着于零延迟,反而更稳定。

如果你做的是视频合成类数字人,口型"卡脸"大概率不是这三个原因,而是模型推理帧率太慢。那种情况建议降分辨率或者改成"静止姿态+只动嘴部区域"的局部重绘方案,能大幅提升流畅度。

4.5 其他几个容易翻车的细节

AIRI在实际演示中翻过的车,我收拾成一张速查表,每一条都是真金白银换来的:

现象原因解决方法
数字人不说话但嘴在动TTS播放延迟,口型先启动了先加载音频再启动口型播放,二者同一时钟
游戏点击坐标偏了缩放截图后没还原坐标记录截屏缩放比例,复原时乘回来
大模型一直返回聊天,不执行操作Prompt里意图描述不明确明确给JSON格式样例,并强调仅当有可用操作时才返回动作
CPU占用过高FunASR模型常驻内存改为按需加载模型,或者用更小的small版本
pyautogui点击没反应目标窗口未激活在执行动作前用pyautogui.click(window.x, window.y)先激活目标窗口

这几个问题都有个共同点:不是技术太复杂,而是细节没对齐。我真切体会到,AI项目的难点往往不是算法,而是把多个独立组件粘合时产生的各种"纬度不一致"问题。

5. 个人实操体会与后续可以继续扩展的方向

5.1 做完AIRI,我对这套方案的性价比总结

从动手做到最终跑通,我一共花了9个傍晚,除去吃饭摸鱼的时间,大约20个小时。费用上,所有的软件组件基本都是免费的,硬性支出是那杯为了熬夜买的美式。

我的体会是:AIRI本质不在"数字人"本身,而在"交互"这个词被真正扩展了。传统数字人产品把交互定义为"用户说一句,它回一句";AIRI把交互定义为"用户说意图,数字人观察环境、做出决策、再执行动作"。后者才更像是人与人之间的协作方式。商业平台不做这件事是因为成本高、场景杂,而自己拼装完全是另一回事——正因为它是拼的,所以每个环节都能替换、能优化、能理解和把控,这一点是商业产品永远给不了你的掌控感。

5.2 我接下来想继续玩的方向

AIRI目前的版本还算粗糙,我后面想接一个语音克隆模型(比如GPT-SoVITS),给数字人换一个更有辨识度的声音,这大概需要重新调整TTS模块。另外一个想法是给数字人增加表情管理:当它识别到用户心情不好时,自动切换安慰模式,这个在Llama基础上加一层情绪分类就能实现。

如果你准备自己复刻一个AIRI,我最后的建议是从小处做起:先跑通语音聊天,再接入口型,最后才碰游戏控制。每一步都验证完再接下一步,别像我一样一上来就想着"全都要",那样一旦出错你会分不清是哪个模块在捣乱。

我在实际使用中最惊喜的时刻,不是它成功操作游戏的瞬间,而是数字人一边跟我聊天一边"抽空"去点鼠标,当时的体验真像有个虽然笨拙但认真的"桌宠"坐在电脑前替你干活。AIRI让我相信,"数字人"不是做出来的,而是拼出来的:如果你也想做一个能说话的"桌宠"、能陪玩的虚拟主播,或者干脆是想验证大模型到底能不能"动手",这套低成本方案都值得你花一个周末来抄作业。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进