欢迎光临
我们一直在努力

不止AI图文生成!依托具身交互数字人,全方位重构世界杯赛事典藏沉浸式体验

一位站在金色舞台上的解说员纪燃,能听、能说、有肢体表达——这正是具身交互智能的核心。本文从魔珐星云控制台配置到原生 JS 全栈工程源码,拆解 Seed Chronicle 如何以参数流 SDK 为中枢,让数字人嵌入选球星→写档案→生插画→翻卡片→存画廊的典藏全链路。

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


一、产品概览:当数字人成为典藏解说员

Seed Chronicle(世界杯典藏回忆卡)是一个把数字人嵌入典藏叙事流程的 Web 应用。用户打开页面,数字人解说员纪燃便以金色舞台形式常驻右侧、自动语音问候;在 AI 工作台填写球员信息或观赛记忆后,豆包 Seed 模型一次性吃下整届赛事上下文(1M+ tokens),生成编年史文案、关键数据与名场面解读,纪燃随即语音播报生成结果;用户翻转 3D 卡片查看背面档案,存入画廊时纪燃语音确认珍藏。整条链路不是"输入球员名→生成一张图"的简单工具流,而是一段有温度的典藏对话。核心能力包括:

  • 球星典藏卡与个人回忆卡双模式,四种卡面风格(复古贴纸 / EA FC 金卡 / 全息镭射 / 决赛限定)
  • 豆包 Seed 1M+ 超长上下文,整届赛事数据一次性注入,模型拥有完整全局视野
  • Seed 文生图模型同步生成卡面插画,铺到 3D 卡片正面
  • 数字人纪燃常驻右侧舞台,连接问候、生成播报、保存确认,全程语音陪伴
  • 3D 卡片支持鼠标跟随倾斜、点击翻转、镭射反光,配合 html2canvas 高清 PNG 导出

在这条链路中,数字人纪燃不是页面角落的装饰动画——她是具身交互智能的载体:有形象、有声音、有肢体表达,在每一个关键节点用语音与用户对话。这区别于传统的图文生成工具,把大语言模型的内容能力、文生图的视觉能力与数字人的具身表达能力绑定在同一次交互里。


二、技术架构总览

项目采用纯原生 HTML + CSS + JavaScript 技术栈(零框架),配合 Python 本地服务器做 API 代理,整体分为四层:

层级职责核心文件
视图层 画廊 / AI 工作台 / 设置弹层 / 数字人舞台 index.html
逻辑层 状态管理 / Seed 调用 / 3D 卡片交互 / 导出 app.js
数字人层 SDK 连接 / 语音播报 / 字幕 / 打断 avatar.js
服务层 静态文件托管 + Seed API 反代(绕过 CORS) server.py

数字人逻辑以 IIFE 封装的原生 JS 单例承载,通过 window.AvatarService 暴露全局接口。这意味着 app.js 中的任何模块都可以在生成卡片、保存画廊时调用纪燃的语音能力——不需要框架的状态管理,不需要构建工具,三个 script 标签即可串联整条具身交互智能链路。


三、前提准备

魔珐星云控制台配置

数字人纪燃的形象、场景、音色、表演均由魔珐星云控制台驱动。以下五步完成驱动应用创建与参数配置,是代码层接入 SDK 前的必要前提。

步骤1:魔珐星云控制台创建驱动应用

在魔珐星云控制台创建一个新的驱动应用,获取 appId 与 appSecret,这两个密钥将填入项目 avatar.js 的 AVATAR_CONFIG。

步骤2:形象配置

为数字人纪燃选择或上传形象资源,配置外观参数。形象配置决定了 SDK 渲染时的角色外观——在典藏回忆卡场景中,纪燃作为世界杯解说员出镜,形象需要匹配金色运动风的视觉调性。这是具身交互智能中"有形象"这一要素的直接来源。

步骤3:场景配置

配置数字人所在的虚拟场景,包括灯光、背景、机位等。场景配置与前端 avatar-stage 容器配合,决定纪燃在页面右侧舞台中的视觉呈现方式。

步骤4:音色配置

为纪燃选择解说员音色,控制语音合成的语速与情感倾向。音色配置直接影响播报卡片生成结果时的听感——热血、凝练、有电影感,是具身交互智能中"能说"这一要素的质量基础。

