一位住在墨色画室里的共画艺术伙伴墨白,能听、能说、有肢体表达——这正是具身交互智能的核心。本文从魔珐星云控制台配置到 React 18 + Vite 7 工程源码,拆解 Collab Canvas 如何以参数流 SDK 为中枢,让数字人嵌入涂鸦→对话→凝练→成画→留存的全链路创作流程。

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


一、产品概览:当数字人成为画室伙伴

Collab Canvas(与 AI 共画)是一个把数字人嵌入创作流程的 Web 应用。它的核心体验不是"输入文字 → 生成图片"的简单工具流,而是一段完整的画室对话:

  1. 用户在宣纸质感的高 DPI 画板上涂鸦几笔
  2. 与数字人墨白进行流式语音对话,描述此刻想画什么
  3. 点击凝练成画,墨白分析整段对话,输出风格 / 情绪 / 色板 / 画面意象的 JSON 报告
  4. 报告被组装成文生图 prompt,生成一幅水墨水彩风格的成品画
  5. 涂鸦底稿 + 成品画 + 对话记录一并存入 IndexedDB 作品墙

在这条链路中,数字人不是装饰——她是具身交互智能的载体:能听、能说、有肢体表达,在创作过程中提供实时的语音反馈与情感陪伴。这区别于传统的图文分离工具,把 AI 的语言能力与人的视觉创作绑定在同一次交互里。


二、技术架构总览

项目采用 React 18 + Vite 7 + TypeScript 技术栈,整体分为五层:

层级 职责 核心文件
视图组件层 落地页 / 工作室 / 作品墙三视图 App.tsx, Studio.tsx, DrawCanvas.tsx, AvatarStage.tsx
状态层 视图切换 / 消息 / 作品 CRUD stores/app.ts (zustand + idb-keyval)
服务层 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 collab-canvas -- --template react-ts
cd collab-canvas
npm install zustand idb-keyval openai

项目的 package.json 锁定了以下依赖:

{
  "dependencies": {
    "idb-keyval": "^6.2.1",
    "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"
  }
}
  • zustand 4.5 提供轻量全局状态(视图切换 / 消息列表 / 作品集)
  • idb-keyval 6.2 用于 IndexedDB 持久化作品墙
  • openai 5.12 以 dangerouslyAllowBrowser 模式直连豆包模型 API
步骤7:配置 Vite 构建端口
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    port: 5276
  }
})
步骤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'
  },
  IMAGE: {
    apiKey: 'your_image_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 IMAGE_CONFIG = {
  SIZE: 'landscape_4_3' as const,
  API_BASE: 'https://trae-api-cn.mchost.guru/api/ide/v1'
} as const

数字人身份也在常量层定义,贯穿整个应用的问候语、字幕、名牌:

export const AVATAR_IDENTITY = {
  name: '墨白',
  role: '共画艺术伙伴',
  greeting: '欢迎来到共画工作室。我是你的共画伙伴墨白。今天想一起画点什么?',
  farewell: '愿你此刻,有所成画。'
} as const

export const STORAGE_KEY = 'collab-artworks' as const

四、技术选型与产品决策

1. 为什么选 React 18 而非 Vue? Collab Canvas 需要在画板、对话舱、数字人三个高频更新的区域之间并发渲染。React 18 的并发渲染能力配合 useSyncExternalStore,可以让数字人单例的状态更新不触发整树重渲染——画板绘制不会被字幕刷新打断。这是具身交互智能场景下多通道并发的刚需。

2. 为什么数字人逻辑写成框架无关单例? 数字人 SDK 的连接、断句、播报是有状态的时序逻辑,不属于任何一次渲染。把 AvatarController 写成纯 TypeScript 类,再通过 useSyncExternalStore 桥接 React,可以保证核心逻辑可测试、可迁移、不随框架版本变化而废弃。

3. 为什么作品存在 IndexedDB 而非后端? 共画是一个偏个人化的创作场景,用户不需要登录就能在本地留存作品。idb-keyval 的 get/set/del/keys 四个 API 足以覆盖作品墙的 CRUD,省去后端部署成本,让整个应用纯前端即可运行。


