一席 omakase 的五道工序:和风日料数字人的三通道感知与端到端火候

本文以日料omakase五道工序为隐喻,系统阐述和风风格数字人的多模态感知架构:三条并行通道(听觉、视觉、业务数据)实现持续感知;预处理精简至三处关键逻辑;端到端SDK统一调度LLM、TTS、口型与动作;CSS端渲实现弱网可用的具身表达;奉客环节支持说话中实时插话响应。核心在于通道并行、状态可观测、渲染下沉。

一席 omakase 的五道工序:和风日料数字人的三通道感知与端到端火候

主题:和风日料;前缀 izakaya-;配色:生成色米 #f5efe0、焦茶 #5c4a38、朱红 #c73e3a、若草绿 #7a8a4e。
组件:Theme06_Izakaya.vue

在板前点一份 omakase,你吃到的不是"一道菜",而是一条有先后的工序链:选材、预处理、烹制、摆盘、奉客。任何一环偷懒,端到你面前的就不是那个味。数字人管家从"客人推帘进店"到"料理长开口应答",走的是一条同构的工序链,只不过它处理的是数据、模型和像素。本篇就按这五道工序往下拆。请留意一件事:这席菜真正的火候不在灶上,而在"三条通道同时进货"——这正是全篇的主概念:多模态感知。

在这里插入图片描述


工序一 · 选材:三条通道同时进货

日料选材从不"跑一趟市场"就完事:鱼要清晨从市场直送,菜要近郊当日到货,酒要酒造按季度供。三条来源各自独立、同时到位。数字人面前也摆着三条并行进货的通道:

  1. 听觉通道——SDK 侧麦克风采集、静音检测(VAD)与语音识别打通,客人说的话边说边进流水线。

  2. 视觉通道——画面理解,让数字人不止"听得见",还"看得见",比如识别到客人举起的酒单。

  3. 业务数据通道——最易被忽略、却恰好是页面里最真实的部分。组件里那几组写死的数据,就是数字人的"当日货源":

// Theme06_Izakaya.vue · script setup(L207-L249 节选)
const quickServices = ref([
  { id: "s1", icon: "🏮", title: "包间预约", content: "大小包间随时预订" },
  { id: "s2", icon: "🍱", title: "菜式推荐", content: "时令主厨精选" },
  { id: "s3", icon: "🍶", title: "清酒搭配", content: "酒造严选推荐" },
  { id: "s4", icon: "🌿", title: "时令菜单", content: "今旬食材告知" },
  { id: "s5", icon: "🐟", title: "刺身鲜度", content: "产地当日直送" },
  { id: "s6", icon: "🎎", title: "宴席定制", content: "法事祝事懇親会" },
]);
const statsData = ref([
  { label: "今日旬食材", value: "9品" },
  { label: "主厨推荐", value: "6道" },
  { label: "包间状态", value: "3空" },
  { label: "清酒品类", value: "18种" },
]);

quickServices(六项服务)、statsData(今日旬食材 9 品 / 主厨推荐 6 道 / 包间 3 空 / 清酒 18 种)、faqList(五条常见问题)、productList(会席板前 / 午市定食 / 主厨套餐)——这些不是装饰文案,而是业务数据通道里的货。它们先被 inject 进来的两个句柄接收,再由智脑按需取用:

// Theme06_Izakaya.vue · script setup(L190-L194)
const appState = inject<AppState>("appState")!;
const appStore = inject<AppStore>("appStore")!;
const containerId = ref(avatarService.getContainerId());

案板也要先备好。containerId 来自 avatarService.getContainerId(),是 CONTAINER_ 前缀的随机 id,挂在 <div :id="containerId" class="izakaya-avatar-canvas"></div> 上——SDK 之后就用它找到盒子、在里面创建 canvas。三条通道再热闹,没有这块案板也落不了刀。

选材的质量决定成品的上限。 日料选鱼看三个硬指标:鱼眼清澈——活鱼眼凸起透明如玻璃,冷藏超 24 小时开始浑浊凹陷;鱼鳃鲜红——鳃叶分明、摸无黏液,鳃若灰褐黏腻说明已冷藏 36 小时以上,只配做熟食;肉有弹性——筷子轻按,凹陷应在 1 秒内回弹。做生食的鱼三项必须全过。专业保鲜以鱼体中心温度 0-2℃ 检测,家庭只能靠这三条经验判断。数字人同理:喂进去的通道越脏、越晚、越单一,后面再好的模型也救不回来。


