一位住在奇幻夜花园里的花语向导萤语,能听、能说、有肢体表达——这正是具身交互智能的核心。本文从魔珐星云控制台配置到 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 应用。它的核心体验不是"输入问题 → 返回答案"的简单问答流,而是一段完整的花园对话:

  1. 用户在深夜的奇幻花园落地页点击"进入花园"
  2. 数字人萤语自动连接并语音问候,以浮层形式常驻右下角
  3. 用户点击任意一株发光植物(月光蕨、星辰藤、萤心兰、霓虹草、梦境蒲、烬焰花)
  4. 萤语用流式语音讲解该植物的花语、习性与养护要点,同时字幕同步展示
  5. 用户在对话面板继续提问,萤语以园艺顾问身份给出温柔诊断

在这条链路中,数字人不是装饰——她是具身交互智能的载体:能听、能说、有肢体表达,在园艺交互过程中提供实时的语音反馈与情感陪伴。这区别于传统的植物百科工具,把 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 把具身交互智能嵌入了一条完整的园艺交互链路:点选 → 讲解 → 对话 → 诊断。数字人萤语不是锦上添花的装饰,而是贯穿全程的花语向导——她自动迎接访客、用流式断句讲解植物、在对话中切换人格给出养护诊断、最终用语音告诉你一株植物的故事。

技术上,这套体验由三个关键设计支撑:

  1. 框架无关的数字人单例:AvatarController 以纯 TypeScript 类承载连接/断句/打断逻辑,useSyncExternalStore 桥接 React,保证核心能力可迁移、不随框架版本废弃
  2. 流式断句播报引擎:LLM 逐字返回、20 字门槛 + 标点检测截断、isStart/isEnd 标记串行队列,让数字人边生成边说、不抢话、可随时打断
  3. 双通道降级设计:数字人在线时走 streamChat(语音 + 字幕),离线时走 llmService(纯文字),保证可用性——这是具身交互智能的容错设计

这条链路的本质是具身交互智能——AI 不只是回答问题的工具,而是有形象、有语音、有肢体表达的花语向导,把语言能力与园艺探索绑定在同一次交互里。当用户听到萤语说出"这是月光蕨,花园里最安静的一株"的那一刻,园艺交互才真正闭环。

魔珐星云 PC 端官方链接https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen2&utm_medium=&utm_term=&utm_content=

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