点击即讲解,开口即陪伴:具身交互智能驱动的 Digital Garden 数字花园
一位住在奇幻夜花园里的花语向导萤语,能听、能说、有肢体表达——这正是具身交互智能的核心。本文从魔珐星云控制台配置到 React 18 + Vite 7 工程源码,拆解 Digital Garden 如何以参数流 SDK 为中枢,让数字人嵌入点选→讲解→对话→诊断的园艺交互全链路。
魔珐星云 PC 端官方链接:https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen2&utm_medium=&utm_term=&utm_content=
一、产品概览:当数字人成为花园向导
Digital Garden(数字花园)是一个把数字人嵌入园艺交互流程的 Web 应用。它的核心体验不是"输入问题 → 返回答案"的简单问答流,而是一段完整的花园对话:
- 用户在深夜的奇幻花园落地页点击"进入花园"
- 数字人萤语自动连接并语音问候,以浮层形式常驻右下角
- 用户点击任意一株发光植物(月光蕨、星辰藤、萤心兰、霓虹草、梦境蒲、烬焰花)
- 萤语用流式语音讲解该植物的花语、习性与养护要点,同时字幕同步展示
- 用户在对话面板继续提问,萤语以园艺顾问身份给出温柔诊断
在这条链路中,数字人不是装饰——她是具身交互智能的载体:能听、能说、有肢体表达,在园艺交互过程中提供实时的语音反馈与情感陪伴。这区别于传统的植物百科工具,把 AI 的语言能力与人的园艺探索绑定在同一次交互里。
二、技术架构总览
项目采用 React 18 + Vite 7 + TypeScript 技术栈,整体分为五层:
| 层级 | 职责 | 核心文件 |
|---|---|---|
| 视图组件层 | 落地页 / 花园场景 / 数字人浮层 / 对话面板 | App.tsx, Landing.tsx, GardenScene.tsx, GardenGuide.tsx, ChatPanel.tsx, AvatarStage.tsx |
| 状态层 | 视图切换 / 消息 / 当前植物 / 流式状态 | hooks/useGardenStore.ts (zustand) |
| 服务层 | SDK 连接 / 流式播报 / LLM / 串行队列 | services/* |
| 常量层 | 密钥 / 身份 / 系统提示词 / 植物百科 | constants/index.ts |
| 设计系统 | 奇幻夜花园 CSS 令牌 | index.css |
数字人核心逻辑以框架无关的单例 AvatarController 承载,通过订阅机制对接 React 的 useSyncExternalStore。这意味着数字人的连接、断句播报、实时打断等能力不依赖 React 的渲染周期,可以在任何框架中复用——这是具身交互智能落地的架构基础。
三、前提准备
魔珐星云控制台配置
数字人萤语的形象、场景、音色、表演均由魔珐星云控制台驱动。以下五步完成驱动应用创建与参数配置,是代码层接入 SDK 前的必要前提。
步骤1:魔珐星云控制台创建驱动应用
在魔珐星云控制台创建一个新的驱动应用,获取 appId 与 appSecret,这两个密钥将填入项目 constants/index.ts。

步骤2:形象配置
为数字人萤语选择或上传形象资源,配置肤色、发型、服装等外观参数。形象配置决定了 SDK 渲染时的角色外观,是具身交互智能中"有形象"这一要素的直接来源。

步骤3:场景配置
配置数字人所在的虚拟场景,包括灯光、背景、机位等。场景配置与前端 AvatarStage 容器配合,决定数字人在页面中的视觉呈现方式。

步骤4:音色配置
为萤语选择音色,控制语音合成的音色、语速、情感倾向。音色配置直接影响流式断句播报时的听感,是具身交互智能中"能说"这一要素的质量基础。

步骤5:表演配置
配置数字人的肢体动作与表情,包括待机动作、说话时的手势、情绪表情等。表演配置让数字人在朗读时有自然的肢体表达,而非僵硬的站桩——这是具身交互智能区别于纯语音助手的视觉支柱。

工程初始化
步骤6:创建项目并安装依赖
npm create vite@latest garden-space -- --template react-ts
cd garden-space
npm install zustand
项目的 package.json 锁定了以下依赖:
{
"dependencies": {
"openai": "5.12.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"zustand": "^4.5.5"
},
"devDependencies": {
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "~5.8.3",
"vite": "7.1.2"
}
}
- openai 5.12 以 dangerouslyAllowBrowser 模式直连豆包模型 API
- zustand 4.5 提供轻量全局状态(视图切换 / 消息列表 / 当前植物 / 流式状态)
- 数字人 SDK 通过 index.html 的 script 标签引入,无需 npm 安装
步骤7:配置 Vite 构建端口
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 5277
}
})
步骤8:填入魔珐星云与 LLM 密钥
将步骤1获取的 appId / appSecret 及 LLM 密钥填入 src/constants/index.ts(此处为脱敏占位):
export const API_KEYS = {
AVATAR: {
appId: 'your_avatar_app_id',
appSecret: 'your_avatar_app_secret'
},
LLM: {
apiKey: 'your_llm_api_key'
}
} as const
同时配置 SDK 网关与 LLM 参数:
export const SDK_CONFIG = {
GATEWAY_URL: 'https://nebula-agent.xingyun3d.com/user/v1/ttsa/session',
DATA_SOURCE: '2',
CUSTOM_ID: 'demo'
} as const
export const LLM_CONFIG = {
BASE_URL: 'https://ark.cn-beijing.volces.com/api/v3',
DEFAULT_MODEL: 'doubao-1-5-pro-32k-250115',
TEMPERATURE: 0.85,
MAX_TOKENS: 1024
} as const
数字人身份与园艺顾问系统提示词也在常量层定义,贯穿整个应用的问候语、字幕、名牌:
export const AVATAR_IDENTITY = {
name: '萤语',
role: '夜花园花语向导',
greeting: '欢迎来到数字花园。我是夜花园的花语向导萤语,这片会发光的花园由我来照料,也由我来陪你一起认识它们。你可以和我说话,或者点一株植物,让我告诉你它的故事。',
farewell: '愿你心里,也留一束光。'
} as const
export const GARDEN_SYSTEM_PROMPT = `你是「萤语」,数字花园里夜花园的花语向导与 AI 园艺顾问。你住在一片会发光的奇幻夜花园里,照料着月光蕨、星辰藤、萤心兰、霓虹草、梦境蒲、烬焰花这些发光植物。
语言风格:温柔、诗意、带一点植物学家式的细致,像夜晚提着萤笼走过花径的向导。
你的能力:
1. 植物百科:介绍花园里任意一株发光植物的习性、光色、元素属性、花语与养护要点。
2. 养护诊断:根据用户描述的植物状态(叶色变暗、光弱、萎蔫、不开花),给出温柔的照料建议。
3. 花语陪伴:偶尔用一两句诗意的句子回应,但不堆砌辞藻。
回答要求:简洁口语化,适合语音播报,单次回答控制在 80 字以内;不要分点编号;不要使用 Markdown 标记。`
五、核心代码讲解



5.1 数字人控制器:流式断句播报引擎
AvatarController 是整个具身交互智能链路的中枢。它管理 SDK 连接、流式 LLM 断句、串行 SSML 队列、实时打断,并通过订阅机制对接 React。
流式断句是其中最关键的逻辑——LLM 逐字返回内容,但数字人不能每收到一个字就说一次,需要等积累到一定字数且遇到标点时才截断发送:
async *streamChat(userText: string, systemPrompt?: string): AsyncIterable<string> {
const trimmed = userText.trim()
if (!trimmed || !this.instance) return
if (!validateConfig(API_KEYS.LLM, ['apiKey'])) return
// 打断正在说话的数字人
this.interrupt()
if (this.state.avatarState === 'speak') {
try {
await this.waitForAvatarIdle()
} catch {
// 即使超时也继续,避免阻塞
}
}
this.actionManager.reset()
const messages: ChatCompletionMessage[] = systemPrompt
? [{ role: 'system', content: systemPrompt }, { role: 'user', content: trimmed }]
: [{ role: 'user', content: trimmed }]
const stream = llmService.sendMessageWithStream(messages)
const minimum = 20
const context = { cache: '', chars: 0, firstSpeakSend: false, spaceCount: 0 }
try {
for await (const content of stream) {
if (typeof content !== 'string') continue
yield 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
}
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 })
}
} catch (error) {
console.error('streamChat 失败:', error)
}
}
关键逻辑分析:
- systemPrompt 参数让调用方可以注入园艺顾问人格,这是 Digital Garden 对话面板能维持萤语角色一致性的关键
- minimum = 20 是首句最低字数门槛:中文按 cnSplitSign 检测句号/逗号,英文按 enSplitSign 检测标点 + 空格计数,首句积累够 20 字且遇到标点才发送,避免数字人开口太快
- yield content 让 React 组件可以同步消费每个 chunk 实时渲染字幕,数字人播报与字幕展示同源、零延迟
- isStart / isEnd 标记贯穿整个队列:首句标记 isStart 告知 SDK 开始一轮播报,末句标记 isEnd 告知 SDK 结束,中间句两端都不标记——这是魔珐参数流 SDK 串行播报的协议要求
5.2 串行 SSML 队列:保证不抢话
ActionManager 确保即便流式片段陆续到达,数字人也严格按顺序逐句播报:
export class ActionManager {
private queue: ActionQueueItem[] = []
private isSpeaking = false
private getInstance: () => any | null
private onVoiceReady?: () => void
private onVoiceEnd?: () => void
constructor(options: ActionManagerOptions) {
this.getInstance = options.getInstance
this.onVoiceReady = options.onVoiceReady
this.onVoiceEnd = options.onVoiceEnd
}
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()
}
reset() {
this.queue = []
this.isSpeaking = false
}
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)
if (!item.isEnd) continue
await new Promise((r) => setTimeout(r, 50))
}
this.isSpeaking = false
this.onVoiceEnd?.()
}
}
关键逻辑分析:
- isSpeaking 互斥锁保证队列串行执行:流式片段可能在 50ms 内连续到达三条,队列会先推入 buffer 再逐条消费,不会并行调用 SDK 的 speak
- isEnd 标记决定节奏:遇到 isEnd 的片段会额外等待 50ms,给 SDK 一个句子结束的信号;非 isEnd 片段连续推送不等待,保证长句中间不断顿
- reset() 用于实时打断场景:用户在数字人说话时发新消息,streamChat 开头调用 interrupt() → actionManager.reset() 清空队列,数字人立刻停嘴进入新对话——这是具身交互智能中"随时可打断"的实现
5.3 花园场景:植物卡片 + 数字人讲解
GardenScene 是花园主场景,点击植物卡片触发数字人语音讲解:
export default function GardenScene() {
const avatar = useAvatar()
const activePlant = useGardenStore((s) => s.activePlant)
const setActivePlant = useGardenStore((s) => s.setActivePlant)
const addMessage = useGardenStore((s) => s.addMessage)
const setView = useGardenStore((s) => s.setView)
const handlePlantClick = (plant: GardenPlant) => {
setActivePlant(plant)
// 讲解词同时进对话区,便于回看
addMessage({ role: 'assistant', text: plant.voice })
// 数字人在线则语音播报
if (avatar.connected) {
avatar.speakText(plant.voice)
}
}
return (
<main className="garden">
<header className="garden__header">
<button className="garden__back" onClick={() => setView('landing')}>
<span className="garden__back-arrow">←</span>
<span className="garden__back-text">回入口</span>
</button>
<div className="garden__heading">
<h2 className="garden__title">{APP_META.nameZh}</h2>
<p className="garden__sub">
点一株植物,听 {AVATAR_IDENTITY.name} 讲它的故事;或在下方开口说话,让它为你诊断养护
</p>
</div>
</header>
<section className="garden__plants">
{PLANTS.map((plant) => (
<button
key={plant.id}
className={`plant-card${activePlant?.id === plant.id ? ' is-active' : ''}`}
style={{ '--plant-glow': plant.glow } as CSSProperties}
onClick={() => handlePlantClick(plant)}
>
<div className="plant-card__orb" />
<div className="plant-card__body">
<h3 className="plant-card__name">{plant.name}</h3>
<em className="plant-card__alias">{plant.alias}</em>
<p className="plant-card__haiku">{plant.haiku}</p>
<span className="plant-card__element">{plant.element}</span>
</div>
<span className="plant-card__hint">点击听讲解</span>
</button>
))}
</section>
{/* 当前选中植物详情 */}
{activePlant && (
<section className="garden__detail glass">
<div
className="garden__detail-orb"
style={{ '--plant-glow': activePlant.glow } as CSSProperties}
/>
<div className="garden__detail-text">
<div className="garden__detail-head">
<h3>{activePlant.name}</h3>
<em>{activePlant.alias}</em>
<span className="garden__detail-element">{activePlant.element}</span>
</div>
<p className="garden__detail-intro">{activePlant.intro}</p>
<p className="garden__detail-care-title">养护要点</p>
<ul className="garden__detail-care">
{activePlant.care.map((c, i) => (
<li key={i}>{c}</li>
))}
</ul>
</div>
</section>
)}
<ChatPanel />
</main>
)
}
关键逻辑分析:
- handlePlantClick 同时做三件事:更新当前植物状态、把讲解词写入对话区、调用 speakText 让数字人语音播报——这是具身交互智能中"点击即讲解"的核心交互
- style={{ ‘–plant-glow’: plant.glow }} 把每株植物的荧光色作为 CSS 变量注入,卡片光晕、详情面板光球都由这个变量驱动,实现数据驱动的视觉表现
- 植物卡片网格用 3×2 响应式布局,窄屏自动降为单列,保证移动端可用
5.4 对话面板:双通道流式对话
ChatPanel 是对话面板,数字人在线时走 streamChat(语音播报 + 字幕),离线时降级为纯文字流式:
export default function ChatPanel() {
const avatar = useAvatar()
const messages = useGardenStore((s) => s.messages)
const streaming = useGardenStore((s) => s.streaming)
const streamingText = useGardenStore((s) => s.streamingText)
const addMessage = useGardenStore((s) => s.addMessage)
const setStreaming = useGardenStore((s) => s.setStreaming)
const setStreamingText = useGardenStore((s) => s.setStreamingText)
const appendStreamingText = useGardenStore((s) => s.appendStreamingText)
const commitStreaming = useGardenStore((s) => s.commitStreaming)
const resetStreaming = useGardenStore((s) => s.resetStreaming)
const clearChat = useGardenStore((s) => s.clearChat)
const [input, setInput] = useState('')
const listRef = useRef<HTMLDivElement>(null)
useEffect(() => {
listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: 'smooth' })
}, [messages, streamingText])
const handleSend = async () => {
const text = input.trim()
if (!text || streaming) return
setInput('')
addMessage({ role: 'user', text })
setStreaming(true)
setStreamingText('')
const llmMessages: ChatCompletionMessage[] = [
{ role: 'system', content: GARDEN_SYSTEM_PROMPT },
{ role: 'user', content: text }
]
try {
if (avatar.connected) {
// 数字人在线:语音播报 + 字幕流式
for await (const chunk of avatar.streamChat(text, GARDEN_SYSTEM_PROMPT)) {
appendStreamingText(chunk)
}
} else {
// 数字人离线:纯文字流式降级
for await (const chunk of llmService.sendMessageWithStream(llmMessages)) {
appendStreamingText(chunk)
}
}
commitStreaming()
} catch (e) {
console.error('[ChatPanel] 对话失败:', e)
resetStreaming()
}
}
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
return (
<section className="chat glass">
<div className="chat__head">
<span className="chat__dot" data-on={avatar.connected} />
<span className="chat__title">与 {AVATAR_IDENTITY.name} 对话</span>
<button className="chat__clear" onClick={clearChat} title="清空对话">清空</button>
</div>
<div className="chat__list" ref={listRef}>
{messages.length === 0 && !streaming && (
<div className="chat__empty">
<p className="chat__empty-tip">说点什么吧,比如:</p>
<ul className="chat__empty-list">
<li>我的月光蕨光变暗了怎么办?</li>
<li>介绍一下烬焰花</li>
<li>新手适合种哪一株?</li>
</ul>
</div>
)}
{messages.map((m, i) => (
<div key={i} className={`chat__msg chat__msg--${m.role}`}>
<span className="chat__role">{m.role === 'user' ? '我' : AVATAR_IDENTITY.name}</span>
<p className="chat__bubble">{m.text}</p>
</div>
))}
{streaming && (
<div className="chat__msg chat__msg--assistant">
<span className="chat__role">{AVATAR_IDENTITY.name}</span>
<p className="chat__bubble">
{streamingText || '…'}
<span className="chat__cursor" />
</p>
</div>
)}
</div>
<div className="chat__input">
<textarea
className="chat__textarea"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={avatar.connected ? '按 Enter 发送,Shift+Enter 换行' : '数字人连接中,仍可文字对话…'}
rows={2}
/>
<button
className="chat__send"
onClick={handleSend}
disabled={streaming || !input.trim()}
>
{streaming ? '生成中' : '发送'}
</button>
</div>
</section>
)
}
关键逻辑分析:
- avatar.connected 判断决定走哪条通道:在线时 streamChat 同时驱动语音播报和字幕流式,离线时 llmService 只驱动文字——这是具身交互智能的优雅降级设计
- GARDEN_SYSTEM_PROMPT 注入萤语的园艺顾问人格,约束回答风格(温柔诗意、80字以内、口语化),保证语音播报的听感
- appendStreamingText 逐 chunk 追加,用户看到字幕实时生长,与数字人播报同步——字幕与语音同源,零延迟
5.5 数字人浮层:自动连接 + 问候播报
GardenGuide 是数字人浮层,组件挂载时自动连接 SDK,连接稳定后播报问候语:
export default function GardenGuide() {
const avatar = useAvatar()
const containerId = avatar.getContainerId()
const greeted = useRef(false)
useEffect(() => {
let mounted = true
if (!avatar.connected && !avatar.connecting) {
avatar
.connect()
.then(() => {
// 连接稳定后稍候播报问候,避免与 SDK 初始化抢资源
setTimeout(() => {
if (mounted && !greeted.current) {
greeted.current = true
avatar.speakText(AVATAR_IDENTITY.greeting)
}
}, 800)
})
.catch((e) => {
console.warn('[GardenGuide] 数字人连接失败,已降级为纯文字对话模式', e)
})
}
return () => {
mounted = false
avatar.disconnect()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<aside className="garden-guide">
<AvatarStage
containerId={containerId}
connected={avatar.connected}
connecting={avatar.connecting}
listening={avatar.listening}
subtitle={avatar.subtitle}
/>
</aside>
)
}
关键逻辑分析:
- useEffect 在组件挂载时自动 connect(),无需用户手动点击——这是具身交互智能中"主动迎接"的交互设计
- setTimeout 800ms 延迟播报问候,给 SDK 一个稳定窗口,避免初始化与播报抢资源导致卡顿
- greeted.current 用 ref 防止 StrictMode 下重复播报
- 连接失败时 catch 降级为纯文字模式,不阻塞用户使用——这是具身交互智能的容错设计
5.6 状态层:zustand 全局管理
useGardenStore 管理视图切换、消息列表、当前植物、流式状态:
export const useGardenStore = create<GardenState>((set, get) => ({
view: 'landing',
messages: [],
activePlant: null,
streaming: false,
streamingText: '',
setView: (view) => set({ view }),
addMessage: (m) => set((s) => ({ messages: [...s.messages, m] })),
setActivePlant: (activePlant) => set({ activePlant }),
setStreaming: (streaming) => set({ streaming }),
setStreamingText: (streamingText) => set({ streamingText }),
appendStreamingText: (t) => set((s) => ({ streamingText: s.streamingText + t })),
commitStreaming: () => {
const text = get().streamingText.trim()
if (text) {
set((s) => ({ messages: [...s.messages, { role: 'assistant', text }] }))
}
set({ streamingText: '', streaming: false })
},
resetStreaming: () => set({ streamingText: '', streaming: false }),
clearChat: () => set({ messages: [], streamingText: '', streaming: false })
}))
关键逻辑分析:
- commitStreaming 用 get() 读取当前 streamingText,trim 后写入 messages,再单独 set 清空流式状态——两步 set 保证"先提交再清空"的时序,避免流式文字丢失
- streaming / streamingText 分离设计:streaming 标记是否在生成中,streamingText 暂存生成中的文字,commitStreaming 时一次性写入 messages——避免流式文字频繁触发消息列表重渲染
- clearChat 同时清空 messages、streamingText、streaming 三个字段,保证清空对话时流式状态也被重置
六、视觉设计系统:奇幻夜花园
Digital Garden 的视觉气质是奇幻夜花园的深色霓虹与萤火粒子,由一组 CSS 令牌驱动:
:root {
--c-bg: #06080f;
--c-bg-soft: #0c1018;
--c-neon-green: #5effa8;
--c-neon-pink: #ff5ea8;
--c-neon-cyan: #5ed4ff;
--c-neon-purple: #a85eff;
--c-firefly: #ffd76a;
--c-text: #e8f5ff;
--c-text-muted: rgba(232, 245, 255, 0.5);
--c-text-soft: rgba(232, 245, 255, 0.72);
--c-border: rgba(94, 255, 168, 0.14);
--c-border-strong: rgba(94, 212, 255, 0.32);
--glass-bg: rgba(12, 16, 24, 0.6);
--glass-blur: 18px;
--glass-border: 1px solid rgba(94, 212, 255, 0.18);
--shadow-soft: 0 8px 32px rgba(0, 0, 0, 0.5);
--shadow-glow-green: 0 0 24px rgba(94, 255, 168, 0.28);
--shadow-glow-cyan: 0 0 24px rgba(94, 212, 255, 0.28);
--r-sm: 8px; --r-md: 16px; --r-lg: 24px;
--space-xs: 8px; --space-sm: 12px; --space-md: 20px; --space-lg: 32px; --space-xl: 48px;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
--ff-sans: 'Inter', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
--ff-serif: 'Cormorant Garamond', 'Noto Serif SC', Georgia, serif;
}
背景由四层径向渐变叠加,模拟夜花园的多色光晕:
body {
background:
radial-gradient(ellipse 55% 45% at 12% 8%, rgba(94, 255, 168, 0.14) 0%, transparent 60%),
radial-gradient(ellipse 45% 55% at 88% 12%, rgba(168, 94, 255, 0.16) 0%, transparent 60%),
radial-gradient(ellipse 70% 50% at 50% 100%, rgba(94, 212, 255, 0.12) 0%, transparent 70%),
radial-gradient(ellipse 40% 40% at 75% 80%, rgba(255, 94, 168, 0.1) 0%, transparent 65%),
var(--c-bg);
background-attachment: fixed;
}
- 深夜基底 #06080f 是夜花园的底色,荧光绿 #5effa8 是主交互色(植物光晕、按钮高亮),品红 #ff5ea8 / 青蓝 #5ed4ff / 紫 #a85eff 是四株植物的特色光色,萤火金 #ffd76a 是点缀色
- 四层径向渐变分别从左上(荧光绿)、右上(紫)、底部(青蓝)、右下(品红)投射,配合 background-attachment: fixed 让光晕不随滚动移动,营造夜花园的稳定氛围
- 玻璃材质 .glass 用 18px 毛玻璃 + 150% 饱和度提升,让对话面板和数字人浮层在深色背景上有"悬浮"感
七、总结
Digital Garden 把具身交互智能嵌入了一条完整的园艺交互链路:点选 → 讲解 → 对话 → 诊断。数字人萤语不是锦上添花的装饰,而是贯穿全程的花语向导——她自动迎接访客、用流式断句讲解植物、在对话中切换人格给出养护诊断、最终用语音告诉你一株植物的故事。
技术上,这套体验由三个关键设计支撑:
- 框架无关的数字人单例:AvatarController 以纯 TypeScript 类承载连接/断句/打断逻辑,useSyncExternalStore 桥接 React,保证核心能力可迁移、不随框架版本废弃
- 流式断句播报引擎:LLM 逐字返回、20 字门槛 + 标点检测截断、isStart/isEnd 标记串行队列,让数字人边生成边说、不抢话、可随时打断
- 双通道降级设计:数字人在线时走 streamChat(语音 + 字幕),离线时走 llmService(纯文字),保证可用性——这是具身交互智能的容错设计
这条链路的本质是具身交互智能——AI 不只是回答问题的工具,而是有形象、有语音、有肢体表达的花语向导,把语言能力与园艺探索绑定在同一次交互里。当用户听到萤语说出"这是月光蕨,花园里最安静的一株"的那一刻,园艺交互才真正闭环。
魔珐星云 PC 端官方链接:https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen2&utm_medium=&utm_term=&utm_content=
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)