行业资讯
不止陈列,更能对话:kiki‘s space 具身交互智能作品集的设计与工程全记录
具身交互智能是让 AI 从屏幕里的一段文字走向能看、能听、能开口应答的伙伴。这一次我把它请进了一个 AI 设计师的个人作品集——kiki’s space。访客不再只是滑动浏览海报与视频而是能直接和主理人 kiki 面对面聊设计、聊灵感、聊合作。本文完整复盘这个站点的立意、视觉系统与数字人全链路的工程实现。魔珐星云 PC 端官方链接https://xingyun3d.com?utm_campaigndailyutm_sourceCSDNwanfen3utm_mediumutm_termutm_content一、为什么一个作品集要做具身交互智能传统作品集网站的问题不在于不好看而在于单向它把作品陈列出来却无法回答访客心里的问题——“这套视觉是怎么想的”“能不能帮我做类似的”“怎么联系”kiki’s space 的立意是把作品集从陈列馆变成会客厅。我们引入具身交互智能——让一位名叫 kiki 的 AI 设计师数字人常驻页面她有形象、有声音、能实时听懂访客的话并即时应答。访客浏览到互动体验区时可以打字也可以点亮麦克风直接说话像和一位有品味的设计师面对面交流。这背后有三个明确的产品决策保留数字人为核心交互而非装饰数字人不是首屏的一个摆件而是承载和设计师对话这一核心体验的主角。克制的高端气质优先视觉上不追求炫技堆砌而是温暖、梦幻、克制、有科技感像创意机构官网而非模板站。工程上复刻而非重造数字人能力从既有的 Vue 项目迁移到 React全链路形象渲染、LLM 流式应答、语音识别、流式断句播报、实时打断忠实复刻保证交互质感不降级。二、技术选型用 React Vite 复刻数字人全链路站点是一个独立的 React 18 Vite 5 子工程面向 PC 端优先、响应式兼顾移动端。之所以从 Vue 迁移到 React是为了统一后续创意类项目的技术栈同时验证数字人核心逻辑能否与框架解耦。迁移策略是分层的框架无关的纯逻辑原样迁移形象服务 avatar、大模型服务 llm、语音识别 asr、工具函数 utils、常量 constants、类型 types——这些不依赖任何框架直接复用。有状态的控制核心重写为单例原 Vue 的响应式 store 改写为一个框架无关的 AvatarController 单例 订阅机制再用 React 的 useSyncExternalStore 对接。视图层用 React 函数组件重写导航、首屏、角色介绍、作品案例、互动体验、联系方式各自成组件。整体页面结构分为五个区块首屏 Hero全屏视频 导航、角色介绍、作品案例、互动体验数字人对话、联系方式。三、视觉系统把温暖·梦幻·克制·科技感写进 CSS 变量高端感来自约束而非元素堆叠。我把四个气质关键词落成了一套设计令牌集中在全局样式里:root { /* 版心 1700px创意机构官网级的横向舒展 */ --shell: 1700px; /* 暖色调色板橙 / 暖黄 / 白 / 玻璃 */ --bg: #fbf6ef; --ink: #241d18; --brand: #ff7a18; /* 主橙 */ --brand-2: #ffab40; /* 暖黄 */ /* 玻璃材质 */ --glass: rgba(255, 255, 255, 0.55); --glass-brd: rgba(255, 255, 255, 0.8); --glass-shadow: 0 24px 70px rgba(150, 88, 24, 0.14); --radius-l: 28px; }三个关键手法支撑起整体气质暖色梦幻底纹用 body::before 铺一层固定的多点径向光晕右上主橙、左侧暖橙、底部暖黄让空白处也有柔光呼吸而非死白。玻璃态卡片统一的 glass 类用 backdrop-filter: blur(18px) saturate(1.4) 做磨砂玻璃配合暖色投影形成克制的科技感。滚动入场动画用 IntersectionObserver 封装的 useReveal让各区块进入视口时轻柔上浮淡入节奏舒缓。首屏则用一段全屏静音循环视频打底缺省时以暖色渐变兜底标题直接点明身份——具身交互智能的 AI 设计师span classNamehero__badge glass✦ AI 设计师 · 具身交互智能/span h1 classNamehero__title 你好我是 span classNamehlkiki/span br / 用设计让想象span classNamehl有温度地/span发生 /h1四、前提准备在魔珐星云控制台配置数字人形象数字人的形象、音色与驱动能力由魔珐星云平台提供。开始编码前需要在控制台完成以下配置并拿到接入密钥。步骤 1创建数字人应用登录魔珐星云控制台新建一个数字人应用作为 kiki 的形象与能力载体。步骤 2配置数字人形象为应用选择或上传数字人形象这里选用契合 AI 设计师气质的形象。步骤 3配置场景为透明全身场景选择透明全身让数字人以透明背景渲染方便与网页的暖色玻璃态背景无缝融合不出现突兀的底色块。步骤 4配置音色挑选温暖、亲和的音色与 kiki 的人设一致用于后续 SSML 语音播报。步骤 5配置表演风格设置数字人的表演动作、表情风格让应答时的肢体语言自然、有温度。步骤 6获取 App 密钥并接入 SDK复制应用的 App 密钥appId / appSecret参考平台文档将数字人接入网页、App 或任意终端。拿到密钥后把它连同 ASR、LLM 的密钥填入项目常量。注意仓库中应使用占位符切勿提交真实密钥。// src/constants/index.ts —— 均为占位符请替换为你自己的密钥 export const API_KEYS { AVATAR: { appId: your_avatar_app_id, appSecret: your_avatar_app_secret }, ASR: { appId: your_asr_app_id, secretId: your_secret_id, secretKey: your_secret_key }, LLM: { apiKey: your_llm_api_key } } as const // 网关与数据源配置 export const SDK_CONFIG { GATEWAY_URL: https://nebula-agent.xingyun3d.com/user/v1/ttsa/session, DATA_SOURCE: 2, CUSTOM_ID: demo } as const // 腾讯云 ASR 引擎参数 export const ASR_CONFIG { ENGINE_MODEL_TYPE: 16k_zh, VOICE_FORMAT: 1, NEEDVAD: 1 // ...其余过滤与转换参数 } as const五、核心代码讲解数字人全链路是怎样跑起来的这一节全部使用项目真实源码按渲染容器 → 连接 → 流式断句播报 → 实时打断 → 语音输入 → 状态订阅的链路逐段拆解。5.1 渲染容器的时序id 必须先于连接存在魔珐 SDK 需要一个已存在的 DOM 容器来挂载渲染。所以舞台组件要先把容器 div 渲染出来其 id 由 avatarService 统一生成连接时再据此 getElementById。下面是舞台组件的核心结构// src/components/AvatarStage.tsx export default function AvatarStage({ containerId, connected, connecting, listening, subtitle }: Props) { return ( div classNamestage div classNamestage__halo / div classNamestage__frame glass {/* SDK 渲染容器id 必须在 connect 前存在 */} div classNamestage__box div id{containerId} classNamestage__sdk / /div {/* 语音监听态、字幕、加载态叠加层... */} /div /div ) }容器 id 的生成用了加密随机数保证多实例不冲突// src/utils/index.ts export function generateContainerId(): string { const bytes crypto.getRandomValues(new Uint8Array(8)) let randomID for (let i 0; i bytes.length; i) { randomID bytes[i].toString(16).padStart(2, 0) } return ${APP_CONFIG.CONTAINER_PREFIX}${randomID} }这里有一个曾踩过的坑魔珐 SDK 会在容器内层注入 overflow: hidden导致数字人抬手动作被裁切。解决办法是在样式里强制覆盖内层容器的溢出行为并让 canvas / video 以 contain 方式自适应/* src/components/AvatarStage.css */ .stage__sdk div { overflow: visible !important; } .stage__sdk canvas, .stage__sdk video { width: auto; height: auto; max-width: none; max-height: 100%; object-fit: contain; background: transparent !important; }5.2 流式断句播报引擎让具身交互智能边想边说这是整个具身交互智能体验的心脏。如果等大模型把整段话生成完再播报访客会经历一段尴尬的沉默正确做法是边生成、边断句、边说。控制器里的 sendUserText 一边消费 LLM 的流式输出、一边按标点断句把可读的句子交给播报队列// src/services/avatar-controller.ts const cnSplitSign /[。… ]/ // 中文断句标点 const enSplitSign /[.?!;:,]/ // 英文断句标点 const stream await llmService.sendMessageWithStream({ provider: openai, model: LLM_CONFIG.DEFAULT_MODEL, apiKey: API_KEYS.LLM.apiKey }, trimmed) if (!stream) return const minimum 20 const context { cache: , chars: 0, firstSpeakSend: false, spaceCount: 0 } for await (const content of stream) { if (typeof content ! string) continue // 累积到最后一条 assistant 气泡用于文字展示 this.appendToLastAssistant(content) 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) { // 首句门槛更高中文按可读字数、英文按空格(词)数达阈值且遇断句标点才播 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 } this.actionManager.speak(context.cache, { isStart: !context.firstSpeakSend, isEnd: false }) context.firstSpeakSend true context.cache context.chars 0 context.spaceCount 0 } // 处理剩余缓存末段标记 isEndtrue触发数字人自然收尾 if (context.cache.length 0) { this.actionManager.speak(context.cache, { isStart: !context.firstSpeakSend, isEnd: true }) } else if (context.firstSpeakSend) { this.actionManager.speak(, { isStart: false, isEnd: true }) }关键逻辑有三点中英文双断句、首句优先中文按累计可读字数超过 minimum20、英文按空格词数超过阈值再加对应的断句标点才触发首句播报避免开口就是半个词首句发出后遇标点即播让节奏跟上语流。文字与语音双通道appendToLastAssistant 把每个字符实时追加到对话气泡访客既能听、也能看两条通道并行。末段收尾流结束后把剩余缓存以 isEndtrue 播报触发数字人自然收尾避免最后一句被吞掉。而真正把文本变成开口说话的是 ActionManager——一个串行的 SSML 播报队列。它保证即便流式片段陆续到达数字人也严格按顺序、不抢话地逐句播报// src/services/action-manager.ts private async processQueue() { if (this.isSpeaking) return if (!this.queue.length) return const instance this.getInstance() if (!instance) return this.isSpeaking true while (this.queue.length) { const item this.queue.shift() if (!item) break this.onVoiceReady?.() instance.speak(item.ssml, item.isStart, item.isEnd) // 调用 SDK 播报 if (!item.isEnd) continue // 流式中间段继续取下一段 await new Promise(r setTimeout(r, 50)) } this.isSpeaking false this.onVoiceEnd?.() }每段文本在入队前都会被包装成 SSML并转义特殊字符避免破坏语音合成协议// src/utils/index.ts export function generateSSML(text: string, options: { pitch?: number speed?: number volume?: number } {}): string { const { pitch 1, speed 1, volume 1 } options const encodeMap: Recordstring, string { : lt;, : gt;, : apos;, : quot;, : amp; } const processedText text .replace(/\n/g, \n) .replace(/[]/g, (str) encodeMap[str] || str) return speak pitch${pitch} speed${speed} volume${volume}${processedText}/speak }5.3 实时打断与等待空闲让对话可以被插话真人对话是可以被打断的。当访客在 kiki 还在说话时又发了新消息必须先让她停下再应答新问题。interrupt 负责清空队列并调用 SDK 的打断接口// src/services/avatar-controller.ts interrupt(): void { if (!this.instance) return try { this.actionManager.reset() if (typeof this.instance.interactiveidle function) { this.instance.interactiveidle() } else if (typeof this.instance.interrupt function) { this.instance.interrupt() } } catch (error) { console.error(打断失败:, error) this.patch({ avatarState: interactive_idle }) } }打断之后不能立刻抢话需要等数字人真正进入空闲态。这里用一个可被回调兑现的 Promise 超时兜底private waitForAvatarIdle(timeout 5000): Promisevoid { if (this.state.avatarState interactive_idle || this.state.avatarState ) { return Promise.resolve() } return new Promise((resolve, reject) { let resolved false const timeoutId setTimeout(() { if (!resolved) { resolved true this.voiceEndResolver null reject(new Error(等待虚拟人停止说话超时)) } }, timeout) // 由 SDK 的 onVoiceStateChange(end) 回调兑现 this.voiceEndResolver () { if (!resolved) { resolved true clearTimeout(timeoutId) resolve() } } }) }5.4 语音输入点亮麦克风直接说话具身交互智能的听由腾讯云实时语音识别承担。React 版把它封装成 useAsr Hook识别到整句结束OnSentenceEnd就回调交给控制器发送// src/hooks/useAsr.ts节选 recognizer.OnRecognitionResultChange (res) { const currentText res.result?.voice_text_str if (currentText) setAsrText(currentText) // 实时中间结果用于字幕 } recognizer.OnSentenceEnd (res) { const resultText res.result?.voice_text_str if (resultText) { setAsrText(resultText); callbacks.onFinished(resultText) } }在互动区里麦克风按钮先打断当前播报、再启动识别识别到完整一句后停止并把文本作为用户输入发送// src/components/Interactive.tsx节选 const toggleMic () { if (!connected) return if (asr.isListening) { asr.stop(); return } interrupt() asr.start({ onFinished: (finalText) { asr.stop(); if (finalText.trim()) sendUserText(finalText.trim()) }, onError: () { setError(语音识别出错请检查麦克风权限); asr.stop() } }) }5.5 框架无关的状态核心单例 useSyncExternalStore为了让数字人逻辑不绑死 React控制器自己维护状态与订阅者集合patch 时通知所有订阅者// src/services/avatar-controller.ts subscribe (listener: () void) { this.listeners.add(listener) return () this.listeners.delete(listener) } getSnapshot () this.state private patch(partial: PartialAvatarStoreState) { this.state { ...this.state, ...partial } this.listeners.forEach(l l()) }React 侧只需一个 Hook 就能把这个外部 store 接进组件的渲染天然并发安全// src/hooks/useAvatar.ts export function useAvatar() { const state useSyncExternalStore( avatarController.subscribe, avatarController.getSnapshot, avatarController.getSnapshot ) return { ...state, connect: () avatarController.connect(), disconnect: () avatarController.disconnect(), sendUserText: (text: string) avatarController.sendUserText(text), interrupt: () avatarController.interrupt(), getContainerId: () avatarService.getContainerId() } }这样一来无论未来换到什么框架只要能订阅这个单例、把容器 id 渲染出来数字人就能跑起来。六、作品区让精选作品与创意实验看得见作品区分海报与视频两类。海报类作品使用设计感封面图视频类作品在封面之上叠加 CSS 流光扫过动效与居中播放角标即便暂无真实 mp4 也有动态视觉视频路径是可选字段一旦放入真实文件即自动播放// src/components/Works.tsx节选 div classNameworks__cover style{{ background: w.gradient }} {w.video ? ( video classNameworks__cover-video poster{w.cover} autoPlay muted loop playsInline source src{w.video} typevideo/mp4 / /video ) : ( img classNameworks__cover-img src{w.cover} alt{w.title} loadinglazy / )} {/* 视频作品流光动效 播放角标 */} {w.type video ( span classNameworks__sheen / span classNameworks__play aria-hiddentrue▶/span / )} /div数据层用一个可选的 video 字段承接这套有则播放、无则动态封面的降级策略让作品区无论素材是否就位都不空场。七、总结一次具身交互智能的轻量落地kiki’s space 证明了一件事具身交互智能不必是重型系统它可以是一个个人作品集里的一位会客厅主理人。我们做的其实是三件事用一套暖色玻璃态设计系统把温暖、梦幻、克制、科技感落成可复用的 CSS 令牌用分层迁移策略把数字人全链路从 Vue 无损搬到 React核心逻辑与框架彻底解耦用流式断句播报 实时打断 语音输入让访客与 kiki 的对话接近真人交流的节奏。对访客而言作品不再是静止的陈列而是可以随时被追问、被回应的对话入口——这正是具身交互智能带来的体验跃迁。如果你也想让自己的作品、品牌或空间开口说话欢迎把这套思路带走让具身交互智能在更多创意场景里成为那位懂你、也会表达的伙伴。魔珐星云 PC 端官方链接https://xingyun3d.com?utm_campaigndailyutm_sourceCSDNwanfen3utm_mediumutm_termutm_content
郑州网站建设
网页设计
企业官网