步骤5:表演配置

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


四、核心代码讲解

4.1 数字人服务:原生 JS 版魔珐 SDK 集成

avatar.js 是整个具身交互智能链路的视觉与语音中枢。它用 IIFE 封装,通过 window.AvatarService 暴露全局接口,管理 SDK 连接、语音播报、字幕同步与实时打断:

(() => {
'use strict';

/* ============== 配置 ============== */
const AVATAR_CONFIG = {
appId: 'your_avatar_app_id',
appSecret: 'your_avatar_app_secret',
gatewayUrl: 'https://nebula-agent.xingyun3d.com/user/v1/ttsa/session',
dataSource: '2',
customId: 'demo',
};

const AVATAR_IDENTITY = {
name: '纪燃',
role: '世界杯典藏解说员',
greeting: '欢迎来到世界杯典藏回忆卡。我是你的解说员纪燃,帮你把整届赛事、十几年生涯、那些熬夜与呐喊,凝成一张可翻转、可珍藏的典藏卡。',
};

/* ============== 状态 ============== */
const avatarState = {
connected: false,
connecting: false,
instance: null,
subtitle: '',
avatarState: '',
listeners: new Set(),
};

/* ============== 订阅机制 ============== */
function subscribe(listener) {
avatarState.listeners.add(listener);
return () => avatarState.listeners.delete(listener);
}

function notify() {
avatarState.listeners.forEach(l => l());
}

function patch(partial) {
Object.assign(avatarState, partial);
notify();
}

/* ============== DOM 引用 ============== */
const $ = (sel) => document.querySelector(sel);
const containerId = 'xmov-avatar-container';

function getSubtitleEl() {
return $('#avatar-subtitle');
}

function getLoadingEl() {
return $('#avatar-loading');
}

function getLoadingTextEl() {
return $('#avatar-loading-text');
}

/* ============== 连接 ============== */
async function connect() {
if (avatarState.connected || avatarState.connecting) return;

const containerEl = document.getElementById(containerId);
if (!containerEl) {
console.error('[Avatar] 容器不存在:', containerId);
return;
}

patch({ connecting: true });
updateLoadingUI(true);

try {
const url = new URL(AVATAR_CONFIG.gatewayUrl);
url.searchParams.append('data_source', AVATAR_CONFIG.dataSource);
url.searchParams.append('custom_id', AVATAR_CONFIG.customId);

let resolveConnect;
let rejectConnect;
const connectPromise = new Promise((res, rej) => {
resolveConnect = res;
rejectConnect = rej;
});

const options = {
containerId: `#${containerId}`,
appId: AVATAR_CONFIG.appId,
appSecret: AVATAR_CONFIG.appSecret,
enableDebugger: false,
gatewayServer: url.toString(),
onProxyWidgetEvent: (event) => console.log('[Avatar SDK事件]', event),
onStateChange: (state) => {
patch({ avatarState: state });
},
onMessage: async (error) => {
// 连接失败时 reject
if (!avatarState.connected) {
rejectConnect(new Error(error.message || 'SDK连接失败'));
}
},
onVoiceStateChange: (status) => {
if (status.includes('end')) {
patch({ avatarState: 'interactive_idle' });
}
},
};

// 创建 SDK 实例
const avatar = new window.XmovAvatar(options);

// 等待初始化
await new Promise(r => setTimeout(r, 3000));

await avatar.init({
onDownloadProgress: (progress) => {
if (progress >= 100) {
resolveConnect(true);
}
},
onClose: () => {
patch({ avatarState: '', connected: false });
},
});

// 等待连接完成或超时
const timeout = new Promise((_, rej) => setTimeout(() => rej(new Error('连接超时')), 15000));
try {
await Promise.race([connectPromise, timeout]);
} catch (e) {
console.warn('[Avatar] 连接等待结束:', e.message);
}

avatarState.instance = avatar;
patch({ connected: true, connecting: false });
updateLoadingUI(false);
console.log('[Avatar] 连接成功');

// 连接成功后播报问候
setTimeout(() => {
speakText(AVATAR_IDENTITY.greeting);
}, 800);

} catch (err) {
console.error('[Avatar] 连接失败:', err);
patch({ connected: false, connecting: false });
updateLoadingUI(false);
}
}

function disconnect() {
if (avatarState.instance) {
try {
avatarState.instance.stop();
avatarState.instance.destroy();
} catch (e) {
console.error('[Avatar] 断开失败:', e);
}
avatarState.instance = null;
patch({ connected: false, avatarState: '' });
}
}

/* ============== 播报 ============== */
function speakText(text) {
if (!avatarState.instance || !text) return;
// 生成 SSML
const ssml = `<speak>${text}</speak>`;
avatarState.instance.speak(ssml, true, true);
patch({ avatarState: 'speak' });
}

function interrupt() {
if (!avatarState.instance) return;
try {
if (typeof avatarState.instance.interactiveidle === 'function') {
avatarState.instance.interactiveidle();
} else if (typeof avatarState.instance.interrupt === 'function') {
avatarState.instance.interrupt();
}
patch({ avatarState: 'interactive_idle' });
} catch (e) {
console.error('[Avatar] 打断失败:', e);
}
}

/* ============== 字幕 ============== */
function setSubtitle(text) {
patch({ subtitle: text });
const el = getSubtitleEl();
if (el) {
el.textContent = text;
el.classList.toggle('is-on', !!text);
}
}

/* ============== UI 更新 ============== */
function updateLoadingUI(show) {
const el = getLoadingEl();
if (el) {
el.style.display = show ? 'flex' : 'none';
}
}

/* ============== 初始化 ============== */
function init() {
console.log('[Avatar] 初始化数字人服务');
connect();
}

// DOM 加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}

/* ============== 暴露全局接口 ============== */
window.AvatarService = {
connect,
disconnect,
speakText,
interrupt,
setSubtitle,
subscribe,
getState: () => ({
connected: avatarState.connected,
connecting: avatarState.connecting,
subtitle: avatarState.subtitle,
avatarState: avatarState.avatarState,
}),
IDENTITY: AVATAR_IDENTITY,
};

})();

