形态键预热
This commit is contained in:
@ -49,6 +49,44 @@ function init() {
|
||||
showStatus("警告: 未找到形态键", "error");
|
||||
} else {
|
||||
console.log(`✓ 构建缓存完成,共 ${totalTargets} 个形态键`);
|
||||
|
||||
// 预热着色器以避免首次动画卡顿
|
||||
console.log('预热着色器中...');
|
||||
morphAdapter.warmupShaders(sceneManager.scene);
|
||||
console.log('✓ 着色器预热完成');
|
||||
|
||||
// 播放120帧预热动画(4秒)
|
||||
console.log('预热渲染管线中...');
|
||||
const dummyFrames = [];
|
||||
const allBlendShapes = Object.keys(morphAdapter.morphTargetCache);
|
||||
console.log(`使用 ${allBlendShapes.length} 个blendshapes进行预热`);
|
||||
|
||||
for (let i = 0; i < 120; i++) {
|
||||
const blendShapes = {};
|
||||
const factor = Math.sin(i * 0.12) * 0.5 + 0.5;
|
||||
|
||||
allBlendShapes.forEach((name, index) => {
|
||||
const phase = (i + index * 5) * 0.18;
|
||||
const value = Math.sin(phase) * 0.5 + 0.5;
|
||||
if (value > 0.25) {
|
||||
blendShapes[name] = value * factor * 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
dummyFrames.push({
|
||||
timeCode: i / 30,
|
||||
blendShapes: blendShapes
|
||||
});
|
||||
}
|
||||
|
||||
animator.loadAnimationFrames(dummyFrames);
|
||||
animator.playAnimation();
|
||||
|
||||
setTimeout(() => {
|
||||
animator.stopAnimation();
|
||||
animator.loadAnimationFrames([]);
|
||||
console.log('✓ 渲染管线预热完成');
|
||||
}, 4000);
|
||||
}
|
||||
},
|
||||
(message) => {
|
||||
@ -105,72 +143,56 @@ async function generateAnimationBatch(text, apiUrl) {
|
||||
|
||||
async function generateAnimationStream(text, apiUrl) {
|
||||
const url = normalizeApiUrl(apiUrl, true);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: text, language: 'zh-CN', first_sentence_split_size: 1 })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `请求失败 (${response.status})`;
|
||||
try {
|
||||
const data = await response.json();
|
||||
if (data?.error) {
|
||||
errorMessage = data.error;
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore json parse errors
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
await generateAnimationBatch(text, apiUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
animator.stopAnimation();
|
||||
animator.loadAnimationFrames([]);
|
||||
animator.startStreaming();
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let started = false;
|
||||
const pendingFrames = [];
|
||||
const streamBufferMs = 300;
|
||||
const streamBufferMs = 1500;
|
||||
const flushBatchMs = 50;
|
||||
const minStartFrames = Math.max(1, Math.round(animator.dataFps * (streamBufferMs / 1000)));
|
||||
const frameBatchSize = Math.max(1, Math.round(animator.dataFps * (flushBatchMs / 1000)));
|
||||
let sentenceTexts = []; // 存储句子文本
|
||||
let sentenceTexts = [];
|
||||
|
||||
const flushFrames = (force = false) => {
|
||||
if (pendingFrames.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!force && pendingFrames.length < frameBatchSize) {
|
||||
return;
|
||||
}
|
||||
const framesToFlush = pendingFrames.splice(0, pendingFrames.length);
|
||||
animator.appendAnimationFrames(framesToFlush);
|
||||
if (!started && animator.animationFrames.length >= minStartFrames) {
|
||||
animator.playAnimation();
|
||||
started = true;
|
||||
if (pendingFrames.length === 0) return;
|
||||
|
||||
const minFlushSize = started ? frameBatchSize : minStartFrames;
|
||||
if (!force && pendingFrames.length < minFlushSize) return;
|
||||
|
||||
const doFlush = () => {
|
||||
const framesToFlush = pendingFrames.splice(0, pendingFrames.length);
|
||||
animator.appendAnimationFrames(framesToFlush);
|
||||
if (!started && animator.animationFrames.length >= minStartFrames) {
|
||||
console.log(`开始播放,已缓冲 ${animator.animationFrames.length} 帧`);
|
||||
animator.playAnimation();
|
||||
started = true;
|
||||
}
|
||||
};
|
||||
|
||||
if (force || !started) {
|
||||
doFlush();
|
||||
} else {
|
||||
if (window.requestIdleCallback) {
|
||||
window.requestIdleCallback(doFlush, { timeout: 50 });
|
||||
} else {
|
||||
setTimeout(doFlush, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMessage = (message) => {
|
||||
if (message.type === 'frame') {
|
||||
pendingFrames.push(message.frame);
|
||||
flushFrames();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (message.type === 'status') {
|
||||
const stageMessage = message.message || 'Streaming';
|
||||
showStatus(stageMessage, 'info');
|
||||
console.log('Stream status:', message);
|
||||
// 保存句子文本并传递给动画器
|
||||
if (message.sentence_texts) {
|
||||
sentenceTexts = message.sentence_texts;
|
||||
animator.sentenceTexts = sentenceTexts;
|
||||
@ -197,52 +219,52 @@ async function generateAnimationStream(text, apiUrl) {
|
||||
}
|
||||
};
|
||||
|
||||
let streamError = null;
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker('streamWorker.js');
|
||||
|
||||
worker.onmessage = (e) => {
|
||||
const { type, message, error } = e.data;
|
||||
|
||||
if (type === 'error') {
|
||||
worker.terminate();
|
||||
animator.endStreaming();
|
||||
reject(new Error(message || error));
|
||||
return;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let message;
|
||||
if (type === 'message') {
|
||||
try {
|
||||
message = JSON.parse(line);
|
||||
handleMessage(message);
|
||||
} catch (err) {
|
||||
continue;
|
||||
worker.terminate();
|
||||
animator.endStreaming();
|
||||
reject(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
handleMessage(message);
|
||||
if (type === 'complete') {
|
||||
worker.terminate();
|
||||
flushFrames(true);
|
||||
if (!started && animator.animationFrames.length > 0) {
|
||||
animator.playAnimation();
|
||||
}
|
||||
animator.endStreaming();
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
handleMessage(JSON.parse(buffer));
|
||||
} catch (err) {
|
||||
// ignore trailing parse errors
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
streamError = err;
|
||||
throw err;
|
||||
} finally {
|
||||
if (!streamError) {
|
||||
flushFrames(true);
|
||||
if (!started && animator.animationFrames.length > 0) {
|
||||
animator.playAnimation();
|
||||
}
|
||||
}
|
||||
animator.endStreaming();
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = (err) => {
|
||||
worker.terminate();
|
||||
animator.endStreaming();
|
||||
reject(new Error('Worker error: ' + err.message));
|
||||
};
|
||||
|
||||
worker.postMessage({
|
||||
url: url,
|
||||
body: JSON.stringify({ text: text, language: 'zh-CN', first_sentence_split_size: 1 })
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeApiUrl(apiUrl, streamEnabled) {
|
||||
@ -343,7 +365,7 @@ function updateBlinkInterval() {
|
||||
const max = parseFloat(document.getElementById('blinkIntervalMax').value) * 1000;
|
||||
animator.blinkParams.intervalMin = min;
|
||||
animator.blinkParams.intervalMax = max;
|
||||
document.getElementById('blinkIntervalValue').textContent = `${min/1000}-${max/1000}`;
|
||||
document.getElementById('blinkIntervalValue').textContent = `${min / 1000}-${max / 1000}`;
|
||||
}
|
||||
|
||||
function updateBlinkDuration(value) {
|
||||
@ -361,7 +383,7 @@ function updateEyeLookInterval() {
|
||||
const max = parseFloat(document.getElementById('eyeLookIntervalMax').value) * 1000;
|
||||
animator.eyeLookParams.intervalMin = min;
|
||||
animator.eyeLookParams.intervalMax = max;
|
||||
document.getElementById('eyeLookIntervalValue').textContent = `${min/1000}-${max/1000}`;
|
||||
document.getElementById('eyeLookIntervalValue').textContent = `${min / 1000}-${max / 1000}`;
|
||||
}
|
||||
|
||||
function updateEyeLookDuration() {
|
||||
@ -382,7 +404,7 @@ function updateExpressionInterval() {
|
||||
const max = parseFloat(document.getElementById('expressionIntervalMax').value) * 1000;
|
||||
animator.expressionParams.intervalMin = min;
|
||||
animator.expressionParams.intervalMax = max;
|
||||
document.getElementById('expressionIntervalValue').textContent = `${min/1000}-${max/1000}`;
|
||||
document.getElementById('expressionIntervalValue').textContent = `${min / 1000}-${max / 1000}`;
|
||||
|
||||
if (window.ExpressionLibrary) {
|
||||
window.ExpressionLibrary.randomPlayer.intervalMin = min;
|
||||
|
||||
Reference in New Issue
Block a user