实时交互式数字人系统构建实战:从架构到代码实现
基于 Web 的实时交互式数字人系统构建实战。文章涵盖系统架构设计、状态机管理、数字人 SDK 集成、AI 对话引擎流式响应及语音处理模块。通过 HTML/CSS/JavaScript 实现前端交互,结合大语言模型 API 与 TTS/ASR 服务完成多模态交互。包含性能优化、错误重试机制及资源预加载等最佳实践。

基于 Web 的实时交互式数字人系统构建实战。文章涵盖系统架构设计、状态机管理、数字人 SDK 集成、AI 对话引擎流式响应及语音处理模块。通过 HTML/CSS/JavaScript 实现前端交互,结合大语言模型 API 与 TTS/ASR 服务完成多模态交互。包含性能优化、错误重试机制及资源预加载等最佳实践。

数字人(Digital Human)是指通过计算机图形学、人工智能等技术创建的、具有人类外观特征的虚拟角色。随着技术的发展,数字人已从简单的 3D 模型进化为能够实时交互、具备智能的"虚拟生命"。
| 场景 | 描述 | 技术要点 |
|---|---|---|
| 虚拟主播 | 直播、新闻播报 | 实时驱动、表情同步 |
| 智能客服 | 企业服务、政务咨询 | 知识库、多轮对话 |
| 虚拟导师 | 在线教育、技能培训 | 教学交互、个性化 |
| 元宇宙社交 | 虚拟会议、社交游戏 | 多用户同步、沉浸感 |
前端层:HTML5 + CSS3 + JavaScript
数字人 SDK:魔珐星云 / ReadyPlayerMe / MediaPipe
AI 模型:OpenAI GPT / 阿里通义千问 / Claude
语音服务:Azure TTS / 讯飞 / Web Speech API
实时通信:WebRTC / WebSocket
数据层 服务层 应用层 用户端 浏览器界面 视频渲染区 对话交互区 数字人控制器 AI 对话引擎 语音处理模块 状态管理器 数字人 SDK 大语言模型 API TTS/ASR 服务 知识库 配置存储 对话历史 用户画像
语音服务 AI 引擎数字人控制器界面用户语音服务 AI 引擎数字人控制器界面用户输入问题/语音触发交互切换到 listen 状态发送用户问题构建 Prompt 调用 LLM 返回 AI 回复切换到 think 状态文字转语音返回音频流切换到 speak 状态更新对话记录切换到 idle 状态
数字人在交互过程中有多种状态,正确的状态管理是保证流畅体验的关键。
初始状态 用户点击连接 SDK 初始化成功 初始化失败 默认待机 用户开始输入 长时间无交互 提交问题给 AI 用户取消 AI 返回回复 AI 返回错误 播放完成 用户打断 用户唤醒 断开连接 Disconnected Connecting Connected Idle Listen Offline Think Speak 待机状态 数字人播放待机动画 说话状态 驱动口型和表情
class AvatarController {
constructor(containerId, config) {
this.container = document.querySelector(containerId);
this.config = config;
this.sdk = null;
this.currentState = 'disconnected';
this.eventHandlers = new Map();
}
/**
* 初始化数字人 SDK
*/
async init() {
try {
this.updateState('connecting');
// 创建 SDK 实例
this.sdk = new XmovAvatar({
containerId: this.container.id,
appId: this.config.appId,
appSecret: this.config.appSecret,
gatewayServer: 'https://nebula-agent.xingyun3d.com/user/v1/ttsa/session',
// 关键回调配置
onMessage: (msg) => this.handleMessage(msg),
onStateChange: (state) => this.handleStateChange(state),
onStatusChange: (status) => this.handleStatusChange(status),
onVoiceStateChange: (voiceState) => this.handleVoiceStateChange(voiceState),
});
await this.sdk.init();
this.updateState('connected');
this.idle(); // 进入待机状态
return true;
} catch (error) {
console.error('SDK 初始化失败:', error);
this.updateState('disconnected');
throw error;
}
}
/**
* 状态管理
*/
updateState(newState) {
const oldState = this.currentState;
this.currentState = newState;
this.emit('stateChange', { oldState, newState });
}
/**
* 待机模式
*/
idle() {
if (this.sdk) {
this.sdk.idle();
this.updateState('idle');
}
}
/**
* 倾听模式
*/
listen() {
if (this.sdk) {
this.sdk.listen();
this.updateState('listen');
}
}
/**
* 思考模式
*/
think() {
if (this.sdk) {
this.sdk.think();
this.updateState('think');
}
}
/**
* 说话模式
*/
async speak(text) {
if (this.sdk) {
this.updateState('speak');
await this.sdk.speak(text);
this.updateState('idle');
}
}
/**
* 销毁实例
*/
destroy() {
if (this.sdk) {
this.sdk.destroy();
this.sdk = null;
this.updateState('disconnected');
}
}
// 事件处理方法
handleMessage(message) {
console.log('SDK 消息:', message);
if (message.code !== 0) {
this.emit('error', message);
}
}
handleStateChange(state) {
console.log('状态变化:', state);
this.emit('avatarStateChange', state);
}
handleStatusChange(status) {
console.log('连接状态:', status);
this.emit('statusChange', status);
}
handleVoiceStateChange(voiceState) {
console.log('语音状态:', voiceState);
this.emit('voiceStateChange', voiceState);
}
// 事件系统
on(event, handler) {
if (!this.eventHandlers.has(event)) {
this.eventHandlers.set(event, []);
}
this.eventHandlers.get(event).push(handler);
}
emit(event, data) {
const handlers = this.eventHandlers.get(event) || [];
handlers.forEach(handler => handler(data));
}
}
class AIConversationEngine {
constructor(config) {
this.apiKey = config.apiKey;
this.model = config.model || 'qwen-plus';
this.baseURL = config.baseURL || 'https://api.modelscope.cn/v1';
this.systemPrompt = config.systemPrompt || this.getDefaultPrompt();
this.conversationHistory = [];
}
/**
* 默认系统提示词
*/
getDefaultPrompt() {
return `你是一个智能助手,名叫"小政"。你的职责是为用户提供专业的咨询服务。服务准则:1. 用简洁、友好、专业的语言回答 2. 不确定的信息诚实告知 3. 超出范围的问题礼貌拒绝并引导 4. 回答控制在 200 字以内`;
}
/**
* 发送消息并获取流式响应
*/
async chat(userMessage, onChunk, onComplete, onError) {
try {
// 添加用户消息到历史
this.conversationHistory.push({ role: 'user', content: userMessage });
// 构建请求消息
const messages = [
{ role: 'system', content: this.systemPrompt },
...this.getRecentHistory()
];
response = (, {
: ,
: {
: ,
:
},
: .({
: .,
: messages,
: ,
: ,
:
})
});
(!response.) {
();
}
reader = response..();
decoder = ();
fullResponse = ;
() {
{ done, value } = reader.();
(done) ;
chunk = decoder.(value);
lines = chunk.().( line.());
( line lines) {
(line.()) {
data = line.();
(data === ) ;
{
parsed = .(data);
content = parsed.[]?.?.;
(content) {
fullResponse += content;
(content);
}
} (e) {
.(, e);
}
}
}
}
..({ : , : fullResponse });
(fullResponse);
} (error) {
.(, error);
(error);
}
}
() {
..(-limit);
}
() {
. = [];
}
() {
. = newPrompt;
}
}
class VoiceProcessor {
constructor(config) {
this.config = config;
this.synthesis = window.speechSynthesis;
this.recognition = null;
this.isListening = false;
}
/**
* 文字转语音
*/
async speak(text, options = {}) {
return new Promise((resolve, reject) => {
// 取消之前的播放
this.synthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = options.lang || 'zh-CN';
utterance.rate = options.rate || 1.0;
utterance.pitch = options.pitch || 1.0;
utterance.volume = options.volume || 1.0;
// 选择语音包
if (options.voiceName) {
const voices = this.synthesis.();
voice = voices.( v. === options.);
(voice) utterance. = voice;
}
utterance. = ();
utterance. = (error);
..(utterance);
});
}
() {
(!( )) {
( ());
;
}
. = ();
.. = ;
.. = ;
.. = ;
finalTranscript = ;
.. = {
interimTranscript = ;
( i = event.; i < event..; i++) {
transcript = event.[i][].;
(event.[i].) {
finalTranscript += transcript;
} {
interimTranscript += transcript;
}
}
({ : finalTranscript, : interimTranscript });
};
.. = {
( (event.));
};
.. = {
. = ;
};
..();
. = ;
}
() {
(. && .) {
..();
. = ;
}
}
() {
..();
}
() {
..();
}
}
class DigitalHumanSystem {
constructor(config) {
this.config = config;
// 初始化各模块
this.avatar = new AvatarController('#avatar-container', {
appId: config.avatarAppId,
appSecret: config.avatarAppSecret
});
this.ai = new AIConversationEngine({
apiKey: config.aiApiKey,
model: config.aiModel,
systemPrompt: config.systemPrompt
});
this.voice = new VoiceProcessor(config.voice);
// UI 状态
this.isProcessing = false;
}
/**
* 初始化系统
*/
async init() {
// 初始化数字人
await this.avatar.init();
// 绑定事件
this.bindEvents();
console.log('数字人系统初始化完成');
}
/**
* 绑定事件
*/
() {
..(, {
(state === && .) {
..();
. = ;
}
});
..(, {
.(, error);
});
}
() {
(.) {
.();
;
}
. = ;
{
..();
..();
fullResponse = ;
..(
text,
{
.(, chunk);
fullResponse += chunk;
},
{
.(, complete);
},
{
.(, error);
}
);
(options. !== ) {
..(fullResponse);
} {
..();
. = ;
}
fullResponse;
} (error) {
.(, error);
..();
. = ;
error;
}
}
() {
..( {
(result);
(result.) {
.(result.);
}
}, {
.(, error);
});
}
() {
..();
}
() {
..(sceneConfig.);
.(, sceneConfig.);
}
() {
..();
..();
..();
}
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数字人交互系统</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app-container">
<!-- 顶部导航 -->
<header class="header">
<h1>智能政务服务大厅</h1>
<div class="controls">
<button id="btn-connect" class="btn primary">连接数字人</button>
<button id="btn-disconnect" class= >断开连接
设置
点击"连接数字人"开始体验
未连接
你好!我是智能政务助手小政,有什么可以帮您的吗?
如何办理身份证?
社保查询流程
不动产登记需要什么材料?
...
发送
系统设置
数字人 AppId
数字人 AppSecret
AI API Key
保存
关闭
/* 全局样式 */
:root {
--primary-color: #1890ff;
--success-color: #52c41a;
--danger-color: #ff4d4f;
--text-color: #333;
--bg-color: #f0f2f5;
--card-bg: #ffffff;
--border-radius: 8px;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-color);
color: var(--text-color);
height: 100vh;
overflow: hidden;
}
.app-container {
display: flex;
flex-direction: column;
height: 100%;
}
/* 顶部导航 */
.header {
background: var(--card-bg);
padding: 16px 24px;
: flex;
: space-between;
: center;
: (--shadow);
: ;
}
{
: ;
: ;
}
{
: flex;
: ;
}
{
: ;
: none;
: (--border-radius);
: pointer;
: ;
: all ;
}
{
: (--primary-color);
: white;
}
{
: ;
}
{
: (--danger-color);
: white;
}
{
: ;
: (--text-color);
}
{
: ;
: not-allowed;
}
{
: ;
: flex;
: ;
: ;
: hidden;
}
{
: ;
: (--card-bg);
: (--border-radius);
: (--shadow);
: flex;
: column;
}
{
: ;
: relative;
: ;
: (--border-radius) (--border-radius) ;
: hidden;
}
{
: absolute;
: ;
: ;
: ;
: ;
: flex;
: center;
: center;
: ;
}
{
: ;
: flex;
: center;
: ;
: solid ;
}
{
: ;
: ;
: ;
: ;
}
{
: (--success-color);
}
{
: (--primary-color);
: pulse infinite;
}
pulse {
, {
: ;
}
{
: ;
}
}
{
: ;
: ;
: (--card-bg);
: (--border-radius);
: (--shadow);
: flex;
: column;
}
{
: ;
: auto;
: ;
: flex;
: column;
: ;
}
{
: ;
}
{
: flex-end;
}
{
: flex-start;
}
{
: center;
}
{
: ;
: ;
: ;
}
{
: (--primary-color);
: white;
}
{
: ;
: (--text-color);
}
{
: ;
: ;
: ;
}
{
: ;
: flex;
: wrap;
: ;
: solid ;
}
{
: ;
: ;
: none;
: ;
: ;
: pointer;
: background ;
}
{
: ;
}
{
: ;
: solid ;
: flex;
: column;
: ;
}
{
: ;
: ;
: ;
: solid ;
: (--border-radius);
: none;
: inherit;
}
{
: none;
: (--primary-color);
}
{
: flex;
: space-between;
: center;
}
{
: ;
: ;
: none;
: ;
: ;
: pointer;
: flex;
: center;
: center;
}
{
: ;
}
{
: (--danger-color);
: white;
: pulse infinite;
}
{
: none;
: fixed;
: ;
: ;
: ;
: ;
: (, , , );
: center;
: center;
: ;
}
{
: flex;
}
{
: white;
: ;
: (--border-radius);
: ;
: ;
}
{
: ;
: ;
}
{
: ;
}
{
: block;
: ;
: ;
: ;
}
{
: ;
: ;
: solid ;
: ;
}
{
: flex;
: flex-end;
: ;
: ;
}
(: ) {
{
: column;
: auto;
}
{
: ;
}
{
: ;
: ;
}
}
// 配置管理
const ConfigManager = {
STORAGE_KEY: 'digital_human_config',
save(config) {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(config));
},
load() {
const data = localStorage.getItem(this.STORAGE_KEY);
return data ? JSON.parse(data) : null;
},
clear() {
localStorage.removeItem(this.STORAGE_KEY);
}
};
// UI 管理器
class UIManager {
constructor() {
this.elements = {
messagesContainer: document.getElementById('chat-messages'),
userInput: document.getElementById('user-input'),
statusIndicator: document.getElementById('status-indicator'),
statusText: document.getElementById('status-text'),
: .(),
: .(),
: .(),
: .()
};
}
() {
messageDiv = .();
messageDiv. = ;
contentDiv = .();
contentDiv. = ;
contentDiv. = content;
messageDiv.(contentDiv);
...(messageDiv);
... = ...;
}
() {
... = ;
}
() {
... = ;
... = text;
}
() {
... = isConnecting;
... = !isConnecting;
}
}
{
() {
. = ;
. = ();
. = .() || .();
.();
}
() {
{
: ,
: ,
: ,
:
};
}
() {
....(, .());
....(, .());
....(, .());
....(, .());
....(, {
(e. === && !e.) {
e.();
.();
}
});
.().( {
btn.(, {
.... = btn.;
.();
});
});
}
() {
(!.()) {
();
;
}
..(, );
..();
{
. = (.);
..();
..(, );
..(, );
} (error) {
.(, error);
..(, );
( + error.);
..();
}
}
() {
(.) {
..();
. = ;
}
..(, );
..();
}
() {
input = .....();
(!input || !.) ;
..(input, );
.... = ;
{
response = ..(input);
..(response, );
} (error) {
.(, error);
..(, );
}
}
() {
btn = ...;
(btn..()) {
.?.();
btn..();
} {
.?.( {
.... = result. || result.;
});
btn..();
}
}
() {
.. && .. && ..;
}
}
.(, {
. = ();
});
// 对话历史管理
class ConversationManager {
constructor(maxHistory = 20) {
this.maxHistory = maxHistory;
this.history = [];
}
addMessage(role, content) {
this.history.push({ role, content });
// 超过限制时删除旧消息
if (this.history.length > this.maxHistory) {
this.history = this.history.slice(-this.maxHistory);
}
}
// 计算 token 数量,避免超出模型限制
estimateTokens(text) {
// 粗略估计:中文约 1.5 字符/token,英文约 4 字符/token
const chineseChars = (text.match(/[一-龥]/g) || []).length;
const otherChars = text.length - chineseChars;
return Math.ceil(chineseChars / 1.5 + otherChars / 4);
}
trimToTokenLimit(maxTokens) {
let totalTokens = 0;
const trimmedHistory = [];
for ( i = .. - ; i >= ; i--) {
tokens = .(.[i].);
(totalTokens + tokens > maxTokens) ;
trimmedHistory.(.[i]);
totalTokens += tokens;
}
. = trimmedHistory;
}
}
class RetryableRequest {
constructor(maxRetries = 3, baseDelay = 1000) {
this.maxRetries = maxRetries;
this.baseDelay = baseDelay;
}
async execute(requestFn) {
let lastError;
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
return await requestFn();
} catch (error) {
lastError = error;
// 判断是否可重试
if (!this.isRetryable(error)) {
throw error;
}
// 指数退避
const delay = this.baseDelay * Math.pow(2, attempt);
await this.sleep(delay);
}
}
throw lastError;
}
isRetryable(error) {
// 429 Too Many Requests
// 500 Internal Server Error
// 502 Bad Gateway
// 503 Service Unavailable
const retryableStatuses = [, , , ];
retryableStatuses.(error.);
}
() {
( (resolve, ms));
}
}
requester = ();
response = requester.( ());
// 使用状态机管理复杂交互
class AvatarStateMachine {
constructor() {
this.states = {
IDLE: 'idle',
LISTEN: 'listen',
THINK: 'think',
SPEAK: 'speak'
};
this.transitions = {
[this.states.IDLE]: [this.states.LISTEN],
[this.states.LISTEN]: [this.states.THINK, this.states.IDLE],
[this.states.THINK]: [this.states.SPEAK, this.states.IDLE],
[this.states.SPEAK]: [this.states.IDLE, this.states.LISTEN]
};
this.currentState = this.states.;
. = [];
}
() {
.[.]?.(newState);
}
() {
(!.(newState)) {
();
}
oldState = .;
. = newState;
.({ oldState, newState });
}
() {
..(observer);
}
() {
..( (event));
}
}
class ResourcePreloader {
constructor() {
this.loadedResources = new Set();
}
async preloadImages(urls) {
const promises = urls.map(url => {
return new Promise((resolve, reject) => {
if (this.loadedResources.has(url)) {
resolve();
return;
}
const img = new Image();
img.onload = () => {
this.loadedResources.add(url);
resolve();
};
img.onerror = reject;
img.src = url;
});
});
return Promise.all(promises);
}
async preloadAudio(urls) {
const promises = urls.map(url => {
return new Promise(() => {
(..(url)) {
();
;
}
audio = ();
audio. = {
..(url);
();
};
audio. = reject;
audio. = url;
});
});
.(promises);
}
}
核心要点包括:
当前阶段 增强交互 多模态融合 边缘部署 情感识别 手势交互 眼神追踪 视觉理解 声音克隆 场景感知 WebGL 加速 本地模型 离线运行
数字人技术正朝着以下方向发展:

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online