真实接口预热
This commit is contained in:
8
examples/3d/.vite/deps/_metadata.json
Normal file
8
examples/3d/.vite/deps/_metadata.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"hash": "7a1c4e14",
|
||||||
|
"configHash": "ff0b84f2",
|
||||||
|
"lockfileHash": "e3b0c442",
|
||||||
|
"browserHash": "3dd711d0",
|
||||||
|
"optimized": {},
|
||||||
|
"chunks": {}
|
||||||
|
}
|
||||||
3
examples/3d/.vite/deps/package.json
Normal file
3
examples/3d/.vite/deps/package.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
@ -27,6 +27,9 @@ BlendShapeAnimator 是一个引擎无关的形态键动画SDK,可以与任何3
|
|||||||
```javascript
|
```javascript
|
||||||
const config = {
|
const config = {
|
||||||
blendShapeScale: 1.0, // 形态键强度缩放
|
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, // 动画数据帧率
|
dataFps: 30, // 动画数据帧率
|
||||||
onStatusChange: (type, msg) => {
|
onStatusChange: (type, msg) => {
|
||||||
// 状态变化回调
|
// 状态变化回调
|
||||||
@ -145,5 +148,7 @@ animator.playAnimation();
|
|||||||
|
|
||||||
- 形态键名称会自动转换为小写进行匹配
|
- 形态键名称会自动转换为小写进行匹配
|
||||||
- SDK使用 `requestAnimationFrame` 进行动画更新
|
- 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
|
- 所有形态键值范围为 0-1
|
||||||
|
|||||||
@ -31,10 +31,48 @@ class BabylonMorphTargetAdapter {
|
|||||||
return totalTargets;
|
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) {
|
warmupShaders(scene) {
|
||||||
console.log('开始shader预热...');
|
console.log('开始shader预热...');
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
// 强制同步更新所有 morph target managers
|
||||||
|
this.meshes?.forEach(mesh => {
|
||||||
|
const mtm = mesh.morphTargetManager;
|
||||||
|
if (mtm) {
|
||||||
|
mtm.enableNormalMorphing = true;
|
||||||
|
mtm.enableTangentMorphing = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 预热:强制触发着色器编译
|
// 预热:强制触发着色器编译
|
||||||
// 使用多种值组合来触发所有可能的shader变体
|
// 使用多种值组合来触发所有可能的shader变体
|
||||||
const testValues = [0, 0.2, 0.4, 0.6, 0.8, 1.0];
|
const testValues = [0, 0.2, 0.4, 0.6, 0.8, 1.0];
|
||||||
|
|||||||
@ -6,6 +6,7 @@ class BabylonSceneManager {
|
|||||||
this.scene = null;
|
this.scene = null;
|
||||||
this.camera = null;
|
this.camera = null;
|
||||||
this.onModelLoaded = null;
|
this.onModelLoaded = null;
|
||||||
|
this.onBeforeRender = null; // 渲染前回调
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -28,6 +29,10 @@ class BabylonSceneManager {
|
|||||||
this.scene.createDefaultEnvironment();
|
this.scene.createDefaultEnvironment();
|
||||||
|
|
||||||
this.engine.runRenderLoop(() => {
|
this.engine.runRenderLoop(() => {
|
||||||
|
// 在渲染前调用动画更新
|
||||||
|
if (this.onBeforeRender) {
|
||||||
|
this.onBeforeRender();
|
||||||
|
}
|
||||||
this.scene.render();
|
this.scene.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
class BlendShapeAnimator {
|
class BlendShapeAnimator {
|
||||||
constructor(config = {}) {
|
constructor(config = {}) {
|
||||||
this.morphTargetAdapter = null;
|
this.morphTargetAdapter = null;
|
||||||
|
this.scene = null;
|
||||||
this.animationFrames = [];
|
this.animationFrames = [];
|
||||||
this.animationShapeNames = [];
|
this.animationShapeNames = [];
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
@ -16,6 +17,17 @@ class BlendShapeAnimator {
|
|||||||
this.streamingWaitStart = null;
|
this.streamingWaitStart = null;
|
||||||
this.streamingStallMs = 0;
|
this.streamingStallMs = 0;
|
||||||
this.sentenceTexts = []; // 句子文本列表
|
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 || {
|
this.blinkParams = config.blinkParams || {
|
||||||
@ -63,8 +75,15 @@ class BlendShapeAnimator {
|
|||||||
// 回调
|
// 回调
|
||||||
this.onStatusChange = config.onStatusChange || (() => {});
|
this.onStatusChange = config.onStatusChange || (() => {});
|
||||||
|
|
||||||
// 启动空闲动画循环
|
// 不再启动独立的 RAF 循环,由外部渲染循环调用 tick()
|
||||||
this._updateIdleAnimations();
|
}
|
||||||
|
|
||||||
|
// 每帧调用,由外部渲染循环驱动
|
||||||
|
tick() {
|
||||||
|
if (this.isPlaying) {
|
||||||
|
this._animateFrameOnce();
|
||||||
|
}
|
||||||
|
this._updateIdleAnimationsOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置形态键适配器
|
// 设置形态键适配器
|
||||||
@ -76,6 +95,8 @@ class BlendShapeAnimator {
|
|||||||
loadAnimationFrames(frames) {
|
loadAnimationFrames(frames) {
|
||||||
this.animationFrames = frames || [];
|
this.animationFrames = frames || [];
|
||||||
this.animationShapeNames = this._collectAnimationShapeNames(this.animationFrames);
|
this.animationShapeNames = this._collectAnimationShapeNames(this.animationFrames);
|
||||||
|
this.lastFrameBlendShapes = {};
|
||||||
|
// 不重置 _hasPrewarmed,因为全局预热只需要做一次
|
||||||
}
|
}
|
||||||
|
|
||||||
appendAnimationFrames(frames) {
|
appendAnimationFrames(frames) {
|
||||||
@ -85,6 +106,7 @@ class BlendShapeAnimator {
|
|||||||
|
|
||||||
this.animationFrames.push(...frames);
|
this.animationFrames.push(...frames);
|
||||||
const newNames = this._collectAnimationShapeNames(frames);
|
const newNames = this._collectAnimationShapeNames(frames);
|
||||||
|
// 不重置 _hasPrewarmed
|
||||||
|
|
||||||
if (newNames.length === 0) {
|
if (newNames.length === 0) {
|
||||||
return;
|
return;
|
||||||
@ -116,7 +138,8 @@ class BlendShapeAnimator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 播放动画
|
// 播放动画
|
||||||
playAnimation() {
|
async playAnimation() {
|
||||||
|
console.log('=== playAnimation 开始 ===');
|
||||||
if (this.animationFrames.length === 0) {
|
if (this.animationFrames.length === 0) {
|
||||||
this.onStatusChange('error', '请先加载动画数据');
|
this.onStatusChange('error', '请先加载动画数据');
|
||||||
return;
|
return;
|
||||||
@ -141,45 +164,24 @@ class BlendShapeAnimator {
|
|||||||
|
|
||||||
// 注意:不停止眨眼,让眨眼继续运行
|
// 注意:不停止眨眼,让眨眼继续运行
|
||||||
|
|
||||||
// 预演前10帧以避免首次播放卡顿
|
this.stopAnimation(false, false);
|
||||||
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.isPlaying = true;
|
this.isPlaying = true;
|
||||||
this.currentFrameIndex = 0;
|
this.currentFrameIndex = 0;
|
||||||
this.animationStartTime = performance.now();
|
|
||||||
this.streamingWaitStart = null;
|
this.streamingWaitStart = null;
|
||||||
this.streamingStallMs = 0;
|
this.streamingStallMs = 0;
|
||||||
|
|
||||||
this._animateFrame();
|
this._primeFirstFrame();
|
||||||
|
this._scheduleAnimationStart();
|
||||||
|
|
||||||
this.onStatusChange('info', '播放中...');
|
this.onStatusChange('info', '播放中...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止动画
|
// 停止动画
|
||||||
stopAnimation(resumeExpressions = true) {
|
stopAnimation(resumeExpressions = true, resetBlendShapes = true) {
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
this._resetAnimationInfluences();
|
if (resetBlendShapes) {
|
||||||
|
this._resetAnimationInfluences();
|
||||||
|
}
|
||||||
|
|
||||||
// 恢复眼球移动
|
// 恢复眼球移动
|
||||||
if (resumeExpressions && this.isEyeLookEnabled) {
|
if (resumeExpressions && this.isEyeLookEnabled) {
|
||||||
@ -196,12 +198,17 @@ class BlendShapeAnimator {
|
|||||||
|
|
||||||
_collectAnimationShapeNames(frames) {
|
_collectAnimationShapeNames(frames) {
|
||||||
const names = new Set();
|
const names = new Set();
|
||||||
|
const threshold = Math.min(this.minBlendShapeValue, 0.02);
|
||||||
|
|
||||||
frames.forEach(frame => {
|
frames.forEach(frame => {
|
||||||
const blendShapes = frame?.blendShapes;
|
const blendShapes = frame?.blendShapes;
|
||||||
if (!blendShapes) return;
|
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);
|
return Array.from(names);
|
||||||
@ -215,10 +222,107 @@ class BlendShapeAnimator {
|
|||||||
this.animationShapeNames.forEach(name => {
|
this.animationShapeNames.forEach(name => {
|
||||||
this.morphTargetAdapter.setInfluence(name, 0);
|
this.morphTargetAdapter.setInfluence(name, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.lastFrameBlendShapes = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 内部动画帧处理
|
_prewarmAnimation() {
|
||||||
_animateFrame() {
|
// 同步预热已移除,改用异步预热
|
||||||
|
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;
|
if (!this.isPlaying) return;
|
||||||
|
|
||||||
const frameStartTime = performance.now();
|
const frameStartTime = performance.now();
|
||||||
@ -232,6 +336,7 @@ class BlendShapeAnimator {
|
|||||||
const elapsed = now - this.animationStartTime - this.streamingStallMs;
|
const elapsed = now - this.animationStartTime - this.streamingStallMs;
|
||||||
const exactFrame = elapsed / frameDuration;
|
const exactFrame = elapsed / frameDuration;
|
||||||
const targetFrameIndex = Math.floor(exactFrame);
|
const targetFrameIndex = Math.floor(exactFrame);
|
||||||
|
const frameProgress = exactFrame - targetFrameIndex;
|
||||||
|
|
||||||
if (targetFrameIndex >= this.animationFrames.length) {
|
if (targetFrameIndex >= this.animationFrames.length) {
|
||||||
if (this.isStreaming && !this.streamingComplete) {
|
if (this.isStreaming && !this.streamingComplete) {
|
||||||
@ -243,12 +348,7 @@ class BlendShapeAnimator {
|
|||||||
if (waitTime > 30000) {
|
if (waitTime > 30000) {
|
||||||
console.warn('Streaming timeout after 30s, stopping animation');
|
console.warn('Streaming timeout after 30s, stopping animation');
|
||||||
this.stopAnimation();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`Animation complete. Total frames: ${this.animationFrames.length}`);
|
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 nextFrame = this.animationFrames[Math.min(targetFrameIndex + 1, this.animationFrames.length - 1)];
|
||||||
const currentBlendShapes = currentFrame?.blendShapes || {};
|
const currentBlendShapes = currentFrame?.blendShapes || {};
|
||||||
const nextBlendShapes = nextFrame?.blendShapes || {};
|
const nextBlendShapes = nextFrame?.blendShapes || {};
|
||||||
const frameProgress = exactFrame - targetFrameIndex;
|
|
||||||
const smoothProgress = this._easeOutQuad(frameProgress);
|
const smoothProgress = this._easeOutQuad(frameProgress);
|
||||||
|
|
||||||
const shapeNames = this.animationShapeNames.length > 0
|
const shapeNames = this.animationShapeNames.length > 0
|
||||||
@ -268,8 +367,6 @@ class BlendShapeAnimator {
|
|||||||
: Object.keys(currentBlendShapes);
|
: Object.keys(currentBlendShapes);
|
||||||
|
|
||||||
let updateCount = 0;
|
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())) {
|
||||||
@ -281,30 +378,36 @@ class BlendShapeAnimator {
|
|||||||
const interpolatedValue = this._lerp(currentValue, nextValue, smoothProgress);
|
const interpolatedValue = this._lerp(currentValue, nextValue, smoothProgress);
|
||||||
const scaledValue = interpolatedValue * this.blendShapeScale;
|
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.morphTargetAdapter.setInfluence(key, scaledValue);
|
||||||
|
this.lastFrameBlendShapes[key] = scaledValue;
|
||||||
updateCount++;
|
updateCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setInfluenceTime = performance.now() - setInfluenceStart;
|
this._lastFrameTime = now;
|
||||||
|
|
||||||
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) {
|
||||||
this.currentSentenceIndex = sentenceIndex;
|
this.currentSentenceIndex = sentenceIndex;
|
||||||
this._updateCurrentSentenceDisplay();
|
this._updateCurrentSentenceDisplay();
|
||||||
}
|
}
|
||||||
|
// 不再递归调用 RAF,由统一循环驱动
|
||||||
requestAnimationFrame(() => this._animateFrame());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置空闲动画
|
// 设置空闲动画
|
||||||
@ -326,8 +429,8 @@ class BlendShapeAnimator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新空闲动画
|
// 更新空闲动画(单次调用,不递归)
|
||||||
_updateIdleAnimations() {
|
_updateIdleAnimationsOnce() {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
|
|
||||||
for (const name in this.idleAnimations) {
|
for (const name in this.idleAnimations) {
|
||||||
@ -359,8 +462,6 @@ class BlendShapeAnimator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => this._updateIdleAnimations());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 眨眼控制
|
// 眨眼控制
|
||||||
|
|||||||
@ -27,6 +27,10 @@ function init() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
animator.setMorphTargetAdapter(morphAdapter);
|
animator.setMorphTargetAdapter(morphAdapter);
|
||||||
|
animator.scene = sceneManager.scene;
|
||||||
|
|
||||||
|
// 把动画更新挂到 Babylon 渲染循环
|
||||||
|
sceneManager.onBeforeRender = () => animator.tick();
|
||||||
|
|
||||||
// 导出全局变量供表情库使用
|
// 导出全局变量供表情库使用
|
||||||
window.animator = animator;
|
window.animator = animator;
|
||||||
@ -41,7 +45,7 @@ function init() {
|
|||||||
|
|
||||||
// 加载3D模型
|
// 加载3D模型
|
||||||
sceneManager.loadModel('head_a01.glb',
|
sceneManager.loadModel('head_a01.glb',
|
||||||
(meshes) => {
|
async (meshes) => {
|
||||||
showStatus("模型加载成功", "success");
|
showStatus("模型加载成功", "success");
|
||||||
const totalTargets = morphAdapter.buildCache(meshes);
|
const totalTargets = morphAdapter.buildCache(meshes);
|
||||||
|
|
||||||
@ -50,43 +54,39 @@ function init() {
|
|||||||
} else {
|
} else {
|
||||||
console.log(`✓ 构建缓存完成,共 ${totalTargets} 个形态键`);
|
console.log(`✓ 构建缓存完成,共 ${totalTargets} 个形态键`);
|
||||||
|
|
||||||
// 预热着色器以避免首次动画卡顿
|
// 预热:直接调用生成动画流程(和用户点击按钮完全一样)
|
||||||
console.log('预热着色器中...');
|
showStatus("预热中...", "info");
|
||||||
morphAdapter.warmupShaders(sceneManager.scene);
|
const apiUrl = document.getElementById('apiUrl').value;
|
||||||
console.log('✓ 着色器预热完成');
|
const streamEnabled = document.getElementById('streamEnabled')?.checked;
|
||||||
|
console.log('预热 apiUrl:', apiUrl, 'stream:', streamEnabled);
|
||||||
// 播放120帧预热动画(4秒)
|
if (apiUrl) {
|
||||||
console.log('预热渲染管线中...');
|
try {
|
||||||
const dummyFrames = [];
|
// 和 generateAnimation 走完全一样的路径
|
||||||
const allBlendShapes = Object.keys(morphAdapter.morphTargetCache);
|
if (streamEnabled) {
|
||||||
console.log(`使用 ${allBlendShapes.length} 个blendshapes进行预热`);
|
await generateAnimationStream('你好', apiUrl);
|
||||||
|
} else {
|
||||||
for (let i = 0; i < 120; i++) {
|
await generateAnimationBatch('你好', apiUrl);
|
||||||
const blendShapes = {};
|
playAnimation();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
// 等播放完
|
||||||
|
await new Promise(resolve => {
|
||||||
dummyFrames.push({
|
const check = () => animator.isPlaying ? requestAnimationFrame(check) : resolve();
|
||||||
timeCode: i / 30,
|
check();
|
||||||
blendShapes: blendShapes
|
});
|
||||||
});
|
// 完全重置状态
|
||||||
|
animator.stopAnimation();
|
||||||
|
animator.loadAnimationFrames([]);
|
||||||
|
animator.endStreaming();
|
||||||
|
morphAdapter.resetAll();
|
||||||
|
console.log('✓ 预热完成');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('预热失败:', e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('预热跳过: apiUrl 为空');
|
||||||
}
|
}
|
||||||
|
|
||||||
animator.loadAnimationFrames(dummyFrames);
|
showStatus("就绪", "success");
|
||||||
animator.playAnimation();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
animator.stopAnimation();
|
|
||||||
animator.loadAnimationFrames([]);
|
|
||||||
console.log('✓ 渲染管线预热完成');
|
|
||||||
}, 4000);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(message) => {
|
(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() {
|
async function generateAnimation() {
|
||||||
const text = document.getElementById('textInput').value.trim();
|
const text = document.getElementById('textInput').value.trim();
|
||||||
const apiUrl = document.getElementById('apiUrl').value;
|
const apiUrl = document.getElementById('apiUrl').value;
|
||||||
@ -185,6 +223,10 @@ async function generateAnimationStream(text, apiUrl) {
|
|||||||
const handleMessage = (message) => {
|
const handleMessage = (message) => {
|
||||||
if (message.type === 'frame') {
|
if (message.type === 'frame') {
|
||||||
pendingFrames.push(message.frame);
|
pendingFrames.push(message.frame);
|
||||||
|
// 每100帧打印一次进度
|
||||||
|
if (pendingFrames.length % 100 === 0) {
|
||||||
|
console.log(`[前端] 已接收 ${pendingFrames.length} 帧, sentenceIndex=${message.frame.sentenceIndex}`);
|
||||||
|
}
|
||||||
flushFrames();
|
flushFrames();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
Binary file not shown.
@ -79,9 +79,10 @@ class TextToBlendShapesService:
|
|||||||
yield {'type': 'error', 'message': '文本为空'}
|
yield {'type': 'error', 'message': '文本为空'}
|
||||||
return
|
return
|
||||||
|
|
||||||
# 测试:只处理第一句
|
# 确保 is_continuation 长度与 sentences 匹配
|
||||||
sentences = sentences[:1]
|
if len(self.is_continuation) != len(sentences):
|
||||||
print(f"[测试模式] 只处理第一句: {sentences[0]}")
|
print(f"[警告] is_continuation 长度 ({len(self.is_continuation)}) 与 sentences 长度 ({len(sentences)}) 不匹配,重置")
|
||||||
|
self.is_continuation = [False] * len(sentences)
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
'type': 'status',
|
'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):
|
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()
|
result_queue = queue.Queue()
|
||||||
@ -113,70 +115,85 @@ class TextToBlendShapesService:
|
|||||||
result_queue.put((index, 'error', None, str(e)))
|
result_queue.put((index, 'error', None, str(e)))
|
||||||
|
|
||||||
# 提交所有句子到线程池并发处理(增加并发数以加速)
|
# 提交所有句子到线程池并发处理(增加并发数以加速)
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
print(f"[调试] 准备提交 {len(sentences)} 个句子到线程池")
|
||||||
for index, sentence in enumerate(sentences):
|
try:
|
||||||
executor.submit(process_and_queue, index, sentence)
|
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||||
|
futures = []
|
||||||
|
for index, sentence in enumerate(sentences):
|
||||||
|
future = executor.submit(process_and_queue, index, sentence)
|
||||||
|
futures.append(future)
|
||||||
|
print(f"[调试] 已提交句子 {index}: {sentence[:20]}...")
|
||||||
|
|
||||||
# 按顺序从队列中取出结果并推送
|
# 按顺序从队列中取出结果并推送
|
||||||
completed = {}
|
completed = {}
|
||||||
next_index = 0
|
next_index = 0
|
||||||
total_frames = 0
|
total_frames = 0
|
||||||
cumulative_time = 0.0 # 累计时间,用于连续句子
|
cumulative_time = 0.0 # 累计时间,用于连续句子
|
||||||
|
|
||||||
while next_index < len(sentences):
|
print(f"[调试] 开始主循环,共 {len(sentences)} 个句子")
|
||||||
# 如果下一个句子还没完成,等待队列
|
while next_index < len(sentences):
|
||||||
if next_index not in completed:
|
print(f"[调试] 主循环 next_index={next_index}, completed keys={list(completed.keys())}")
|
||||||
yield {
|
# 如果下一个句子还没完成,等待队列
|
||||||
'type': 'status',
|
if next_index not in completed:
|
||||||
'stage': 'processing',
|
yield {
|
||||||
'sentence_index': next_index,
|
'type': 'status',
|
||||||
'sentences': len(sentences),
|
'stage': 'processing',
|
||||||
'message': f'正在处理 {next_index + 1}/{len(sentences)}'
|
'sentence_index': next_index,
|
||||||
}
|
'sentences': len(sentences),
|
||||||
|
'message': f'正在处理 {next_index + 1}/{len(sentences)}'
|
||||||
|
}
|
||||||
|
|
||||||
# 从队列中获取结果
|
# 从队列中获取结果
|
||||||
while next_index not in completed:
|
while next_index not in completed:
|
||||||
try:
|
try:
|
||||||
index, status, frames, error = result_queue.get(timeout=1)
|
index, status, frames, error = result_queue.get(timeout=1)
|
||||||
completed[index] = (status, frames, error)
|
completed[index] = (status, frames, error)
|
||||||
print(f"[主线程] 收到句子 {index} 的处理结果")
|
print(f"[主线程] 收到句子 {index} 的处理结果, status={status}")
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
print(f"[调试] 队列为空,继续等待 next_index={next_index}")
|
||||||
|
continue
|
||||||
|
|
||||||
# 推送下一个句子的帧
|
# 推送下一个句子的帧
|
||||||
status, frames, error = completed[next_index]
|
status, frames, error = completed[next_index]
|
||||||
if status == 'error':
|
print(f"[主线程] 句子 {next_index} 状态: {status}, 帧数: {len(frames) if frames else 0}, 错误: {error}")
|
||||||
yield {'type': 'error', 'message': f'句子 {next_index} 处理失败: {error}'}
|
if status == 'error':
|
||||||
return
|
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
|
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'}")
|
print(f"[调试] 句子 {next_index} 对应文本: {sentences[next_index] if next_index < len(sentences) else 'N/A'}")
|
||||||
|
|
||||||
# 如果不是连续句子,重置累计时间
|
# 如果不是连续句子,重置累计时间
|
||||||
if not is_continuation and next_index > 0:
|
if not is_continuation and next_index > 0:
|
||||||
cumulative_time = 0.0
|
cumulative_time = 0.0
|
||||||
|
|
||||||
for frame in frames:
|
for frame in frames:
|
||||||
# 调整时间码:从累计时间开始
|
# 调整时间码:从累计时间开始
|
||||||
frame['timeCode'] = cumulative_time + frame['timeCode']
|
frame['timeCode'] = cumulative_time + frame['timeCode']
|
||||||
frame['sentenceIndex'] = next_index
|
frame['sentenceIndex'] = next_index
|
||||||
total_frames += 1
|
total_frames += 1
|
||||||
yield {'type': 'frame', 'frame': frame}
|
yield {'type': 'frame', 'frame': frame}
|
||||||
|
|
||||||
# 更新累计时间为当前句子的最后一帧时间
|
# 更新累计时间为当前句子的最后一帧时间
|
||||||
if frames:
|
if frames:
|
||||||
cumulative_time = frames[-1]['timeCode']
|
cumulative_time = frames[-1]['timeCode']
|
||||||
|
|
||||||
next_index += 1
|
next_index += 1
|
||||||
|
|
||||||
print(f"[主线程] 流式传输完成,共 {total_frames} 帧")
|
print(f"[主线程] 流式传输完成,共 {total_frames} 帧,处理了 {next_index} 个句子")
|
||||||
yield {
|
yield {
|
||||||
'type': 'end',
|
'type': 'end',
|
||||||
'frames': total_frames
|
'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):
|
def _process_sentence(self, sentence, output_dir, index):
|
||||||
"""处理单个句子: TTS -> A2F -> 解析"""
|
"""处理单个句子: TTS -> A2F -> 解析"""
|
||||||
@ -286,6 +303,9 @@ class TextToBlendShapesService:
|
|||||||
# 12字以上:前6字,再6字,剩下的
|
# 12字以上:前6字,再6字,剩下的
|
||||||
parts = [first[:6], first[6:12], first[12:]]
|
parts = [first[:6], first[6:12], first[12:]]
|
||||||
|
|
||||||
|
# 过滤空字符串
|
||||||
|
parts = [p for p in parts if p.strip()]
|
||||||
|
|
||||||
# 替换第一句为多个小句
|
# 替换第一句为多个小句
|
||||||
sentences = parts + sentences[1:]
|
sentences = parts + sentences[1:]
|
||||||
# 标记后续部分为连续播放
|
# 标记后续部分为连续播放
|
||||||
@ -293,18 +313,31 @@ class TextToBlendShapesService:
|
|||||||
print(f"[拆分优化] 第一句({length}字)拆分为{len(parts)}部分: {[len(p) for p in parts]} - 连续播放")
|
print(f"[拆分优化] 第一句({length}字)拆分为{len(parts)}部分: {[len(p) for p in parts]} - 连续播放")
|
||||||
|
|
||||||
if not max_sentence_length or max_sentence_length <= 0:
|
if not max_sentence_length or max_sentence_length <= 0:
|
||||||
|
print(f"[拆分] 最终句子数: {len(sentences)}, is_continuation长度: {len(self.is_continuation)}")
|
||||||
return sentences
|
return sentences
|
||||||
|
|
||||||
limited = []
|
limited = []
|
||||||
for sentence in sentences:
|
new_is_continuation = []
|
||||||
|
for i, sentence in enumerate(sentences):
|
||||||
if len(sentence) <= max_sentence_length:
|
if len(sentence) <= max_sentence_length:
|
||||||
limited.append(sentence)
|
limited.append(sentence)
|
||||||
|
new_is_continuation.append(self.is_continuation[i] if i < len(self.is_continuation) else False)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
start = 0
|
start = 0
|
||||||
|
first_part = True
|
||||||
while start < len(sentence):
|
while start < len(sentence):
|
||||||
limited.append(sentence[start:start + max_sentence_length])
|
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
|
start += max_sentence_length
|
||||||
|
|
||||||
|
self.is_continuation = new_is_continuation
|
||||||
|
print(f"[拆分] 最终句子数: {len(limited)}, is_continuation长度: {len(self.is_continuation)}")
|
||||||
return limited
|
return limited
|
||||||
|
|
||||||
def _prepare_output_paths(self, output_dir: str = None, suffix: str = None):
|
def _prepare_output_paths(self, output_dir: str = None, suffix: str = None):
|
||||||
|
|||||||
Reference in New Issue
Block a user