当AI旅行规划师住进3D地球仪:云游YunTrip具身交互智能产品设计全记录
一、写在前面
你有没有想过,一次完美的旅行规划应该是什么样的?
不是打开攻略网站面对几百篇文章发呆,不是在地图App里反复标记对比,而是——站在一颗地球仪前,转动它,目光落在某个城市上,然后一个懂旅行的AI伙伴告诉你:「这里3月樱花最美,5天行程这样安排,当地人有你不知道的吃法。」
这就是我们做「云游 YunTrip」的初衷:把全球旅行目的地装进一颗可交互的3D地球仪,让AI数字人成为你的私人旅行顾问——这就是具身交互智能的魅力所在。
**魔珐星云PC端官方**:https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen3&utm_medium=&utm_term=&utm_content=
二、从「Demo」到「产品」:一次关键的形态进化
2.1 起点:一个能对话的数字人
项目最初的样子,和市面上大多数数字人Demo差不多——屏幕右边站着一个数字人,左边是一些配置面板,用户可以打字或说话跟它对话。功能完整,但缺少「场景感」。
坦白说,用户看到一个数字人站在面板旁边,第一反应不是「好酷」,而是「所以呢?它能帮我做什么?」
我们缺的不是技术能力,而是一个让用户一眼就懂的使用场景。
2.2 转折:如果数字人住在地球仪里呢?
灵感来自一次很随意的讨论:
「如果首页不是面板,而是一颗地球仪呢?」
「用户点哪个国家,数字人就讲哪里的旅行攻略。」
「像真正的旅行顾问一样,指着地图给你推荐。」
这个想法瞬间把整个产品逻辑串通了:地球仪 = 内容载体,数字人 = 具身交互智能入口,点击 = 需求触发。 三者形成一个完整闭环,用户不需要思考「我该干什么」,看到地球仪就想点,点了就想听。
2.3 方案确定:3D地球仪 + 弹出卡片 + 悬浮气泡
经过几轮讨论,我们锁定了最终的产品形态:
| 模块 | 功能 | 交互方式 |
|---|---|---|
| 3D地球仪 | 展示全球26个热门目的地 | 拖拽旋转、缩放、点击标记 |
| 目的地卡片 | 展示景点、美食、最佳季节 | 点击标记弹出,右侧滑入 |
| 数字人气泡 | AI旅行规划师对话交互 | 悬浮按钮,点击展开 |
| 导航栏 | 按区域筛选目的地 | 点击区域标签切换视角 |
三、创建魔珐星云驱动应用
3.1 创建驱动应用
1、登录魔珐星云控制台,应用管理-创建驱动应用-横屏模式

3.2 形象配置
选择适合旅行顾问场景的数字人形象,建议阳光、亲和的风格。

3.3 场景配置
配置数字人的展示场景,推荐使用简洁干净的背景,与地球仪的深色调形成协调。

3.4 音色配置
旅行规划师的音色很关键——我们选择了温暖、有活力的音色,让用户感觉像在和一个见多识广的朋友聊天。

3.5 表演配置
配置数字人的待机动作和说话时的肢体语言,让交互更自然。

四、核心代码深度解析

