在健身领域大量应用仍停留在单向视频播放、静态指令推送的浅层应用层面仅实现课程推送、文字任务下发缺少真人化实时交互无法动态指导、情绪陪伴用户全程被动跟练极易中途放弃。真正落地可用的健身 Agent核心突破在于叠加 3D 具身数字人实时交互能力以 Agent 逻辑完成训练任务规划、动作识别、数据闭环同时依托 AI 人物实现实时动作示范、节奏引导、情绪鼓励陪伴让训练从被动观看升级为主动式、沉浸式双向互动。一、传统健身 Agent单向输出缺失实时跟练交互很多健身 Agent本质是预制视频合集或简单指令列表打开后只能跟着固定视频练动作错了没人提醒、节奏乱了没人引导、练累了没人鼓励全程像对着 DVD 独自练。核心短板很明显没有实时交互、没法跟着任务灵活练、缺少陪伴感很难长期坚持。二、三类健身交互模式对比从单向内容到具身智能交互梳理现有健身 Agent 交互形态可清晰看到不同方案的落地差距方案一预制视频类 Agent交互形式AI Agent 推送固定视频单向被动播放核心问题无实时动作示范、节奏无法动态同步、无双向互动反馈实际体验被动跟练枯燥机械缺少陪伴感极易半途而废方案二简单指令类 Agent交互形式Agent 下发固定训练任务、文字指令核心问题无可视化动作演示训练节奏僵硬缺少情绪陪伴与正向激励实际体验机械执行任务训练氛围感弱用户参与意愿低方案三数字人 健身 Agent具身交互类交互形式Agent 负责任务规划、数据识别、逻辑闭环 3D 数字人实时动作示范、节奏引导、情绪鼓励核心优势跟随训练任务动态联动、实时纠错、灵活调整节奏、全程陪伴共情实际体验复刻真人私教式带练有节奏、有温度、有互动显著提升长期坚持意愿视频、纯指令仅能完成信息传递数字人 健身 Agent 实现实时双向交互这是健身类 AI 应用最核心的代际差异。三、AI 人物健身 Agent实时驱动适配跟练需求魔珐星云打造的健身 Agent核心是打通AI Agent 逻辑能力 端侧实时数字人交互能力双重壁垒。区别于传统方案数字人依赖云端预制画面、延迟高、无法动态响应的局限依托自研AI 端渲与端侧解算技术数字人不再是固定演示形象可根据 Agent 下发的训练指令实时生成匹配的动作、表情、手势同步完成动作示范、节奏调节、即时鼓励同时支持实时打断、动态适配用户训练状态适配家用健身、智能硬件、社区运动、线下场馆等多场景落地。魔珐星云核心技术为AI 端渲与端侧解算依托自研文生 3D 多模态大模型云端仅下发轻量级驱动指令终端本地实时渲染彻底解决传统方案高延迟、高成本问题。让健身 Agent 从 “文字 / 视频工具”升级为具备实时带练、动态陪伴、多场景可落地的具身智能私教。点击官网抢先体验https://xingyun3d.com/四、从零搭建智能健身私教完整方案下面我用星云SDKJS版本实际搭建一个可运行的智能健身顾问。准备工作星云官网注册账号https://xingyun3d.com/创建应用驱动并保存 App ID 和 App Secret这是后续接入SDK的唯一凭证文本大模型APIKey获取ASR服务商我选的是讯飞4.1 项目结构smart-fitness-advisor/ ├── src/ │ ├── App.vue # 主界面健身顾问UI │ ├── components/ │ │ └── AvatarRender.vue # 数字人渲染组件 │ ├── services/ │ │ ├── AvatarService.ts # 数字人服务封装 │ │ ├── FitnessService.ts # 健身逻辑服务 │ │ └── LLMService.ts # AI对话服务 │ └── stores/ │ └── app.ts # 全局状态管理4.2 核心服务AvatarService 封装数字人的所有交互都围绕XmovAvatar实例展开。我将它封装成一个单例服务// src/services/AvatarService.ts import { ref } from vue // 健身状态枚举 export type FitnessState idle | listen | think | speak | demo // 健身建议数据 const fitnessSuggestions [ { tag: 热身, content: 运动前做5分钟动态拉伸激活关节防止受伤。 }, { tag: 核心, content: 核心训练要注意呼吸配合发力时呼气还原时吸气。 }, { tag: 力量, content: 力量训练每组做到力竭最后1-2个动作最难但最有效。 }, { tag: 拉伸, content: 拉伸时要感到轻微酸痛但不要到疼痛的程度保持30秒。 }, { tag: 有氧, content: 有氧训练保持心率在最大心率的60%-80%效果最好。 }, ] class AvatarService { private static instance: AvatarService | null null private avatar: any null private currentState: FitnessState idle // 健身相关状态 public todayCalories ref(0) public todayMinutes ref(0) public streak ref(3) public currentExercise refstring | null(null) private constructor() {} public static getInstance(): AvatarService { if (!AvatarService.instance) { AvatarService.instance new AvatarService() } return AvatarService.instance } public async init(containerId: string, appId: string, appSecret: string) { if (this.avatar) return this.avatar new (window as any).XmovAvatar({ containerId, appId, appSecret, gatewayServer: https://nebula-agent.xingyun3d.com/user/v1/ttsa/session, hardwareAcceleration: prefer-hardware, enableLogger: true, onMessage: (msg: any) { console.log([SDK] 消息:, msg) }, onStateChange: (state: string) { console.log([SDK] 状态变化:, state) this.currentState state as FitnessState }, onVoiceStateChange: (status: string) { console.log([SDK] 语音状态:, status) if (status voice_end) { this.avatar?.interactiveIdle() } }, onDownloadProgress: (progress: number) { console.log([SDK] 资源加载: ${progress}%) }, }) await this.avatar.init() console.log([SDK] 数字人初始化完成) } // 健身引导说话 public speakFitnessAdvice(exercise: string, advice: string) { const ssml speak action namegesture parampoint_right / 今天我们来做${exercise}。${advice} /speak this.avatar?.speak(ssml, true, true) } // 鼓励用户 public speakEncouragement() { const encouragements [ 太棒了继续保持这个节奏, 你的动作越来越标准了, 不错不错继续加油汗水不会骗人, 感觉到了吗这就是进步的味道, ] const msg encouragements[Math.floor(Math.random() * encouragements.length)] this.avatar?.speak(msg, true, true) } // 切换状态 public setState(state: FitnessState) { switch (state) { case idle: this.avatar?.idle() break case listen: this.avatar?.listen() break case think: this.avatar?.think() break case demo: this.avatar?.interactiveIdle() break } } // 更新健身数据 public updateFitnessData(exercise: string, calories: number, minutes: number) { this.currentExercise.value exercise this.todayCalories.value calories this.todayMinutes.value minutes // 训练完成后给予鼓励 this.speakEncouragement() } // 获取健身建议 public getFitnessSuggestion(tag: string): string { const suggestion fitnessSuggestions.find(s s.tag tag) return suggestion?.content || 坚持就是胜利 } public destroy() { this.avatar?.destroy() this.avatar null } } export const avatarService AvatarService.getInstance()4.3 健身逻辑服务// src/services/FitnessService.ts export interface Exercise { id: number name: string icon: string duration: number // 分钟 level: 入门 | 初级 | 中级 | 高级 calories: number // 预计消耗卡路里 benefits: string } export const exerciseLibrary: Exercise[] [ { id: 1, name: 热身运动, icon: , duration: 5, level: 入门, calories: 30, benefits: 激活身体肌肉预防运动损伤 }, { id: 2, name: 核心训练, icon: , duration: 15, level: 初级, calories: 120, benefits: 增强核心力量提高身体稳定性 }, { id: 3, name: 力量训练, icon: ️, duration: 20, level: 中级, calories: 180, benefits: 增加肌肉力量塑造健美体型 }, { id: 4, name: 有氧运动, icon: , duration: 30, level: 初级, calories: 250, benefits: 提升心肺功能高效燃烧脂肪 }, { id: 5, name: 拉伸放松, icon: , duration: 10, level: 入门, calories: 40, benefits: 缓解肌肉酸痛提高身体柔韧性 }, { id: 6, name: 全身燃脂, icon: ⚡, duration: 25, level: 高级, calories: 300, benefits: 全身肌肉参与快速燃脂塑形 }, ] export class FitnessService { private static instance: FitnessService | null null public todayProgress ref(0) private constructor() {} public static getInstance(): FitnessService { if (!FitnessService.instance) { FitnessService.instance new FitnessService() } return FitnessService.instance } // 开始训练 public startExercise(exercise: Exercise): string { const template 好的让我们开始${exercise.name}这个动作主要锻炼${exercise.benefits}。建议训练时长${exercise.duration}分钟我来给你计时开始吧 return template } // 完成训练 public completeExercise(exercise: Exercise): { calories: number; minutes: number } { this.todayProgress.value Math.min(100, this.todayProgress.value 20) return { calories: exercise.calories, minutes: exercise.duration } } // 获取每日建议 public getDailyTip(): string { const tips [ 运动前记得补充水分运动中也要适当补水。, 保持呼吸均匀这有助于提高运动效果。, 每天坚持30分钟您会看到明显的进步, 运动后要做拉伸帮助肌肉恢复。, 合理的休息同样重要给身体恢复的时间。, 记住运动要循序渐进不要急于求成。, ] return tips[Math.floor(Math.random() * tips.length)] } }4.4 前端界面!-- src/App.vue 核心部分 -- script setup langts import { ref, onMounted, provide } from vue import SdkRender from ./components/AvatarRender.vue import { avatarService } from ./services/AvatarService import { exerciseLibrary, FitnessService } from ./services/FitnessService const fitnessService FitnessService.getInstance() const selectedExercise refnumber | null(null) const currentAdvice ref(您好我是您的智能健身私教。今天想做什么样的运动呢我可以帮您制定计划、实时指导动作。) const todayProgress ref(45) provide(avatarService, avatarService) // 选择训练项目 function selectExercise(id: number) { selectedExercise.value id const exercise exerciseLibrary.find(e e.id id) if (exercise) { currentAdvice.value fitnessService.startExercise(exercise) avatarService.speakFitnessAdvice(exercise.name, exercise.benefits) } } // 完成训练 function completeExercise() { if (selectedExercise.value) { const exercise exerciseLibrary.find(e e.id selectedExercise.value) if (exercise) { const result fitnessService.completeExercise(exercise) avatarService.updateFitnessData(exercise.name, result.calories, result.minutes) todayProgress.value fitnessService.todayProgress.value currentAdvice.value 太棒了你完成了${exercise.name}消耗了约${result.calories}卡路里继续保持 } } } // 获取随机建议 function getRandomAdvice() { currentAdvice.value fitnessService.getDailyTip() avatarService.speak(currentAdvice.value, true, true) } // 开始今日训练 function startTodayWorkout() { currentAdvice.value 很好让我们开始今天的训练。先做5分钟热身然后进入主要训练内容。准备好了吗跟着我的节奏动起来 avatarService.setState(demo) selectedExercise.value 1 } /script template div classmain !-- 左侧训练菜单 -- div classsidebar div classlogo 智能健身私教/div div classprogress-section div classprogress-label今日进度/div div classprogress-bar div classprogress-fill :style{ width: todayProgress % }/div /div div classprogress-text{{ todayProgress }}%/div /div div classexercise-list div v-foritem in exerciseLibrary :keyitem.id classexercise-item :class{ active: selectedExercise item.id } clickselectExercise(item.id) div classexercise-icon{{ item.icon }}/div div classexercise-info div classexercise-name{{ item.name }}/div div classexercise-meta {{ item.duration }}分钟 · {{ item.level }} · {{ item.calories }}卡 /div /div /div /div div classactions button classbtn-primary clickstartTodayWorkout 开始训练 /button button v-ifselectedExercise classbtn-complete clickcompleteExercise ✅ 完成训练 /button /div /div !-- 中间数字人 指导 -- div classcenter div classadvice-card div classadvice-label 私教指导/div div classadvice-text{{ currentAdvice }}/div button classadvice-refresh clickgetRandomAdvice 换个建议 /button /div div classavatar-container SdkRender / /div /div !-- 右侧数据面板 -- div classstats-panel div classstats-title 训练数据/div div classstats-grid div classstat-item div classstat-value{{ avatarService.todayCalories.value }}/div div classstat-label今日消耗(卡)/div /div div classstat-item div classstat-value{{ avatarService.todayMinutes.value }}/div div classstat-label训练时长(分)/div /div div classstat-item div classstat-value{{ avatarService.streak.value }}/div div classstat-label连续天数/div /div /div div classweekly-chart div classchart-title本周训练/div div classbars div classbar-item v-for(height, i) in [60,80,40,90,70,50,30] :keyi div classbar :style{ height: height % }/div div classbar-label{{ [一,二,三,四,五,六,日][i] }}/div /div /div /div div classtip-card div classtip-title 今日小贴士/div div classtip-text{{ fitnessService.getDailyTip() }}/div /div /div /div /template4.5 数字人组件!-- src/components/AvatarRender.vue -- script setup langts import { onMounted, onUnmounted } from vue import { avatarService } from ../services/AvatarService const APP_ID import.meta.env.VITE_XINGYUN_APP_ID const APP_SECRET import.meta.env.VITE_XINGYUN_APP_SECRET onMounted(async () { try { await avatarService.init(avatar-container, APP_ID, APP_SECRET) avatarService.setState(idle) // 初始化完成后自动打招呼 setTimeout(() { avatarService.speak(你好我是你的智能健身私教。今天准备好训练了吗, true, true) }, 2000) } catch (e) { console.error(数字人初始化失败:, e) } }) onUnmounted(() { avatarService.destroy() }) /script template div idavatar-container classavatar-wrapper/div /template style scoped .avatar-wrapper { width: 100%; height: 100%; min-height: 400px; } /style4.6 运行打开浏览器访问 http://localhost:5173点击「初始化数字人」按钮。等待3D资源加载完成后首次大约10-20秒你就能看到一个活灵活现的数字人出现在页面上了。在输入框输入文本点击「让TA说」——数字人会用选定的音色开口说话口型、表情、手势全部实时生成。五、关键技术解析5.1 流式对话边生成边说话这是数字人健身私教最核心的能力。大模型的输出是流式的比如豆包、通义千问用户不需要等它全部生成完再说出来。// 模拟大模型流式输出 → 数字人实时播报 async function chatWithCoach(userMessage: string) { // 显示用户消息 appendMessage(user, userMessage) // 模拟大模型流式输出 const response await streamLLMResponse(userMessage) // 关键数字人边接收边说话 let isFirstChunk true for await (const chunk of response) { const isLastChunk isLastResponseChunk(response, chunk) avatarService.avatar.speak(chunk.text, isFirstChunk, isLastChunk) isFirstChunk false // 实时追加到聊天框 appendMessage(coach, chunk.text) } // 播报结束切换回空闲状态 avatarService.setState(idle) }关键规则第一段is_start true最后一段is_end true两段 speak 之间必须用interactiveIdle()或listen()做状态切换这里的两段 speak指的是两件不相关的事不是流式输出的多个 chunk。正确理解is_start/is_end是针对「一次对话轮次」的一次完整的数字人说话内部可以分成多个speak()调用比如流式输出时每个 chunk 调一次但这一整个轮次只需要一组is_starttrue和is_endtrue。例如 用户问推荐一个练腹的动作 数字人回答流式分3段输出 chunk1: 推荐你做卷腹。 → speak(chunk1, is_starttrue, is_endfalse) chunk2: 这个动作主要锻炼上腹。 → speak(chunk2, is_startfalse, is_endfalse) chunk3: 每组15个做3组。 → speak(chunk3, is_startfalse, is_endtrue)核心原则同一轮回答的多个 chunk 是一个原子操作中间不能被状态切换打断只有两轮回答之间才需要状态隔离。5.2 健身状态机设计数字人在健身场景中的状态流转待机(idle) → 用户选择训练项目 ↓ 引导演示(demo) → 数字人演示动作用户跟练 ↓ 倾听(listen) → 数字人观察用户状态等待用户反馈 ↓ 思考(think) → 分析用户表现准备评价 ↓ 反馈(speak) → 给出评价和建议 ↓ 鼓励(speak) → 正向激励提升用户动力 ↓ 待机(idle) → 进入下一轮或结束这个状态机保证了数字人的行为是有目的的不是随机执行动画。5.3 SSML 动作标记让数字人做健身动作星云的 SSML 支持在说话时触发预设动作KAKey Action可以让数字人在演示健身动作时更生动// 数字人一边演示拉伸动作一边说话 function demoStretch() { const ssml speak ue4event typeka/type dataaction_semanticstretch_arm_right/action_semantic/data /ue4event 跟着我做——右手伸直向左伸展保持30秒。感受到了吗右肩有拉伸感。 /speak avatarService.avatar.speak(ssml, true, true) }通过action_semantic可以查询当前数字人角色支持的所有动作列表。首次加载时动作素材会从CDN下载每个约100KB后续直接走本地缓存。六、踩坑记录整理坑1容器宽高必须明确指定现象init 成功控制台无报错但页面一片空白。原因SDK 内部用容器的 offsetWidth 和 offsetHeight 创建画布。用 flex 或 height: auto 初始化时都是 0。解决!-- ✅ 正确 -- div idavatar-container stylewidth: 540px; height: 960px;/div !-- ❌ 错误 -- div idavatar-container stylewidth: 100%;/div坑2只能 localhost 或 HTTPS 下运行现象用局域网IP访问如 192.168.1.100:5173SDK 报错。原因SDK 用了麦克风、WebGL 等受限制的浏览器API这些只在安全上下文localhost/HTTPS下可用。解决开发用 localhost部署必须上 HTTPS。可以用 ngrok 做本地映射测试。坑3健身数据没有持久化现象刷新页面后今天的训练数据全没了。原因数据都在内存里ref没做本地存储。解决加一个 localStorage 持久化// 保存 localStorage.setItem(fitness_today, JSON.stringify({ calories: avatarService.todayCalories.value, minutes: avatarService.todayMinutes.value, date: new Date().toDateString() })) // 读取 const saved localStorage.getItem(fitness_today) if (saved) { const data JSON.parse(saved) if (data.date new Date().toDateString()) { avatarService.todayCalories.value data.calories avatarService.todayMinutes.value data.minutes } }七、总结这套方案的真实体验用了两周搭完这个系统说说我的感受真正打动我的地方-1秒响应实测从用户选择训练项目到数字人开始说话稳定在 900-1100ms。对比视频跟练 App 的无人感这个体验是质变。-有温度的交互数字人会在你完成训练后说太棒了会在你想偷懒时说再坚持一下。这种即时反馈是纯文字或视频给不了的。-端侧渲染成本可控不需要为每个用户配备 GPU 服务器素材缓存后复用大规模部署的可行性很高。需要注意的地方首次加载 10-20 秒需要加 loading 引导动作演示和语音的时序对齐需要手动调数据持久化要自己做SDK 不提供HTTPS 是硬性要求调试环境要注意适合的场景 vs 不适合的场景✅ 强烈推荐⚠️ 需要评估健身房/企业健康终端纯App用户可能更习惯纯文字家庭智能健身接电视/平板低性能设备端侧渲染有要求线下展会/品牌体验网络不稳定环境AI私教一对一场景需要精确动作纠正的场景需要额外骨骼检测如果你想做一个真正能陪你练的数字人教练而不是一个仅能执行预制动画的单向展示工具星云 SDK 健身业务逻辑的这套组合是目前我看到最可行的方案。它把最难的部分数字人渲染、表情联动、实时响应替你解决了你只需要专注健身业务的体验设计。相关资源星云SDK文档https://www.xingyun3d.com/developers如果你也对这个方向感兴趣欢迎评论区交流。觉得有用的话转发一下让更多人看到数字人健身私教的可能性。专属体验链接https://xingyun3d.com/?utm_campaigndailyutm_sourcejixinghuiKoc129文章出自YoLo♪原文链接https://blog.csdn.net/chenchenchencl/article/details/161076752
健身 Agent:不止视频,更有 AI 人物实时跟练交互
在健身领域大量应用仍停留在单向视频播放、静态指令推送的浅层应用层面仅实现课程推送、文字任务下发缺少真人化实时交互无法动态指导、情绪陪伴用户全程被动跟练极易中途放弃。真正落地可用的健身 Agent核心突破在于叠加 3D 具身数字人实时交互能力以 Agent 逻辑完成训练任务规划、动作识别、数据闭环同时依托 AI 人物实现实时动作示范、节奏引导、情绪鼓励陪伴让训练从被动观看升级为主动式、沉浸式双向互动。一、传统健身 Agent单向输出缺失实时跟练交互很多健身 Agent本质是预制视频合集或简单指令列表打开后只能跟着固定视频练动作错了没人提醒、节奏乱了没人引导、练累了没人鼓励全程像对着 DVD 独自练。核心短板很明显没有实时交互、没法跟着任务灵活练、缺少陪伴感很难长期坚持。二、三类健身交互模式对比从单向内容到具身智能交互梳理现有健身 Agent 交互形态可清晰看到不同方案的落地差距方案一预制视频类 Agent交互形式AI Agent 推送固定视频单向被动播放核心问题无实时动作示范、节奏无法动态同步、无双向互动反馈实际体验被动跟练枯燥机械缺少陪伴感极易半途而废方案二简单指令类 Agent交互形式Agent 下发固定训练任务、文字指令核心问题无可视化动作演示训练节奏僵硬缺少情绪陪伴与正向激励实际体验机械执行任务训练氛围感弱用户参与意愿低方案三数字人 健身 Agent具身交互类交互形式Agent 负责任务规划、数据识别、逻辑闭环 3D 数字人实时动作示范、节奏引导、情绪鼓励核心优势跟随训练任务动态联动、实时纠错、灵活调整节奏、全程陪伴共情实际体验复刻真人私教式带练有节奏、有温度、有互动显著提升长期坚持意愿视频、纯指令仅能完成信息传递数字人 健身 Agent 实现实时双向交互这是健身类 AI 应用最核心的代际差异。三、AI 人物健身 Agent实时驱动适配跟练需求魔珐星云打造的健身 Agent核心是打通AI Agent 逻辑能力 端侧实时数字人交互能力双重壁垒。区别于传统方案数字人依赖云端预制画面、延迟高、无法动态响应的局限依托自研AI 端渲与端侧解算技术数字人不再是固定演示形象可根据 Agent 下发的训练指令实时生成匹配的动作、表情、手势同步完成动作示范、节奏调节、即时鼓励同时支持实时打断、动态适配用户训练状态适配家用健身、智能硬件、社区运动、线下场馆等多场景落地。魔珐星云核心技术为AI 端渲与端侧解算依托自研文生 3D 多模态大模型云端仅下发轻量级驱动指令终端本地实时渲染彻底解决传统方案高延迟、高成本问题。让健身 Agent 从 “文字 / 视频工具”升级为具备实时带练、动态陪伴、多场景可落地的具身智能私教。点击官网抢先体验https://xingyun3d.com/四、从零搭建智能健身私教完整方案下面我用星云SDKJS版本实际搭建一个可运行的智能健身顾问。准备工作星云官网注册账号https://xingyun3d.com/创建应用驱动并保存 App ID 和 App Secret这是后续接入SDK的唯一凭证文本大模型APIKey获取ASR服务商我选的是讯飞4.1 项目结构smart-fitness-advisor/ ├── src/ │ ├── App.vue # 主界面健身顾问UI │ ├── components/ │ │ └── AvatarRender.vue # 数字人渲染组件 │ ├── services/ │ │ ├── AvatarService.ts # 数字人服务封装 │ │ ├── FitnessService.ts # 健身逻辑服务 │ │ └── LLMService.ts # AI对话服务 │ └── stores/ │ └── app.ts # 全局状态管理4.2 核心服务AvatarService 封装数字人的所有交互都围绕XmovAvatar实例展开。我将它封装成一个单例服务// src/services/AvatarService.ts import { ref } from vue // 健身状态枚举 export type FitnessState idle | listen | think | speak | demo // 健身建议数据 const fitnessSuggestions [ { tag: 热身, content: 运动前做5分钟动态拉伸激活关节防止受伤。 }, { tag: 核心, content: 核心训练要注意呼吸配合发力时呼气还原时吸气。 }, { tag: 力量, content: 力量训练每组做到力竭最后1-2个动作最难但最有效。 }, { tag: 拉伸, content: 拉伸时要感到轻微酸痛但不要到疼痛的程度保持30秒。 }, { tag: 有氧, content: 有氧训练保持心率在最大心率的60%-80%效果最好。 }, ] class AvatarService { private static instance: AvatarService | null null private avatar: any null private currentState: FitnessState idle // 健身相关状态 public todayCalories ref(0) public todayMinutes ref(0) public streak ref(3) public currentExercise refstring | null(null) private constructor() {} public static getInstance(): AvatarService { if (!AvatarService.instance) { AvatarService.instance new AvatarService() } return AvatarService.instance } public async init(containerId: string, appId: string, appSecret: string) { if (this.avatar) return this.avatar new (window as any).XmovAvatar({ containerId, appId, appSecret, gatewayServer: https://nebula-agent.xingyun3d.com/user/v1/ttsa/session, hardwareAcceleration: prefer-hardware, enableLogger: true, onMessage: (msg: any) { console.log([SDK] 消息:, msg) }, onStateChange: (state: string) { console.log([SDK] 状态变化:, state) this.currentState state as FitnessState }, onVoiceStateChange: (status: string) { console.log([SDK] 语音状态:, status) if (status voice_end) { this.avatar?.interactiveIdle() } }, onDownloadProgress: (progress: number) { console.log([SDK] 资源加载: ${progress}%) }, }) await this.avatar.init() console.log([SDK] 数字人初始化完成) } // 健身引导说话 public speakFitnessAdvice(exercise: string, advice: string) { const ssml speak action namegesture parampoint_right / 今天我们来做${exercise}。${advice} /speak this.avatar?.speak(ssml, true, true) } // 鼓励用户 public speakEncouragement() { const encouragements [ 太棒了继续保持这个节奏, 你的动作越来越标准了, 不错不错继续加油汗水不会骗人, 感觉到了吗这就是进步的味道, ] const msg encouragements[Math.floor(Math.random() * encouragements.length)] this.avatar?.speak(msg, true, true) } // 切换状态 public setState(state: FitnessState) { switch (state) { case idle: this.avatar?.idle() break case listen: this.avatar?.listen() break case think: this.avatar?.think() break case demo: this.avatar?.interactiveIdle() break } } // 更新健身数据 public updateFitnessData(exercise: string, calories: number, minutes: number) { this.currentExercise.value exercise this.todayCalories.value calories this.todayMinutes.value minutes // 训练完成后给予鼓励 this.speakEncouragement() } // 获取健身建议 public getFitnessSuggestion(tag: string): string { const suggestion fitnessSuggestions.find(s s.tag tag) return suggestion?.content || 坚持就是胜利 } public destroy() { this.avatar?.destroy() this.avatar null } } export const avatarService AvatarService.getInstance()4.3 健身逻辑服务// src/services/FitnessService.ts export interface Exercise { id: number name: string icon: string duration: number // 分钟 level: 入门 | 初级 | 中级 | 高级 calories: number // 预计消耗卡路里 benefits: string } export const exerciseLibrary: Exercise[] [ { id: 1, name: 热身运动, icon: , duration: 5, level: 入门, calories: 30, benefits: 激活身体肌肉预防运动损伤 }, { id: 2, name: 核心训练, icon: , duration: 15, level: 初级, calories: 120, benefits: 增强核心力量提高身体稳定性 }, { id: 3, name: 力量训练, icon: ️, duration: 20, level: 中级, calories: 180, benefits: 增加肌肉力量塑造健美体型 }, { id: 4, name: 有氧运动, icon: , duration: 30, level: 初级, calories: 250, benefits: 提升心肺功能高效燃烧脂肪 }, { id: 5, name: 拉伸放松, icon: , duration: 10, level: 入门, calories: 40, benefits: 缓解肌肉酸痛提高身体柔韧性 }, { id: 6, name: 全身燃脂, icon: ⚡, duration: 25, level: 高级, calories: 300, benefits: 全身肌肉参与快速燃脂塑形 }, ] export class FitnessService { private static instance: FitnessService | null null public todayProgress ref(0) private constructor() {} public static getInstance(): FitnessService { if (!FitnessService.instance) { FitnessService.instance new FitnessService() } return FitnessService.instance } // 开始训练 public startExercise(exercise: Exercise): string { const template 好的让我们开始${exercise.name}这个动作主要锻炼${exercise.benefits}。建议训练时长${exercise.duration}分钟我来给你计时开始吧 return template } // 完成训练 public completeExercise(exercise: Exercise): { calories: number; minutes: number } { this.todayProgress.value Math.min(100, this.todayProgress.value 20) return { calories: exercise.calories, minutes: exercise.duration } } // 获取每日建议 public getDailyTip(): string { const tips [ 运动前记得补充水分运动中也要适当补水。, 保持呼吸均匀这有助于提高运动效果。, 每天坚持30分钟您会看到明显的进步, 运动后要做拉伸帮助肌肉恢复。, 合理的休息同样重要给身体恢复的时间。, 记住运动要循序渐进不要急于求成。, ] return tips[Math.floor(Math.random() * tips.length)] } }4.4 前端界面!-- src/App.vue 核心部分 -- script setup langts import { ref, onMounted, provide } from vue import SdkRender from ./components/AvatarRender.vue import { avatarService } from ./services/AvatarService import { exerciseLibrary, FitnessService } from ./services/FitnessService const fitnessService FitnessService.getInstance() const selectedExercise refnumber | null(null) const currentAdvice ref(您好我是您的智能健身私教。今天想做什么样的运动呢我可以帮您制定计划、实时指导动作。) const todayProgress ref(45) provide(avatarService, avatarService) // 选择训练项目 function selectExercise(id: number) { selectedExercise.value id const exercise exerciseLibrary.find(e e.id id) if (exercise) { currentAdvice.value fitnessService.startExercise(exercise) avatarService.speakFitnessAdvice(exercise.name, exercise.benefits) } } // 完成训练 function completeExercise() { if (selectedExercise.value) { const exercise exerciseLibrary.find(e e.id selectedExercise.value) if (exercise) { const result fitnessService.completeExercise(exercise) avatarService.updateFitnessData(exercise.name, result.calories, result.minutes) todayProgress.value fitnessService.todayProgress.value currentAdvice.value 太棒了你完成了${exercise.name}消耗了约${result.calories}卡路里继续保持 } } } // 获取随机建议 function getRandomAdvice() { currentAdvice.value fitnessService.getDailyTip() avatarService.speak(currentAdvice.value, true, true) } // 开始今日训练 function startTodayWorkout() { currentAdvice.value 很好让我们开始今天的训练。先做5分钟热身然后进入主要训练内容。准备好了吗跟着我的节奏动起来 avatarService.setState(demo) selectedExercise.value 1 } /script template div classmain !-- 左侧训练菜单 -- div classsidebar div classlogo 智能健身私教/div div classprogress-section div classprogress-label今日进度/div div classprogress-bar div classprogress-fill :style{ width: todayProgress % }/div /div div classprogress-text{{ todayProgress }}%/div /div div classexercise-list div v-foritem in exerciseLibrary :keyitem.id classexercise-item :class{ active: selectedExercise item.id } clickselectExercise(item.id) div classexercise-icon{{ item.icon }}/div div classexercise-info div classexercise-name{{ item.name }}/div div classexercise-meta {{ item.duration }}分钟 · {{ item.level }} · {{ item.calories }}卡 /div /div /div /div div classactions button classbtn-primary clickstartTodayWorkout 开始训练 /button button v-ifselectedExercise classbtn-complete clickcompleteExercise ✅ 完成训练 /button /div /div !-- 中间数字人 指导 -- div classcenter div classadvice-card div classadvice-label 私教指导/div div classadvice-text{{ currentAdvice }}/div button classadvice-refresh clickgetRandomAdvice 换个建议 /button /div div classavatar-container SdkRender / /div /div !-- 右侧数据面板 -- div classstats-panel div classstats-title 训练数据/div div classstats-grid div classstat-item div classstat-value{{ avatarService.todayCalories.value }}/div div classstat-label今日消耗(卡)/div /div div classstat-item div classstat-value{{ avatarService.todayMinutes.value }}/div div classstat-label训练时长(分)/div /div div classstat-item div classstat-value{{ avatarService.streak.value }}/div div classstat-label连续天数/div /div /div div classweekly-chart div classchart-title本周训练/div div classbars div classbar-item v-for(height, i) in [60,80,40,90,70,50,30] :keyi div classbar :style{ height: height % }/div div classbar-label{{ [一,二,三,四,五,六,日][i] }}/div /div /div /div div classtip-card div classtip-title 今日小贴士/div div classtip-text{{ fitnessService.getDailyTip() }}/div /div /div /div /template4.5 数字人组件!-- src/components/AvatarRender.vue -- script setup langts import { onMounted, onUnmounted } from vue import { avatarService } from ../services/AvatarService const APP_ID import.meta.env.VITE_XINGYUN_APP_ID const APP_SECRET import.meta.env.VITE_XINGYUN_APP_SECRET onMounted(async () { try { await avatarService.init(avatar-container, APP_ID, APP_SECRET) avatarService.setState(idle) // 初始化完成后自动打招呼 setTimeout(() { avatarService.speak(你好我是你的智能健身私教。今天准备好训练了吗, true, true) }, 2000) } catch (e) { console.error(数字人初始化失败:, e) } }) onUnmounted(() { avatarService.destroy() }) /script template div idavatar-container classavatar-wrapper/div /template style scoped .avatar-wrapper { width: 100%; height: 100%; min-height: 400px; } /style4.6 运行打开浏览器访问 http://localhost:5173点击「初始化数字人」按钮。等待3D资源加载完成后首次大约10-20秒你就能看到一个活灵活现的数字人出现在页面上了。在输入框输入文本点击「让TA说」——数字人会用选定的音色开口说话口型、表情、手势全部实时生成。五、关键技术解析5.1 流式对话边生成边说话这是数字人健身私教最核心的能力。大模型的输出是流式的比如豆包、通义千问用户不需要等它全部生成完再说出来。// 模拟大模型流式输出 → 数字人实时播报 async function chatWithCoach(userMessage: string) { // 显示用户消息 appendMessage(user, userMessage) // 模拟大模型流式输出 const response await streamLLMResponse(userMessage) // 关键数字人边接收边说话 let isFirstChunk true for await (const chunk of response) { const isLastChunk isLastResponseChunk(response, chunk) avatarService.avatar.speak(chunk.text, isFirstChunk, isLastChunk) isFirstChunk false // 实时追加到聊天框 appendMessage(coach, chunk.text) } // 播报结束切换回空闲状态 avatarService.setState(idle) }关键规则第一段is_start true最后一段is_end true两段 speak 之间必须用interactiveIdle()或listen()做状态切换这里的两段 speak指的是两件不相关的事不是流式输出的多个 chunk。正确理解is_start/is_end是针对「一次对话轮次」的一次完整的数字人说话内部可以分成多个speak()调用比如流式输出时每个 chunk 调一次但这一整个轮次只需要一组is_starttrue和is_endtrue。例如 用户问推荐一个练腹的动作 数字人回答流式分3段输出 chunk1: 推荐你做卷腹。 → speak(chunk1, is_starttrue, is_endfalse) chunk2: 这个动作主要锻炼上腹。 → speak(chunk2, is_startfalse, is_endfalse) chunk3: 每组15个做3组。 → speak(chunk3, is_startfalse, is_endtrue)核心原则同一轮回答的多个 chunk 是一个原子操作中间不能被状态切换打断只有两轮回答之间才需要状态隔离。5.2 健身状态机设计数字人在健身场景中的状态流转待机(idle) → 用户选择训练项目 ↓ 引导演示(demo) → 数字人演示动作用户跟练 ↓ 倾听(listen) → 数字人观察用户状态等待用户反馈 ↓ 思考(think) → 分析用户表现准备评价 ↓ 反馈(speak) → 给出评价和建议 ↓ 鼓励(speak) → 正向激励提升用户动力 ↓ 待机(idle) → 进入下一轮或结束这个状态机保证了数字人的行为是有目的的不是随机执行动画。5.3 SSML 动作标记让数字人做健身动作星云的 SSML 支持在说话时触发预设动作KAKey Action可以让数字人在演示健身动作时更生动// 数字人一边演示拉伸动作一边说话 function demoStretch() { const ssml speak ue4event typeka/type dataaction_semanticstretch_arm_right/action_semantic/data /ue4event 跟着我做——右手伸直向左伸展保持30秒。感受到了吗右肩有拉伸感。 /speak avatarService.avatar.speak(ssml, true, true) }通过action_semantic可以查询当前数字人角色支持的所有动作列表。首次加载时动作素材会从CDN下载每个约100KB后续直接走本地缓存。六、踩坑记录整理坑1容器宽高必须明确指定现象init 成功控制台无报错但页面一片空白。原因SDK 内部用容器的 offsetWidth 和 offsetHeight 创建画布。用 flex 或 height: auto 初始化时都是 0。解决!-- ✅ 正确 -- div idavatar-container stylewidth: 540px; height: 960px;/div !-- ❌ 错误 -- div idavatar-container stylewidth: 100%;/div坑2只能 localhost 或 HTTPS 下运行现象用局域网IP访问如 192.168.1.100:5173SDK 报错。原因SDK 用了麦克风、WebGL 等受限制的浏览器API这些只在安全上下文localhost/HTTPS下可用。解决开发用 localhost部署必须上 HTTPS。可以用 ngrok 做本地映射测试。坑3健身数据没有持久化现象刷新页面后今天的训练数据全没了。原因数据都在内存里ref没做本地存储。解决加一个 localStorage 持久化// 保存 localStorage.setItem(fitness_today, JSON.stringify({ calories: avatarService.todayCalories.value, minutes: avatarService.todayMinutes.value, date: new Date().toDateString() })) // 读取 const saved localStorage.getItem(fitness_today) if (saved) { const data JSON.parse(saved) if (data.date new Date().toDateString()) { avatarService.todayCalories.value data.calories avatarService.todayMinutes.value data.minutes } }七、总结这套方案的真实体验用了两周搭完这个系统说说我的感受真正打动我的地方-1秒响应实测从用户选择训练项目到数字人开始说话稳定在 900-1100ms。对比视频跟练 App 的无人感这个体验是质变。-有温度的交互数字人会在你完成训练后说太棒了会在你想偷懒时说再坚持一下。这种即时反馈是纯文字或视频给不了的。-端侧渲染成本可控不需要为每个用户配备 GPU 服务器素材缓存后复用大规模部署的可行性很高。需要注意的地方首次加载 10-20 秒需要加 loading 引导动作演示和语音的时序对齐需要手动调数据持久化要自己做SDK 不提供HTTPS 是硬性要求调试环境要注意适合的场景 vs 不适合的场景✅ 强烈推荐⚠️ 需要评估健身房/企业健康终端纯App用户可能更习惯纯文字家庭智能健身接电视/平板低性能设备端侧渲染有要求线下展会/品牌体验网络不稳定环境AI私教一对一场景需要精确动作纠正的场景需要额外骨骼检测如果你想做一个真正能陪你练的数字人教练而不是一个仅能执行预制动画的单向展示工具星云 SDK 健身业务逻辑的这套组合是目前我看到最可行的方案。它把最难的部分数字人渲染、表情联动、实时响应替你解决了你只需要专注健身业务的体验设计。相关资源星云SDK文档https://www.xingyun3d.com/developers如果你也对这个方向感兴趣欢迎评论区交流。觉得有用的话转发一下让更多人看到数字人健身私教的可能性。专属体验链接https://xingyun3d.com/?utm_campaigndailyutm_sourcejixinghuiKoc129文章出自YoLo♪原文链接https://blog.csdn.net/chenchenchencl/article/details/161076752