工序二 · 预处理:下刀之前,组件只动三处

预处理最考验手艺,动作却极少:净、切、码。组件侧也一样——从"客人点了什么"到"这句话被送进数字人",真正动手的只有三处。

第一刀:selectQuickService 把短册上的选择接住。 左侧那六条竖排短册(v-for="(svc, i) in quickServices")点一下,就走这里:

// Theme06_Izakaya.vue(L304-L309)
const selectQuickService = (service: any) => {
  selectedService.value = service;
  ElMessage.success(`您选择了:${service.title}`);
  const tipText = `好的,您选择了${service.title},${service.content}。让我为您安排详情。`;
  speakText(tipText);
};

第二刀:playProductContent 用 contentMap 把"菜品 id"翻成"料理长的话"。 点击菜单卡或台面上的小碟都会走到这里,是一张查表——查得到就用 contentMap 里的原话,查不到才退回 product.content 兜底:

// Theme06_Izakaya.vue(L324-L334 节选)
const playProductContent = (product: any) => {
  const contentMap: Record<string, string> = {
    p1: "会席板前套餐:主厨位9道时令料理,含八寸、先付、椀物、造里、焼物、焚合、強肴、水物、茶事。本日造里含寒鰤、真鲷、乌贼三种。人均680元,板前位限8席,建议提前3天预约。",
    p2: "午市定食套餐:每日11:30-14:00供应,主菜三选一(照烧鸡腿/味噌烤鳕鱼/炸虾定食),含小鉢2品、椀物、御饭、漬物,搭配小壶清酒。人均180-220元,无需预约。",
    p3: "主厨套餐:主厨个人推荐12道菜品鉴,含清酒pairing4杯(精选18种酒造)。食材以今旬为主,主厨会与客人互动介绍每道菜的创作思路。人均1280元,限板前6席,需提前5天预约确认忌口。",
  };
  const content = contentMap[product.id] || product.content;
  speakText(content);
  chatHistory.value.push({ role: "avatar", content });
  scrollToBottom();
};

第三刀:speakText 是一把共用刀。 上面两条路径最后都汇到它,聊天输入 sendChatMsg 也走它。它只做两件事——把文本写进全局状态、然后交给 store 去发送:

// Theme06_Izakaya.vue(L370-L379)
const speakText = (text: string) => {
  isAvatarSpeaking.value = true;
  subtitleText.value = text;
  appState.ui.text = text;
  appStore.sendMessage().then(() => {
    setTimeout(() => { isAvatarSpeaking.value = false; }, text.length * 50);
  });
};

注意它的"估时":text.length * 50 毫秒之后才把 isAvatarSpeaking 置回 false——这是一个纯前端的粗略计时,用来驱动字幕框的"正在说"样式。真正的开始/结束,其实由后面工序五的回流事件说了算,这把刀只是先占个位。

还有一处暗中下刀:字幕回流。组件并不自己改写字幕,只写一句 watch(() => appState.ui.subTitleText, (val) => { subtitleText.value = val || ""; })——谁写进全局字段,就显示谁。

刀工本身就是专业。 日料刺身有三种主流切法:薄切(うすぎり)——用于比目鱼、鲷鱼等肉质细腻的白身鱼,薄到能透出盘底花纹,刀刃与鱼肉纹理呈 45° 角,一刀到底不补刀;厚切(あつぎり)——用于三文鱼、金枪鱼等脂肪丰富的赤身鱼,厚度约 15mm,入口即化,刀角度垂直于纹理;丝切(糸ぎり)——用于鲣鱼的皮烧(かつおのたたき),切至发丝粗细,撒葱花与山葵。寿司的正手握(おまかせ握り)是师傅单手成型,反手握用于客人可自行蘸酱的手卷(てまき)。专业厨师会依鱼种、季节甚至客人体型调整切法。这和 speakText 是同一种克制:刀口越准,动作越少;动作越少,出来的东西越稳。


工序三 · 烹制:一锅文火,ask() 端到端

先说清一件事:这口锅换过了。 项目早期的做法是"前端自建 DeepSeek 流式请求 → 前端按标点拆句 → 前端拼 SSML → 再交给数字人播报"——三道工序全摊在前端,任何一段逻辑抖动都会变成体验抖动。现在这条路已经整体迁移到端到端(端对端)SDK,全局对象是 window.XingyunAvatarAgent。构造它需要一整组参数,其中每一项都对得上一个真实职责:

