跳到主要内容实时交互式数字人系统构建实战:从架构到代码实现 | 极客日志JavaScriptAI大前端
实时交互式数字人系统构建实战:从架构到代码实现
基于 Web 的实时交互式数字人系统构建实战。文章涵盖系统架构设计、状态机管理、数字人 SDK 集成、AI 对话引擎流式响应及语音处理模块。通过 HTML/CSS/JavaScript 实现前端交互,结合大语言模型 API 与 TTS/ASR 服务完成多模态交互。包含性能优化、错误重试机制及资源预加载等最佳实践。
片刻34 浏览 技术背景
什么是数字人?
数字人(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
待机状态
数字人播放待机动画
说话状态
驱动口型和表情
核心技术实现
1. 数字人 SDK 集成
SDK 初始化
class AvatarController {
constructor(containerId, config) {
this.container = document.querySelector(containerId);
this.config = config;
this.sdk = null;
. = ;
. = ();
}
() {
{
.();
. = ({
: ..,
: ..,
: ..,
: ,
: .(msg),
: .(state),
: .(status),
: .(voiceState),
});
..();
.();
.();
;
} (error) {
.(, error);
.();
error;
}
}
() {
oldState = .;
. = newState;
.(, { oldState, newState });
}
() {
(.) {
..();
.();
}
}
() {
(.) {
..();
.();
}
}
() {
(.) {
..();
.();
}
}
() {
(.) {
.();
..(text);
.();
}
}
() {
(.) {
..();
. = ;
.();
}
}
() {
.(, message);
(message. !== ) {
.(, message);
}
}
() {
.(, state);
.(, state);
}
() {
.(, status);
.(, status);
}
() {
.(, voiceState);
.(, voiceState);
}
() {
(!..(event)) {
..(event, []);
}
..(event).(handler);
}
() {
handlers = ..(event) || [];
handlers.( (data));
}
}
this
currentState
'disconnected'
this
eventHandlers
new
Map
async
init
try
this
updateState
'connecting'
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
onStateChange
(state) =>
this
handleStateChange
onStatusChange
(status) =>
this
handleStatusChange
onVoiceStateChange
(voiceState) =>
this
handleVoiceStateChange
await
this
sdk
init
this
updateState
'connected'
this
idle
return
true
catch
console
error
'SDK 初始化失败:'
this
updateState
'disconnected'
throw
updateState
newState
const
this
currentState
this
currentState
this
emit
'stateChange'
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
this
updateState
'idle'
destroy
if
this
sdk
this
sdk
destroy
this
sdk
null
this
updateState
'disconnected'
handleMessage
message
console
log
'SDK 消息:'
if
code
0
this
emit
'error'
handleStateChange
state
console
log
'状态变化:'
this
emit
'avatarStateChange'
handleStatusChange
status
console
log
'连接状态:'
this
emit
'statusChange'
handleVoiceStateChange
voiceState
console
log
'语音状态:'
this
emit
'voiceStateChange'
on
event, handler
if
this
eventHandlers
has
this
eventHandlers
set
this
eventHandlers
get
push
emit
event, data
const
this
eventHandlers
get
forEach
handler =>
handler
2. AI 对话引擎
流式对话实现
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(10)
];
const response = await fetch(`${this.baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
model: this.model,
messages: messages,
stream: true,
temperature: 0.7,
max_tokens: 2000
})
});
if (!response.ok) {
throw new Error(`API 请求失败:${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim());
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const content = parsed.choices[0]?.delta?.content;
if (content) {
fullResponse += content;
onChunk(content);
}
} catch (e) {
console.warn('解析 chunk 失败:', e);
}
}
}
}
this.conversationHistory.push({ role: 'assistant', content: fullResponse });
onComplete(fullResponse);
} catch (error) {
console.error('AI 对话失败:', error);
onError(error);
}
}
getRecentHistory(limit) {
return this.conversationHistory.slice(-limit);
}
clearHistory() {
this.conversationHistory = [];
}
updateSystemPrompt(newPrompt) {
this.systemPrompt = newPrompt;
}
}
3. 语音处理模块
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.getVoices();
const voice = voices.find(v => v.name === options.voiceName);
if (voice) utterance.voice = voice;
}
utterance.onend = () => resolve();
utterance.onerror = (error) => reject(error);
this.synthesis.speak(utterance);
});
}
startListening(onResult, onError) {
if (!('webkitSpeechRecognition' in window)) {
onError(new Error('浏览器不支持语音识别'));
return;
}
this.recognition = new webkitSpeechRecognition();
this.recognition.lang = 'zh-CN';
this.recognition.continuous = false;
this.recognition.interimResults = true;
let finalTranscript = '';
this.recognition.onresult = (event) => {
let interimTranscript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
finalTranscript += transcript;
} else {
interimTranscript += transcript;
}
}
onResult({ final: finalTranscript, interim: interimTranscript });
};
this.recognition.onerror = (event) => {
onError(new Error(event.error));
};
this.recognition.onend = () => {
this.isListening = false;
};
this.recognition.start();
this.isListening = true;
}
stopListening() {
if (this.recognition && this.isListening) {
this.recognition.stop();
this.isListening = false;
}
}
stopSpeaking() {
this.synthesis.cancel();
}
getVoices() {
return this.synthesis.getVoices();
}
}
4. 主控制器集成
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);
this.isProcessing = false;
}
async init() {
await this.avatar.init();
this.bindEvents();
console.log('数字人系统初始化完成');
}
bindEvents() {
this.avatar.on('voiceStateChange', (state) => {
if (state === 'end' && this.isProcessing) {
this.avatar.idle();
this.isProcessing = false;
}
});
this.avatar.on('error', (error) => {
console.error('数字人错误:', error);
});
}
async handleUserInput(text, options = {}) {
if (this.isProcessing) {
console.warn('系统正在处理中,请稍候');
return;
}
this.isProcessing = true;
try {
this.avatar.listen();
this.avatar.think();
let fullResponse = '';
await this.ai.chat(
text,
(chunk) => {
console.log('AI 回复片段:', chunk);
fullResponse += chunk;
},
(complete) => {
console.log('完整回复:', complete);
},
(error) => {
console.error('AI 错误:', error);
}
);
if (options.useVoice !== false) {
await this.avatar.speak(fullResponse);
} else {
this.avatar.idle();
this.isProcessing = false;
}
return fullResponse;
} catch (error) {
console.error('处理失败:', error);
this.avatar.idle();
this.isProcessing = false;
throw error;
}
}
startVoiceInput(onResult) {
this.voice.startListening((result) => {
onResult(result);
if (result.final) {
this.handleUserInput(result.final);
}
}, (error) => {
console.error('语音识别错误:', error);
});
}
stopVoiceInput() {
this.voice.stopListening();
}
switchScene(sceneConfig) {
this.ai.updateSystemPrompt(sceneConfig.systemPrompt);
console.log('场景已切换:', sceneConfig.name);
}
destroy() {
this.avatar.destroy();
this.voice.stopSpeaking();
this.voice.stopListening();
}
}
完整代码示例
HTML 结构
<!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="btn danger" disabled>断开连接</button>
<button id="btn-settings" class="btn secondary">设置</button>
</div>
</header>
<main class="main-content">
<div class="avatar-section">
<div id="avatar-container" class="avatar-container">
<div class="avatar-placeholder">
<p>点击"连接数字人"开始体验</p>
</div>
</div>
<div class="avatar-status">
<span class="status-indicator" id="status-indicator"></span>
<span id="status-text">未连接</span>
</div>
</div>
<div class="chat-section">
<div class="chat-messages" id="chat-messages">
<div class="message system">
<div class="message-content">你好!我是智能政务助手小政,有什么可以帮您的吗?</div>
</div>
</div>
<div class="quick-questions">
<button class="quick-btn">如何办理身份证?</button>
<button class="quick-btn">社保查询流程</button>
<button class="quick-btn">不动产登记需要什么材料?</button>
</div>
<div class="input-area">
<textarea id="user-input" placeholder="输入您的问题..."></textarea>
<div class="input-actions">
<button id="btn-voice" class="btn-icon" title="语音输入"><svg>...</svg></button>
<button id="btn-send" class="btn primary">发送</button>
</div>
</div>
</div>
</main>
</div>
<div id="settings-modal" class="modal">
<div class="modal-content">
<h2>系统设置</h2>
<div class="form-group">
<label>数字人 AppId</label>
<input type="text" id="input-appid" placeholder="请输入 AppId">
</div>
<div class="form-group">
<label>数字人 AppSecret</label>
<input type="password" id="input-secret" placeholder="请输入 AppSecret">
</div>
<div class="form-group">
<label>AI API Key</label>
<input type="password" id="input-apikey" placeholder="请输入 API Key">
</div>
<div class="modal-actions">
<button id="btn-save-settings" class="btn primary">保存</button>
<button id="btn-close-settings" class="btn secondary">关闭</button>
</div>
</div>
</div>
<script src="https://your-cdn.com/avatar-sdk.js"></script>
<script src="js/app.js"></script>
</body>
</html>
CSS 样式
: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;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow);
z-index: 10;
}
.header h1 {
font-size: 20px;
font-weight: 600;
}
.controls {
display: flex;
gap: 12px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.btn.primary {
background: var(--primary-color);
color: white;
}
.btn.primary:hover {
background: #40a9ff;
}
.btn.danger {
background: var(--danger-color);
color: white;
}
.btn.secondary {
background: #f5f5f5;
color: var(--text-color);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.main-content {
flex: 1;
display: flex;
gap: 24px;
padding: 24px;
overflow: hidden;
}
.avatar-section {
flex: 1;
background: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
}
.avatar-container {
flex: 1;
position: relative;
background: #000;
border-radius: var(--border-radius) var(--border-radius) 0 0;
overflow: hidden;
}
.avatar-placeholder {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
color: #666;
}
.avatar-status {
padding: 12px 16px;
display: flex;
align-items: center;
gap: 8px;
border-top: 1px solid #eee;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ccc;
}
.status-indicator.connected {
background: var(--success-color);
}
.status-indicator.connecting {
background: var(--primary-color);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.chat-section {
flex: 1;
max-width: 500px;
background: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
max-width: 80%;
}
.message.user {
align-self: flex-end;
}
.message.assistant {
align-self: flex-start;
}
.message.system {
align-self: center;
}
.message-content {
padding: 12px 16px;
border-radius: 12px;
line-height: 1.5;
}
.message.user .message-content {
background: var(--primary-color);
color: white;
}
.message.assistant .message-content {
background: #f5f5f5;
color: var(--text-color);
}
.message.system .message-content {
background: #e6f7ff;
color: #1890ff;
font-size: 12px;
}
.quick-questions {
padding: 12px 16px;
display: flex;
flex-wrap: wrap;
gap: 8px;
border-top: 1px solid #eee;
}
.quick-btn {
padding: 6px 12px;
background: #f0f2f5;
border: none;
border-radius: 16px;
font-size: 12px;
cursor: pointer;
transition: background 0.3s;
}
.quick-btn:hover {
background: #d9d9d9;
}
.input-area {
padding: 16px;
border-top: 1px solid #eee;
display: flex;
flex-direction: column;
gap: 12px;
}
#user-input {
width: 100%;
min-height: 60px;
padding: 12px;
border: 1px solid #ddd;
border-radius: var(--border-radius);
resize: none;
font-family: inherit;
}
#user-input:focus {
outline: none;
border-color: var(--primary-color);
}
.input-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.btn-icon {
width: 36px;
height: 36px;
border: none;
background: #f5f5f5;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon:hover {
background: #e6e6e6;
}
.btn-icon.recording {
background: var(--danger-color);
color: white;
animation: pulse 1s infinite;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
padding: 24px;
border-radius: var(--border-radius);
width: 400px;
max-width: 90%;
}
.modal-content h2 {
margin-bottom: 20px;
font-size: 18px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
}
@media (max-width: 768px) {
.main-content {
flex-direction: column;
overflow-y: auto;
}
.avatar-section {
min-height: 300px;
}
.chat-section {
max-width: 100%;
min-height: 400px;
}
}
应用入口
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);
}
};
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'),
btnConnect: document.getElementById('btn-connect'),
btnDisconnect: document.getElementById('btn-disconnect'),
btnSend: document.getElementById('btn-send'),
btnVoice: document.getElementById('btn-voice')
};
}
addMessage(content, type = 'assistant') {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = content;
messageDiv.appendChild(contentDiv);
this.elements.messagesContainer.appendChild(messageDiv);
this.elements.messagesContainer.scrollTop = this.elements.messagesContainer.scrollHeight;
}
clearMessages() {
this.elements.messagesContainer.innerHTML = '';
}
updateStatus(status, text) {
this.elements.statusIndicator.className = `status-indicator ${status}`;
this.elements.statusText.textContent = text;
}
setConnecting(isConnecting) {
this.elements.btnConnect.disabled = isConnecting;
this.elements.btnDisconnect.disabled = !isConnecting;
}
}
class App {
constructor() {
this.system = null;
this.ui = new UIManager();
this.config = ConfigManager.load() || this.getDefaultConfig();
this.initUI();
}
getDefaultConfig() {
return {
avatarAppId: '',
avatarAppSecret: '',
aiApiKey: '',
aiModel: 'qwen-plus'
};
}
initUI() {
this.ui.elements.btnConnect.addEventListener('click', () => this.connect());
this.ui.elements.btnDisconnect.addEventListener('click', () => this.disconnect());
this.ui.elements.btnSend.addEventListener('click', () => this.sendMessage());
this.ui.elements.btnVoice.addEventListener('click', () => this.toggleVoice());
this.ui.elements.userInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
document.querySelectorAll('.quick-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.ui.elements.userInput.value = btn.textContent;
this.sendMessage();
});
});
}
async connect() {
if (!this.validateConfig()) {
alert('请先配置 API 密钥');
return;
}
this.ui.updateStatus('connecting', '连接中...');
this.ui.setConnecting(true);
try {
this.system = new DigitalHumanSystem(this.config);
await this.system.init();
this.ui.updateStatus('connected', '已连接');
this.ui.addMessage('数字人已连接,可以开始对话了!', 'system');
} catch (error) {
console.error('连接失败:', error);
this.ui.updateStatus('', '连接失败');
alert('连接失败:' + error.message);
this.ui.setConnecting(false);
}
}
disconnect() {
if (this.system) {
this.system.destroy();
this.system = null;
}
this.ui.updateStatus('', '未连接');
this.ui.setConnecting(false);
}
async sendMessage() {
const input = this.ui.elements.userInput.value.trim();
if (!input || !this.system) return;
this.ui.addMessage(input, 'user');
this.ui.elements.userInput.value = '';
try {
const response = await this.system.handleUserInput(input);
this.ui.addMessage(response, 'assistant');
} catch (error) {
console.error('发送消息失败:', error);
this.ui.addMessage('抱歉,处理您的请求时出现错误。', 'system');
}
}
toggleVoice() {
const btn = this.ui.elements.btnVoice;
if (btn.classList.contains('recording')) {
this.system?.stopVoiceInput();
btn.classList.remove('recording');
} else {
this.system?.startVoiceInput((result) => {
this.ui.elements.userInput.value = result.final || result.interim;
});
btn.classList.add('recording');
}
}
validateConfig() {
return this.config.avatarAppId && this.config.avatarAppSecret && this.config.aiApiKey;
}
}
document.addEventListener('DOMContentLoaded', () => {
window.app = new App();
});
最佳实践与优化
1. 性能优化
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);
}
}
estimateTokens(text) {
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 (let i = this.history.length - 1; i >= 0; i--) {
const tokens = this.estimateTokens(this.history[i].content);
if (totalTokens + tokens > maxTokens) break;
trimmedHistory.unshift(this.history[i]);
totalTokens += tokens;
}
this.history = trimmedHistory;
}
}
2. 错误处理与重试
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) {
const retryableStatuses = [429, 500, 502, 503];
return retryableStatuses.includes(error.status);
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
const requester = new RetryableRequest();
const response = await requester.execute(() => fetch('https://api.example.com/data'));
3. 状态同步
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.IDLE;
this.observers = [];
}
canTransition(newState) {
return this.transitions[this.currentState]?.includes(newState);
}
transition(newState) {
if (!this.canTransition(newState)) {
throw new Error(`不能从 ${this.currentState} 转换到 ${newState}`);
}
const oldState = this.currentState;
this.currentState = newState;
this.notifyObservers({ oldState, newState });
}
subscribe(observer) {
this.observers.push(observer);
}
notifyObservers(event) {
this.observers.forEach(observer => observer(event));
}
}
4. 资源预加载
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((resolve, reject) => {
if (this.loadedResources.has(url)) {
resolve();
return;
}
const audio = new Audio();
audio.oncanplaythrough = () => {
this.loadedResources.add(url);
resolve();
};
audio.onerror = reject;
audio.src = url;
});
});
return Promise.all(promises);
}
}
总结与展望
技术要点总结
- 架构设计:模块化设计,分离数字人控制、AI 对话、语音处理等职责
- 状态管理:使用状态机管理数字人的各种状态和转换
- 流式交互:实现 AI 回复的流式输出,提升用户体验
- 错误处理:完善的重试机制和错误边界处理
- 性能优化:资源预加载、对话历史管理等优化手段
技术演进方向
当前阶段
增强交互
多模态融合
边缘部署
情感识别
手势交互
眼神追踪
视觉理解
声音克隆
场景感知
WebGL 加速
本地模型
离线运行
未来展望
- 更自然的交互:结合情感计算、手势识别,实现更接近真人的交互体验
- 多模态融合:视觉、听觉、触觉的多模态感知和表达
- 个性化定制:根据用户喜好调整数字人的外观、声音、性格
- 边缘计算:通过 WebGL、WebGPU 等技术实现浏览器端的高性能渲染
- 行业深化:在政务、金融、教育、医疗等领域的深度应用
参考资源
相关免费在线工具
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
- 随机西班牙地址生成器
随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online