4.1 经纬度转3D坐标:地球仪的数学基础
整个地球仪交互的起点,是把地理坐标(经纬度)映射到Three.js的3D空间坐标。这个转换函数虽然只有7行,但它是所有标记点定位、相机飞行、区域导航的基石。
// src/components/WorldMap.vue
// 经纬度转 3D 坐标
function latLngToVector3(lat: number, lng: number, radius: number): THREE.Vector3 {
const phi = (90 - lat) * (Math.PI / 180)
const theta = (lng + 180) * (Math.PI / 180)
return new THREE.Vector3(
-radius * Math.sin(phi) * Math.cos(theta),
radius * Math.cos(phi),
radius * Math.sin(phi) * Math.sin(theta)
)
}
逐行解析
- 第1行:纬度转余角(phi),因为Three.js的球体坐标系以Y轴为极点,而地理纬度以赤道为0度,需要90度偏移
- 第2行:经度加180度后转弧度,Three.js的theta从X轴负方向开始逆时针计算,与地理经度的本初子午线起点不同
- 第3-6行:球坐标转直角坐标的标准公式,-x是因为Three.js的X轴方向与地理经度方向相反
这个函数被调用于三个场景:标记点放置(radius = EARTH_RADIUS * 1.01)、相机飞行目标(radius = EARTH_RADIUS + 8)、区域导航(radius = EARTH_RADIUS + 12)。不同的radius值决定了标记点贴在球面、相机停在球外、还是飞到更远的视角。
4.2 地球渲染:程序化纹理 + NASA高清贴图的双保险策略
地球渲染是整个项目视觉效果的核心。我们采用了一个「双保险」策略:先用Canvas程序化生成一个基础纹理确保页面加载就有内容,再异步加载NASA Blue Marble高清贴图覆盖上去。
// src/components/WorldMap.vue - createEarth()核心逻辑
function createEarth() {
const geometry = new THREE.SphereGeometry(EARTH_RADIUS, 64, 64)
// 第一步:程序化生成备用纹理(确保首屏有内容)
const canvas = document.createElement('canvas')
canvas.width = 2048
canvas.height = 1024
const ctx = canvas.getContext('2d')!
// 海洋渐变背景
const oceanGrad = ctx.createLinearGradient(0, 0, 0, canvas.height)
oceanGrad.addColorStop(0, '#0a1628')
oceanGrad.addColorStop(0.5, '#0d1f3c')
oceanGrad.addColorStop(1, '#0a1628')
ctx.fillStyle = oceanGrad
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 用椭圆近似七大洲轮廓
ctx.fillStyle = '#1a3a5c'
ctx.strokeStyle = '#2a5a8c'
const continents = [
{ x: 300, y: 250, w: 300, h: 220, rot: -0.1 }, // 北美
{ x: 480, y: 520, w: 140, h: 280, rot: 0.15 }, // 南美
{ x: 1020, y: 230, w: 180, h: 140, rot: 0.1 }, // 欧洲
{ x: 1050, y: 420, w: 200, h: 300, rot: 0 }, // 非洲
{ x: 1280, y: 250, w: 400, h: 250, rot: -0.05 }, // 亚洲
{ x: 1550, y: 580, w: 180, h: 140, rot: 0.1 }, // 澳洲
{ x: 1024, y: 940, w: 800, h: 80, rot: 0 }, // 南极
]
continents.forEach(c => {
ctx.save()
ctx.translate(c.x, c.y)
ctx.rotate(c.rot)
ctx.beginPath()
ctx.ellipse(0, 0, c.w / 2, c.h / 2, 0, 0, Math.PI * 2)
ctx.fill()
ctx.stroke()
ctx.restore()
})
const fallbackTexture = new THREE.CanvasTexture(canvas)
const material = new THREE.MeshPhongMaterial({
map: fallbackTexture,
specular: new THREE.Color(0x222244),
shininess: 15,
})
earth = new THREE.Mesh(geometry, material)
scene.add(earth)
// 第二步:异步加载NASA Blue Marble真实卫星影像覆盖
const loader = new THREE.TextureLoader()
loader.load(
'https://unpkg.com/three-globe@2.41.12/example/img/earth-blue-marble.jpg',
(texture) => {
texture.colorSpace = THREE.SRGBColorSpace
material.map = texture
material.needsUpdate = true // 触发材质重新渲染
},
undefined,
() => { console.log('Using fallback procedural earth texture') }
)
// 第三步:叠加夜间灯光作为自发光贴图
loader.load(
'https://unpkg.com/three-globe@2.41.12/example/img/earth-night.jpg',
(nightTexture) => {
nightTexture.colorSpace = THREE.SRGBColorSpace
material.emissiveMap = nightTexture
material.emissive = new THREE.Color(0x444444)
material.emissiveIntensity = 0.8
material.needsUpdate = true
}
)
}
设计思路解析:
为什么需要「双保险」?因为NASA高清贴图有2MB+,在网络不佳时可能需要数秒加载。如果等贴图加载完才渲染地球,用户会看到一个空白页面。程序化纹理在1毫秒内就能生成,确保用户打开页面就看到一颗蓝色星球,然后高清贴图无缝覆盖。
夜间灯光贴图(earth-night.jpg)作为emissiveMap叠加后,地球上的城市区域会微微发光——东京、纽约、伦敦的灯光在暗面清晰可见,这个效果远超预期。
4.3 大气层Shader:一行公式的视觉魔法
地球边缘那圈蓝色光晕,是整个视觉效果中最「值回票价」的部分。实现它只需要一个自定义Shader:
// src/components/WorldMap.vue - 大气层片段着色器
varying vec3 vNormal;
void main() {
float intensity = pow(0.65 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0);
gl_FragColor = vec4(0.3, 0.6, 1.0, 1.0) * intensity;
}
原理拆解:
- dot(vNormal, viewDirection) 计算法线与视线的夹角余弦值
- 球体边缘的法线垂直于视线,点积接近0;球心法线平行于视线,点积接近1
- 0.65 - dot(…) 反转后,边缘值大、中心值小
- pow(…, 2.0) 做二次方衰减,让光晕更集中在边缘
- 最终效果:球体中心几乎透明,边缘渐变为蓝色光晕,完美模拟大气散射
配合 AdditiveBlending(加法混合)和 BackSide(背面渲染),这个光晕自然地叠加在地球边缘之外。
4.4 相机飞行:三次缓动让交互有「重量感」
点击目的地后相机飞行动画的核心,是一个自定义缓动函数。不是简单的linear插值,而是模拟物理运动的「先加速后减速」:
// src/components/WorldMap.vue
function flyCameraTo(target: THREE.Vector3) {
autoRotate = false
if (autoRotateTimer) clearTimeout(autoRotateTimer)
const start = camera.position.clone()
const duration = 1500
const startTime = Date.now()
function animate() {
const elapsed = Date.now() - startTime
const t = Math.min(elapsed / duration, 1)
// 三次缓动函数:前半程加速,后半程减速
const ease = t < 0.5
? 4 * t * t * t // 加速段:三次方增长
: 1 - Math.pow(-2 * t + 2, 3) / 2 // 减速段:三次方衰减
camera.position.lerpVectors(start, target, ease)
camera.lookAt(0, 0, 0)
controls.update()
if (t < 1) requestAnimationFrame(animate)
}
animate()
// 交互结束后8秒恢复自转
autoRotateTimer = setTimeout(() => { autoRotate = true }, 8000)
}
为什么用三次缓动而不是线性? 线性插值会让相机匀速移动,看起来像机械臂。三次缓动让起步有「推力感」,到达有「刹车感」,整个飞行有了物理世界的重量感。用户感知到的不是「画面在动」,而是「视角在飞」。
4.5 流式播报引擎:首句即发声的核心算法
这是整个项目中逻辑最复杂的模块。目标是将LLM的流式输出实时转化为具身交互智能数字人,实现同步语音、表情、肢体多模态播报,让用户在1秒内就能听到回复。
// src/stores/app.ts - sendMessage()核心流式播报逻辑
async sendMessage(): Promise<string | undefined> {
// ... 前置校验省略
// 如果数字人正在说话,先打断
await this.interrupt()
if (avatarState.value === 'speak') {
await this.waitForAvatarIdle()
}
actionManager.reset()
const stream = await llmService.sendMessageWithStream({
provider: 'openai', model: llm.model, apiKey: llm.apiKey
}, ui.text)
if (!stream) return
// 核心策略:缓存文本直到遇到标点且达到最小可读长度
const minimum = 20
const context = {
cache: '', // 缓存文本
chars: 0, // 可读字符计数
firstSpeakSend: false, // 首句是否已发送
spaceCount: 0 // 英文空格计数
}
// 中英文标点正则
const cnSplitSign = /[。?!;… ,:]/
const enSplitSign = /[.?!;:,]/
let firstSentenceResolved = false
const firstSentencePromise = new Promise<void>((resolve) => {
;(async () => {
for await (const content of stream) {
if (typeof content !== 'string') continue
context.cache += content
if (content.startsWith(' ')) context.spaceCount += 1
const chars = content.match(/[\u4e00-\u9fa5a-zA-Z0-9]/g)?.length ?? 0
let shouldSend = false
if (!context.firstSpeakSend) {
// 首句策略:可读字符 >= 20 且遇到标点才发送
shouldSend = context.spaceCount
? context.spaceCount > minimum - 1 && enSplitSign.test(content)
: context.chars > minimum && cnSplitSign.test(content)
} else {
// 后续句策略:遇到标点即发送
shouldSend = context.spaceCount
? enSplitSign.test(content)
: cnSplitSign.test(content)
}
if (!shouldSend) { context.chars += chars; continue }
// 发送缓存文本给数字人播报
actionManager.speak(context.cache, {
isStart: !context.firstSpeakSend,
isEnd: false
})
// 第一句发送后立即resolve,让sendMessage返回
if (!context.firstSpeakSend && !firstSentenceResolved) {
firstSentenceResolved = true
context.firstSpeakSend = true
resolve()
} else if (context.firstSpeakSend) {
context.firstSpeakSend = true
}
// 清空缓存
context.cache = ''
context.chars = 0
context.spaceCount = 0
}
// 流结束,处理剩余文本
if (context.cache.length > 0) {
actionManager.speak(context.cache, { isStart: !context.firstSpeakSend, isEnd: true })
if (!firstSentenceResolved) { firstSentenceResolved = true; resolve() }
}
})()
})
await firstSentencePromise
return 'success'
}
算法设计要点:
- 首句门槛高:必须积累20个可读字符(汉字/字母/数字)且遇到标点才发送。这是为了避免LLM开头输出「好的,让我来帮你」这种无意义前缀被立即播报
- 后续句门槛低:首句发出后,遇到任何标点就发送。因为后续内容都是有效信息
- 中英文****分治:英文以空格为词边界,需要积累更多空格(相当于词数)才触发;中文直接按标点断句
- Promise竞速:sendMessage在第一句发出后就返回,不等整个流处理完。后台异步继续处理后续段落
- 兜底机制:流结束时的残余文本、短回复、异常情况,都有resolve兜底,防止Promise永远挂起
4.6 组件编排:App.vue的三组件协同
整个应用只有三个核心组件,通过App.vue编排,事件驱动协作:
// src/App.vue
<script setup lang="ts">
import { provide, ref, onMounted, nextTick } from 'vue'
import WorldMap from './components/WorldMap.vue'
import DestinationCard from './components/DestinationCard.vue'
import AvatarBubble from './components/AvatarBubble.vue'
import { appState, appStore } from './stores/app'
import type { Destination } from './data/destinations'
// 提供全局状态和方法
provide('appState', appState)
provide('appStore', appStore)
// 选中的目的地
const selectedDestination = ref<Destination | null>(null)
function onSelectDestination(dest: Destination) {
selectedDestination.value = dest
}
function onCloseCard() {
selectedDestination.value = null
}
function onPlan(dest: Destination) {
// 关闭卡片,展开数字人气泡
selectedDestination.value = null
// 自动发送消息(由 AvatarBubble 内的 appState.ui.text 驱动)
setTimeout(() => {
appStore.sendMessage()
}, 500)
}
// 点击地图空白处关闭卡片
function onMapClick() {
selectedDestination.value = null
}
</script>
<template>
<div class="app-container" @click="onMapClick">
<!-- 3D 地球仪主内容 -->
<WorldMap @select-destination="onSelectDestination" />
<!-- 目的地信息卡片 -->
<DestinationCard
:destination="selectedDestination"
@close="onCloseCard"
@plan="onPlan"
/>
<!-- 数字人悬浮气泡 -->
<AvatarBubble />
</div>
</template>
交互链路:WorldMap点击标记 → emit select-destination → App设置selectedDestination → DestinationCard滑入展示 → 用户点击「让云游帮我规划」 → emit plan → App关闭卡片并调用sendMessage → LLM流式返回 → AvatarBubble自动展开播报。
整条链路零耦合,每个组件只关心自己的状态和事件。
4.7 数字人气泡:SDK容器与UI展示的分离设计
数字人气泡的交互设计解决了一个核心矛盾:数字人需要SDK容器始终存在于DOM中才能连接,但UI上不需要一直展示。
我们把「SDK容器」和「UI展示」分离——容器始终渲染(保证连接成功),UI层通过opacity控制显隐:
// src/components/AvatarBubble.vue
// 数字人渲染层 - 始终存在于DOM,通过CSS控制显隐
// <div class="avatar-render-layer" :class="{ expanded: isExpanded }" @click.stop>
// <AvatarRender />
// </div>
// 关键交互:数字人说话时自动展开气泡
watch(() => appState.ui.subTitleText, (newText) => {
if (newText && !isExpanded.value) {
isExpanded.value = true // 自动展开
}
})
/* 默认隐藏但DOM存在 */
.avatar-render-layer {
position: fixed;
right: 100px;
bottom: 300px;
width: 320px;
height: 55vh;
max-height: 600px;
z-index: 99;
pointer-events: none;
opacity: 0;
transform: scale(0.8) translateY(20px);
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
/* 展开时显示 */
.avatar-render-layer.expanded {
opacity: 1;
transform: scale(1) translateY(0);
pointer-events: auto;
}
4.8 目的地卡片:信息密度与视觉美感的平衡
点击标记点后,右侧滑入的信息卡片承载了核心内容。我们在卡片设计上遵循了「三层递进」原则:
- 第一眼:封面图 + 标签(文化/美食/自然)→ 建立第一印象
- 第二眼:一句话描述 + 热门景点 → 激发兴趣
- 第三眼:最佳旅行时间 + 「让云游帮我规划」按钮 → 触发行动
最后那个按钮是关键——它不只是个CTA,而是连接「浏览模式」和「对话模式」的桥梁。点击后,系统自动生成一条旅行规划请求,发送给AI数字人,用户在气泡中看到个性化的行程方案。
五、技术架构:魔珐星云参数流驱动的三条数据流
整个系统的技术架构可以概括为三条并行的数据流,它们以魔珐星云XmovAvatar SDK的参数流架构为核心枢纽协同工作:
5.1 视觉流:地球仪渲染(前端自包含)
Three.js场景 → 地球纹理(NASA Blue Marble) → 大气层Shader → 标记点(CSS2D) → 星空粒子
视觉流完全由前端Three.js驱动,不依赖魔珐星云。地球仪采用Three.js构建,加载NASA Blue Marble高清卫星影像作为主纹理,叠加夜间灯光作为自发光贴图,bump map提供地形凹凸感。半透明云层作为地球子节点跟随旋转。
这条流独立运行的好处是:即使数字人SDK未连接,地球仪依然可以正常展示和交互,用户不会看到空白页面。
5.2 对话流:魔珐星云参数流架构深度解析
用户输入 → LLM流式返回 → 标点断句 → 首句优先发送 → ActionManager队列 → XmovAvatar.speak() → 魔珐云端参数流解算 → 数字人口型/动作同步
这是整个项目的核心交互链路,也是魔珐星云参数流架构的价值所在。
什么是参数流?
传统数字人方案(视频流)的工作方式是:云端渲染好完整的数字人视频,编码为H.264/H.265视频流,通过网络传输到浏览器播放。这种方案的致命问题是延迟高达2-3秒——因为必须等云端渲染完整帧才能开始传输。
魔珐星云的参数流架构完全不同:云端不渲染视频,而是解算文本对应的「参数」——口型参数、面部表情参数、肢体动作参数——将这些参数流式推送到客户端,由客户端SDK在本地驱动3D模型渲染。传输的数据量从视频流的几Mbps降低到参数流的几Kbps,延迟从2-3秒降低到约500ms。
SDK加载:三级容错机制
// src/utils/sdk-loader.ts
export async function ensureSDKsLoaded(): Promise<boolean> {
try {
// 并行加载三个SDK
await Promise.all([
loadSDK('/cryptojs.js').catch(() => {
// 本地失败则回退CDN
return loadSDK('https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.js')
}),
loadSDK('/speechrecognizer.js'),
loadSDK('https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar@latest.js')
])
// 轮询等待SDK初始化完成
await Promise.all([
waitForSDK('cryptoJS'),
waitForSDK('speechRecognizer'),
waitForSDK('xmovAvatar')
])
return true
} catch (error) {
console.error('SDK加载失败:', error)
return false
}
}
三个SDK各司其职:CryptoJS负责ASR语音识别的签名加密,SpeechRecognizer封装WebAudio API实现实时语音转文字,XmovAvatar是魔珐星云的核心驱动SDK负责数字人渲染和参数流解算。
连接流程:从网关握手到参数流就绪
// src/services/avatar.ts - connect()核心流程
async connect(config: AvatarConfig, callbacks: AvatarCallbacks): Promise<any> {
const { appId, appSecret } = config
// 1. 构建魔珐星云网关URL
const url = new URL(SDK_CONFIG.GATEWAY_URL)
url.searchParams.append('data_source', SDK_CONFIG.DATA_SOURCE)
url.searchParams.append('custom_id', SDK_CONFIG.CUSTOM_ID)
// 2. 创建SDK实例,传入容器ID和密钥
const avatar = new window.XmovAvatar({
containerId: `#${this.containerId}`,
appId,
appSecret,
gatewayServer: url.toString(), // 指向魔珐云端参数流服务
onStateChange, // 数字人状态变化回调
onVoiceStateChange: (status) => {
if (status.includes('end')) {
onVoiceStateChange?.(status) // 说话结束回调
}
}
})
// 3. 初始化:下载数字人模型资源
await avatar.init({
onDownloadProgress: (progress) => {
if (progress >= 100) resolve(true) // 模型下载完成
}
})
// 4. 等待网关连接建立(15秒超时保护)
await Promise.race([connectPromise, connectTimeout])
return avatar
}
连接过程的关键节点:
- 网关****握手:SDK通过gatewayServer URL与魔珐云端建立WebSocket连接,协商参数流协议
- 模型下载:根据appId对应的数字人形象,下载3D模型、贴图、动作数据
- 状态机就绪:onStateChange回调开始工作,数字人进入interactive_idle待机状态
ActionManager:流式播报的队列调度器
魔珐星云的speak()接口支持流式调用——通过isStart/isEnd标记告诉SDK当前是流式的第几段。ActionManager封装了这个队列调度逻辑:
// src/services/action-manager.ts
class ActionManager {
private queue: ActionQueueItem[] = []
private isSpeaking = false
private instanceRef: Ref<any | null>
speak(text: string, options: SpeakOptions = {}) {
const ssml = generateSSML(text.replace(/\n+/g, '\n'))
this.queue.push({
ssml,
isStart: options.isStart ?? false,
isEnd: options.isEnd ?? false
})
this.processQueue()
}
private async processQueue() {
if (this.isSpeaking) return // 防止并发
const instance = this.instanceRef.value
if (!instance) return // SDK未连接则跳过
this.isSpeaking = true
while (this.queue.length) {
const item = this.queue.shift()
if (!item) break
this.onVoiceReady?.() // 通知UI:开始说话
// 调用魔珐SDK的speak,传入SSML和流式标记
instance.speak(item.ssml, item.isStart, item.isEnd)
if (!item.isEnd) continue // 流式中间段,不等
}
this.isSpeaking = false
this.onVoiceEnd?.() // 通知UI:说完
}
}
这里的核心设计:
- isStart=true:告诉魔珐云端「这是第一段,开始新的播报会话」
- isEnd=false:告诉云端「后面还有内容,不要关闭会话」
- isEnd=true:告诉云端「这是最后一段,可以结束播报」
- 队列机制:即使LLM流式返回很快,speak调用也是串行的,避免多段内容同时播报导致口型混乱
实时打断:参数流的双工优势
传统视频流方案无法实现真正的「打断」——因为视频已经预渲染了一段,中途停止会看到数字人嘴巴突然闭合的突兀画面。参数流架构下,云端实时解算参数,打断时只需清空参数队列,数字人立即进入idle状态,过渡自然。
// src/stores/app.ts - interrupt()方法
interrupt(): void {
if (!appState.avatar.instance) {
return
}
try {
// 重置动作管理器队列
actionManager.reset()
// 调用虚拟人实例的打断方法
// SDK 的 interactive_idle() 方法用于打断当前说话
// SDK 会通过 onStateChange 回调通知状态变化为 'interactive_idle'
if (typeof appState.avatar.instance.interactiveidle === 'function') {
appState.avatar.instance.interactiveidle()
console.log('已调用 interactive_idle() 打断方法,等待 SDK 通过 onStateChange 回调更新状态')
}
} catch (error) {
console.error('打断失败:', error)
// 如果打断失败,直接设置状态为交互空闲,确保逻辑继续执行
avatarState.value = 'interactive_idle'
}
}
5.3 交互流:地球仪 ↔ 卡片 ↔ 气泡 ↔ 魔珐云端
点击标记 → 地球飞向目的地 → 弹出信息卡片 → 点击规划 → 关闭卡片 → 气泡自动展开 → LLM流式返回 → ActionManager队列 → XmovAvatar.speak() → 魔珐云端参数流解算 → 数字人口型同步
三条流在App.vue中汇聚,通过provide/inject共享状态,各组件独立但协同。魔珐星云参数流架构是连接「文本理解」和「视觉呈现」的核心枢纽——LLM返回的文字经过断句策略切分后,通过ActionManager队列调度,以流式参数的形式推送到魔珐云端,云端实时解算出驱动数字人口型、表情、动作的参数流,回传到客户端SDK渲染。
六、写在最后
回顾整个项目,最大的收获不是技术实现,而是一个产品认知:
具身交互智能的价值不在于「数字人能说话」,而在于「在正确的场景中,用正确的方式触发交互」。
一个站在空白页面上的数字人,用户不知道跟它说什么。但一个站在地球仪旁边、当你点击东京时自动开始讲解日本旅行的数字人,用户会觉得「它懂我」。
这就是场景的力量。技术是基础,场景是灵魂。
云游YunTrip的旅程才刚刚开始。下一步,我们计划加入更多目的地、支持行程一键导出、以及基于用户偏好的智能推荐。如果你也对具身交互智能感兴趣,欢迎交流。
**魔珐星云PC端官方**:https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen3&utm_medium=&utm_term=&utm_content=
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)