这一套绝了!AI 大模型写故事,星云 SDK 驱动 3D 数字人实时演绎

前阵子家里小朋友睡前总要听故事,我一开始用 ChatGPT 生成文字再用语音朗读,结果他听了几句就跑掉了。原因很简单:单纯文本、预录视频都不存在具身交互智能,只能单向输出,无法实现人和 AI 双向实时共情互动;而市面上很多数字人方案存在延迟高、没法打断、成本高的原因,根本不适合这种实时交互场景。

于是我干脆自己搭了一个"故事大王陪伴官":前端用 Vue 3 + Vite,AI Coding 用 Trae Work 生成骨架,大脑接 DeepSeek/通义千问成故事推理创作,魔珐星云提供完整具身交互智能;一句话理清分工:大模型让 AI 学会思考,魔珐星云赋予 AI 可感知、可互动的 3D 躯体与实时拟人表达能力。小朋友说一句"我想听小兔子的故事",大模型生成故事,3D 卡通数字人实时口型、表情、动作一起播报,还能语音输入、切换形象、在手机上用。

整段链路端到端延迟大概在 500ms 左右,核心代码只有一套,屏幕端数字人跑通之后,想下沉到人形服务机器人也能复用。

一、项目效果与整体架构

最终效果是这样的:打开网页,左侧是 3D 数字人舞台,右侧是聊天面板。小朋友可以打字,也可以按住麦克风说话;说完之后,大模型生成一段中文故事,数字人立刻开始讲,嘴巴和表情会跟着内容动,讲完还能继续追问。

我把主要能力拆成了几块:

  • 前端界面 :Vue 3 + Vite,AvatarStage.vue 负责数字人舞台,ChatPanel.vue 负责聊天输入和消息展示。
  • 语音输入useSpeechRecognition.js 封装浏览器 Web Speech API,按住说话、松开发送。
  • 大脑chat.js 调用 OpenAI 兼容接口,支持 DeepSeek、通义千问、智谱、OpenAI 等多个模型动态切换。
  • 身体useAvatarSDK.js 封装魔珐星云 XmovAvatar SDK,负责连接、播报、打断、音量控制。
  • 配置useMagicKeys.js 管理多组 AppID/AppSecret 钥匙串,aiConfig.js 管理大模型 API Key,都存在 localStorage 里。

整个调用链路很短:用户输入 -> 语音识别转文字 -> chat.js 调用大模型 -> 拿到故事文本 -> speakWithInterrupt 交给星云 SDK -> 数字人开始播报。链路中的关键耗时其实只有大模型生成时间和 SDK 响应时间,星云这边靠参数流 + 端侧渲染,能把延迟压到约 500ms。

二、用 Trae Work 搭骨架

这个项目不是我从零一行行敲出来的,而是完全用 Trae Work 从零来实现的。Trae IDE 想必大家应该都用过了,而 Trae Work 面向更广泛人群的 AI 原生工作台,提供网页 / 桌面 / 移动三端,支持 Work、Code、Design 三种模式。

我先用 Trae Work 把需求描述清楚:"一个 Vue 3 的 3D 数字人故事陪伴页面,左侧舞台右侧聊天,支持语音输入和多模型切换"。Trae Work 帮我生成了基础目录、组件结构和 composable 雏形,我再根据魔珐星云的 SDK 文档做接入和调优。

Trae Work 生成的核心文件大致如下:

  • src/components/AvatarStage.vue:3D 数字人舞台,负责未连接时的空状态和连接后的渲染容器。
  • src/components/ChatPanel.vue:聊天面板,包含消息列表、输入框、语音按钮和音量控制。
  • src/composables/useAvatarSDK.js:封装星云 SDK 的连接、播报、打断、音量等方法。
  • src/utils/chat.js:封装大模型调用逻辑。
  • src/composables/useSpeechRecognition.js:封装 Web Speech API 的语音识别。

我用 Trae Work 最多的地方是生成样板代码和边界处理。比如语音输入这个 composable,它要处理浏览器不支持、权限被拒绝、没有检测到语音、网络错误等场景,自己手写容易漏,Trae Work 能一次性给出比较完整的结构,我再根据业务微调。

javascript 复制代码
// src/composables/useSpeechRecognition.js
export function useSpeechRecognition(options = {}) {
  const SpeechRecognition =
    window.SpeechRecognition || window.webkitSpeechRecognition;

  const isSupported = ref(!!SpeechRecognition);
  const isListening = ref(false);
  const transcript = ref("");
  const error = ref(null);

  function start() {
    // 每次创建新实例,避免状态混乱
    if (recognition) {
      try { recognition.abort(); } catch (e) {}
    }
    recognition = createRecognition();
    transcript.value = "";
    recognition.start();
  }

  function stop() {
    if (recognition && isListening.value) {
      try { recognition.stop(); } catch (e) {}
    }
  }

  // ... onresult / onerror / onend 处理
}