五、核心代码讲解

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 = systemPrompt
    ? [{ role: 'system' as const, content: systemPrompt }, { role: 'user' as const, content: trimmed }]
    : undefined

  const stream = messages
    ? llmService.sendMessageWithStream(messages)
    : llmService.sendMessageWithStream({ provider: 'openai', model: LLM_CONFIG.DEFAULT_MODEL, apiKey: API_KEYS.LLM.apiKey }, trimmed)

  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 参数让调用方可以注入不同角色的人格(共画伙伴 vs 画作分析师),这是 Collab Canvas 对话舱与凝练成画两个场景共用一个控制器的关键
  • 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 共画画板:高 DPI Canvas + 宣纸底

DrawCanvas 通过 forwardRef + useImperativeHandle 暴露 clear / exportDataURL / hasStrokes 三个方法给父组件,画板核心逻辑如下:

const DrawCanvas = forwardRef<DrawCanvasHandle, Props>(
  ({ tool, color, brushSize, onStrokeEnd }, ref) => {
    const canvasRef = useRef<HTMLCanvasElement>(null)
    const ctxRef = useRef<CanvasRenderingContext2D | null>(null)
    const drawingRef = useRef(false)
    const lastPtRef = useRef<{ x: number; y: number } | null>(null)
    const [hasStrokes, setHasStrokes] = useState(false)

    useEffect(() => {
      const canvas = canvasRef.current
      if (!canvas) return
      const dpr = window.devicePixelRatio || 1
      const rect = canvas.getBoundingClientRect()
      canvas.width = rect.width * dpr
      canvas.height = rect.height * dpr
      const ctx = canvas.getContext('2d')
      if (!ctx) return
      ctx.scale(dpr, dpr)
      ctx.lineCap = 'round'
      ctx.lineJoin = 'round'
      ctxRef.current = ctx
      paintPaper(ctx, rect.width, rect.height)
    }, [])

    const paintPaper = (ctx: CanvasRenderingContext2D, w: number, h: number) => {
      ctx.fillStyle = PAPER_BG
      ctx.fillRect(0, 0, w, h)
      ctx.save()
      ctx.globalAlpha = 0.04
      for (let i = 0; i < 200; i++) {
        ctx.fillStyle = Math.random() > 0.5 ? '#5b8a9e' : '#2a2a2a'
        ctx.fillRect(Math.random() * w, Math.random() * h, 1.5, 1.5)
      }
      ctx.restore()
    }

关键逻辑分析:

  • 高 DPI 适配:canvas.width = rect.width * dpr 将画布物理像素放大到设备像素比倍数,再 ctx.scale(dpr, dpr) 将绘图坐标系缩放回来,保证 Retina 屏下线条不糊
  • paintPaper 宣纸底:先用宣纸米 #e8e0d0 铺底,再以 4% 透明度随机散布 200 个墨青/墨黑噪点,模拟宣纸纤维质感——这让用户落笔时有"在纸上画"的体感,而非冰冷的白色 canvas
  • drawingRef / lastPtRef 用 ref 而非 state 存绘画中间态:pointermove 事件每秒触发数十次,用 state 会触发大量重渲染,ref 不触发渲染,绘制流畅度取决于设备性能而非 React 调度

useImperativeHandle 暴露的接口让 Studio 组件可以在凝练成画时导出涂鸦底稿:

useImperativeHandle(ref, () => ({
  clear: () => {
    const ctx = ctxRef.current
    const canvas = canvasRef.current
    if (!ctx || !canvas) return
    const rect = canvas.getBoundingClientRect()
    paintPaper(ctx, rect.width, rect.height)
    setHasStrokes(false)
  },
  exportDataURL: () => {
    const canvas = canvasRef.current
    if (!canvas || !hasStrokes) return null
    return canvas.toDataURL('image/png')
  },
  hasStrokes: () => hasStrokes
}))

5.4 凝练成画:从对话到画作的全链路

Studio 组件中的 handleConjure 是整个应用的高潮——把对话 + 涂鸦凝练成一幅画:

const handleConjure = useCallback(async () => {
  if (generating) return
  const sketch = canvasRef.current?.exportDataURL() ?? ''
  if (messages.length < 2) {
    addMessage({ role: 'assistant', text: '先和我聊聊你想画什么,再凝练成画吧。' })
    avatar.speakText('先和我聊聊你想画什么,再凝练成画吧。')
    return
  }

  setGenerating(true)
  try {
    const analysis = await analyzeArtwork(messages)
    const imageUrl = generateArtworkImage(analysis)
    const art: Artwork = {
      id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
      createdAt: Date.now(),
      sketch,
      ...analysis,
      imageUrl,
      conversation: messages.map((m) => `${m.role === 'user' ? '访客' : '墨白'}${m.text}`).join('\n')
    }
    await saveArtwork(art)
    setCurrentArtwork(art)

    const speakText = `我为你凝练了一幅画,叫《${analysis.title}》。${analysis.caption}`
    avatar.speakText(speakText)
    addMessage({ role: 'assistant', text: speakText })
  } catch (err) {
    console.error('凝练成画失败:', err)
    avatar.speakText('这一次墨色未散,我们再画一笔试试。')
  } finally {
    setGenerating(false)
  }
}, [generating, messages, addMessage, avatar, saveArtwork, setCurrentArtwork])

关键逻辑分析:

  • sketch = canvasRef.current?.exportDataURL() 先从画板导出涂鸦底稿的 dataURL,存入 Artwork.sketch 字段——这样作品墙里既能看到 AI 成品图也能看到用户的原始涂鸦,两条创作轨迹并存
  • analyzeArtwork(messages) 把整段对话发给 LLM,用 CANVAS_ARTIST_PROMPT 指令要求返回严格 JSON(style/mood/palette/imagery/title/caption),解析失败时兜底墨青默认色
  • speakText 朗读凝练结果让数字人有"完成感":墨白会说出画作的标题和情绪注脚,这不仅是视觉输出更是具身交互智能的语音闭环——用户听到画被命名的那一刻,创作才真正结束

画作分析 prompt 与文生图 prompt 组装:

export const CANVAS_ARTIST_PROMPT = `你是一位温柔而有品味的"共画艺术伙伴"名叫墨白。你的工作,是帮助人们把一段说不清的念头,凝练成一幅可以留存的画。

访客会向你描述他此刻想画的东西——可能是一个场景、一段情绪、一个意象、或者只是几笔涂鸦的灵感。请认真倾听,然后用 JSON 的形式返回一份"画作分析报告"。

请严格按照以下 JSON 格式返回(不要返回 markdown 代码块,不要包含多余文字):

{
  "style": "画风标签(英文,如:ink wash / watercolor / gouache / oil pastel / line art / ukiyo-e / minimalist)",
  "mood": "情绪基调(英文,如:serene / melancholic / joyful / mysterious / nostalgic / hopeful)",
  "palette": ["#xxxxxx", "#xxxxxx", "#xxxxxx"],
  "imagery": "一段英文的画面描述,用于 AI 生成一幅画。要有具体的场景、光线、意象、氛围。风格参考:水墨 / 水彩 / 留白 / 极简,低饱和柔和色调",
  "title": "一个中文标题(3-8字),诗意、有画面感",
  "caption": "一句中文小诗(10-20字),作为这幅画的情绪注脚"
}

回应要求:
- palette 的 3 个颜色要能代表这幅画的主色调
- imagery 必须是英文,描述具体画面(不要抽象概念)
- title 与 caption 要克制、诗意,避免说教
- 只返回 JSON,不要包含任何前后缀或代码块符号`
export function buildImagePrompt(analysis: ArtworkAnalysis): string {
  const colors = analysis.palette.join(', ')
  const style = [
    analysis.style || 'ink wash painting',
    'soft watercolor',
    'dreamy atmosphere',
    'low saturation',
    'gentle lighting',
    'xuan paper texture',
    'cinematic composition',
    'no text, no signature'
  ].join(', ')
  return `${analysis.imagery}. Style: ${style}. Dominant palette: ${colors}.`
}
  • CANVAS_ARTIST_PROMPT 把 LLM 的角色从"聊天伙伴"切换到"画作分析师",要求输出结构化 JSON 而非自然语言——这是同一套数字人 SDK 在不同场景下的人格切换
  • buildImagePrompt 在 LLM 输出的 imagery 基础上叠加了一组稳定美学关键词(xuan paper texture / low saturation / no text),保证文生图结果始终落在水墨水彩区间,不会跑偏成油画或 3D 渲染

5.5 数字人舞台:SDK canvas 居中与布局对齐

AvatarStage 是数字人渲染的视觉容器,需要解决 SDK 注入 canvas 后的三个排版问题:底部对齐、名牌溢出、canvas 偏右。

底部对齐——数字人 bottom 从 230px 调到 32px,与画板/对话舱的 padding 一致:

.avatar-stage {
  position: fixed;
  right: 14vh;
  bottom: 32px;
  width: 340px;
  height: 80vh;
  max-height: 720px;
  z-index: 20;
  pointer-events: none;
  animation: fade-in 0.6s var(--ease) both;
}

名牌移入框内——top 从 -14px 改为 12px,名牌不再溢出 frame 上边界:

.avatar-stage__nameplate {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  z-index: 6;
  pointer-events: none;
}

SDK canvas 居中——魔珐 SDK 给 canvas 设了 position: absolute + left 偏移 + transform: scale,导致数字人偏右,用三层覆盖解决:

.avatar-stage__sdk canvas {
  left: 0 !important;
  right: 0 !important;
  margin-left: auto !important;
  margin-right: auto !important;
  transform-origin: center center !important;
}

关键逻辑分析:

  • bottom: 32px 与 studio__body 的 padding: var(–space-lg)(即 32px)精确对齐,画板、对话舱、数字人三个框底部齐平,形成视觉上的"并排三栏"
  • transform-origin: center center 是 canvas 居中的关键:SDK 的 scale(0.929) 默认以 0 0(top-left)为原点缩放,缩小后右边空出约 10px,改为以中心缩放后左右边距对称——配合 left:0; right:0; margin:auto 的 absolute 居中,canvas 最终左右边距均为 21.4px,差 0
  • 窄屏(768px 以下)数字人缩为右上角 180px 小窗(top: 64px; bottom: auto),避免遮挡底部的对话舱输入框和凝练成画按钮

5.6 状态层:zustand + IndexedDB 作品墙

作品墙的持久化由 zustand + idb-keyval 协同完成:

export const useAppStore = create<AppState>((set, get) => ({
  view: 'landing',
  messages: [],
  currentArtwork: null,
  artworks: [],
  generating: false,

  setView: (view) => set({ view }),
  addMessage: (msg) => set((s) => ({ messages: [...s.messages, msg] })),
  setMessages: (msgs) => set({ messages: msgs }),
  clearMessages: () => set({ messages: [] }),
  setCurrentArtwork: (art) => set({ currentArtwork: art }),
  setGenerating: (v) => set({ generating: v }),

  loadArtworks: async () => {
    const allKeys = await idbKeys()
    const items: Artwork[] = []
    for (const k of allKeys) {
      if (String(k).startsWith(STORAGE_KEY)) {
        const v = await idbGet(k as string)
        if (v) items.push(v as Artwork)
      }
    }
    items.sort((a, b) => b.createdAt - a.createdAt)
    set({ artworks: items })
  },

  saveArtwork: async (art) => {
    await idbSet(`${STORAGE_KEY}-${art.id}`, art)
    set((s) => ({ artworks: [art, ...s.artworks.filter((a) => a.id !== art.id)] }))
  },

  deleteArtwork: async (id) => {
    await idbDel(`${STORAGE_KEY}-${id}`)
    set((s) => ({ artworks: s.artworks.filter((a) => a.id !== id) }))
  }
}))

关键逻辑分析:

  • idbKeys() 遍历所有 IndexedDB 键,用 startsWith(STORAGE_KEY) 过滤出本应用的作品——因为 idb-keyval 是全局共享的 key-value 存储,不加前缀会混入其他应用的数据
  • saveArtwork 先 idbSet 写入 IndexedDB 再 set 更新内存,保证页面刷新后数据不丢;deleteArtwork 同理先 idbDel 再 set 过滤
  • loadArtworks 按 createdAt 降序排序,最新凝练的画排在作品墙最前面

六、视觉设计系统:墨色画室

Collab Canvas 的视觉气质是东方画室的宁静与艺术感,由一组 CSS 令牌驱动:

:root {
  --c-bg: #0e1418;
  --c-bg-soft: #161e24;
  --c-ink-blue: #5b8a9e;
  --c-paper: #e8e0d0;
  --c-cinnabar: #c8553c;
  --c-gold: #c9a96e;
  --c-text: #ece8de;
  --c-text-muted: rgba(236, 232, 222, 0.55);
  --c-text-soft: rgba(236, 232, 222, 0.75);
  --c-border: rgba(91, 138, 158, 0.14);
  --c-border-strong: rgba(201, 169, 110, 0.32);

  --glass-bg: rgba(22, 30, 36, 0.6);
  --glass-blur: 18px;
  --glass-border: 1px solid rgba(201, 169, 110, 0.16);

  --shadow-soft: 0 8px 32px rgba(0, 0, 0, 0.45);
  --shadow-glow: 0 0 40px rgba(91, 138, 158, 0.22);

  --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 60% 50% at 15% 12%, rgba(91, 138, 158, 0.18) 0%, transparent 60%),
    radial-gradient(ellipse 50% 60% at 85% 18%, rgba(201, 169, 110, 0.12) 0%, transparent 60%),
    radial-gradient(ellipse 70% 50% at 50% 100%, rgba(200, 85, 60, 0.08) 0%, transparent 70%),
    var(--c-bg);
  background-attachment: fixed;
}
  • 深蓝基底 #0e1418 是画室的夜色,墨青 #5b8a9e 是主交互色(画笔默认色、按钮高亮),宣纸米 #e8e0d0 是画板底色和标题色,朱砂 #c8553c 是删除/清空按钮色,金赭 #c9a96e 是凝练成画 CTA 的渐变起点
  • 三层径向渐变分别从左上(墨青光晕)、右上(金赭暖光)、底部(朱砂微光)投射,配合 background-attachment: fixed 让光晕不随滚动移动,营造画室灯光的稳定氛围
  • 玻璃材质 .glass 用 18px 毛玻璃 + 130% 饱和度提升,让对话舱和数字人相框在深色背景上有"悬浮"感

七、总结

Collab Canvas 把具身交互智能嵌入了一条完整的创作链路:涂鸦 → 对话 → 凝练 → 成画 → 留存。数字人墨白不是锦上添花的装饰,而是贯穿全程的伙伴——她听你描述、用流式断句即时回应、在凝练时切换人格分析画作、最终用语音告诉你画的名字。

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

  1. 框架无关的数字人单例:AvatarController 以纯 TypeScript 类承载连接/断句/打断逻辑,useSyncExternalStore 桥接 React,保证核心能力可迁移、不随框架版本废弃
  2. 流式断句播报引擎:LLM 逐字返回、20 字门槛 + 标点检测截断、isStart/isEnd 标记串行队列,让数字人边生成边说、不抢话、可随时打断
  3. 双层 prompt 架构:COMPANION_PROMPT 驱动对话舱的温柔伙伴人格,CANVAS_ARTIST_PROMPT 驱动凝练成画的结构化分析,同一套 SDK 在两个场景间无缝切换

这条链路的本质是具身交互智能——AI 不只是生成图片的工具,而是有形象、有语音、有肢体表达的创作伙伴,把语言能力与视觉创作绑定在同一次交互里。当用户听到墨白说出"我为你凝练了一幅画,叫《此刻》"的那一刻,创作才真正闭环。

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

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