真实接口预热

This commit is contained in:
yinsx
2026-01-04 09:58:26 +08:00
parent b56492f80f
commit 2bd183463d
10 changed files with 386 additions and 207 deletions

View File

@ -0,0 +1,8 @@
{
"hash": "7a1c4e14",
"configHash": "ff0b84f2",
"lockfileHash": "e3b0c442",
"browserHash": "3dd711d0",
"optimized": {},
"chunks": {}
}

View File

@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@ -27,6 +27,9 @@ BlendShapeAnimator 是一个引擎无关的形态键动画SDK可以与任何3
```javascript
const config = {
blendShapeScale: 1.0, // 形态键强度缩放
minBlendShapeValue: 0.1, // ignore values below this to avoid spikes
deltaThreshold: 0.002, // skip setInfluence when delta is tiny
prewarmFrameCount: 30, // number of frames used for warmup
dataFps: 30, // 动画数据帧率
onStatusChange: (type, msg) => {
// 状态变化回调
@ -145,5 +148,7 @@ animator.playAnimation();
- 形态键名称会自动转换为小写进行匹配
- SDK使用 `requestAnimationFrame` 进行动画更新
- Blendshape values below 0.1 are zeroed to reduce CPU load
- Startup prewarms prewarmFrameCount frames to hide the first-frame hitch
- 空闲动画会在主动画播放时自动暂停
- 所有形态键值范围为 0-1

View File

@ -31,10 +31,48 @@ class BabylonMorphTargetAdapter {
return totalTargets;
}
async warmupInvisible(scene) {
console.log('开始预热...');
const startTime = performance.now();
const allTargets = Object.values(this.morphTargetCache).flat();
const totalTargets = allTargets.length;
console.log(`预热 ${totalTargets} 个 morph targets`);
// 多轮预热,用不同值组合
const rounds = 10;
for (let r = 0; r < rounds; r++) {
const val = (r % 2 === 0) ? 1.0 : 0;
allTargets.forEach(mt => mt.influence = val);
scene.render();
await new Promise(r => requestAnimationFrame(r));
}
// 重置
allTargets.forEach(mt => mt.influence = 0);
scene.render();
// 等待几帧让 GPU 完全稳定
for (let i = 0; i < 5; i++) {
await new Promise(r => requestAnimationFrame(r));
}
console.log(`预热完成,耗时 ${(performance.now() - startTime).toFixed(2)}ms`);
}
warmupShaders(scene) {
console.log('开始shader预热...');
const startTime = performance.now();
// 强制同步更新所有 morph target managers
this.meshes?.forEach(mesh => {
const mtm = mesh.morphTargetManager;
if (mtm) {
mtm.enableNormalMorphing = true;
mtm.enableTangentMorphing = true;
}
});
// 预热:强制触发着色器编译
// 使用多种值组合来触发所有可能的shader变体
const testValues = [0, 0.2, 0.4, 0.6, 0.8, 1.0];

View File

@ -6,6 +6,7 @@ class BabylonSceneManager {
this.scene = null;
this.camera = null;
this.onModelLoaded = null;
this.onBeforeRender = null; // 渲染前回调
}
init() {
@ -28,6 +29,10 @@ class BabylonSceneManager {
this.scene.createDefaultEnvironment();
this.engine.runRenderLoop(() => {
// 在渲染前调用动画更新
if (this.onBeforeRender) {
this.onBeforeRender();
}
this.scene.render();
});

View File

@ -2,6 +2,7 @@
class BlendShapeAnimator {
constructor(config = {}) {
this.morphTargetAdapter = null;
this.scene = null;
this.animationFrames = [];
this.animationShapeNames = [];
this.isPlaying = false;
@ -16,6 +17,17 @@ class BlendShapeAnimator {
this.streamingWaitStart = null;
this.streamingStallMs = 0;
this.sentenceTexts = []; // 句子文本列表
this.minBlendShapeValue = typeof config.minBlendShapeValue === 'number'
? config.minBlendShapeValue
: 0.1; // Skip tiny blendshape inputs to avoid stalls
this.deltaThreshold = typeof config.deltaThreshold === 'number'
? config.deltaThreshold
: 0.002; // Skip re-applying nearly identical values
this.prewarmFrameCount = typeof config.prewarmFrameCount === 'number'
? config.prewarmFrameCount
: 30;
this.lastFrameBlendShapes = {};
this._hasPrewarmed = false;
// 空闲动画参数
this.blinkParams = config.blinkParams || {
@ -63,8 +75,15 @@ class BlendShapeAnimator {
// 回调
this.onStatusChange = config.onStatusChange || (() => {});
// 启动空闲动画循环
this._updateIdleAnimations();
// 不再启动独立的 RAF 循环,由外部渲染循环调用 tick()
}
// 每帧调用,由外部渲染循环驱动
tick() {
if (this.isPlaying) {
this._animateFrameOnce();
}
this._updateIdleAnimationsOnce();
}
// 设置形态键适配器
@ -76,6 +95,8 @@ class BlendShapeAnimator {
loadAnimationFrames(frames) {
this.animationFrames = frames || [];
this.animationShapeNames = this._collectAnimationShapeNames(this.animationFrames);
this.lastFrameBlendShapes = {};
// 不重置 _hasPrewarmed因为全局预热只需要做一次
}
appendAnimationFrames(frames) {
@ -85,6 +106,7 @@ class BlendShapeAnimator {
this.animationFrames.push(...frames);
const newNames = this._collectAnimationShapeNames(frames);
// 不重置 _hasPrewarmed
if (newNames.length === 0) {
return;
@ -116,7 +138,8 @@ class BlendShapeAnimator {
}
// 播放动画
playAnimation() {
async playAnimation() {
console.log('=== playAnimation 开始 ===');
if (this.animationFrames.length === 0) {
this.onStatusChange('error', '请先加载动画数据');
return;
@ -141,45 +164,24 @@ 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, false);
this.isPlaying = true;
this.currentFrameIndex = 0;
this.animationStartTime = performance.now();
this.streamingWaitStart = null;
this.streamingStallMs = 0;
this._animateFrame();
this._primeFirstFrame();
this._scheduleAnimationStart();
this.onStatusChange('info', '播放中...');
}
// 停止动画
stopAnimation(resumeExpressions = true) {
stopAnimation(resumeExpressions = true, resetBlendShapes = true) {
this.isPlaying = false;
if (resetBlendShapes) {
this._resetAnimationInfluences();
}
// 恢复眼球移动
if (resumeExpressions && this.isEyeLookEnabled) {
@ -196,12 +198,17 @@ class BlendShapeAnimator {
_collectAnimationShapeNames(frames) {
const names = new Set();
const threshold = Math.min(this.minBlendShapeValue, 0.02);
frames.forEach(frame => {
const blendShapes = frame?.blendShapes;
if (!blendShapes) return;
Object.keys(blendShapes).forEach(name => names.add(name));
Object.entries(blendShapes).forEach(([name, value]) => {
if (Math.abs(value || 0) >= threshold) {
names.add(name);
}
});
});
return Array.from(names);
@ -215,10 +222,107 @@ class BlendShapeAnimator {
this.animationShapeNames.forEach(name => {
this.morphTargetAdapter.setInfluence(name, 0);
});
this.lastFrameBlendShapes = {};
}
// 内部动画帧处理
_animateFrame() {
_prewarmAnimation() {
// 同步预热已移除,改用异步预热
this._hasPrewarmed = true;
}
// 异步分帧预热 - 预热所有 morph targets确保 GPU 真正渲染
async prewarmAsync(scene) {
if (!this.morphTargetAdapter) {
return;
}
// 获取所有可用的 morph targets而不只是当前动画用到的
const allShapes = this.morphTargetAdapter.morphTargetCache
? Object.keys(this.morphTargetAdapter.morphTargetCache)
: this.animationShapeNames;
const shapes = allShapes.filter(
name => !this.disabledShapesInAnimation.includes(name.toLowerCase())
);
if (shapes.length === 0) return;
console.log(`异步预热 ${shapes.length} 个 morph targets...`);
// 用不同的值组合预热,触发所有可能的 shader 变体
const testValues = [0.2, 0.5, 0.8, 1.0];
for (const val of testValues) {
shapes.forEach(name => this.morphTargetAdapter.setInfluence(name, val));
if (scene) scene.render();
await new Promise(r => requestAnimationFrame(r));
}
// 重置并等待几帧确保稳定
shapes.forEach(name => this.morphTargetAdapter.setInfluence(name, 0));
for (let i = 0; i < 5; i++) {
if (scene) scene.render();
await new Promise(r => requestAnimationFrame(r));
}
this._hasPrewarmed = true;
console.log('异步预热完成');
}
_primeFirstFrame() {
if (!this.morphTargetAdapter || this.animationFrames.length === 0) {
return;
}
const firstFrame = this.animationFrames[0];
const blendShapes = firstFrame?.blendShapes || {};
this.lastFrameBlendShapes = {};
for (const key in blendShapes) {
const lower = key.toLowerCase();
if (this.disabledShapesInAnimation.includes(lower)) {
continue;
}
const scaledValue = (blendShapes[key] || 0) * this.blendShapeScale;
if (Math.abs(scaledValue) < this.minBlendShapeValue) {
this.morphTargetAdapter.setInfluence(key, 0);
this.lastFrameBlendShapes[key] = 0;
continue;
}
this.morphTargetAdapter.setInfluence(key, scaledValue);
this.lastFrameBlendShapes[key] = scaledValue;
}
}
_touchAnimationShapes() {
if (!this.morphTargetAdapter || !Array.isArray(this.animationShapeNames) || this.animationShapeNames.length === 0) {
return;
}
const touchValue = Math.max(this.minBlendShapeValue * 1.2, 0.05);
const touched = [];
this.animationShapeNames.forEach(name => {
this.morphTargetAdapter.setInfluence(name, touchValue);
touched.push(name);
});
touched.forEach(name => {
this.morphTargetAdapter.setInfluence(name, 0);
});
}
_scheduleAnimationStart() {
// 只设置开始时间,动画由统一的 RAF 循环驱动
this.animationStartTime = performance.now();
this.streamingWaitStart = null;
this.streamingStallMs = 0;
}
// 内部动画帧处理(单次调用,不递归)
_animateFrameOnce() {
if (!this.isPlaying) return;
const frameStartTime = performance.now();
@ -232,6 +336,7 @@ class BlendShapeAnimator {
const elapsed = now - this.animationStartTime - this.streamingStallMs;
const exactFrame = elapsed / frameDuration;
const targetFrameIndex = Math.floor(exactFrame);
const frameProgress = exactFrame - targetFrameIndex;
if (targetFrameIndex >= this.animationFrames.length) {
if (this.isStreaming && !this.streamingComplete) {
@ -243,12 +348,7 @@ class BlendShapeAnimator {
if (waitTime > 30000) {
console.warn('Streaming timeout after 30s, stopping animation');
this.stopAnimation();
return;
}
if (waitTime > 1000 && Math.floor(waitTime / 1000) !== Math.floor((waitTime - 16) / 1000)) {
console.log(`Still waiting... ${Math.floor(waitTime / 1000)}s`);
}
requestAnimationFrame(() => this._animateFrame());
return;
}
console.log(`Animation complete. Total frames: ${this.animationFrames.length}`);
@ -260,7 +360,6 @@ class BlendShapeAnimator {
const nextFrame = this.animationFrames[Math.min(targetFrameIndex + 1, this.animationFrames.length - 1)];
const currentBlendShapes = currentFrame?.blendShapes || {};
const nextBlendShapes = nextFrame?.blendShapes || {};
const frameProgress = exactFrame - targetFrameIndex;
const smoothProgress = this._easeOutQuad(frameProgress);
const shapeNames = this.animationShapeNames.length > 0
@ -268,8 +367,6 @@ class BlendShapeAnimator {
: Object.keys(currentBlendShapes);
let updateCount = 0;
const setInfluenceStart = performance.now();
for (const key of shapeNames) {
// 跳过禁用列表中的 blendshape,让空闲动画继续控制它们
if (this.disabledShapesInAnimation.includes(key.toLowerCase())) {
@ -281,30 +378,36 @@ class BlendShapeAnimator {
const interpolatedValue = this._lerp(currentValue, nextValue, smoothProgress);
const scaledValue = interpolatedValue * this.blendShapeScale;
const previousValue = this.lastFrameBlendShapes[key] ?? 0;
if (Math.abs(scaledValue) < this.minBlendShapeValue) {
if (Math.abs(previousValue) >= this.minBlendShapeValue) {
this.morphTargetAdapter.setInfluence(key, 0);
this.lastFrameBlendShapes[key] = 0;
updateCount++;
}
continue;
}
if (Math.abs(scaledValue - previousValue) < this.deltaThreshold) {
continue;
}
this.morphTargetAdapter.setInfluence(key, scaledValue);
this.lastFrameBlendShapes[key] = scaledValue;
updateCount++;
}
const setInfluenceTime = performance.now() - setInfluenceStart;
this._lastFrameTime = now;
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;
if (sentenceIndex !== this.currentSentenceIndex) {
this.currentSentenceIndex = sentenceIndex;
this._updateCurrentSentenceDisplay();
}
requestAnimationFrame(() => this._animateFrame());
// 不再递归调用 RAF由统一循环驱动
}
// 设置空闲动画
@ -326,8 +429,8 @@ class BlendShapeAnimator {
};
}
// 更新空闲动画
_updateIdleAnimations() {
// 更新空闲动画(单次调用,不递归)
_updateIdleAnimationsOnce() {
const now = performance.now();
for (const name in this.idleAnimations) {
@ -359,8 +462,6 @@ class BlendShapeAnimator {
}
}
}
requestAnimationFrame(() => this._updateIdleAnimations());
}
// 眨眼控制

View File

@ -27,6 +27,10 @@ function init() {
});
animator.setMorphTargetAdapter(morphAdapter);
animator.scene = sceneManager.scene;
// 把动画更新挂到 Babylon 渲染循环
sceneManager.onBeforeRender = () => animator.tick();
// 导出全局变量供表情库使用
window.animator = animator;
@ -41,7 +45,7 @@ function init() {
// 加载3D模型
sceneManager.loadModel('head_a01.glb',
(meshes) => {
async (meshes) => {
showStatus("模型加载成功", "success");
const totalTargets = morphAdapter.buildCache(meshes);
@ -50,43 +54,39 @@ function init() {
} 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;
// 预热:直接调用生成动画流程(和用户点击按钮完全一样)
showStatus("预热中...", "info");
const apiUrl = document.getElementById('apiUrl').value;
const streamEnabled = document.getElementById('streamEnabled')?.checked;
console.log('预热 apiUrl:', apiUrl, 'stream:', streamEnabled);
if (apiUrl) {
try {
// 和 generateAnimation 走完全一样的路径
if (streamEnabled) {
await generateAnimationStream('你好', apiUrl);
} else {
await generateAnimationBatch('你好', apiUrl);
playAnimation();
}
// 等播放完
await new Promise(resolve => {
const check = () => animator.isPlaying ? requestAnimationFrame(check) : resolve();
check();
});
dummyFrames.push({
timeCode: i / 30,
blendShapes: blendShapes
});
}
animator.loadAnimationFrames(dummyFrames);
animator.playAnimation();
setTimeout(() => {
// 完全重置状态
animator.stopAnimation();
animator.loadAnimationFrames([]);
console.log('✓ 渲染管线预热完成');
}, 4000);
animator.endStreaming();
morphAdapter.resetAll();
console.log('✓ 预热完成');
} catch (e) {
console.warn('预热失败:', e);
}
} else {
console.warn('预热跳过: apiUrl 为空');
}
showStatus("就绪", "success");
}
},
(message) => {
@ -95,6 +95,44 @@ function init() {
);
}
// 偷偷调接口预热 - 完全走一遍生成动画流程
async function warmupWithApi() {
console.log('=== warmupWithApi 开始 ===');
const apiUrl = document.getElementById('apiUrl').value;
console.log('apiUrl:', apiUrl);
try {
// 完全走一遍 generateAnimationBatch 流程
console.log('调用 generateAnimationBatch...');
await generateAnimationBatch('你', apiUrl);
console.log('generateAnimationBatch 完成, frames:', animator.animationFrames.length);
// 播放动画
console.log('调用 playAnimation...');
animator.playAnimation();
// 等待播放完成
await new Promise(resolve => {
const checkDone = () => {
if (!animator.isPlaying) {
resolve();
} else {
requestAnimationFrame(checkDone);
}
};
requestAnimationFrame(checkDone);
});
// 清空
animator.loadAnimationFrames([]);
morphAdapter.resetAll();
console.log('✓ 预热完成');
} catch (e) {
console.warn('预热失败:', e.message, e);
}
}
async function generateAnimation() {
const text = document.getElementById('textInput').value.trim();
const apiUrl = document.getElementById('apiUrl').value;
@ -185,6 +223,10 @@ async function generateAnimationStream(text, apiUrl) {
const handleMessage = (message) => {
if (message.type === 'frame') {
pendingFrames.push(message.frame);
// 每100帧打印一次进度
if (pendingFrames.length % 100 === 0) {
console.log(`[前端] 已接收 ${pendingFrames.length} 帧, sentenceIndex=${message.frame.sentenceIndex}`);
}
flushFrames();
return;
}

View File

@ -1,56 +0,0 @@
<!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>

View File

@ -79,9 +79,10 @@ class TextToBlendShapesService:
yield {'type': 'error', 'message': '文本为空'}
return
# 测试:只处理第一句
sentences = sentences[:1]
print(f"[测试模式] 只处理第一句: {sentences[0]}")
# 确保 is_continuation 长度与 sentences 匹配
if len(self.is_continuation) != len(sentences):
print(f"[警告] is_continuation 长度 ({len(self.is_continuation)}) 与 sentences 长度 ({len(sentences)}) 不匹配,重置")
self.is_continuation = [False] * len(sentences)
yield {
'type': 'status',
@ -92,9 +93,10 @@ class TextToBlendShapesService:
}
# 打印句子列表用于调试
print(f"[调试] 发送给前端的句子列表:")
print(f"[调试] 发送给前端的句子列表 (共{len(sentences)}句, is_continuation长度={len(self.is_continuation)}):")
for i, s in enumerate(sentences):
print(f" [{i}] {s}")
cont = self.is_continuation[i] if i < len(self.is_continuation) else False
print(f" [{i}] {s} (连续={cont})")
# 使用队列来收集处理完成的句子
result_queue = queue.Queue()
@ -113,9 +115,14 @@ class TextToBlendShapesService:
result_queue.put((index, 'error', None, str(e)))
# 提交所有句子到线程池并发处理(增加并发数以加速)
print(f"[调试] 准备提交 {len(sentences)} 个句子到线程池")
try:
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
futures = []
for index, sentence in enumerate(sentences):
executor.submit(process_and_queue, index, sentence)
future = executor.submit(process_and_queue, index, sentence)
futures.append(future)
print(f"[调试] 已提交句子 {index}: {sentence[:20]}...")
# 按顺序从队列中取出结果并推送
completed = {}
@ -123,7 +130,9 @@ class TextToBlendShapesService:
total_frames = 0
cumulative_time = 0.0 # 累计时间,用于连续句子
print(f"[调试] 开始主循环,共 {len(sentences)} 个句子")
while next_index < len(sentences):
print(f"[调试] 主循环 next_index={next_index}, completed keys={list(completed.keys())}")
# 如果下一个句子还没完成,等待队列
if next_index not in completed:
yield {
@ -139,20 +148,23 @@ class TextToBlendShapesService:
try:
index, status, frames, error = result_queue.get(timeout=1)
completed[index] = (status, frames, error)
print(f"[主线程] 收到句子 {index} 的处理结果")
print(f"[主线程] 收到句子 {index} 的处理结果, status={status}")
except queue.Empty:
print(f"[调试] 队列为空,继续等待 next_index={next_index}")
continue
# 推送下一个句子的帧
status, frames, error = completed[next_index]
print(f"[主线程] 句子 {next_index} 状态: {status}, 帧数: {len(frames) if frames else 0}, 错误: {error}")
if status == 'error':
print(f"[错误] 句子 {next_index} 处理失败: {error}")
yield {'type': 'error', 'message': f'句子 {next_index} 处理失败: {error}'}
return
# 如果是连续句子,调整时间码使其无缝衔接
is_continuation = self.is_continuation[next_index] if next_index < len(self.is_continuation) else False
print(f"[主线程] 正在推送句子 {next_index}{len(frames)}{'(连续)' if is_continuation else ''}")
print(f"[主线程] 正在推送句子 {next_index}/{len(sentences)-1}{len(frames)}{'(连续)' if is_continuation else ''}")
print(f"[调试] 句子 {next_index} 对应文本: {sentences[next_index] if next_index < len(sentences) else 'N/A'}")
# 如果不是连续句子,重置累计时间
@ -172,11 +184,16 @@ class TextToBlendShapesService:
next_index += 1
print(f"[主线程] 流式传输完成,共 {total_frames}")
print(f"[主线程] 流式传输完成,共 {total_frames},处理了 {next_index} 个句子")
yield {
'type': 'end',
'frames': total_frames
}
except Exception as e:
import traceback
print(f"[错误] 流式处理异常: {e}")
traceback.print_exc()
yield {'type': 'error', 'message': f'流式处理异常: {str(e)}'}
def _process_sentence(self, sentence, output_dir, index):
"""处理单个句子: TTS -> A2F -> 解析"""
@ -286,6 +303,9 @@ class TextToBlendShapesService:
# 12字以上前6字再6字剩下的
parts = [first[:6], first[6:12], first[12:]]
# 过滤空字符串
parts = [p for p in parts if p.strip()]
# 替换第一句为多个小句
sentences = parts + sentences[1:]
# 标记后续部分为连续播放
@ -293,18 +313,31 @@ class TextToBlendShapesService:
print(f"[拆分优化] 第一句({length}字)拆分为{len(parts)}部分: {[len(p) for p in parts]} - 连续播放")
if not max_sentence_length or max_sentence_length <= 0:
print(f"[拆分] 最终句子数: {len(sentences)}, is_continuation长度: {len(self.is_continuation)}")
return sentences
limited = []
for sentence in sentences:
new_is_continuation = []
for i, sentence in enumerate(sentences):
if len(sentence) <= max_sentence_length:
limited.append(sentence)
new_is_continuation.append(self.is_continuation[i] if i < len(self.is_continuation) else False)
continue
start = 0
first_part = True
while start < len(sentence):
limited.append(sentence[start:start + max_sentence_length])
# 第一部分继承原来的 is_continuation后续部分标记为连续
if first_part:
new_is_continuation.append(self.is_continuation[i] if i < len(self.is_continuation) else False)
first_part = False
else:
new_is_continuation.append(True)
start += max_sentence_length
self.is_continuation = new_is_continuation
print(f"[拆分] 最终句子数: {len(limited)}, is_continuation长度: {len(self.is_continuation)}")
return limited
def _prepare_output_paths(self, output_dir: str = None, suffix: str = None):