从单向展示到实时交互:我用「魔珐星云」做了一个 OC 桌宠 Demo
- 写在最前面
-
- 什么是魔珐星云「具身驱动」?
- [1. 在这个项目中你最终会得到什么?](#1. 在这个项目中你最终会得到什么?)
- [2. 它不是播放器,而是 AI 屏幕的实时表达层](#2. 它不是播放器,而是 AI 屏幕的实时表达层)
- [3. 环境准备](#3. 环境准备)
-
- [3.1 安装工具](#3.1 安装工具)
- [3.2 创建项目目录结构](#3.2 创建项目目录结构)
- [4. 第一步:拿到 App ID / App Secret(魔珐星云后台)](#4. 第一步:拿到 App ID / App Secret(魔珐星云后台))
- [5. 第二步 装依赖 & 启动脚本(package.json)](#5. 第二步 装依赖 & 启动脚本(package.json))
- [6. 第三步:主进程 main.js(给 AI 表达层一个可驻留的终端容器)](#6. 第三步:主进程 main.js(给 AI 表达层一个可驻留的终端容器))
- [7. 第四步:界面 index.html(构建 AI 屏幕的交互载体)](#7. 第四步:界面 index.html(构建 AI 屏幕的交互载体))
- [8. 第五步:配置 config.js(角色设定 + 动作映射 + LLM )](#8. 第五步:配置 config.js(角色设定 + 动作映射 + LLM ))
- [9. 第六步:染进程 renderer.js(把文本输出变成"可见的表达")](#9. 第六步:染进程 renderer.js(把文本输出变成“可见的表达”))
-
- [9.1 初始化 SDK:XmovAvatar 怎么连上去?](#9.1 初始化 SDK:XmovAvatar 怎么连上去?)
- [9.2 让数字人做动作:SSML 的正确姿势](#9.2 让数字人做动作:SSML 的正确姿势)
- [9.3 为什么"聊天不会弹黑色窗"?](#9.3 为什么“聊天不会弹黑色窗”?)
- [9.4 窗口拖拽移动怎么做的?](#9.4 窗口拖拽移动怎么做的?)
- [9.5 面板不重叠的秘密:互斥 + 主进程改宽度](#9.5 面板不重叠的秘密:互斥 + 主进程改宽度)
- [10. 第七步:查询 KA 动作池(把动作名填进 config.js)](#10. 第七步:查询 KA 动作池(把动作名填进 config.js))
-
- [10.1 鉴权怎么计算?(X-TOKEN)](#10.1 鉴权怎么计算?(X-TOKEN))
- [10.2 写 tools/query_ka.js](#10.2 写 tools/query_ka.js)
- [11. 常见坑位排雷](#11. 常见坑位排雷)
-
- [11.1 SDK 错误码 10003 是什么?](#11.1 SDK 错误码 10003 是什么?)
- [11.2 为什么我用 IP 地址打开会报错?](#11.2 为什么我用 IP 地址打开会报错?)
- [11.3 如何避免积分消耗过快?](#11.3 如何避免积分消耗过快?)
- [11.4 我接大模型对话需要自己提供 Key 吗?](#11.4 我接大模型对话需要自己提供 Key 吗?)
- [一键复盘:从 0 到 1 的正确开发顺序](#一键复盘:从 0 到 1 的正确开发顺序)
- 结尾体验

🌌你好!这里是 晓雨的笔记本 在所有感兴趣的领域扩展知识,感谢你的陪伴与支持~ 👋 欢迎添加文末好友,不定期掉落福利资讯
写在最前面
版权声明:本文为青山雨原创,遵循 CC 4.0 BY-SA 协议。转载请注明出处。
数字人这件事,其实已经不再是"高配电脑才能玩"的重资产项目了。现在只要一台普通笔记本,8GB 内存即可流畅运行,无需高端显卡,也能把数字人稳定跑起来,做成一个常驻桌面的 OC 桌宠。
但真正让我觉得这件事有意思的,不只是"跑起来了",而是很多人以为自己做的是数字人,最后做出来的其实只是单向展示、无法实时互动的传统方案:形象能出现,内容能播,但很难真正承接 AI 的实时表达。真正难的,从来不是把角色摆到屏幕上,而是把 AI 输出继续往下走,变成语音、口型、表情、动作,以及可持续的互动反馈。
魔珐星云「具身驱动」 的价值,就在于它把这条原本很重、很碎的链路压缩得足够轻量:准备好容器、填好应用信息、完成初始化,然后就可以用相对简单的调用,让角色开口说话、带动作表现、实时响应互动。对开发者来说,这不仅意味着"低配电脑也能跑",也意味着数字人的开发门槛确实被进一步拉低了。传统数字人采用云端集中渲染方案,核心流程是由云端 GPU 完成画面生成,再将结果下发至终端呈现。这种方式强依赖网络、延迟高、无法实时打断,仅适用于固定讲解、宣传片等低交互场景,本质仍是单向展示工具,无法支撑连续自然的实时交互。
对于想快速做原型、验证玩法,或者把角色能力接进自己产品里的人来说,这种降低门槛不只是省设备、省成本,更重要的是省掉了大量底层链路的折腾时间。你可以先把它当成一个 OC 桌宠来做,但往大看,它已经是成熟的轻量化 AI 屏幕终端:让 AI 不只会回答,还能被真正地"看见"和"感知到"。
什么是魔珐星云「具身驱动」?
它是一套让 "AI 有形象、会说话、能做动作" 的数字人驱动能力,核心作用是把文本输出实时转化为数字人的语音、口型、表情和动作,并在页面里实时呈现。
六大核心能力
- 高质量:生成逼真 3D 形象,实时输出自然生动的声音、表情与动作,赋予人物真实可信的表达力。
- 低延时:端到端响应约 500ms,交互实时流畅;支持随时打断,贴近真人对话体验。
- 低成本:仅需百元级芯片即可运行,大幅降低部署门槛,支持大规模普及。
- 高并发:支持千万级设备同时驱动,轻松应对批量化接入,保障体验稳定可靠。
- 多风格:覆盖超写实、二次元、卡通、美型等多样角色风格和人设,场景与角色可灵活选择。
- 多终端:全面适配手机、车机、Pad、PC、电视与大屏,兼容 Android、iOS、鸿蒙等主流系统。
| 维度 | 传统数字人 | 魔珐星云具身驱动 |
|---|---|---|
| 表达方式 | 预制内容播放、单向展示 | 文本实时驱动语音、表情、动作 |
| 工程位置 | 播放器 / 展示层 | AI 屏幕表达层 / 具身底座 |
| 交互能力 | 弱交互、难接实时链路 | 可接 LLM、可接事件、可扩展工具 |
| 业务价值 | 看起来像"人" | 真正进入服务终端场景 |
核心技术优势
依托文生 3D 多模态动作大模型 与AI 端渲和解算技术,魔珐星云打破了 3D 数字人生成领域的 "高质量、低成本、低延迟" 不可能三角,让高水准的数字人体验不再是昂贵且延迟的奢侈品。这一突破真正支持 AI 具身智能在各行业的大规模落地应用。
开发与场景适配
在开发侧,它更像 "拿来即用" 的角色表现层:只需准备容器、初始化 SDK、建立会话,就能让角色开口或做动作。还支持用 KA(技能动作)的语义指令触发预设动作表演,快速实现更丰富的互动。
适合打造桌宠、虚拟助手、导览员、客服形象、互动 NPC 等需要 "角色表现" 的场景。
官方链接: https://xingyun3d.com?utm_campaign=daren&utm_source=xiaoyu
- **邀请码:**JH2A7JY55F
- 通过邀请码注册的用户可以获得1000积分进行体验测试
AI辅助开发(使用Kimi Code)
- 直接投喂 :将
Xmov_Skill.md文件直接拖入 AI 对话框。 - 指令跟随:拖入文件后,直接发送后续的构建指令(如"根据Xmov_Skill.md,完成一个段子制造机数字人。所有内容都在一个页面内,不要水平滚动。页面风格为极简风")。
Kimi辅助Code
下面让我们开始开发一个属于自己的OC桌宠吧~
1. 在这个项目中你最终会得到什么?
运行后,你会看到一个:
- 透明背景、永远置顶、无边框的桌宠窗口
- 左侧可以打开 聊天面板 / 待办工具面板
- 点击人物会触发:语音 + KA 动作
- 支持 "长时间不理自动说话" + "投喂/清洁/好感数值" + "待办列表"
桌宠展示
2. 它不是播放器,而是 AI 屏幕的实时表达层
魔珐星云具身驱动(JS SDK)核心就是把 AI 输出从"文字"升级为"3D 多模态 ":你给它文本,它会实时生成 语音、表情、动作,并驱动 3D 数字人渲染出来。
最常用的就两件事:
- 初始化 XmovAvatar(绑定容器、appId/appSecret、gatewayServer)
- 调用
speak(ssml, is_start, is_end)让数字人说话,或用 SSML 触发 KA 动作
3. 环境准备
3.1 安装工具
- Node.js:建议 18+
- npm
- Windows
- CPU
3.2 创建项目目录结构
plain
xingyun-oc-pet/
├─ package.json
├─ main.js # Electron 主进程:透明置顶窗口、IPC
├─ index.html # 页面:数字人容器 + UI
├─ renderer.js # 渲染进程:SDK 初始化、交互、状态
├─ config.js # OC 设定、动作映射、配置
└─ tools/query_ka.js# 查询 KA 动作池(可选)
4. 第一步:拿到 App ID / App Secret(魔珐星云后台)
需要去魔珐星云平台创建"驱动应用",选择角色、音色、表演风格,然后拿到:
- App ID
- App Secret
官方文档明确写了创建入口与初始化参数位置。
⚠️ 建议:不要把密钥硬编码进仓库(尤其公开仓库),下面教程将使用占位符。
5. 第二步 装依赖 & 启动脚本(package.json)
在 package.json 里保留两条脚本:一个启动 Electron,一个查询 KA。
plain
{
"scripts": {
"start": "electron .",
"query-ka": "node tools/query_ka.js"
},
"dependencies": {
"axios": "^1.6.0",
"crypto-js": "^4.2.0",
"electron": "^28.0.0"
}
}
安装依赖:
plain
npm i
启动桌宠:
plain
npm start
6. 第三步:主进程 main.js(给 AI 表达层一个可驻留的终端容器)
main.js 的职责只有一个:创建一个透明置顶窗口,并提供 IPC:
- renderer 要拖拽移动窗口:
pet:getBounds/pet:setPos - renderer 打开面板时,主进程让窗口向左变宽:
pet:setLayout - 退出:
pet:quit
下面是 main.js代码:
plain
const { app, BrowserWindow, screen, ipcMain, globalShortcut } = require("electron");
const http = require("http");
const fs = require("fs");
const path = require("path");
app.commandLine.appendSwitch("ignore-gpu-blocklist");
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
app.commandLine.appendSwitch("disable-features", "OutOfBlinkCors");
let server = null;
let win = null;
// ---- 窗口布局参数(可按需微调)----
const BASE_W = 320; // 桌宠基础宽度(人物+底栏)
const BASE_H = 480;
const PANEL_W = 300; // 侧栏宽度(聊天/工具)
const MARGIN = 20;
let clickThrough = false; // 默认不穿透(因为要操作输入框/菜单)
function startStaticServer(rootDir, port = 0) {
return new Promise((resolve) => {
server = http.createServer((req, res) => {
try {
let urlPath = decodeURIComponent((req.url || "/").split("?")[0]);
if (urlPath === "/") urlPath = "/index.html";
const filePath = path.join(rootDir, urlPath);
if (!filePath.startsWith(rootDir)) {
res.writeHead(403);
return res.end("Forbidden");
}
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
return res.end("Not found");
}
const ext = path.extname(filePath).toLowerCase();
const mime = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css" }[ext] || "application/octet-stream";
res.writeHead(200, { "Content-Type": mime });
res.end(data);
});
} catch (e) {
res.writeHead(500);
res.end("Error");
}
});
server.listen(port, "127.0.0.1", () => resolve(server.address().port));
});
}
function applyClickThrough(enabled) {
clickThrough = !!enabled;
if (!win) return;
win.setIgnoreMouseEvents(clickThrough);
win.webContents.send("pet:clickThroughChanged", clickThrough);
}
// 保持右下角锚点不变:窗口变宽时"向左长"
function setWindowLayout({ toolsOpen = false, chatOpen = false }) {
if (!win) return;
const b = win.getBounds();
const right = b.x + b.width;
const bottom = b.y + b.height;
const newW = BASE_W + (toolsOpen ? PANEL_W : 0) + (chatOpen ? PANEL_W : 0);
const newH = BASE_H;
win.setBounds(
{ x: Math.round(right - newW), y: Math.round(bottom - newH), width: newW, height: newH },
false
);
}
async function createWindow() {
const primaryDisplay = screen.getPrimaryDisplay();
const { width, height } = primaryDisplay.workAreaSize;
const rootDir = __dirname;
const port = await startStaticServer(rootDir);
win = new BrowserWindow({
width: BASE_W,
height: BASE_H,
x: width - BASE_W - MARGIN,
y: height - BASE_H - MARGIN,
transparent: true,
frame: false,
resizable: false,
alwaysOnTop: true,
skipTaskbar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
webSecurity: false,
backgroundThrottling: false,
},
});
await win.loadURL(`http://127.0.0.1:${port}/index.html`);
applyClickThrough(false);
// 可选:隐藏热键切穿透
globalShortcut.register("Control+Alt+P", () => {
applyClickThrough(!clickThrough);
});
}
// -------- IPC --------
ipcMain.handle("pet:getBounds", () => {
if (!win) return { x: 0, y: 0, width: 0, height: 0 };
return win.getBounds();
});
ipcMain.on("pet:setPos", (_e, pos) => {
if (!win || !pos) return;
win.setPosition(Math.round(pos.x), Math.round(pos.y), false);
});
ipcMain.on("pet:quit", () => app.quit());
ipcMain.on("pet:setClickThrough", (_e, enabled) => applyClickThrough(!!enabled));
ipcMain.on("pet:setLayout", (_e, layout) => setWindowLayout(layout || {}));
// ------------------------------------------------------------
app.whenReady().then(createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});
app.on("will-quit", () => {
try { globalShortcut.unregisterAll(); } catch {}
try { server?.close?.(); } catch {}
});
7. 第四步:界面 index.html(构建 AI 屏幕的交互载体)

index.html 负责将页面布局分为两个部分,分别是"右侧桌宠区固定 + 左侧面板区扩展",具体如下:
#base固定在右侧,宽度--base-w#panels在左侧,打开聊天/工具时显示.side- 数字人的容器是
#sdk - SDK 用官方脚本引入:
xmovAvatar@latest.js
plain
<div id="sdk"></div>
<script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar@latest.js"></script>
<script src="./renderer.js"></script>
✅ 小提醒:官方 SDK 文档提过,某些能力只支持 localhost 或 https,直接用 IP/其他域名可能报错。 在 main.js 里可以启动本地静态服务 127.0.0.1,为了更加稳定。
8. 第五步:配置 config.js(角色设定 + 动作映射 + LLM )

config.js 里分为 4 个模块:
- 具身驱动密钥 :
appId/appSecret(从平台获取) - 人设:名字、性格、口头禅、背景、NG 话题
- 动作映射 actions :填写对应的
action_semantic( - LLM(可选) :如果你要接 DeepSeek / OpenAI 兼容接口,就填
endpoint/apiKey/model
⚠️ 重要:星云 SDK 不会自动给你"大模型 key"。官方 demo 也是让你自己填"大模型 key"。 所以:LLM 这块完全是"你自己选供应商 + 你自己出 key"。
下面是我这边设计的一套提示词,大家可以根据自己的OC设定,设计提示词:
plain
const OC_CONFIG = {
// 具身驱动密钥(从星云后台拿)
appId: "YOUR_APP_ID",
appSecret: "YOUR_APP_SECRET",
// 角色设定
name: "莱缇娅",
personality: "内心敏感细腻、轻微社牛",
catchphrases: [
"数据不会说谎,但我会哦~",
"哎哎哎等一下!这个 bug 我好像见过!",
"别担心,有本小姐在,再烂的代码也能给你救回来!"
],
backstory: "(略)",
ngTopics: [
"禁止触碰她的兔子挂饰",
"禁止在她面前提"数据清零"",
"禁止未经允许进入她工作室的"黑箱区"",
"禁止嘲笑她的双丸子头"
],
ui: {
avatarScale: 0.50,
randomIntervalSec: 14,
idleSpeakAfterSec: 60
},
// 动作映射(用 query_ka.js 查到真实 action_semantic 再填)
actions: {
idle: ["Standing_Idle_01", "Look_Around"],
feed: "Happy_Jump",
clean: "Shy_Hide",
greet: "Wave_Hello",
click: ["Wave_Hello", "Happy_Jump", "Look_Around"]
},
randomLines: [
"数据流有点乱...让我捋一捋...",
"秩序区的全息投影又抽风了?别慌,我来!"
],
tools: {
weather: { location: "Los Angeles" }
},
// 接 deepseek 兼容接口
llm: {
enabled: false,
endpoint: "https://api.deepseek.com/chat/completions",
apiKey: "YOUR_LLM_KEY",
model: "deepseek-chat"
}
};
9. 第六步:染进程 renderer.js(把文本输出变成"可见的表达")
9.1 初始化 SDK:XmovAvatar 怎么连上去?
SDK 文档的标准初始化长这样:传 containerId/appId/appSecret/gatewayServer,并且可以接各种回调(widget、message、stateChange)。
在 initAvatar() 里需要做 3 个关键优化:
- 先检查 WebGL:没有 WebGL 就不渲染(否则会黑屏/报错)
- proxyWidget 吞掉 subtitle_on/off:让 SDK 不要弹默认字幕窗,只保留语音+动作
onStateChange("idle")作为"登场时机":一进入 idle 就播一句语音,然后启动"长时间不理自动说话"
官方文档也说明了:speak(ssml, is_start, is_end) 支持直接文本或 SSML。
9.2 让数字人做动作:SSML 的正确姿势
官方文档给了 "技能 KA 指令" 的 SSML 结构:在 <speak> 里塞 <ue4event><type>ka</type>...<action_semantic>...</action_semantic>。
在 buildSSML() 里就是按这个结构拼的:
- 纯动作:
buildSSML("", "Happy_Jump") - 动作 + 说话:
buildSSML("谢谢投喂~", "Happy_Jump")
9.3 为什么"聊天不会弹黑色窗"?
因为把 SDK 的默认字幕事件代理掉了:
proxyWidget.subtitle_on/off = () => {}onWidgetEvent再兜底,把包含 subtitle 的事件直接 return
这跟官方文档里 "proxyWidget 可以重写默认行为" 的说法一致。
9.4 窗口拖拽移动怎么做的?
用的是一个很实用的思路:
- renderer 监听
mousedown/mousemove/mouseup mousedown时用ipcRenderer.invoke("pet:getBounds")拿到窗口当前坐标mousemove时计算新坐标,用ipcRenderer.send("pet:setPos")移动窗口
并且加了"排除区域":
- 点到底部栏、工具面板、聊天面板、信息面板 → 不触发拖拽(否则输入框根本点不了)
9.5 面板不重叠的秘密:互斥 + 主进程改宽度
在"待办事件"和"聊天界面"中, toggleTools() / toggleChat() 做了互斥:开一个就关另一个。
然后 applyBodyFlags() 会通知主进程:
plain
ipcRenderer.send("pet:setLayout", { toolsOpen, chatOpen });
主进程根据开关计算新宽度,并保持右下角锚点不动。
10. 第七步:查询 KA 动作池(把动作名填进 config.js)
现在的 actions 里像 Wave_Hello / Happy_Jump 这些,需要以 真实的 action_semantic 为准。
官方给了 KA 查询接口:
- host:
https://nebula-agent.xingyun3d.com - path:
GET /user/v1/external/lite_ka_summary - 需要三段鉴权头:
X-APP-ID / X-TIMESTAMP / X-TOKEN
10.1 鉴权怎么计算?(X-TOKEN)
文档说 X-TOKEN 的算法是:
plain
md5(lower_api_path + lower_method + sort_json_str + secret + timestamp)
其中:
api_path不包含 host,要小写method小写data用"key 排序后"的 JSON 字符串(GET 查询时一般是{})timestamp是秒级时间戳(60 秒有效)
10.2 写 tools/query_ka.js
创建 tools/query_ka.js:
plain
const axios = require("axios");
const CryptoJS = require("crypto-js");
const HOST = "https://nebula-agent.xingyun3d.com";
const API_PATH = "/user/v1/external/lite_ka_summary"; // 不带 host
const { appId, appSecret } = require("../config.js"); // 如果你 config.js 没有 module.exports,就改成手填
function stableJsonStringify(obj) {
if (!obj || typeof obj !== "object") return "{}";
const keys = Object.keys(obj).sort();
const sorted = {};
for (const k of keys) sorted[k] = obj[k];
return JSON.stringify(sorted).replace(/\s+/g, "");
}
function md5Hex(str) {
return CryptoJS.MD5(str).toString();
}
function buildHeaders({ ak, secret, method, apiPath, data }) {
const t = Math.floor(Date.now() / 1000);
const lowerPath = apiPath.toLowerCase();
const lowerMethod = method.toLowerCase();
const sortJsonStr = stableJsonStringify(data || {});
const signSrc = `${lowerPath}${lowerMethod}${sortJsonStr}${secret}${t}`;
const token = md5Hex(signSrc);
return {
"X-APP-ID": ak,
"X-TIMESTAMP": String(t),
"X-TOKEN": token
};
}
async function main() {
if (!appId || !appSecret || appId.includes("YOUR_")) {
console.error("请先在 config.js 里填 appId/appSecret(或改成环境变量读取)");
process.exit(1);
}
const method = "GET";
const data = {};
const headers = buildHeaders({
ak: appId,
secret: appSecret,
method,
apiPath: API_PATH,
data
});
const url = `${HOST}${API_PATH}`;
const resp = await axios.request({
url,
method,
headers,
data
});
console.log("KA summary:");
console.log(JSON.stringify(resp.data, null, 2));
}
main().catch((e) => {
console.error("query-ka failed:", e?.response?.data || e.message);
process.exit(1);
});
上面逻辑就是把文档里的鉴权步骤用 Node 写了一遍。
运行:
plain
npm run query-ka
拿到结果后,去 config.js -> actions 把需要的动作语义填进去即可。
11. 常见坑位排雷
11.1 SDK 错误码 10003 是什么?
文档错误码里写了 START_SESSION_ERROR = 10003,含义是会话启动(start_session)异常。
通常排查顺序:
appId/appSecret是否填对(最常见)- 网络能否访问
nebula-agent.xingyun3d.com - 是否用
localhost/127.0.0.1(文档说某些能力不支持 IP/非本机域名) - 容器是否存在(10001 容器不存在)
11.2 为什么我用 IP 地址打开会报错?
文档直接写了:SDK 中某些方法仅支持 localhost 或 https。 用 Electron 内置本地服务器跑 127.0.0.1,这个问题基本就解决了。
11.3 如何避免积分消耗过快?
文档 FAQ 提到:调试时用基础音色、长时间无互动可切离线模式等。
当前的桌宠做法也比较"省"------没有持续字幕渲染、人物缩放也比较小。
11.4 我接大模型对话需要自己提供 Key 吗?
需要。官方 Demo 的接入说明里也明确了:大模型与 ASR 的连接参数需要自行配置。
一键复盘:从 0 到 1 的正确开发顺序
GitHub传送门:https://github.com/qingshanyu9/XingyunOCPet
- 创建项目目录 +
package.json(先能npm start) - 写
main.js:先让透明置顶窗口跑起来 - 写
index.html:先把#sdk容器和 UI 布局摆好 - 引入 SDK 脚本 + 写
renderer.js的initAvatar()(先让人出现) - 用
speak()跑通语音(再加 SSML 动作) - 再做:拖拽、菜单、面板互斥、聊天、待办
- 最后补:
query_ka.js查动作池,完善actions映射
结尾体验
实际做下来,最直观的体验就是两点:硬件要求低 、开发节奏快 。 角色的接入非常简单:页面放一个容器、填好 App ID / App Secret,初始化实例后直接调用 <font style="background-color:rgb(255,245,235);">speak(...)</font> 就能驱动数字人开口说话;想让角色配合动作,用 SSML 加上 KA 指令即可。
在这种低配(无独显、纯 CPU) 的笔记本上也能跑起来,桌宠这种形态从"窗口悬浮、拖拽移动"到"能说会动",核心代码量不大,就几百行,调通链路也不绕。再加上 OC 的设定也很好做:把名字、性格、口头禅、随机台词,以及不同交互对应的动作语义集中写在一个配置文件里,基本不用改主逻辑,就能快速换一个完全不同的角色气质,甚至可以做多套设定一键切换。
这件事最让我有感触的一点是:数字人门槛确实在降低。以前大家一提到数字人,第一反应往往是"贵""重""要高配设备""要很复杂的渲染链路",但真正上手之后会发现,至少在这种桌面原型和轻量交互场景里,它已经没有想象中那么遥远了。
更进一步说,这次做出来的虽然是一个 OC 桌宠,但它本质上不只是"桌面上多了一个会说话的角色"。同样的初始化方式、语音驱动、动作触发和交互逻辑,稍微改一下形态,就可以继续往虚拟助手、导览员、客服形象、互动 NPC 甚至各类 AI 服务终端上延展。也就是说,它表面上是一个小 Demo,底层验证的其实是另一件事:很多原本只能展示内容的屏幕,正在被升级成一个可交互、可表达、可持续在线的 AI 入口。
所以最终成品看起来像是"把一个能互动的数字人,塞进桌面角落里随时待命",但如果把视角再放大一点,它验证的并不只是"桌宠能不能做",而是今天的数字人是不是正在从一个高门槛的展示物,逐渐变成一个更轻、更快、更容易落地的交互组件。
hello,这里是 晓雨的笔记本。如果你喜欢我的文章,欢迎三连给我鼓励和支持:👍点赞 📁 关注 💬评论,我会给大家带来更多有用有趣的文章。
原文链接 👉 ,⚡️更新更及时。
欢迎大家点开下面名片,添加好友交流。