关键逻辑分析:

  • IIFE 封装避免污染全局命名空间,window.AvatarService 暴露 connect/speakText/interrupt 等方法。app.js 在生成卡片后通过 window.AvatarService.speakText 让纪燃播报结果——这是具身交互智能在零框架场景下的落地方案
  • AVATAR_IDENTITY 定义纪燃的身份与问候语,贯穿页面名牌、连接问候、播报文案。connect 成功后延迟 800ms 播报问候,给魔珐 SDK 一个稳定窗口,避免初始化与播报抢资源——这与 Digital Garden 中 GardenGuide 组件的 greeted ref 设计同源
  • subscribe + patch 实现发布-订阅模式。avatarState 变化时通知所有监听者,这与 React 项目中 useSyncExternalStore 的桥接逻辑同源——是具身交互智能跨框架复用的架构基础
  • speakText 将纯文本包裹为 SSML 格式发送给魔珐参数流 SDK,SDK 驱动数字人发音并同步肢体动作。speak 的第二个参数 true 表示打断当前播报,第三个参数 true 表示启用表情动画——让纪燃在说话时有自然的嘴唇同步与面部表情
  • onVoiceStateChange 回调在语音播放结束时将状态切回 interactive_idle,让数字人回到待机姿态,而非停留在说话姿势——这是具身交互智能中"有肢体表达"的细节
  • onMessage 回调在 SDK 连接失败时 reject connectPromise,配合 15 秒超时兜底,防止无限等待。连接失败不阻塞用户使用,这是具身交互智能的容错设计

4.2 数字人播报集成:关键节点的语音反馈

app.js 在两个关键节点调用纪燃的语音能力,让具身交互智能贯穿用户旅程:

// 生成卡片成功后(app.js onGenerate 函数末尾)
if (window.AvatarService && window.AvatarService.getState().connected) {
const announceText = card.type === 'player'
? `已为${card.playerName}生成典藏卡,${card.content.title}`
: `已生成回忆卡,${card.content.title}`;
window.AvatarService.speakText(announceText);
}