// src/services/avatar.ts · connect()(L76-L115 节选)
const constructorOptions = {
  container: containerEl,
  appId: APP_ID,
  appSecret: APP_SECRET,
  enableDebugger: false,
  gatewayServer: url.toString(),
  onWidgetEvent: (event: any) => {
    if (event.type === 'subtitle_on') { onSubtitleOn(event.text) }
    else if (event.type === 'subtitle_off') { onSubtitleOff() }
  },
  onStateChange,
  onMessage: async (error: any) => { /* 连接期错误 → reject */ },
  agentCallbacks: {   // 端到端 SDK 专用回调组
    onError(error: any) { /* ... */ },
    onConversationChange(event: any) {
      if (event?.state === 'completed') { /* 一轮对话结束 */ }
    },
    onAgentStateChange(state: string) { /* ... */ },
  },
}
const agent = new window.XingyunAvatarAgent(constructorOptions)

两个细节值得记下:enableDebugger: false 关掉了调试面板;gatewayServer 不是裸 URL,而是拼接过的网关地址——SDK_CONFIG 里定义了三段常量,connect() 用 URL 把它们拼到查询串上:

// src/constants/index.ts(L262-L266)
export const SDK_CONFIG = {
  GATEWAY_URL: "https://nebula-agent.xingyun3d.com/user/v1/ttsa_v2/session",
  DATA_SOURCE: "2",
  CUSTOM_ID: "demo",
} as const;

agentCallbacks 里的 onConversationChange 尤其关键:event.state === 'completed'**** 就代表一轮对话结束——它是"这锅出菜了"的信号灯,没有它,前端只能靠猜时间。

点火。 构造完还要初始化,等资源下载到 100% 才算真正开灶:

// src/services/avatar.ts · connect()(L123-L134 节选)
await agent.init({
  onDownloadProgress: (progress: number) => { if (progress >= 100) { resolve(true) } },
  onClose: () => { onStateChange(''); console.log('SDK连接关闭') }
})

一勺出菜。 所有对话最终都归到 store 的一个方法上——它把文本交给 SDK 内置的 ask(),一条链路直达"LLM 回复 + TTS + 口型 + 表情 + 动作":

// src/stores/app.ts · sendMessage()(L81-L94 节选)
async sendMessage(): Promise<string | undefined> {
  const { ui, avatar } = appState
  if (!ui.text || !avatar.instance) return
  try {
    await avatar.instance.ask(ui.text)   // Brain 自动 LLM → 自动 speak
    return ui.text
  } catch (error) { console.error('发送消息失败:', error); throw error }
}

直接播报。 问候语和快捷播报不走 LLM,走的是"生成 SSML → 丢给数字人念"这条短路。.speak(ssml, true, true) 的两个布尔位就是"直接播 + 可打断";generateSSML 的产物极简,只包一层带三档参数的 <speak>(<speak pitch="1" speed="1" volume="1">${text}</speak>):

// src/stores/app.ts · speakGreeting()(L99-L108 节选) / src/utils/index.ts(L47-L54)
speakGreeting(text: string): void {
  const avatar = appState.avatar.instance
  if (!avatar || !text) return
  try { avatar.speak(generateSSML(text), true, true) }
  catch (error) { console.error('问候播报失败:', error) }
}

组件在初始化时正是走的这条路:先写好字幕,再让 store 直接播报欢迎语。

// Theme06_Izakaya.vue · initAvatar()(L396-L410 节选)
const initContent = "いらっしゃいませ、田中屋へようこそ!私は料理长の田中でございます。本日の旬食材は寒鰤と春の筍でございます。ご予約、お料理、清酒のことなんでもお声がけください。";
try {
  await appStore.connectAvatar();
  isAvatarReady.value = true;
  ElMessage.success("料理长 田中 已就位");
  subtitleText.value = initContent;
  appStore.speakGreeting(initContent);
} catch (err) { console.error("数字人初始化失败:", err); } finally { isLoading.value = false; }

