形态键预热
This commit is contained in:
@ -6,6 +6,7 @@ class BabylonMorphTargetAdapter {
|
|||||||
|
|
||||||
buildCache(meshes) {
|
buildCache(meshes) {
|
||||||
this.morphTargetCache = {};
|
this.morphTargetCache = {};
|
||||||
|
this.meshes = meshes;
|
||||||
let totalTargets = 0;
|
let totalTargets = 0;
|
||||||
|
|
||||||
meshes.forEach(mesh => {
|
meshes.forEach(mesh => {
|
||||||
@ -30,6 +31,44 @@ class BabylonMorphTargetAdapter {
|
|||||||
return totalTargets;
|
return totalTargets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
warmupShaders(scene) {
|
||||||
|
console.log('开始shader预热...');
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
// 预热:强制触发着色器编译
|
||||||
|
// 使用多种值组合来触发所有可能的shader变体
|
||||||
|
const testValues = [0, 0.2, 0.4, 0.6, 0.8, 1.0];
|
||||||
|
|
||||||
|
for (let pass = 0; pass < testValues.length; pass++) {
|
||||||
|
const value = testValues[pass];
|
||||||
|
for (const targets of Object.values(this.morphTargetCache)) {
|
||||||
|
targets.forEach(mt => {
|
||||||
|
mt.influence = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每次设置后都渲染,确保shader编译
|
||||||
|
if (scene) {
|
||||||
|
scene.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置所有影响值
|
||||||
|
for (const targets of Object.values(this.morphTargetCache)) {
|
||||||
|
targets.forEach(mt => {
|
||||||
|
mt.influence = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后渲染一次确保重置生效
|
||||||
|
if (scene) {
|
||||||
|
scene.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = performance.now() - startTime;
|
||||||
|
console.log(`shader预热完成,耗时 ${elapsed.toFixed(2)}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
setInfluence(name, value) {
|
setInfluence(name, value) {
|
||||||
const lowerName = name.toLowerCase();
|
const lowerName = name.toLowerCase();
|
||||||
const targets = this.morphTargetCache[lowerName];
|
const targets = this.morphTargetCache[lowerName];
|
||||||
|
|||||||
@ -141,6 +141,29 @@ class BlendShapeAnimator {
|
|||||||
|
|
||||||
// 注意:不停止眨眼,让眨眼继续运行
|
// 注意:不停止眨眼,让眨眼继续运行
|
||||||
|
|
||||||
|
// 预演前10帧以避免首次播放卡顿
|
||||||
|
console.log('预演前10帧...');
|
||||||
|
const framesToPreview = Math.min(10, this.animationFrames.length);
|
||||||
|
for (let i = 0; i < framesToPreview; i++) {
|
||||||
|
const frame = this.animationFrames[i];
|
||||||
|
const blendShapes = frame?.blendShapes || {};
|
||||||
|
for (const key in blendShapes) {
|
||||||
|
if (this.disabledShapesInAnimation.includes(key.toLowerCase())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.morphTargetAdapter.setInfluence(key, blendShapes[key] * this.blendShapeScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 重置为0
|
||||||
|
for (let i = 0; i < framesToPreview; i++) {
|
||||||
|
const frame = this.animationFrames[i];
|
||||||
|
const blendShapes = frame?.blendShapes || {};
|
||||||
|
for (const key in blendShapes) {
|
||||||
|
this.morphTargetAdapter.setInfluence(key, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('预演完成');
|
||||||
|
|
||||||
this.stopAnimation(false);
|
this.stopAnimation(false);
|
||||||
this.isPlaying = true;
|
this.isPlaying = true;
|
||||||
this.currentFrameIndex = 0;
|
this.currentFrameIndex = 0;
|
||||||
@ -149,6 +172,7 @@ class BlendShapeAnimator {
|
|||||||
this.streamingStallMs = 0;
|
this.streamingStallMs = 0;
|
||||||
|
|
||||||
this._animateFrame();
|
this._animateFrame();
|
||||||
|
|
||||||
this.onStatusChange('info', '播放中...');
|
this.onStatusChange('info', '播放中...');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,7 +221,8 @@ class BlendShapeAnimator {
|
|||||||
_animateFrame() {
|
_animateFrame() {
|
||||||
if (!this.isPlaying) return;
|
if (!this.isPlaying) return;
|
||||||
|
|
||||||
const now = performance.now();
|
const frameStartTime = performance.now();
|
||||||
|
const now = frameStartTime;
|
||||||
if (this.streamingWaitStart !== null && this.animationFrames.length > this.currentFrameIndex + 1) {
|
if (this.streamingWaitStart !== null && this.animationFrames.length > this.currentFrameIndex + 1) {
|
||||||
this.streamingStallMs += now - this.streamingWaitStart;
|
this.streamingStallMs += now - this.streamingWaitStart;
|
||||||
this.streamingWaitStart = null;
|
this.streamingWaitStart = null;
|
||||||
@ -242,8 +267,11 @@ class BlendShapeAnimator {
|
|||||||
? this.animationShapeNames
|
? this.animationShapeNames
|
||||||
: Object.keys(currentBlendShapes);
|
: Object.keys(currentBlendShapes);
|
||||||
|
|
||||||
|
let updateCount = 0;
|
||||||
|
const setInfluenceStart = performance.now();
|
||||||
|
|
||||||
for (const key of shapeNames) {
|
for (const key of shapeNames) {
|
||||||
// 跳过禁用列表中的 blendshape,让空闲动画继续控制它们
|
// 跳过禁用列表中的 blendshape,让空闲动画继续控制它们
|
||||||
if (this.disabledShapesInAnimation.includes(key.toLowerCase())) {
|
if (this.disabledShapesInAnimation.includes(key.toLowerCase())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -254,10 +282,21 @@ class BlendShapeAnimator {
|
|||||||
const scaledValue = interpolatedValue * this.blendShapeScale;
|
const scaledValue = interpolatedValue * this.blendShapeScale;
|
||||||
|
|
||||||
this.morphTargetAdapter.setInfluence(key, scaledValue);
|
this.morphTargetAdapter.setInfluence(key, scaledValue);
|
||||||
|
updateCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setInfluenceTime = performance.now() - setInfluenceStart;
|
||||||
|
|
||||||
this.currentFrameIndex = targetFrameIndex;
|
this.currentFrameIndex = targetFrameIndex;
|
||||||
|
|
||||||
|
// 性能监控前100帧
|
||||||
|
if (targetFrameIndex < 100) {
|
||||||
|
const totalFrameTime = performance.now() - frameStartTime;
|
||||||
|
if (totalFrameTime > 16.67) {
|
||||||
|
console.warn(`帧${targetFrameIndex}耗时${totalFrameTime.toFixed(2)}ms (setInfluence: ${setInfluenceTime.toFixed(2)}ms, 调用${updateCount}次)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新当前句子显示
|
// 更新当前句子显示
|
||||||
const sentenceIndex = currentFrame?.sentenceIndex ?? -1;
|
const sentenceIndex = currentFrame?.sentenceIndex ?? -1;
|
||||||
if (sentenceIndex !== this.currentSentenceIndex) {
|
if (sentenceIndex !== this.currentSentenceIndex) {
|
||||||
|
|||||||
@ -49,6 +49,44 @@ function init() {
|
|||||||
showStatus("警告: 未找到形态键", "error");
|
showStatus("警告: 未找到形态键", "error");
|
||||||
} else {
|
} else {
|
||||||
console.log(`✓ 构建缓存完成,共 ${totalTargets} 个形态键`);
|
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) => {
|
(message) => {
|
||||||
@ -105,72 +143,56 @@ async function generateAnimationBatch(text, apiUrl) {
|
|||||||
|
|
||||||
async function generateAnimationStream(text, apiUrl) {
|
async function generateAnimationStream(text, apiUrl) {
|
||||||
const url = normalizeApiUrl(apiUrl, true);
|
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.stopAnimation();
|
||||||
animator.loadAnimationFrames([]);
|
animator.loadAnimationFrames([]);
|
||||||
animator.startStreaming();
|
animator.startStreaming();
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buffer = '';
|
|
||||||
let started = false;
|
let started = false;
|
||||||
const pendingFrames = [];
|
const pendingFrames = [];
|
||||||
const streamBufferMs = 300;
|
const streamBufferMs = 1500;
|
||||||
const flushBatchMs = 50;
|
const flushBatchMs = 50;
|
||||||
const minStartFrames = Math.max(1, Math.round(animator.dataFps * (streamBufferMs / 1000)));
|
const minStartFrames = Math.max(1, Math.round(animator.dataFps * (streamBufferMs / 1000)));
|
||||||
const frameBatchSize = Math.max(1, Math.round(animator.dataFps * (flushBatchMs / 1000)));
|
const frameBatchSize = Math.max(1, Math.round(animator.dataFps * (flushBatchMs / 1000)));
|
||||||
let sentenceTexts = []; // 存储句子文本
|
let sentenceTexts = [];
|
||||||
|
|
||||||
const flushFrames = (force = false) => {
|
const flushFrames = (force = false) => {
|
||||||
if (pendingFrames.length === 0) {
|
if (pendingFrames.length === 0) return;
|
||||||
return;
|
|
||||||
}
|
const minFlushSize = started ? frameBatchSize : minStartFrames;
|
||||||
if (!force && pendingFrames.length < frameBatchSize) {
|
if (!force && pendingFrames.length < minFlushSize) return;
|
||||||
return;
|
|
||||||
}
|
const doFlush = () => {
|
||||||
const framesToFlush = pendingFrames.splice(0, pendingFrames.length);
|
const framesToFlush = pendingFrames.splice(0, pendingFrames.length);
|
||||||
animator.appendAnimationFrames(framesToFlush);
|
animator.appendAnimationFrames(framesToFlush);
|
||||||
if (!started && animator.animationFrames.length >= minStartFrames) {
|
if (!started && animator.animationFrames.length >= minStartFrames) {
|
||||||
animator.playAnimation();
|
console.log(`开始播放,已缓冲 ${animator.animationFrames.length} 帧`);
|
||||||
started = true;
|
animator.playAnimation();
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (force || !started) {
|
||||||
|
doFlush();
|
||||||
|
} else {
|
||||||
|
if (window.requestIdleCallback) {
|
||||||
|
window.requestIdleCallback(doFlush, { timeout: 50 });
|
||||||
|
} else {
|
||||||
|
setTimeout(doFlush, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMessage = (message) => {
|
const handleMessage = (message) => {
|
||||||
if (message.type === 'frame') {
|
if (message.type === 'frame') {
|
||||||
pendingFrames.push(message.frame);
|
pendingFrames.push(message.frame);
|
||||||
flushFrames();
|
flushFrames();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === 'status') {
|
if (message.type === 'status') {
|
||||||
const stageMessage = message.message || 'Streaming';
|
const stageMessage = message.message || 'Streaming';
|
||||||
showStatus(stageMessage, 'info');
|
showStatus(stageMessage, 'info');
|
||||||
console.log('Stream status:', message);
|
console.log('Stream status:', message);
|
||||||
// 保存句子文本并传递给动画器
|
|
||||||
if (message.sentence_texts) {
|
if (message.sentence_texts) {
|
||||||
sentenceTexts = message.sentence_texts;
|
sentenceTexts = message.sentence_texts;
|
||||||
animator.sentenceTexts = sentenceTexts;
|
animator.sentenceTexts = sentenceTexts;
|
||||||
@ -197,52 +219,52 @@ async function generateAnimationStream(text, apiUrl) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let streamError = null;
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
const worker = new Worker('streamWorker.js');
|
||||||
while (true) {
|
|
||||||
const { value, done } = await reader.read();
|
worker.onmessage = (e) => {
|
||||||
if (done) {
|
const { type, message, error } = e.data;
|
||||||
break;
|
|
||||||
|
if (type === 'error') {
|
||||||
|
worker.terminate();
|
||||||
|
animator.endStreaming();
|
||||||
|
reject(new Error(message || error));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true });
|
if (type === 'message') {
|
||||||
const lines = buffer.split('\n');
|
|
||||||
buffer = lines.pop() || '';
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (!line.trim()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let message;
|
|
||||||
try {
|
try {
|
||||||
message = JSON.parse(line);
|
handleMessage(message);
|
||||||
} catch (err) {
|
} 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 {
|
worker.onerror = (err) => {
|
||||||
handleMessage(JSON.parse(buffer));
|
worker.terminate();
|
||||||
} catch (err) {
|
animator.endStreaming();
|
||||||
// ignore trailing parse errors
|
reject(new Error('Worker error: ' + err.message));
|
||||||
}
|
};
|
||||||
}
|
|
||||||
} catch (err) {
|
worker.postMessage({
|
||||||
streamError = err;
|
url: url,
|
||||||
throw err;
|
body: JSON.stringify({ text: text, language: 'zh-CN', first_sentence_split_size: 1 })
|
||||||
} finally {
|
});
|
||||||
if (!streamError) {
|
});
|
||||||
flushFrames(true);
|
|
||||||
if (!started && animator.animationFrames.length > 0) {
|
|
||||||
animator.playAnimation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
animator.endStreaming();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeApiUrl(apiUrl, streamEnabled) {
|
function normalizeApiUrl(apiUrl, streamEnabled) {
|
||||||
@ -343,7 +365,7 @@ function updateBlinkInterval() {
|
|||||||
const max = parseFloat(document.getElementById('blinkIntervalMax').value) * 1000;
|
const max = parseFloat(document.getElementById('blinkIntervalMax').value) * 1000;
|
||||||
animator.blinkParams.intervalMin = min;
|
animator.blinkParams.intervalMin = min;
|
||||||
animator.blinkParams.intervalMax = max;
|
animator.blinkParams.intervalMax = max;
|
||||||
document.getElementById('blinkIntervalValue').textContent = `${min/1000}-${max/1000}`;
|
document.getElementById('blinkIntervalValue').textContent = `${min / 1000}-${max / 1000}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBlinkDuration(value) {
|
function updateBlinkDuration(value) {
|
||||||
@ -361,7 +383,7 @@ function updateEyeLookInterval() {
|
|||||||
const max = parseFloat(document.getElementById('eyeLookIntervalMax').value) * 1000;
|
const max = parseFloat(document.getElementById('eyeLookIntervalMax').value) * 1000;
|
||||||
animator.eyeLookParams.intervalMin = min;
|
animator.eyeLookParams.intervalMin = min;
|
||||||
animator.eyeLookParams.intervalMax = max;
|
animator.eyeLookParams.intervalMax = max;
|
||||||
document.getElementById('eyeLookIntervalValue').textContent = `${min/1000}-${max/1000}`;
|
document.getElementById('eyeLookIntervalValue').textContent = `${min / 1000}-${max / 1000}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateEyeLookDuration() {
|
function updateEyeLookDuration() {
|
||||||
@ -382,7 +404,7 @@ function updateExpressionInterval() {
|
|||||||
const max = parseFloat(document.getElementById('expressionIntervalMax').value) * 1000;
|
const max = parseFloat(document.getElementById('expressionIntervalMax').value) * 1000;
|
||||||
animator.expressionParams.intervalMin = min;
|
animator.expressionParams.intervalMin = min;
|
||||||
animator.expressionParams.intervalMax = max;
|
animator.expressionParams.intervalMax = max;
|
||||||
document.getElementById('expressionIntervalValue').textContent = `${min/1000}-${max/1000}`;
|
document.getElementById('expressionIntervalValue').textContent = `${min / 1000}-${max / 1000}`;
|
||||||
|
|
||||||
if (window.ExpressionLibrary) {
|
if (window.ExpressionLibrary) {
|
||||||
window.ExpressionLibrary.randomPlayer.intervalMin = min;
|
window.ExpressionLibrary.randomPlayer.intervalMin = min;
|
||||||
|
|||||||
56
examples/3d/performance-test.html
Normal file
56
examples/3d/performance-test.html
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>性能测试</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: monospace; padding: 20px; }
|
||||||
|
.log { margin: 5px 0; }
|
||||||
|
.warn { color: orange; }
|
||||||
|
.error { color: red; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3>动画性能监控</h3>
|
||||||
|
<div id="logs"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const logs = document.getElementById('logs');
|
||||||
|
|
||||||
|
// 监控 requestAnimationFrame 性能
|
||||||
|
let lastTime = performance.now();
|
||||||
|
let frameCount = 0;
|
||||||
|
let maxFrameTime = 0;
|
||||||
|
|
||||||
|
function monitorFrame() {
|
||||||
|
const now = performance.now();
|
||||||
|
const delta = now - lastTime;
|
||||||
|
|
||||||
|
if (frameCount > 0 && delta > 20) {
|
||||||
|
const log = document.createElement('div');
|
||||||
|
log.className = delta > 50 ? 'log error' : 'log warn';
|
||||||
|
log.textContent = `帧 ${frameCount}: ${delta.toFixed(2)}ms (目标: 16.67ms)`;
|
||||||
|
logs.appendChild(log);
|
||||||
|
|
||||||
|
if (delta > maxFrameTime) {
|
||||||
|
maxFrameTime = delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTime = now;
|
||||||
|
frameCount++;
|
||||||
|
|
||||||
|
if (frameCount < 300) {
|
||||||
|
requestAnimationFrame(monitorFrame);
|
||||||
|
} else {
|
||||||
|
const summary = document.createElement('div');
|
||||||
|
summary.className = 'log';
|
||||||
|
summary.textContent = `\n总结: 最大帧时间 ${maxFrameTime.toFixed(2)}ms`;
|
||||||
|
logs.appendChild(summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('性能监控已启动,将监控前300帧');
|
||||||
|
requestAnimationFrame(monitorFrame);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
68
examples/3d/streamWorker.js
Normal file
68
examples/3d/streamWorker.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// Web Worker for handling streaming response parsing
|
||||||
|
self.onmessage = async function(e) {
|
||||||
|
const { url, body } = e.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: body
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMessage = `请求失败 (${response.status})`;
|
||||||
|
try {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data?.error) {
|
||||||
|
errorMessage = data.error;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
self.postMessage({ type: 'error', message: errorMessage });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
self.postMessage({ type: 'error', message: 'No response body' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(line);
|
||||||
|
self.postMessage({ type: 'message', message: message });
|
||||||
|
} catch (err) {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer.trim()) {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(buffer);
|
||||||
|
self.postMessage({ type: 'message', message: message });
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.postMessage({ type: 'complete' });
|
||||||
|
} catch (err) {
|
||||||
|
self.postMessage({ type: 'error', message: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
Binary file not shown.
@ -79,6 +79,10 @@ class TextToBlendShapesService:
|
|||||||
yield {'type': 'error', 'message': '文本为空'}
|
yield {'type': 'error', 'message': '文本为空'}
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 测试:只处理第一句
|
||||||
|
sentences = sentences[:1]
|
||||||
|
print(f"[测试模式] 只处理第一句: {sentences[0]}")
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
'type': 'status',
|
'type': 'status',
|
||||||
'stage': 'split',
|
'stage': 'split',
|
||||||
|
|||||||
Reference in New Issue
Block a user