// 保存画廊后(app.js saveToGallery 函数)
if (window.AvatarService && window.AvatarService.getState().connected) {
window.AvatarService.speakText('已为你珍藏这张典藏卡。');
}

关键逻辑分析:

  • 先检查 window.AvatarService 是否存在且 connected 为 true,再根据卡片类型构建播报文案——这是具身交互智能的优雅降级设计:数字人在线时语音播报,离线时静默跳过,不阻塞主流程
  • 纪燃说出"已为梅西生成典藏卡,潘帕斯雄鹰·梅西"的那一刻,大模型的内容能力与数字人的具身表达能力完成了闭环。用户不只是看到一张卡片,而是听到一位解说员为这张卡片命名——这是具身交互智能赋予典藏的温度
  • saveToGallery 中的"已为你珍藏这张典藏卡"是整条链路的收尾语音,配合卡片存入画廊的动画,让保存动作有了仪式感

4.3 Seed 调用与降级策略:整届赛事上下文一次性注入

app.js 中的 callSeed 函数是内容引擎的核心。它负责把整届赛事上下文 + 球员/观赛信息组装成 prompt,调用 Seed 模型获取 JSON 格式的卡片档案,为纪燃的语音播报提供内容素材:

async function callSeed(messages, apiKey) {
if (!apiKey) {
// 未填 Key:直接走本地 demo,避免无意义 fetch
const last = messages[messages.length 1].content;
const isPlayer = last.includes('请为球员');
const payload = isPlayer
? extractPlayerFields(last)
: extractMemoryFields(last);
return JSON.stringify(mockGenerate(isPlayer ? 'player' : 'memory', payload));
}
try {
const res = await fetch(SEED_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: SEED_MODEL,
messages,
temperature: 0.7,
// Seed 1M+ 超长上下文:此处一次性把整届赛事 + 球员生涯全部喂入
// 依赖豆包 Seed-evolving 的长上下文能力,无需分段
response_format: { type: 'json_object' },
}),
});
if (!res.ok) {
const errText = await res.text().catch(() => '');
throw new Error(`Seed 接口错误 ${res.status}: ${errText.slice(0, 200)}`);
}
const data = await res.json();
return data.choices?.[0]?.message?.content ?? '';
} catch (err) {
// CORS / 网络错误:降级到本地 mock,保证 Demo 可用
console.warn('[Seed] 调用失败,已切换到本地 Demo 模式:', err.message);
const last = messages[messages.length 1].content;
const isPlayer = last.includes('请为球员');
const payload = isPlayer ? extractPlayerFields(last) : extractMemoryFields(last);
// 在文本中附加提示,让 UI 知道当前是 Demo 数据
const mockData = mockGenerate(isPlayer ? 'player' : 'memory', payload);
mockData.__demo = true;
mockData.__demoReason = err.message;
return JSON.stringify(mockData);
}
}

关键逻辑分析:

  • SEED_ENDPOINT 指向 /api/seed/chat/completions,由 server.py 代理转发到火山方舟。前端不直接请求外部域名,完全绕过 CORS
  • 未填 apiKey 时直接走 mockGenerate 本地降级,保证双击 HTML 即可体验完整 UI 流程——这是活动页 Demo 的刚需
  • fetch 失败时同样降级到本地 mock,并附加 __demo 标记让 UI 显示提示。用户不会因为接口问题卡在某一步
  • response_format 设为 json_object 强制模型输出 JSON,避免解析 markdown 包裹的额外复杂度。模型返回的 JSON 经 normalizeCardData 做长度兜底后,同时送给卡面渲染和纪燃语音播报

4.4 Prompt 工程:编年史官人格与字数硬约束

buildPlayerMessages 函数构建球员卡的 system + user prompt,是具身交互智能内容层的核心。它需要同时完成三件事:注入整届赛事全局上下文、定义编年史官人格、约束输出字数:

function buildPlayerMessages(playerName, country) {
const ctx = state.tournamentContext || '(用户未提供完整赛事上下文,请基于你对世界杯的公开记忆进行补全)';
return [
{
role: 'system',
content:
'你是世界杯编年史官,专精于把整届赛事、球员十几年生涯浓缩为球星卡背面档案。' +
'你必须严格输出 JSON,且字段与用户指定一致。文字热血、凝练、有电影感,数字精准。' +
'【硬性长度限制】title ≤ 14 个汉字(含绰号和间隔符);chronicle 严格 120-140 字;' +
'keyStats 共 3 条,每条 ≤ 14 字;legendaryMoment 严格 70-90 字。超长会被截断,请严格遵守。',
},
{
role: 'user',
content:
`【整届赛事全局数据】
${ctx}

请为球员「${playerName}${country ? `(所属国家队:${country}` : ''}输出一张典藏球星卡背面档案。
要求输出严格的 JSON(所有文本字段必须严格遵守上面的字数上限):
{
"title": "卡面标题,含绰号,总共 ≤14 个汉字,如『潘帕斯雄鹰·梅西』",
"chronicle": "120-140 字编年史",
"keyStats": ["3 条,每条 ≤14 字"],
"legendaryMoment": "70-90 字名场面解读",
"rarity": "common|silver|gold|black|final",
"rating": "数字 75-99",
"position": "位置缩写 FW/MF/DF/GK",
"jerseyNumber": "球衣号码"
}
`,
},
];
}

关键逻辑分析:

  • system prompt 中的"编年史官"人格定义了文案风格:热血、凝练、有电影感。这不是随便写的形容词——它直接决定了模型输出的文字质感,也是纪燃语音播报时的文案来源。文字有温度,纪燃说出来才有感染力
  • 硬性长度限制(title ≤ 14 字、chronicle 120-140 字等)是卡面物理空间决定的。3D 卡片背面区域有限,超长文字会溢出或截断。prompt 层约束 + 代码层 clampText 双重兜底
  • 整届赛事全局上下文通过 state.tournamentContext 注入,这是 Seed 1M+ 上下文能力的直接应用——传统模型需要分段摘要的场景,这里一次性喂入,让模型在生成编年史时拥有完整的全局视野

4.5 3D 卡片交互:鼠标跟随倾斜与翻转

bindCardInteractions 函数为每张卡片绑定鼠标跟随倾斜和点击翻转效果,是纪燃语音之外的视觉体验核心:

function bindCardInteractions(card3d) {
if (!card3d) return;
const scene = card3d.parentElement;
let flipped = false, dragging = false, startX = 0, startY = 0;
// 标记是否来自 touch,避免移动端 touch 结束后 click 事件二次触发
let touchJustEnded = false;

const applyTilt = (e) => {
if (flipped) return;
const r = scene.getBoundingClientRect();
const x = (e.clientX r.left) / r.width;
const y = (e.clientY r.top) / r.height;
const rotY = (x 0.5) * 25;
const rotX = (0.5 y) * 18;
card3d.style.setProperty('–mx', `${x * 100}%`);
card3d.style.setProperty('–my', `${y * 100}%`);
card3d.style.setProperty('–holo-angle', `${x * 360}deg`);
// 叠加小角度倾斜(注意:不覆盖 .flipped 的 rotateY(180deg))
card3d.style.transform = `rotateY(${rotY}deg) rotateX(${rotX}deg)`;
};
const resetTilt = () => {
if (!flipped) card3d.style.transform = '';
};
const toggleFlip = () => {
flipped = !flipped;
card3d.classList.toggle('flipped', flipped);
// 关键:清除内联 transform,让 CSS 类全权控制翻转状态;回正时保留过渡
card3d.style.transform = '';
};

scene.addEventListener('mousemove', (e) => { if (!touchJustEnded) applyTilt(e); });
scene.addEventListener('mouseleave', resetTilt);
scene.addEventListener('click', (ev) => {
if (touchJustEnded) { touchJustEnded = false; return; }
// 点击到操作按钮,不触发翻转
if (ev.target.closest('.card-action-btn')) return;
toggleFlip();
});

// 移动端触摸翻转
scene.addEventListener('touchstart', (e) => {
dragging = true;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
}, { passive: true });
scene.addEventListener('touchend', (e) => {
const dx = e.changedTouches[0].clientX startX;
const dy = e.changedTouches[0].clientY startY;
if (Math.abs(dx) < 10 && Math.abs(dy) < 10) {
touchJustEnded = true;
toggleFlip();
}
dragging = false;
});
}

关键逻辑分析:

  • applyTilt 通过鼠标在卡片区域内的相对位置计算倾斜角度,最大旋转 25 度(Y 轴)和 18 度(X 轴),产生自然的 3D 跟随效果
  • –mx / –my CSS 变量驱动镭射反光层的位置,–holo-angle 驱动全息层的旋转角度——鼠标移动时卡面产生真实的光线折射变化
  • toggleFlip 翻转时清除内联 transform,让 CSS 类 .flipped 全权控制 rotateY(180deg)。这避免了内联样式与 CSS 类的优先级冲突
  • touchJustEnded 标记解决移动端触摸结束后 click 事件二次触发的问题——这是移动端适配的常见陷阱
  • touchstart/touchend 事件实现移动端触摸翻转,通过判断移动距离小于 10px 来区分点击与滑动

4.6 API 代理服务器:107 行 Python 解决 CORS

server.py 是整个项目的服务端基础设施,用 Python 标准库实现静态文件托管和 API 反向代理,为具身交互智能链路提供稳定的后端支撑:

class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=STATIC_DIR, **kwargs)