这口锅背后站着的,是"专属智脑"——让智能体知道"我是谁、知道什么、应该做什么"。 端到端 SDK 的 Brain 不是随手接一个通用模型就完事,它要同时回答三层问题:它知道什么(只答日料相关:忌口、鲜度、清酒、旬、预约)、它是谁(料理长田中,不是别家的客服)、它要做什么(该报价报价、该引导预约就引导预约,涉及生食风险要主动提示)。LLM_CONFIG 里主题 6 的 SYSTEM_PROMPT 正是这套"人设与边界"的落点,它明确要求语言"谦和细腻、表述克制有礼",并在涉及生食风险时提示肠胃敏感人群谨慎选择。而且这颗脑不绑死某一家大模型——豆包、DeepSeek、ChatGPT 这些用惯的模型都能接;没有现成模型时,可以直接用魔珐星云自研智脑——低延迟、强逻辑、场景化理解、数据安全可控,专为具身交互场景优化。这就是从通用回答升级为面向具体身份和业务的智能体能力。

火候的黄金比例,在日料里是醋饭。 寿司的灵魂是醋饭(すしめし),传统江户前(江戸前)配方为 米:醋:糖:盐 = 10:1:0.5:0.3。以 1000g 米(约 5 合)为例:用 100ml 米醋(约 5 勺)、50g 白糖(约 2.5 勺)、30g 盐(约 1.5 勺)加热煮沸后冷却,倒入刚煮好的热饭中,用手持饭扇(うちわ)快速扇凉至体温(35-38℃),同时用饭切(しゃもじ)从底部翻拌以免饭粒破碎。这个比例的科学依据是:醋饭 pH 约 4.2-4.5(弱酸性),既能抑制细菌又不显酸;糖盐平衡让醋饭与鱼的鲜味形成"回甘"的层次。端到端链路也是一样——配料比例(prompt、网关、回调)定好了,火候(一次 ask())才稳。

收锅。 页面卸载或刷新时,连接必须按规矩熄灭——disconnect() 内部只有一句 agent.destroy('user_disconnect'),参数 'user_disconnect' 就是给网关的断开理由。组件在 onUnmounted 里调用它,并把 appState.avatar.instance 置回 null,避免热更新或路由切换时残留一个"没人管的灶"。


工序四 · 摆盘:CSS 把这一席端上台面

菜出锅了,但客人先看到的是摆盘。摆盘改不了味道,却决定了"这一席值不值"。数字人开口之前的每一帧视觉,都由组件里的 CSS 决定——这也是**"多模态表达"真正落地的地方:它的目标是实时生成统一、连续、同步的具身表达**。介绍当日的寒鰤时,田中的语气会压低、眼神落在台面的小碟上;讲到清酒 pairing,手势顺着酒器打开、语调轻快起来;讲到生食风险,则换成放缓、克制的提醒语气。语言、口型、表情、眼神、头部动作、手势和身体动作,由 SDK 统一实时生成——统一、连续、同步,和风日料不再是干巴巴地念菜单。而承托这一切的舞台,是下面这几块真实的样式。

暖帘(のれん)——顶部那道招牌。 十一条帘条横排,交替红白,第 5-8 条刻意留"缺口"(miss)营造"掀开一角"的错觉;每条底边用 border-radius: 0 0 50% 50% 收出布帘的垂坠弧:

