魔珐星云打造上海历史大屏数字人
今天我们使用魔珐星云豆包大模型搭建的数字人的项目【上海历史文化大屏】。这个项目主要通过数字人去讲解上海历史文化通过调用大模型实时智能的互动实现现代化技术和历史文化同屏。先看看魔珐星云的具身驱动介绍。引用于官网~魔珐星云具身驱动将 AI 的表达从“文本”升级为“ 3D 多模态”。 它可基于文本输入实时生成语音、表情与动作驱动 3D 数字人或人形机器人实现如真人般自然的表达。 相比传统仅能输出文字或语音的 AI 星云赋予 AI 更丰富的表现力与更自然的交互体验。环境准备这里关于环境只简单的说一下。魔珐星云账号访问魔珐星云官网 https://www.xingyun3d.com进入控制台 → 创建应用魔珐星云支持很多形象配置可以根据自己的需要配置。点击创建好的应用查看密钥记录App ID、App Secret后面配置应用需要用到。搭建数字人我们直接使用魔珐星云具身驱动SDKJS版本进行搭建创建一个网页端的大屏。这里只提供一些核心的操作步骤的搭建思路详细源码可以我的开源项目。引入SDK在index.html的head中引入魔珐星云的SDKscript srchttps://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatarlatest.js/script定义数字人的位置注意idavatar-active这个div这块区域就是数字人展示的位置js会根据这个#avatar-active去加载数字人。div classsdk-container div idsdk !-- 等待初始化时的占位符 -- div classplaceholder idplaceholder div classplaceholder-icon breathing️/div p正在准备城市讲解员.../p psmall请稍候即将为您呈现上海的魅力/small/p /div !-- 数字人激活时的展示区域 -- div classavatar-active idavatar-active !-- SDK将在这里渲染数字人 -- /div /div /div魔珐星云SDK配置这里的配置分为两个部分第一部分可以直接在config.js中配置我们的key和secret第二部分如果我们启动项目时没有配置key和secret则在页面使用时提示我们通过页面进行配置配置后会保存在本地的localStorage中下次启动可以直接使用。function getConfigFromStorage(key, defaultValue) { try { const stored localStorage.getItem(key); return stored ! null stored ! ? stored : defaultValue; } catch (e) { return defaultValue; } } // 默认配置 const defaultXingyunConfig { appId: , appSecret: , gatewayServer: https://nebula-agent.xingyun3d.com/user/v1/ttsa/session }; const defaultDoubaoConfig { apiKey: , apiUrl: https://ark.cn-beijing.volces.com/api/v3/chat/completions, model: }; // 从localStorage加载配置优先使用localStorage中的配置 const xingyunConfig { appId: getConfigFromStorage(xingyun_appId, defaultXingyunConfig.appId), appSecret: getConfigFromStorage(xingyun_appSecret, defaultXingyunConfig.appSecret), gatewayServer: getConfigFromStorage(xingyun_gatewayServer, defaultXingyunConfig.gatewayServer) }; const doubaoConfig { apiKey: getConfigFromStorage(doubao_apiKey, defaultDoubaoConfig.apiKey), apiUrl: getConfigFromStorage(doubao_apiUrl, defaultDoubaoConfig.apiUrl), model: getConfigFromStorage(doubao_model, defaultDoubaoConfig.model) }; // 保存配置到localStorage function saveConfigToStorage(config) { try { if (config.xingyun) { localStorage.setItem(xingyun_appId, config.xingyun.appId || ); localStorage.setItem(xingyun_appSecret, config.xingyun.appSecret || ); localStorage.setItem(xingyun_gatewayServer, config.xingyun.gatewayServer || defaultXingyunConfig.gatewayServer); // 更新当前配置 xingyunConfig.appId config.xingyun.appId || ; xingyunConfig.appSecret config.xingyun.appSecret || ; xingyunConfig.gatewayServer config.xingyun.gatewayServer || defaultXingyunConfig.gatewayServer; } if (config.doubao) { localStorage.setItem(doubao_apiKey, config.doubao.apiKey || ); localStorage.setItem(doubao_apiUrl, config.doubao.apiUrl || defaultDoubaoConfig.apiUrl); localStorage.setItem(doubao_model, config.doubao.model || ); // 更新当前配置 doubaoConfig.apiKey config.doubao.apiKey || ; doubaoConfig.apiUrl config.doubao.apiUrl || defaultDoubaoConfig.apiUrl; doubaoConfig.model config.doubao.model || ; } return true; } catch (e) { console.error(保存配置失败:, e); return false; } } // 检查配置是否完整 function isConfigComplete() { return xingyunConfig.appId xingyunConfig.appSecret doubaoConfig.apiKey doubaoConfig.model; }初始化SDK这里会先检查我们是否配置了对应的appId和appSecret如果配置了则会进入到初始化的流程如果没有配置则会跳转到配置页面。// 初始化SDK async function initSDK() { if (isInitialized) return; // 检查魔珐星云配置 if (!xingyunConfig.appId || !xingyunConfig.appSecret) { const shouldConfig confirm(魔珐星云配置不完整请先配置相关信息\n\n是否打开配置页面); if (shouldConfig) { openConfigModal(); } return; } showLoading(true); updateStatus(avatar-status, 初始化中...); updateLoadingProgress(0); try { // 使用config.js中的配置 const config xingyunConfig; // 创建SDK实例 liteSDK new XmovAvatar({ containerId: #avatar-active, appId: config.appId, appSecret: config.appSecret, gatewayServer: config.gatewayServer, // 事件回调 onWidgetEvent(data) { }, onNetworkInfo(networkInfo) { }, onMessage(message) { console.log(onMessage-----------------, message); }, onStateChange(state) { if (state connected) { onSDKConnected(); } else if (state disconnected) { onSDKDisconnected(); } }, onStatusChange(status) { }, onStateRenderChange(state, duration) { }, onVoiceStateChange(status) { handleVoiceStateChange(status); }, enableLogger: false }); // 调用官方init方法 await liteSDK.init({ onDownloadProgress: (progress) { updateLoadingProgress(progress); } }); isInitialized true; updateStatus(avatar-status, 就绪); hideLoading(); document.getElementById(placeholder).classList.add(hidden); document.getElementById(avatar-active).classList.add(show); // 注意不在初始化完成后立即开始介绍 // 等待用户点击按钮才开始讲解 } catch (error) { updateStatus(avatar-status, 初始化失败); hideLoading(); alert(初始化过程中发生错误: error.message); } }调用SDK讲解初始化后如果有用户交互我们需要调用魔珐星云SDK的讲解方法代码如下async function speakMessage(text) { if (!isInitialized || !liteSDK) { console.error(SDK未初始化); alert(SDK未初始化请等待初始化完成); return; } if (isSpeaking) { return; } isSpeaking true; updateStatus(avatar-status, 讲解中); // 字幕区域显示正在为您讲解... updateSubtitle(正在为您讲解...); // 存储当前要说的句子但不立即更新字幕 // 字幕将在语音状态变为speaking时更新 currentSpeakingSentence text; try { // 调用官方speak方法 const result liteSDK.speak(text, true, true); // 检查浏览器音频权限 if (navigator.mediaDevices navigator.mediaDevices.getUserMedia) { } else { console.warn(浏览器不支持音频设备); } } catch (error) { console.error(讲解失败:, error); isSpeaking false; updateStatus(avatar-status, 讲解失败); alert(讲解失败: error.message); } }结束讲解有讲解则对应的需要有取消讲解的方法async function stopSpeaking() { if (!isInitialized || !liteSDK) { return; } try { // 调用SDK的interactiveidle方法停止流式讲解 if (typeof liteSDK.offlineMode function) { liteSDK.offlineMode(); } // 停止流式输出 isStreaming false; // 清空句子队列 sentenceQueue []; // 重置状态 isSpeaking false; isSpeakingSentence false; currentSpeakingSentence ; // 隐藏结束讲解按钮 hideStopButton(); // 更新状态显示 updateStatus(avatar-status, 就绪); liteSDK.onlineMode(); // 清空字幕 updateSubtitle(欢迎来到上海城市展厅我是您的智能讲解员); // 触发讲解结束事件 if (typeof onSpeakingEnded function) { onSpeakingEnded(); } } catch (error) { console.error(停止讲解失败:, error); } }用户交互当用户进行交互时我们需要调用豆包大模型流式的获取答案然后调用具身大模型去讲解async function submitQuestion() { const questionInput document.getElementById(user-question); const question questionInput.value.trim(); if (!question) { alert(请输入您的问题); return; } // 显示加载状态 questionInput.disabled true; questionInput.placeholder 正在思考中...; try { // 调用豆包API获取答案流式输出实时讲解 await callDoubaoAPI(question); // 流式输出完成后显示完成提示 setTimeout(() { updateSubtitle(讲解完成); setTimeout(() { updateSubtitle(); }, 2000); }, 1000); } catch (error) { console.error(处理问题时出错:, error); // 使用默认回答 const defaultAnswer 抱歉我现在无法回答您的问题。请稍后再试或者询问关于上海历史、现在或未来的其他问题。; await speakBySentences(defaultAnswer); } finally { // 恢复输入框状态 questionInput.disabled false; questionInput.placeholder 请输入您想了解的问题...; questionInput.value ; } }调用豆包大模型这里简单的使用豆包大模型进行交互可以体验出整个交互的流程代码仍存在优化空间后面进行优化。async function callDoubaoAPI(prompt) { const config doubaoConfig; if (!config.apiKey || !config.model) { const shouldConfig confirm(豆包API配置不完整请先配置相关信息\n\n是否打开配置页面); if (shouldConfig) { openConfigModal(); } return; } const requestData { model: config.model, stream: true, messages: [ { content: prompt, role: user } ] }; let fullContent ; let accumulatedContent ; // 重置队列和状态 sentenceQueue []; isStreaming false; isSpeakingSentence false; try { const response await fetch(config.apiUrl, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${config.apiKey} }, body: JSON.stringify(requestData) }); if (!response.ok) { throw new Error(HTTP error! status: ${response.status}); } const reader response.body.getReader(); const decoder new TextDecoder(); updateSubtitle(正在为您讲解...); // 显示结束讲解按钮 showStopButton(); while (true) { const { done, value } await reader.read(); if (done) { break; } const chunk decoder.decode(value, { stream: true }); const lines chunk.split(\n); for (const line of lines) { if (line.startsWith(data: )) { const dataStr line.slice(6); if (dataStr [DONE]) { continue; } try { const data JSON.parse(dataStr); if (data.choices data.choices.length 0) { const delta data.choices[0].delta; if (delta.content) { fullContent delta.content; accumulatedContent delta.content; // 检查是否包含句子结束标点 const sentences accumulatedContent.match(/[^。.!?][。.!?]/g); if (sentences sentences.length 0) { // 提取完整的句子 const completeSentence sentences[sentences.length - 1]; // 移除已处理的句子 const lastSentenceIndex accumulatedContent.lastIndexOf(completeSentence); accumulatedContent accumulatedContent.substring(lastSentenceIndex completeSentence.length); // 将句子加入队列 sentenceQueue.push(completeSentence); // 尝试处理队列不阻塞继续读取流式数据 processSentenceQueue().catch(err { console.error(处理句子队列时出错:, err); }); } } } } catch (e) { console.warn(解析流式数据失败:, e); } } } } // 处理剩余的内容 if (accumulatedContent.trim().length 0) { sentenceQueue.push(accumulatedContent.trim()); await processSentenceQueue(); // 等待所有句子说完 while (sentenceQueue.length 0 || isSpeakingSentence) { await sleep(100); } // 发送结束标识 await streamSpeak(, false, true); } else if (isStreaming) { // 如果没有剩余内容但已经开始流式输出发送结束标识 while (isSpeakingSentence) { await sleep(100); } await streamSpeak(, false, true); } return fullContent; } catch (error) { console.error(调用豆包API出错:, error); throw error; } }到这里核心的流程代码已经展现让我们看一下整个系统的效果吧~页面效果系统配置我们先看下项目效果首先进入到项目后会先判断服务端是否配置了魔珐星云的配置如果没有则会弹出配置页面我们可以手动在页面上配置体验。加载数字人可以看到加载数字人会有一个形象初始化的过程。数字人加载成功页面形象初始化成功后让我们一起看看加载完数字人的页面。讲解演示讲解过程中我们可以看到数字人的口型表情还有肢体动作都在实时发生变化。 写在最后本文讲解了如何用魔珐星云具身SDK集成大模型搭建一个可交互智能数字人应用。经过博主亲身体验整个搭建的过程还是比较便捷的直接通过SDK的调用大大的减小了我们开发的难度。通过本次的项目搭建对数字人的使用和发展有了新的见解。完整的项目源码大家可以前往github上下载。项目源码https://github.com/hack-feng/xingyun-shanghai魔珐星云官网https://xingyun3d.com?utm_campaigndailyutm_sourcejixinghuiKoc43希望这篇教程对你有帮助有任何问题欢迎在评论区留言交流~关注【笑小枫】一起玩转更多AI应用开发