这里有一个细节:Web Speech API 在 Chrome 下表现不错,但 Safari 和移动端兼容性参差不齐,所以我做了 isSupported 判断,不支持的时候按钮会自动隐藏,避免用户点完没反应。

三、大模型接入:不只是调接口

故事里的大脑我选了 DeepSeek 和通义千问做主力。项目里我把多个模型做成了可配置,避免以后某个模型不稳定或者价格上涨就要改代码。

模型配置写在 src/constants/aiModels.js 里,所有接口都是 OpenAI 兼容格式:

javascript 复制代码
// src/constants/aiModels.js
export const AI_PROVIDERS = [
  {
    id: "qwen",
    name: "通义千问(阿里云百炼)",
    defaultModel: "qwen-plus",
    proxyPrefix: "/proxy-api/qwen",
    target: "https://dashscope.aliyuncs.com/compatible-mode",
  },
  {
    id: "deepseek",
    name: "DeepSeek",
    defaultModel: "deepseek-chat",
    proxyPrefix: "/proxy-api/deepseek",
    target: "https://api.deepseek.com",
  },
  // ...
];

本地开发时,Vite 的 proxy 把这些前缀转发到对应的官方地址,避免前端直接暴露真实 API 域名:

javascript 复制代码
// vite.config.js
server: {
  proxy: {
    "/proxy-api/deepseek": {
      target: "https://api.deepseek.com",
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/proxy-api\/deepseek/, ""),
    },
  },
}

运行时配置优先从 localStorage 读,没有才回退到 .env

javascript 复制代码
// src/utils/aiConfig.js
export function getActiveAiConfig() {
  const saved = localStorage.getItem(STORAGE_KEY);
  if (saved) {
    const parsed = JSON.parse(saved);
    const provider = findProvider(parsed.providerId);
    if (provider && parsed.apiKey) {
      return {
        providerId: provider.id,
        apiKey: parsed.apiKey,
        model: parsed.model || provider.defaultModel,
        proxyPrefix: provider.proxyPrefix,
      };
    }
  }

  return {
    providerId: "deepseek",
    apiKey: import.meta.env.VITE_AI_API_KEY || "",
    model: import.meta.env.VITE_AI_MODEL || "deepseek-chat",
    proxyPrefix: "/proxy-api/deepseek",
  };
}

调用大模型的地方 chat.js 就很简单了,动态读取当前配置,拼一个 system prompt 让模型用儿童友好的语气讲故事:

javascript 复制代码
// src/utils/chat.js
export async function chat(prompt) {
  const config = getActiveAiConfig();

  const res = await fetch(`${config.proxyPrefix}/v1/chat/completions`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.apiKey}`,
    },
    body: JSON.stringify({
      model: config.model,
      messages: [
        {
          role: "system",
          content:
            "你是一位亲切可爱的故事大王陪伴官,专门为小朋友讲故事...",
        },
        { role: "user", content: prompt },
      ],
    }),
  });

  // ...
}

这里踩过一个坑:通义千问的某个模型返回时 content 是空的,真正的故事内容藏在 reasoning 字段里,而且 reasoning 前面往往有一段英文思考过程。我不得不写了一个 extractStoryFromReasoning 函数,按几种模式去提取中文正文,比如匹配 *(Story Body)* 标记、匹配 "Drafting the Story" 后的中文字符、或者直接找第一个连续中文段落。

javascript 复制代码
function extractStoryFromReasoning(reasoning) {
  const sectionMatch = reasoning.match(
    /(?:\*\(Greeting\)\*|\*\(Story Body\)\*)\s*([\s\S]*?)(?=\n\s*\*\(|$)/
  );
  if (sectionMatch) return sectionMatch[1].trim();

  const draftingMatch = reasoning.match(
    /(?:Drafting the Story)[\s\S]*?\n\s*([\u4e00-\u9fa5][\s\S]*?)(?=\n\s*\*\*Check|$)/
  );
  if (draftingMatch) return draftingMatch[1].trim();

  const chineseBlockMatch = reasoning.match(/[\u4e00-\u9fa5]{10,}[\s\S]*$/);
  if (chineseBlockMatch) return chineseBlockMatch[0].trim();

  return reasoning;
}

这个坑耗了我小半天,因为一开始我以为是自己 prompt 写错了,后来打开浏览器控制台看到原始响应才发现问题。

四、魔珐星云具身驱动 SDK:把故事"演"出来

大模型只能输出文字,要让数字人把故事"讲"出来,需要一层"身体"。我接入的是魔珐星云 XmovAvatar SDK,也就是魔珐星云具身驱动 SDK

魔珐星云并非单纯 3D 形象生成工具,而是AI 终端时代全域具身交互底层开放平台,核心价值是为所有大模型、Agent 落地标准化具身交互智能。

接入流程大致分三步:在魔珐星云开放平台注册应用,拿到 AppID 和 AppSecret;前端创建 XmovAvatar 实例并完成 init() 初始化,等待资源下载和网关会话确认;之后调用 speak() 方法就能让数字人开口。

小提示:如果在注册时填写邀请码 JMFASQDPXB 可以获得 1000 调用积分

下面是我在项目中的几个重点代码片段:

SDK 初始化封装在 useAvatarSDK.js 里:

javascript 复制代码
// src/composables/useAvatarSDK.js
function createSdkInstance({ appId, appSecret }) {
  sdk.value = new window.XmovAvatar({
    containerId: "#avatar-container",
    appId,
    appSecret,
    gatewayServer: "https://nebula-agent.xingyun3d.com/user/v1/ttsa/session",
    onMessage: (msg) => {
      if (msg && msg.error_code && _sessionReject) {
        _sessionReject(msg.error_reason || "认证失败");
      }
    },
    onWidgetEvent: (eventName, data) => {
      handleWidgetEvent(eventName, data);
    },
  });
}

连接时先 init 下载资源,再等待网关会话确认,最后把状态置为 connected:

javascript 复制代码
async function doConnect({ appId, appSecret }) {
  if (sdk.value) {
    console.warn("[SDK] 已有实例,先清理");
    destroySdk();
  }

  status.value = "connecting";
  createSdkInstance({ appId, appSecret });

  await sdk.value.init({
    onDownloadProgress: (p) => {
      progress.value = p;
    },
  });

  // 等待网关会话确认,5s 超时兜底
  await new Promise((resolve, reject) => {
    _sessionReject = reject;
    setTimeout(() => {
      if (_sessionReject) {
        _sessionReject = null;
        resolve();
      }
    }, 5000);
  });

  status.value = "connected";
  isConnected.value = true;
}

这里有个小踩坑:如果连接成功后不清理旧实例直接再次连接,SDK 会报错或出现两个渲染层。所以我在 doConnect 开头加了判断,有旧实例就先 destroySdk()

播报故事时,为了让用户可以随时打断,我用的是 speakWithInterrupt

javascript 复制代码
function speakWithInterrupt(text, isStart = true, isEnd = true) {
  if (!sdk.value || !isConnected.value) return;
  sdk.value.interactiveidle();
  setTimeout(() => {
    sdk.value.speak(text, isStart, isEnd);
  }, 150);
}

interactiveidle 会让数字人先进入可打断状态,然后再说新内容,即时中断,这是具身交互智能的关键能力。这样小朋友在听故事的时候突然问新问题,数字人会立刻停下来回答,而不是把旧故事念完。

为了让界面不那么单调,我还做了多形象切换:小猴子、小熊熊、小兔子等,每个形象有对应的 emoji、主题色和空状态渐变背景。配置存在 useMagicKeys.js 里,爸爸妈可以在设置面板里新增、编辑、切换不同的钥匙串。

javascript 复制代码
// src/composables/useMagicKeys.js
export function useMagicKeys() {
  const configs = ref([]);
  const activeId = ref("");

  const activeConfig = computed(() =>
    configs.value.find((c) => c.id === activeId.value)
  );

  function addConfig(config) {
    const item = { id: generateId(), ...config };
    configs.value.push(item);
    activeId.value = item.id;
    persist();
  }

  // ...
}

五、体验已部署的故事大王应用

如果你暂时不想本地跑代码,可以直接打开我已经部署好的线上版本:

aistory.uviewpro.cn/

打开后,左侧是 3D 数字人舞台,右侧是聊天面板。第一次进入时,数字人会显示"还在睡觉"的空状态,需要爸爸妈妈先帮忙完成两步初始化。

使用步骤

  1. 连接魔珐星云魔法钥匙

    点击页面上的设置入口(桌面端在右上角,移动端在卡片顶部),进入"爸爸妈妈的魔法钥匙串",填入你在魔珐星云开放平台申请到的 AppID 和 AppSecret,然后点击"召唤故事大王"。稍等几秒,等资源下载和网关会话确认完成,数字人就会从睡眠状态醒来,出现在舞台上。

  2. 设置大模型 API key

    同样在设置面板里选择你喜欢的大模型,比如 DeepSeek 或通义千问,填入对应的 API Key。这个 Key 只保存在浏览器本地 localStorage 里,不会传到我的服务器上。配置好后,数字人就有"大脑"了。

  1. 开始聊天或随机故事

    现在可以让小朋友开口了。点击输入框旁边的麦克风按钮,按住说话,松开后文字会自动填入输入框;也可以直接打字。说一句"我想听小兔子的故事",大模型生成故事,数字人立刻开始播报。如果不确定想听什么,面板里还有"故事骰子"按钮,点一下就能收获一个惊喜。

  2. 切换形象和调节音量(可选)

    设置面板里可以切换小猴子、小熊熊、小兔子等不同形象,每个形象都有自己的主题色。聊天面板顶部还能独立调节数字人音量,避免和系统音量混在一起。

建议在 Chrome 浏览器里体验,Web Speech API 的识别效果最稳定;Safari 和移动端如果语音识别按钮没出现,说明当前环境不支持,也可以直接打字互动。

儿童友好设计

既然是给小朋友用的,技术术语必须藏起来。我把所有面向孩子的文案都集中到了 src/constants/kidsText.js 里,比如 AppID 叫"魔法钥匙编号",AppSecret 叫"魔法咒语",断开连接叫"让大王休息"。

javascript 复制代码
// src/constants/kidsText.js
export const KIDS_TEXT = {
  CONFIG_TITLE: "🔑 爸爸妈妈的魔法钥匙串",
  BTN_CONNECT: "✨ 召唤故事大王",
  BTN_DISCONNECT: "💤 让大王休息",
  KEY_APP_ID_LABEL: "魔法钥匙编号",
  KEY_APP_SECRET_LABEL: "魔法咒语",
};

交互上我也尽量贴近儿童习惯,并支持自适应布局:

  • 按住说话:按钮按住开始识别,松开自动把文字填进输入框,不需要小朋友打字。
  • 自动关闭聊天面板:移动端收到 AI 回复后,聊天面板会延迟 2 秒自动收起,让小朋友把注意力放回数字人身上。
  • 消息状态保留 :切换聊天窗口时不会清空历史记录,这个是通过把 messages 状态从 ChatPanel.vue 提升到 App.vue 实现的。
  • 音量独立控制:在聊天面板顶部可以调节数字人音量,不会和系统音量混在一起。

PC端UI:

移动端UI:

六、延伸

真正跑通之后我才意识到,数字人对小朋友有没有吸引力,关键不是大模型多聪明,而是它能不能像真人一样即时回应。小朋友不会耐心等一个聊天框慢慢打字,他们要的是"面对面"的互动感。

当下 AI 行业正在完成从纯文本对话,到全域具身交互智能的转型,只靠聊天框的 AI 很难走进教育、门店、机器人等线下真实场景,魔珐星云提供标准化底层能力,让任何开发者都能低成本给大模型装上具备完整具身交互智能的 3D 躯体。

我现在只做了网页版,但这件事让我觉得,未来如果把同样的交互逻辑放进服务机器人、AR 眼镜,甚至家里的人形机器人里,可能会打开更多有意思的场景。对我个人来说,故事大王更像是一个起点:它让我看到,大模型不只是用来回答问题的工具,还可以变成一个能被看见、能互动、有情绪的小角色。

接下来我还想试试能不能让数字人识别小朋友的语音情绪,或者根据对话内容自动切换表情和动作。这些能力现在看起来已经没那么远了。

七、总结

整个项目做下来,我最大的感受是:Trae Work 负责把工程骨架快速搭起来,降低前端工程开发门槛。大模型负责生成内容,赋予 AI 思考创作能力,魔珐星云 SDK 负责把内容"演"出来,提供整套标准化具身交互智能底层能力。三者分工很明确。如果没有 Trae Work,我可能还在纠结目录结构和组件划分;如果没有星云 SDK,故事大王就只是一个文字聊天机器人,对小朋友吸引力会差很多。

最后想跟大家讨论个问题:

你会把 3D 数字人用在什么场景?教育陪伴、客服导览,还是人形机器人?

欢迎在评论区交流。

相关推荐
墨风如雪1 小时前
WorkBuddy小白教程
aigc
穷人小水滴1 小时前
(科幻) 备用肉身虫 (Backup Body Bug, BBB) 系列设定 (202607 更新)
aigc·bug·科幻
张彦峰ZYF1 小时前
大模型LLM ACA - ACP认证考试真题冲刺演练二答案参考
人工智能·lora·llm·embedding·rag·few-shot
耀耀切克闹灬1 小时前
短链跳转
前端
SoaringPigeon1 小时前
NVIDIA博客-预训练去想象,微调去行动:世界-动作模型 (WAM) 的崛起
人工智能·深度学习·机器人·自动驾驶
孟陬1 小时前
高质量全方位的 AI 工具 sse-stuntman — Mock SSE 接口
前端·node.js·openai
Cao_Ron1 小时前
给 ECharts 6 graph 节点叠加角标:从原理到性能优化
前端
甲维斯2 小时前
骚操作N:把GPT5.6接入Claude Code大发神威!
人工智能
WebGirl2 小时前
内网穿透方式
前端