/* Theme06_Izakaya.vue(L470-L505 节选) */
.izakaya-noren-strip {
  position: relative; flex: 1; height: 100%;
  background: #f5efe0; border-right: 1px solid rgba(92, 74, 56, 0.15);
  transition: transform 0.5s ease; transform-origin: top center;
}
.izakaya-noren-strip.red { background: #c73e3a; }
.izakaya-noren-strip.miss { visibility: hidden; pointer-events: none; }
.izakaya-noren-curve {
  position: absolute; bottom: 0; left: 0; right: 0; height: 14px;
  background: inherit; border-radius: 0 0 50% 50%;
}

transform-origin: top center 是帘子"从顶垂下"的物理锚点,配合 :hover 时奇偶列反向摆动的关键帧(izakaya-sway-l / izakaya-sway-r),一进门就是"风穿过帘子"的动感。

短册(たんざく)——左侧那排竖写菜单。 关键在 writing-mode: vertical-rl,让中文标题竖着写、从右往左排;配合 letter-spacing 拉开字距,才像挂在绳上的木牌:

/* Theme06_Izakaya.vue(L608-L634 节选) */
.izakaya-tanzaku-paper {
  position: relative; width: 120px; padding: 10px 8px 12px;
  background: #faf7ef; border: 1px solid #c73e3a;
  box-shadow: 2px 3px 0 rgba(92, 74, 56, 0.15); transition: all 0.25s ease;
}
.izakaya-tanzaku-text {
  writing-mode: vertical-rl; text-align: center;
  font-size: 14px; letter-spacing: 4px; color: #5c4a38; font-weight: 600;
  height: 100px; display: flex; align-items: center; justify-content: center;
}

box-shadow 用无模糊的硬边(2px 3px 0)——这是刻意的"纸片叠纸片"质感,而不是那种软塌塌的投影。悬停时 transform: translateX(-2px) rotate(-1deg) 让纸片轻轻"翻"一下,选中后整张纸换若草绿。

板前料理台——中央木纹台面。 木纹是纯 CSS 模拟的:横向 repeating-linear-gradient 做深浅条纹,#5c4a38 与 #4a3a2a 每 42px 交替一次,零图片成本。台面再叠一层上沿高光,数字人 canvas 就"坐"在这块台面上:

/* Theme06_Izakaya.vue(L763-L800 节选) */
.izakaya-counter-wood {
  position: absolute; inset: 0; z-index: 0;
  background: repeating-linear-gradient(90deg, #5c4a38 0 40px, #4a3a2a 40px 42px, #5c4a38 42px 80px);
}
.izakaya-counter-top {
  position: absolute; top: 0; left: 0; right: 0; height: 30px; z-index: 2;
  background: linear-gradient(to bottom, #7a6652, #5c4a38);
  border-bottom: 1px solid rgba(0,0,0,0.3);
}
.izakaya-avatar-canvas {
  position: relative; z-index: 3; flex: 1; min-height: 0;
  margin-top: 30px; background: transparent;
}

z-index 的 0/2/3 三层分工很清楚:木纹最底、台沿在中、数字人在最上——木纹永远只是背景,绝不抢人。 数字人 canvas 背景设为透明,正是为了让它自然"长"在台面上,这也是"多模态表达"能被无缝嵌入的前提。

挂牌数据——右侧高低错落的木牌。 每块牌由"麻绳 + 三角挂点 + 牌体"三件套组成。麻绳同样用 repeating-linear-gradient 做出编织感,挂点则用 clip-path 切一个正三角:

/* Theme06_Izakaya.vue(L899-L921 节选) */
.izakaya-board-rope {
  position: absolute; top: -24px; left: 50%; transform: translateX(-50%);
  width: 2px; height: 34px; z-index: 1;
  background: repeating-linear-gradient(90deg, #5c4a38 0 2px, #8a7360 2px 4px);
}
.izakaya-board-hook {
  position: absolute; top: -10px; left: 50%; transform: translateX(-50%);
  width: 16px; height: 10px; background: #5c4a38; z-index: 2;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

四块牌用 .board-1 ~ .board-4 各自不同的 margin-top(0 / 10px / 6px / 14px)错位排开,模拟"挂在墙上、被手碰过高低不一"的真实感。clip-path: polygon(50% 0, 100% 100%, 0 100%) 的三个顶点是标准正三角——这一刀和台面小碟(.izakaya-saucer 的 border-radius: 50% + 虚线内圈 .izakaya-saucer-ring)共同组成本主题的几何语言。

FAQ 扇形放射线——底部那一折纸。 这一处用了 conic-gradient(圆锥渐变),从矩形下边中点向外放射两道 30° 的朱红扇面,像折扇的扇骨:

/* Theme06_Izakaya.vue(L1039-L1045) */
.izakaya-faq-fan {
  position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background: conic-gradient(from 0deg at 50% 100%, rgba(199,62,58,0.1) 0deg 30deg, transparent 30deg 330deg, rgba(199,62,58,0.1) 330deg 360deg);
}

at 50% 100% 把渐变圆心钉在矩形底边中点——所以射线是"从下往上扇形展开",而不是常见的中心放射。pointer-events: none 保证它只做背景、不挡点击。

"正在说"的字幕框与在线点。 字幕只在数字人开口时才加一圈若草绿描边,这个状态由 speakText 里那个估时器驱动;而"在职/出张中"状态灯把"人在不在"这件抽象事变成一眼可见的小圆点:

/* Theme06_Izakaya.vue(L843-L853 · L738-L749 节选) */
.izakaya-caption { flex-shrink: 0; padding: 14px 18px; background: #faf7ef; border: 1px solid #5c4a38; transition: all 0.3s ease; }
.izakaya-caption.speaking { box-shadow: 0 0 0 2px rgba(122, 138, 78, 0.3); }
.izakaya-online-dot { width: 8px; height: 8px; border-radius: 50%; background: #bbb; }
.izakaya-itamae-online.online .izakaya-online-dot {
  background: #7a8a4e; box-shadow: 0 0 8px rgba(122, 138, 78, 0.8);
  animation: izakaya-pulse 1.8s ease-in-out infinite;
}

摆盘不占云端一分钱——这就是 AI 端渲,让实时交互真正变成终端基础能力。 和风日料店未必有稳定的公网:板前的平板常年挂在店里局域网、客人手机信号被木结构厚墙挡掉,弱网甚至断网是常态。上面这套木纹、麻绳、扇骨、帘浪,全部是浏览器本地算出来的 CSS;数字人的口型与动作帧,也由 SDK 下沉到终端侧渲染。把渲染从云端挪到端上,直接省掉三笔账:云端 GPU 成本(不必为每个并发用户养一张卡)、带宽占用(不再来回传视频流)、以及延迟(画面不用等网络往返)。弱网环境下,云端方案会先糊成马赛克,端渲方案则还能稳住基本观感。省带宽、省机器、弱网也不掉链子——和风日料能用,才是真的能用。

摆盘的分寸,日料里对应的是清酒与旬。 清酒四大分类由精米步合决定:纯米酒(じゅんまい)70% 以下、本酿造酒(ほんじょうぞう)60% 以下、吟酿(ぎんじょう)50% 以下、大吟酿(だいぎんじょう)40% 以下——精米步合越低,用的越是米粒靠近中心的纯淀粉部分,风味越细腻、价格越高。温度带也是一门学问:冷酒(ひや,10-15℃)配吟酿与大吟酿,凸显果香;常温(じょうおん,20-25℃)配纯米酒,保留米香;温酒(おかん,40-45℃)配本酿造与部分纯米酒,加温后辛口更柔和。把大吟酿加热到 40℃,会被行家视为"暴殄天物"。而旬(しゅん)是日料最核心的食材观——每种食材只在它最肥美的那段时间出现:鲷鱼(たい)的旬是春天樱花季、三文鱼(さけ)的旬是秋冬洄游产卵期、松叶蟹的旬是 11-3 月、海鳗(うなぎ)的旬是盛夏土用丑日(うなぎの土用丑の日)。线上的"当日旬食材"标签,本质和摆盘一样,都是在传递"我们会把最当下的好东西端给你"这句承诺。


工序五 · 奉客:数字人不闭眼,说话时也听得见插话

最后一道工序是奉客——把菜端到客人面前,并看着他吃。如果客人刚夹起一筷子就说"这个不要葱",好的师傅会立刻停下、调整,而不是把话听完才反应。

多模态感知要做的,是持续知道板前的这一席正在发生什么。 它的目标,不是简单把客人的话转成文字,也不是把现场画面识成标签。真正要解决的是:客人推帘进店、目光在吧台的旬食材挂牌上多停了一秒,数字人要先一步招呼;客人举起的是一张酒单、指的是一碟小钵,系统要接得住;即使料理长田中自己正在介绍今天的寒鰤,也能继续听见客人那句"这个不要葱"的插话,让交流继续进行。核心优势:持续感知,而不是一次输入。

这恰恰是多模态感知最见功力的地方,也是本篇要重点讲清的一句:AI 自己在说话的时候,依然能听见用户的插话,并即时响应。

传统"回合制"语音链路是这样的:录音 → 等静音 → 识别 → 请求大模型 → 等完整回复 → 再合成语音 → 播放。这套流程里,数字人一旦开口就是"聋的"——因为它正在播放音频、根本没在采集;客人这时候插一句"等一下",系统只能等它播完才发现。体验上就像师傅背对着你自说自话。

端到端 SDK 把这条链路拆成了三条并行通道(回到工序一:听觉 / 视觉 / 业务数据),并用回调把状态实时吐给组件:

  • 听觉通道常开、支持打断。 说话与听音不再是互斥的两件事,客人插话会被即时捕获并中断当前播报——这正是 speakGreeting 里 avatar.speak(ssml, true, true) 第三个布尔位的含义:可直接播、且可被打断。

  • 状态可观测。 SDK 通过 onWidgetEvent 把字幕状态推出来——event.type === 'subtitle_on' 时把文本交给 onSubtitleOn,subtitle_off 时清空;再由 store 写进 appState.ui.subTitleText,组件的 watch 立刻把字幕换掉。字幕不是前端猜的,是 SDK 报的。

  • 对话轮次可判定。agentCallbacks.onConversationChange 里 event.state === 'completed' 明确告诉你"这一轮说完了",配合 onAgentStateChange(state) 观察数字人自身的状态机、onError 兜住异常——前端因此能在正确的时机做下一件事,而不是靠 setTimeout 硬猜。

把这三条缝起来看:听得见(听觉)→ 看得见(视觉)→ 有据可依(业务数据)→ 三条合流交给同一个智脑。所以客人打断时,数字人不是"被打断后从头再来",而是把新的那句话直接接进正在进行的对话上下文里——打断即响应,这才是"不闭眼"的奉客。组件侧对此的呼应也很直白:isAvatarSpeaking 状态同时驱动字幕框描边(.izakaya-caption.speaking)与估时复位,让"正在说 / 已说完"在视觉上始终和感知状态对齐。

奉客前的最后一道安全检查,日料里叫"生食风险"。 生食最大的隐患是异尖线虫(アニサキス)与副溶血性弧菌:异尖线虫寄生于海鱼体内(主要是鳕鱼、鲭鱼、三文鱼),幼虫经 -20℃ 冷冻 24 小时以上方可杀死,这是刺身安全的硬标准;副溶血性弧菌在 60℃ 15 秒即可消灭,却常见于生牡蛎、生虾。孕妇与免疫力低下者应避免生食。专业日料店会在菜单上标注"本店家禽、鱼类均经冷冻处理",或提示"生食有风险,请根据自身体质选择"——这既是法律合规(日本食品卫生法要求),也是对客人负责。数字人服务同理:能力再强,也要能把边界和安全提示说到位——这正是"专属智脑"知道"我应该做什么"的那一半。


收席:五道工序,一张台账

一席 omakase 吃完,把五道工序和它们对应的技术环节并排记下来:

工序对应技术环节关键位置
选材三条数据通道接入(听觉 / 视觉 / 业务数据)+ inject 句柄与容器 idquickServices / statsData / containerId
预处理组件侧三把刀:selectQuickService、playProductContent(contentMap)、speakTextL304-L334、L370-L379
烹制端到端 SDK:构造 → init() → ask() / speak() → destroy()services/avatar.ts、stores/app.ts
摆盘CSS 视觉层(暖帘 / 短册 / 木纹台面 / 挂牌 / 扇骨)+ 端侧渲染L470-L1045
奉客多模态感知:三通道并行、说话时仍可被插话、字幕与轮次可观测onWidgetEvent / agentCallbacks

三条最值得带走的经验:

  1. 主概念是"同时",不是"更多"。 多模态感知的价值不在于通道数量,而在于通道并行且互不阻塞——尤其"AI 说话时仍能听见插话"。这是回合制链路永远做不到的事,也是端到端 SDK 与"前端自建流式"最本质的分野。

  2. 能用回调说清的,绝不靠 setTimeout 猜。 字幕交给 onWidgetEvent,轮次交给 onConversationChange,状态交给 onAgentStateChange。组件里那个 text.length * 50 只是过渡期的估时占位,真正的信号源在 SDK。

  3. 端渲是把成本与延迟同时压下去的一步。 木纹、麻绳、扇骨都是本地 CSS,数字人画面也在端侧渲染——云端只管大脑、不用管画面,弱网才能用得住。

选材要三条通道齐备,下刀要少而准,烹制要一锅到底,摆盘要懂分寸,奉客要睁着眼——数字人这一席,和板前那一席,做的是同一件事。


本篇代码引用自 src/components/Theme06_Izakaya.vue、src/stores/app.ts、src/services/avatar.ts、src/constants/index.ts、src/utils/index.ts,所有类名、函数名与常量均与源码一致。行业知识(刀工切法、鲜度三要点、醋饭比例、清酒分类与温度带、旬之食材、生食风险)为公开行业资料,与具体产品无关。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值