《从酒头到杯沿:精酿小酒馆数字人的 T0--T4 五帧链路》
一个精酿小酒馆的数字人界面,真正值得讲的不是配色,而是一杯酒从"被点"到"被端上桌"之间被切开的五帧。本文沿着这条时间线逐帧拆解,顺带把「AI 端渲」这件事讲透。
晚上九点,店内六张小圆桌坐满,左侧那排酒头把手亮着霓虹橙。你走到吧台前,抬手,落在第三个酒头上。接下来几十秒里发生的一切,在人眼看来是连续的,但在工程上可以被切成五帧:T0 点击、T1 感知、T2 决策、T3 表达与渲染、T4 收束。
这套界面用的不是"前端自己拼"的老链路,而是端到端 SDK------全局 window.XingyunAvatarAgent。五帧里的每一帧,都对应这条链路上的一段。

T0|手指落在酒头上:交互的起点
酒头不是装饰,它是导航。它们由 quickServices 渲染,点击事件走 selectQuickService:
ts
const selectQuickService = (service: any) => {
selectedService.value = service;
ElMessage.success(`已为您选择:${service.title}`);
const tipText = `好嘞,您选了${service.title},${service.desc}。有什么想具体了解的尽管问我。`;
speakText(tipText);
};
顺序很直接:局部状态更新 → 轻量 Toast 反馈 → 拼接话术文本 → speakText 驱动数字人开口。酒头点击的频率天然不高,所以这里没有做 debounce,也没有做打断处理------"打断"这件事属于云端那一侧(后面 T3 会讲)。
这一帧的视觉语言由两段 CSS 承担。标题是整块界面的"招牌":
css
.brewery-neon-title {
font-size: 34px;
font-weight: 900;
margin: 0;
letter-spacing: 6px;
color: #f5edd8;
text-shadow:
0 0 4px #f5edd8,
0 0 12px #ff7a1a,
0 0 24px #ff7a1a,
0 0 48px rgba(255, 122, 26, 0.6);
font-family: 'STKaiti', 'KaiTi', serif;
}
四层 text-shadow 从内到外分工:第一层是纯米白的"灯芯",保证笔画本身不被光晕糊掉;第二、三层是实色霓虹橙,形成光晕扩散;第四层换成半透明 rgba 做最外层柔化,避免硬边。字体特意用楷体 STKaiti 而非常规黑体,因为横竖撇捺的粗细变化能让光晕在笔画边缘呈现出更像灯管弯折的过渡。
真正的把手则是纯 CSS 拼出来的------帽身、金属杆、底座三段:
css
.tap-handle-cap {
width: 30px;
height: 18px;
border-radius: 6px 6px 2px 2px;
background: linear-gradient(180deg, #ff9a4a 0%, #ff7a1a 50%, #d95a00 100%);
box-shadow:
inset 0 -3px 6px rgba(0, 0, 0, 0.4),
inset 0 2px 4px rgba(255, 255, 255, 0.25),
0 2px 4px rgba(0, 0, 0, 0.5);
position: relative;
}
.tap-handle-cap::before {
content: '';
position: absolute;
top: 2px;
left: 50%;
transform: translateX(-50%);
width: 18px;
height: 4px;
border-radius: 2px;
background: rgba(255, 255, 255, 0.35);
}
.tap-handle-shank {
width: 6px;
height: 18px;
background: linear-gradient(90deg, #4a4640 0%, #7a7670 40%, #a09a90 50%, #7a7670 60%, #4a4640 100%);
}
帽身用纵向渐变做"上亮下暗"的把手反光,inset 0 -3px 6px 是帽檐下方的阴影,inset 0 2px 4px 是顶部高光,::before 则是帽顶那道白色反光条。金属杆 tap-handle-shank 用 5 段横向渐变模拟不锈钢拉丝:两侧深、中间最亮,一条高光带就让圆柱体立了起来。被选中时,把手向右平移 3px 并把阴影换成霓虹光晕:
css
.brewery-tap.active {
transform: translateX(3px);
}
.brewery-tap.active .tap-handle-cap {
background: linear-gradient(180deg, #ffb876 0%, #ff8c3a 50%, #ff6a00 100%);
box-shadow:
inset 0 -3px 6px rgba(0, 0, 0, 0.35),
inset 0 2px 4px rgba(255, 255, 255, 0.3),
0 0 14px rgba(255, 122, 26, 0.8);
}
3px 的位移模拟真实酒头被向下扳的手感,14px 的橙色光晕让人产生"酒正在流出来"的错觉。T0 到此结束:点击已经变成一句待播报的话术。
T1|感知现场:听觉、视觉、业务数据三路同时开着
从这一帧开始,数字人不再只是"被触发",而是持续在感知现场。多模态感知要做的,是持续知道小酒馆正在发生什么:目标不是简单把麦克风里的声音转成文字,也不是给画面加个"识图"开关;真正要解决的是------你抬手点第三个酒头、后厨喊一句"IPA 只剩最后一桶"、隔壁桌随口问这款苦不苦,这些同时发生的事都得被接住;即使数字人自己正在说话,也能继续听见用户的插话,让交流继续进行。落到实现上,它是听觉、视觉、业务数据三条通道并行输入,而不是等用户打完字才睁眼。核心优势:持续感知,而不是一次输入。
-
听觉通道:麦克风拾音持续在线,用户在数字人说话时插话,也能被听见。
-
视觉通道 :数字人知道自己的画面区域在哪------组件用
:id="containerId"占好容器,avatarService.getContainerId()返回唯一 id,底层 SDK 通过它把 canvas 挂进.brewery-avatar-canvas,数字人的"视野"与 UI 布局因此始终对齐。 -
业务数据通道 :右侧那四颗泡沫不是纯装饰,它们直接绑定
statsData------今日打酒量 128L、在售酒款数 24、上新桶数 3、酒头利用率 87%。这些字段是现场的实时状态,数字人在回答"今天上新了什么"时有真实依据。
业务数据的可视化用了同一套 radial 与 inset 思路:
css
.brewery-foam {
position: absolute;
border-radius: 50%;
background: #f5edd8;
border: 2px solid #ff7a1a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
transition: transform 0.3s ease;
box-shadow:
0 4px 14px rgba(255, 122, 26, 0.3),
inset 0 -4px 8px rgba(255, 122, 26, 0.15),
inset 0 2px 4px rgba(255, 255, 255, 0.6);
}
.foam-1 { width: 120px; height: 120px; top: 0; left: 10px; }
.foam-2 { width: 90px; height: 90px; top: 80px; left: 90px; }
.foam-3 { width: 110px; height: 110px; top: 150px; left: 20px; }
.foam-4 { width: 80px; height: 80px; bottom: 10px; left: 70px; }
三个 box-shadow 分工明确:0 4px 14px 是外部投影,inset 0 -4px 8px 是内底部阴影(球体下沉感),inset 0 2px 4px 是内顶部高光(泡泡反光)。四个尺寸不等的圆用绝对定位错开,模拟啤酒泡沫自然堆叠的疏密节奏;hover 时 transform: scale(1.06) 配 20px 光晕,像气泡在杯里微微膨胀。
一句要划重点的事实:AI 自己在说话时,依然能听见用户的插话并即时响应 。这不是前端"监听输入框"能实现的------它属于 SDK 侧的持续感知,前端能拿到的只是它抛出来的事件(见 T3 的 subtitle_on / subtitle_off)。
T2|智脑决策:我是谁、我知道什么、我该做什么
感知之后要决策。专属智脑要做的,是让智能体知道"我是谁、知道什么、应该做什么"------从通用回答升级为面向具体身份和业务的智能体能力。它要同时回答三层问题:它知道什么(精酿的发酵、风格、苦度、侍酒温度这些专业事实,不是泛泛而谈)、它是谁(精酿侍酒师·大强,不是通用客服)、它要做什么(推荐、讲解、还是引导报名)。
组件里保留了一层本地话术,走关键词命中:
ts
if (userMsg.includes("IPA") || userMsg.includes("ipa") || userMsg.includes("世涛") ||
userMsg.includes("小麦") || userMsg.includes("柏林酸") || userMsg.includes("风格")) {
replyContent = "我们店里主打六种风格:1. IPA 琥珀------苦香清晰,入门进阶首选;......";
} else if (userMsg.includes("苦度") || userMsg.includes("IBU") || userMsg.includes("ABV") ||
userMsg.includes("度数") || userMsg.includes("酒精度")) {
replyContent = "IBU 是苦度单位,数字越高越苦:IPA 琥珀 IBU58 偏苦,世涛 32 中等,柏林酸只有 8 几乎无苦。......";
} else {
replyContent = "你可以告诉我是独饮小酌、朋友聚餐还是晚餐佐餐......";
}
speakText(replyContent);
关键词覆盖 IPA / 世涛 / 小麦 / 柏林酸 / 风格 / 苦度 / IBU / ABV / 存酒 / 侍酒 / 温度 / 杯型 / 精酿 / 入门,未命中的分支走通用推荐文案。注意这只是"兜底话术",真正的一轮对话核心并不在这里------speakText 最终调的是 appStore.sendMessage(),而它内部就是 SDK 的 ask(),由智脑侧决定实际回复。
专属智脑的第二个特点是不绑死任何一家大模型:豆包、DeepSeek、ChatGPT 这些用惯的模型都可以接;没有现成模型时,也可以直接接入魔珐星云自研的智脑------低延迟、强逻辑、场景化理解、数据安全可控,专为具身交互场景优化。这四个卖点恰好对应精酿店最在意的两件事:晚高峰时不能卡,会员数据不能出。网关与来源标识写在常量里:
ts
export const SDK_CONFIG = {
GATEWAY_URL: "https://nebula-agent.xingyun3d.com/user/v1/ttsa_v2/session",
DATA_SOURCE: "2",
CUSTOM_ID: "demo",
} as const;
智脑要答得准,前提是它手里有结构化的事实底座。界面上的酒款卡片就是这个底座的可见部分:
css
.brewery-card {
background: #221d16;
border: 1px solid #3a2a1c;
border-radius: 8px;
padding: 10px;
cursor: pointer;
transition: all 0.3s ease;
}
.brewery-card:hover {
transform: translateY(-2px);
border-color: #ff7a1a;
box-shadow: 0 6px 16px rgba(255, 122, 26, 0.25);
}
.card-metric {
display: flex;
align-items: baseline;
gap: 2px;
background: #16130f;
border-radius: 4px;
padding: 4px 6px;
border: 1px solid #2a2420;
}
IPA 琥珀的 ABV 6.5% / IBU 58 / 适饮 8-10℃、世涛的 5.8% / 32 / 12-14℃、柏林酸的 3.5% / 8 / 4-6℃、德式小麦的 4.8% / 14 / 7-9℃------这些数字既渲染在卡片上,也是智脑回答"哪款度数低"时的事实来源。T2 结束时,一句话术已经定稿,接下来交给表达层。
T3|表达生成与端侧渲染:一条 ask() 直达七种模态
这是整条链路上最重的一帧,也是本文的主概念:AI 端渲------把渲染下沉到终端侧完成。
T3.1 一条链路,而不是四段拼接
这套项目是从"前端自建 DeepSeek 流式 + 前端拆句 + 前端生成 SSML"迁移过来的。老链路的形态是:前端调 LLM 拿流式文本 → 前端按标点拆句 → 前端拼 SSML → 再想办法让嘴型和文本对齐。四段各写各的,任何一段的时钟漂移都会让"话说到哪、嘴动到哪"错位。
端到端(端对端) :一条 ask() 打通整条链路。它把「前端自建 LLM 流式 + 前端按标点拆句 + 前端手写 SSML」的三段拼装,收成一次 avatar.instance.ask(text)------LLM 回复 → 语音合成 → 口型/表情/动作一次编排完成,前端不再插手中间环节。
迁移后的接入层只依赖一个全局构造器和三个回调。构造参数长这样:
ts
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) => { /* ... */ },
agentCallbacks: {
onError(error: any) { /* ... */ },
onConversationChange(event: any) {
if (event?.state === 'completed') { /* 一轮对话结束 */ }
},
onAgentStateChange(state: string) { /* ... */ },
},
}
const agent = new window.XingyunAvatarAgent(constructorOptions)
await agent.init({ onDownloadProgress, onClose })
初始化完成后,一轮对话的全部代码只有一行:
ts
await avatar.instance.ask(text)
ask() 是一条链路直达:LLM 回复 → TTS → 口型 → 表情 → 动作。前端不再自己调 LLM、不拆句、不手写 SSML。组件侧的 speakText 只是把 UI 态写进去再交给 store:
ts
const speakText = (text: string) => {
isAvatarSpeaking.value = true;
subtitleText.value = text;
appState.ui.text = text;
appStore.sendMessage().then(() => {
setTimeout(() => {
isAvatarSpeaking.value = false;
}, text.length * 50);
});
};
appState.ui.text = text 写入全局文本,appStore.sendMessage() 内部即 await avatar.instance.ask(ui.text)。text.length * 50 是给前端字幕挂件的经验估时------30 字的侍酒话术约 1.5 秒,正好贴合正常语速;真实的字幕起止由 SDK 通过 subtitle_on / subtitle_off 事件广播,组件用 watch(() => appState.ui.subTitleText) 接住。
T3.2 副概念:多模态表达要"统一、连续、同步"
多模态表达 要做的,是实时生成统一、连续、同步的具身表达。一条 ask() 换来的,是语言、口型、表情、眼神、头部、手势、身体动作统一实时生成。关键词是统一、连续、同步------不是七个模块各出一个结果再硬对齐,而是同一份表达意图下同时生成。对精酿场景来说,这种同步感直接决定"像不像一个真人侍酒师":说到"这款有点冲"时,眉头和手势得跟语气一起动;报出"IBU 58"时语气要稳、手势像在指那张酒款卡;讲到"柏林酸只有 8,几乎不苦"时,语调又该轻快起来,否则再准的文案也像念稿。语言、口型、表情、眼神、头部动作、手势和身体动作统一实时生成------统一、连续、同步,小酒馆里的讲解才不会是干巴巴的读表。
界面上,表达状态用一小段动画外显:
css
.brewery-subtitle {
background: #1c1813;
border: 1px solid #3a2a1c;
border-left: 3px solid #ff7a1a;
border-radius: 6px;
padding: 10px 14px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
transition: all 0.3s ease;
}
.brewery-subtitle.speaking {
border-color: #ff7a1a;
box-shadow: 0 4px 18px rgba(255, 122, 26, 0.35);
animation: speak-glow 2s ease-in-out infinite;
}
@keyframes speak-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 122, 26, 0.4); }
50% { box-shadow: 0 0 0 8px rgba(255, 122, 26, 0); }
}
speak-glow 用扩散的环形阴影做"呼吸节奏",和数字人说话的顿挫对拍------这是前端唯一需要为"正在说话"这件事做的事,剩下的都交给 SDK。
T3.3 主概念:为什么要把渲染下沉到端侧
AI 端渲:让实时交互真正变成终端基础能力。它的意思是:渲染这一步不在云端 GPU 集群里做完再推视频流,而是把渲染下沉到终端侧完成------小酒馆往往弱网甚至断网,晚高峰几桌人并发,云端那套推流最先扛不住。它换来三笔账:
-
云端 GPU 成本:不再为每一路会话长期占用云端显卡,会话数上去时成本曲线不会再线性飙升。
-
带宽占用:不需要把整帧画面当视频流持续下发,网络压力从"推送每一帧"降级为"下发指令与少量资产"。
-
延迟 :本地生成、本地渲染,省掉了"上传 → 云端推理 → 编码 → 下行 → 解码"一整串往返,开口的响应更跟手;弱网环境下依然可用,这对店内 WiFi 时好时坏的线下场景是刚需。
一句话:降低云 GPU 成本、降低带宽占用、降低延迟、提升稳定性------省带宽、省机器、弱网也不掉链子,小酒馆能用,才是真的能用。
承载渲染结果的画布在组件里只是一个占位容器,纹理和边框是纯 CSS 兜底:
css
.brewery-avatar-canvas {
flex: 1;
min-height: 0;
background:
repeating-linear-gradient(90deg,
#3a2a1c 0 2px,
#4a3726 2px 4px,
#3a2a1c 4px 16px,
#4a3726 16px 18px),
#3a2a1c;
border-radius: 6px;
border: 2px solid #5a4028;
position: relative;
overflow: hidden;
}
repeating-linear-gradient 每 18px 循环一次(2px 高光 + 2px 过渡 + 12px 深色 + 2px 过渡),做出酒窖木板墙的肌理。这样在 SDK 资产还在下载、渲染尚未起来的那几秒,界面也不是一块空白。同一手法也用在吧台木纹板上:
css
.brewery-bar-top {
height: 36px;
margin-top: 6px;
background:
repeating-linear-gradient(90deg,
#4a3726 0 4px,
#5a4028 4px 6px,
#4a3726 6px 24px,
#5a4028 24px 26px),
linear-gradient(180deg, #5a4028 0%, #3a2a1c 100%);
border-radius: 8px 8px 0 0;
position: relative;
}
.brewery-bar-top::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(180deg, #6a4f35 0%, rgba(255, 122, 26, 0.3) 50%, transparent 100%);
border-radius: 8px 8px 0 0;
}
内层 repeating-linear-gradient 做横向木纹(4px 深棕 + 2px 稍亮 + 18px 深棕 + 2px 稍亮),外层 linear-gradient 从上到下整体压暗;::before 补一条 3px 的台面顶高光,接近真实木质吧台在暖光下的边缘反射。这一帧结束,酒已经"流进杯里"。
T4|一轮收束:completed 之后,杯子放回杯垫
一轮对话的结束不是"猜"出来的,是广播出来的。agentCallbacks.onConversationChange 会在 event.state === 'completed' 时通知前端,onAgentStateChange(state) 则同步数字人的整体状态机。字幕的起止另走 onWidgetEvent 的 subtitle_on / subtitle_off。
生命周期上,T4 要保证连接不被泄漏。组件在 onMounted 里挂了一个卸载钩子,在 onUnmounted 里再兜一次:
ts
onMounted(() => {
initAvatar();
updateDateTime();
setInterval(updateDateTime, 1000);
window.addEventListener("beforeunload", () => {
avatarService.disconnect(appState.avatar.instance);
});
});
onUnmounted(() => {
if (appState.avatar.instance) {
avatarService.disconnect(appState.avatar.instance);
appState.avatar.instance = null;
}
});
avatarService.disconnect() 内部只做一件事:agent.destroy('user_disconnect')。路由切换与刷新两条路径都覆盖到了,不会留下一条僵尸会话。另外,开场白(问候语)走的是另一条更短的通道------appStore.speakGreeting(initContent),它内部用 generateSSML() 拼好 SSML 再 avatar.speak(ssml, true, true) 直接播报,不经 LLM:
ts
speakGreeting(text: string): void {
const avatar = appState.avatar.instance
if (!avatar || !text) return
const ssml = generateSSML(text)
avatar.speak(ssml, true, true)
}
这一帧的视觉落点是底部的杯垫 FAQ。手风琴只维护一个 expandedFaq 引用,toggleFaq 保证同时只展开一条:
ts
const toggleFaq = (id: number) => {
expandedFaq.value = expandedFaq.value === id ? null : id;
};
杯垫的同心圆用 dashed 边框叠加做出来:
css
.brewery-coaster-faq {
margin: 2px 24px 0;
padding: 14px 28px;
background: #1c1813;
border-radius: 24px;
border: 2px dashed #3a2a1c;
box-shadow:
inset 0 0 0 2px #16130f,
0 4px 16px rgba(0, 0, 0, 0.4);
position: relative;
}
.coaster-inner {
width: 90px;
height: 90px;
border-radius: 50%;
border: 2px dashed #ff7a1a;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: radial-gradient(circle, #2a2420 0%, #1c1813 100%);
position: relative;
}
.coaster-inner::before {
content: '';
position: absolute;
inset: 4px;
border-radius: 50%;
border: 1px dashed rgba(255, 122, 26, 0.4);
}
外层 border: 2px dashed 模拟杯垫外沿的编织纹,inset 0 0 0 2px #16130f 做出"虚线之外还有一圈实色内层"的双边框错觉;左侧圆形装饰再叠两层虚线(外圈 2px 实色 + 内圈 1px 半透明)加 radial-gradient 背景,还原纸质杯垫的同心圆图案,"侍酒小百科"四个字就嵌在圆心。到这里,一轮对话闭环,杯子被放回杯垫上。
帧外·知识底座:大强必须答对的六件事
界面可以很花,但侍酒师不能答错。以下是这个主题数字人必须掌握的专业事实------它们与 T1 的业务数据通道、T2 的智脑回答一一对应。
一、精酿与工业拉格到底差在哪
核心差异在发酵菌种和原料自由度。工业拉格(青岛、百威一类)用驯化过的酿酒酵母属(Saccharomyces pastorianus),发酵温度严格控制在 4-9℃,发酵周期 7-14 天,产物干净、稳定、成本低;精酿啤酒(尤其美式精酿)常用艾尔酵母(Saccharomyces cerevisiae),发酵温度 16-24℃,发酵周期 3-6 周,配方里允许出现水果、咖啡、胡椒、燕麦甚至贝类,风味复杂度远高于工业量产。美国精酿协会 2014 年给出的精酿三原则:年产不超过 600 万桶、酒厂不被工业集团控制、不使用淀粉替代麦芽。
二、风格分类的全景
主流分类体系来自英国 BJCP(啤酒法官认证计划),目前收录 90 余种风格。按发酵方式分两大类:艾尔 Ale (上发酵,酵母在液面聚集,水果酯香与酚类香突出)和 拉格 Lager(下发酵,酵母沉底,清爽干净)。子类包括 IPA(印度淡色艾尔,高酒花苦香)、世涛/波特(深色烘烤麦芽,咖啡巧克力风味)、小麦啤(高比例小麦,香蕉丁香酯香)、酸啤(乳酸菌发酵,柏林酸小麦/兰比克)、皮尔森(拉格子类,清爽带花香酒花)、德式小麦、比利时三料/四料(高酒精度麦芽甜香)。
三、IBU 与 ABV:两个独立的坐标系
IBU(International Bitterness Units,国际苦度单位)范围 0-120,代表啤酒中异 α-酸的浓度:0-20 为低苦(酸啤、小麦啤),20-40 为中苦(世涛、拉格),40-60 为偏高(美式 IPA),60+ 为高苦(双倍 IPA、帝国 IPA);超过 120 后舌感趋于饱和,差异已无法靠口感区分。ABV(Alcohol By Volume)是体积百分比:3.5% 以下为低度数(社交酒),5-6% 为中等(世涛、小麦),8%+ 为高度数(帝国世涛、三料)。两者互相独立------双倍 IPA 可能 ABV 10% 而 IBU 120,帝国世涛可能 ABV 12% 而 IBU 仅 60。
四、侍酒温度与杯型搭配
温度影响香气挥发和口感密度,杯型影响香气聚集:拉格 4-7℃ 配直筒啤酒杯(清爽感最大化);德式小麦 7-9℃ 配大肚杯(大肚兜住香蕉丁香酯香,宽口让泡沫溢出);IPA 8-10℃ 配郁金香杯(细长杯身把酒花香气聚到杯口);世涛 12-14℃ 配品脱杯或郁金香杯(温度略高才放得出烘烤麦芽的巧克力咖啡香);酸啤 4-6℃ 配 flute 细长杯(像香槟一样,让乳酸酸香与气泡持续)。打酒前杯具要冰杯 1 分钟,泡沫保留 1-2cm 最能锁住香气。
五、酿造四原料与底层工艺
水 / 麦芽 / 啤酒花 / 酵母------《德国啤酒纯酿法》自 1516 年起规定只能用这四种。水决定 pH 与总硬度(皮尔森的硬水正是其清爽口感的关键);麦芽是谷物发芽后烘干,颜色从浅金到深棕,烘烤程度决定酒色与焦糖/咖啡风味;啤酒花是荨麻科植物的球果,提供苦味(平衡麦芽甜)、香气(柑橘、松针、热带水果)与防腐;酵母把麦芽糖转化为酒精和二氧化碳,同时产生酯类(香蕉/苹果香)和酚类(丁香/胡椒香)等次级代谢产物。主流程:糖化(65℃ 下水解淀粉)→ 过滤 → 煮沸加酒花 → 冷却 → 接种酵母 → 发酵 → 装瓶后熟。
六、精酿文化的发展脉络
现代精酿运动起源于 1960 年代英国的 CAMRA 运动,反对工业拉格对传统艾尔的取代。1980 年美国通过家庭酿酒合法化法案,催生美式 IPA------大量使用卡斯卡特、奇努克等美国酒花,突出柑橘松针香。2010 年后"新英格兰 IPA"崛起,用大量干投酒花与燕麦增加酒体,追求朦胧浑浊和热带水果汁感。中国精酿 2012 年起步,2018-2021 年井喷,目前北京、上海、成都、武汉是四大重镇,本地厂牌如北平机器、拾捌精酿、楚门、熊猫精酿各具风格。
帧外·可迁移:这条时间线留给其它主题的五条经验
-
端侧渲染是"体验下限"的保险:把渲染下沉终端后,云端 GPU、带宽、延迟三笔账同时下降,弱网下也不至于开不了口。任何要在线下门店跑的数字人主题,都该优先考虑端渲。
-
一条
ask()胜过四段拼接:自己调 LLM、自己拆句、自己写 SSML 的组合,最大的隐患是各段时钟不同步。端到端链路把"统一、连续、同步"交给 SDK,前端只负责 UI 态。 -
多层 text-shadow 是低成本霓虹:4 层(内白 + 2 层实色 + 外 rgba 柔化)比 SVG 灯牌轻,scoped CSS 下不泄漏,任何"夜间氛围"主题都能直接复用。
-
inset box-shadow 三段式做体积:外投影 + 内底阴影 + 内顶高光,能把一个 div 变成立体泡沫球、按钮或酒头帽,比伪元素拼接更省事。
-
容器与 UI 解耦 :
:id="containerId"让 SDK 只关心挂载点,Vue 组件只关心布局;主题切换时只换一个 Vue 文件,canvas 逻辑一动不动。