def end_headers(self):
# CORS 头(允许浏览器跨域取字体等资源,冗余保险)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
super().end_headers()

def do_OPTIONS(self):
self.send_response(200)
self.end_headers()

def do_POST(self):
if self.path.startswith("/api/seed/"):
self.proxy_to_seed(SEED_BASE, "/api/seed/")
elif self.path.startswith("/api/image/"):
self.proxy_to_seed(SEED_IMAGE_BASE, "/api/image/")
else:
self.send_error(404, "Not Found")

def proxy_to_seed(self, upstream_base, prefix):
upstream_path = self.path[len(prefix):]
upstream_url = f"{upstream_base}/{upstream_path}"

length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(length) if length else b""
auth = self.headers.get("Authorization", "")

req = urllib.request.Request(
upstream_url,
data=body,
method="POST",
headers={
"Content-Type": "application/json",
"Authorization": auth,
},
)
try:
print(f"[PROXY] POST {upstream_url} body_len={len(body)}", flush=True)
with urllib.request.urlopen(req, timeout=180) as resp:
data = resp.read()
self.send_response(resp.status)
self.send_header("Content-Type", resp.headers.get("Content-Type", "application/json; charset=utf-8"))
self.send_header("Content-Length", str(len(data)))
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(data)
print(f"[PROXY] <- {resp.status} {len(data)} bytes", flush=True)
except urllib.error.HTTPError as e:
err_body = e.read()
print(f"[PROXY] !! HTTP {e.code}: {err_body[:300]!r}", flush=True)
self.send_response(e.code)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(err_body)
except Exception as e:
print(f"[PROXY] !! EXC {type(e).__name__}: {e}", flush=True)
msg = json.dumps({"error": str(e)}).encode("utf-8")
self.send_response(502)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(msg)

关键逻辑分析:

  • /api/seed/* 转发到火山方舟 chat/completions 接口,/api/image/* 转发到 Seedream 文生图接口。两条路径共用同一个 proxy_to_seed 方法
  • Authorization 头原样透传给火山方舟,API Key 存储在浏览器 localStorage,不经过 server.py 保存——密钥管理完全在前端
  • 180 秒超时覆盖 Seed 模型生成长文案的耗时,Access-Control-Allow-Origin: * 让浏览器端的 fetch 请求不被 CORS 拦截
  • end_headers 方法统一添加 CORS 头,do_OPTIONS 处理预检请求,proxy_to_seed 包含完整的异常处理(HTTPError 和通用 Exception)

五、视觉设计系统:金色球场

Seed Chronicle 的视觉气质是世界杯的金色荣耀与球场绿茵的沉浸感,由一组 CSS 令牌驱动:

:root {
bg0: #05070f;
bg1: #0a0e1a;
bg2: #111828;
ink0: #f4efe3;
ink1: #c9c2b0;
ink2: #8b8676;
ink3: #5a5647;
gold0: #ffe9a0;
gold1: #d4af37;
gold2: #8a6a1a;
gold3: #4a3a12;
red: #e4002b;
greenpitch: #0f4d2a;
greenglow: #1db954;

raritycommon: #9aa4b2;
raritysilver: #d8dde6;
raritygold: #f5c542;
rarityblack: #2a2a2a;
rarityfinal: lineargradient(135deg, #ff4d4d, #ffb800, #ffffff);

cardw: 340px;
cardh: 480px;
radius: 14px;
shadowlg: 0 30px 80px 20px rgba(0, 0, 0, 0.8), 0 10px 30px 10px rgba(212, 175, 55, 0.15);

fontdisplay: 'Bebas Neue', 'Oswald', sansserif;
fonthead: 'Oswald', 'Noto Sans SC', sansserif;
fontbody: 'Noto Sans SC', systemui, sansserif;
fontmono: 'JetBrains Mono', monospace;
}

背景由四层元素叠加,营造球场灯光的层次感:

.bglayer {
position: fixed; inset: 0; zindex: 1; overflow: hidden;
background: radialgradient(ellipse at top, #15213a 0%, var(bg0) 60%);
}
.bggradient {
position: absolute; inset: 0;
background:
radialgradient(circle at 20% 10%, rgba(228, 0, 43, 0.18), transparent 50%),
radialgradient(circle at 80% 0%, rgba(212, 175, 55, 0.18), transparent 50%),
radialgradient(circle at 50% 100%, rgba(15, 77, 42, 0.35), transparent 60%);
}
.bggrid {
position: absolute; inset: 0;
backgroundimage:
lineargradient(rgba(212, 175, 55, 0.05) 1px, transparent 1px),
lineargradient(90deg, rgba(212, 175, 55, 0.05) 1px, transparent 1px);
backgroundsize: 48px 48px;
maskimage: radialgradient(ellipse at center, black 30%, transparent 75%);
}

  • 深色基底 #05070f 是球场夜色,金色 #d4af37 是主交互色(按钮、稀有度标记、镭射反光),红色 #e4002b 是决赛限定的点缀色,绿茵 #0f4d2a 是画廊区域的底色暗示
  • 三层径向渐变分别从左上(红色微光)、右上(金色暖光)、底部(绿茵光)投射,配合 48px 间距的金色网格线(mask-image 渐隐边缘)模拟球场的灯光与草皮纹理
  • 奖杯光晕 .bg-trophy-glow 用 800px 的金色径向渐变 + 6 秒脉冲动画,在页面顶部中央产生呼吸感的荣耀光效
  • 数字人舞台 .avatar-stage 用金色玻璃态外框(backdrop-filter: blur(16px) + 金色描边)让纪燃在右侧舞台上有"悬浮"感,与整体金色球场视觉统一

六、总结

Seed Chronicle 把具身交互智能嵌入了一条完整的典藏链路:选球星 → 写档案 → 生插画 → 翻卡片 → 存画廊。数字人纪燃不是锦上添花的装饰,而是贯穿全程的解说员——她自动迎接访客、在卡片生成后播报结果、在保存画廊时语音确认。

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

  • 魔珐参数流 SDK 集成:IIFE 封装的原生 JS 数字人服务,通过 window.AvatarService 暴露全局接口,connect/speakText/interrupt 三个核心方法覆盖连接、播报、打断全链路——这是具身交互智能在零框架场景的落地方案
  • Seed 1M+ 超长上下文:整届赛事数据一次性注入 prompt,模型在生成编年史时拥有完整全局视野。文案经纪燃语音播报,让大模型的内容能力与数字人的具身表达能力形成闭环
  • CSS 3D 卡片系统:鼠标跟随倾斜 + 点击翻转 + 镭射反光 + 四种卡面风格,配合 html2canvas 高清导出,让数字内容可以沉淀为可分享的实体
  • 这条链路的本质是具身交互智能——AI 不只是生成文案的工具,而是有形象、有声音、有肢体表达的解说员,把大语言模型的内容能力、文生图的视觉能力与数字人的具身表达能力绑定在同一次交互里。当用户听到纪燃说出"已为梅西生成典藏卡,潘帕斯雄鹰·梅西"的那一刻,典藏才真正有了温度。

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

    赞(0)
    未经允许不得转载:171主机测评 » 不止AI图文生成!依托具身交互数字人,全方位重构世界杯赛事典藏沉浸式体验
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址