首次播放不掉帧
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
data/
|
||||
a2f_venv/
|
||||
external/
|
||||
nul
|
||||
@ -12,6 +12,7 @@ class BlendShapeAnimator {
|
||||
this.idleAnimations = {};
|
||||
this.blendShapeScale = config.blendShapeScale || 1.0;
|
||||
this.dataFps = config.dataFps || 30;
|
||||
this.playbackSpeed = config.playbackSpeed || 1.0; // 播放倍速:0.5=慢速, 1.0=正常, 2.0=快速
|
||||
this.isStreaming = false;
|
||||
this.streamingComplete = true;
|
||||
this.streamingWaitStart = null;
|
||||
@ -333,7 +334,7 @@ class BlendShapeAnimator {
|
||||
}
|
||||
|
||||
const frameDuration = 1000 / this.dataFps;
|
||||
const elapsed = now - this.animationStartTime - this.streamingStallMs;
|
||||
const elapsed = (now - this.animationStartTime - this.streamingStallMs) * this.playbackSpeed;
|
||||
const exactFrame = elapsed / frameDuration;
|
||||
const targetFrameIndex = Math.floor(exactFrame);
|
||||
const frameProgress = exactFrame - targetFrameIndex;
|
||||
@ -710,6 +711,8 @@ class BlendShapeAnimator {
|
||||
updateConfig(key, value) {
|
||||
if (key === 'blendShapeScale') {
|
||||
this.blendShapeScale = value;
|
||||
} else if (key === 'playbackSpeed') {
|
||||
this.playbackSpeed = value;
|
||||
} else if (this[key]) {
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
@ -51,6 +51,13 @@
|
||||
style="width: 100%; cursor: pointer;">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>播放倍速: <span id="speedValue">1.0x</span></label>
|
||||
<input type="range" id="speedSlider" min="0.25" max="3" step="0.25" value="1.0"
|
||||
oninput="updatePlaybackSpeed(this.value)"
|
||||
style="width: 100%; cursor: pointer;">
|
||||
</div>
|
||||
|
||||
<button id="generateBtn" onclick="generateAnimation()">生成动画</button>
|
||||
<button onclick="playAnimation()">播放动画</button>
|
||||
<button onclick="stopAnimation()">停止动画</button>
|
||||
|
||||
@ -54,37 +54,14 @@ function init() {
|
||||
} else {
|
||||
console.log(`✓ 构建缓存完成,共 ${totalTargets} 个形态键`);
|
||||
|
||||
// 预热:直接调用生成动画流程(和用户点击按钮完全一样)
|
||||
// 预热:使用写死的预热数据,不请求接口
|
||||
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();
|
||||
});
|
||||
// 完全重置状态
|
||||
animator.stopAnimation();
|
||||
animator.loadAnimationFrames([]);
|
||||
animator.endStreaming();
|
||||
morphAdapter.resetAll();
|
||||
await warmupWithLocalData();
|
||||
console.log('✓ 预热完成');
|
||||
} catch (e) {
|
||||
console.warn('预热失败:', e);
|
||||
}
|
||||
} else {
|
||||
console.warn('预热跳过: apiUrl 为空');
|
||||
}
|
||||
|
||||
showStatus("就绪", "success");
|
||||
}
|
||||
@ -95,6 +72,47 @@ function init() {
|
||||
);
|
||||
}
|
||||
|
||||
// 使用本地预热数据进行预热(不请求接口)
|
||||
async function warmupWithLocalData() {
|
||||
console.log('=== warmupWithLocalData 开始 ===');
|
||||
|
||||
// 加载预热数据
|
||||
const response = await fetch('预热数据.json');
|
||||
const frames = await response.json();
|
||||
console.log(`加载预热数据: ${frames.length} 帧`);
|
||||
|
||||
// 保存原始播放速度,设置预热倍速(4倍速快速预热)
|
||||
const originalSpeed = animator.playbackSpeed;
|
||||
animator.playbackSpeed = 4.0;
|
||||
console.log(`预热倍速: ${animator.playbackSpeed}x`);
|
||||
|
||||
// 加载并播放
|
||||
animator.loadAnimationFrames(frames);
|
||||
animator.playAnimation();
|
||||
|
||||
// 等待播放完成
|
||||
await new Promise(resolve => {
|
||||
const checkDone = () => {
|
||||
if (!animator.isPlaying) {
|
||||
resolve();
|
||||
} else {
|
||||
requestAnimationFrame(checkDone);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(checkDone);
|
||||
});
|
||||
|
||||
// 重置状态
|
||||
animator.stopAnimation();
|
||||
animator.loadAnimationFrames([]);
|
||||
morphAdapter.resetAll();
|
||||
|
||||
// 恢复原始播放速度
|
||||
animator.playbackSpeed = originalSpeed;
|
||||
|
||||
console.log('✓ 本地数据预热完成');
|
||||
}
|
||||
|
||||
// 偷偷调接口预热 - 完全走一遍生成动画流程
|
||||
async function warmupWithApi() {
|
||||
console.log('=== warmupWithApi 开始 ===');
|
||||
@ -133,7 +151,7 @@ async function warmupWithApi() {
|
||||
}
|
||||
}
|
||||
|
||||
async function generateAnimation() {
|
||||
async function generateAnimation() {
|
||||
const text = document.getElementById('textInput').value.trim();
|
||||
const apiUrl = document.getElementById('apiUrl').value;
|
||||
const btn = document.getElementById('generateBtn');
|
||||
@ -176,6 +194,19 @@ async function generateAnimationBatch(text, apiUrl) {
|
||||
|
||||
animator.loadAnimationFrames(data.frames);
|
||||
console.log("动画数据:", data.frames);
|
||||
// 打印可复制的 JSON 格式,过滤掉值为0的字段
|
||||
const filteredFrames = data.frames.map(frame => {
|
||||
const filtered = { timeCode: frame.timeCode, blendShapes: {} };
|
||||
for (const [key, value] of Object.entries(frame.blendShapes)) {
|
||||
if (value !== 0) {
|
||||
filtered.blendShapes[key] = value;
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
});
|
||||
console.log("========== 复制以下内容作为预热数据 ==========");
|
||||
console.log(JSON.stringify(filteredFrames));
|
||||
console.log("========== 复制结束 ==========");
|
||||
showStatus(`动画生成成功!共 ${data.frames.length} 帧`, "success");
|
||||
}
|
||||
|
||||
@ -355,6 +386,11 @@ function updateFps(value) {
|
||||
document.getElementById('fpsValue').textContent = value;
|
||||
}
|
||||
|
||||
function updatePlaybackSpeed(value) {
|
||||
animator.updateConfig('playbackSpeed', parseFloat(value));
|
||||
document.getElementById('speedValue').textContent = value + 'x';
|
||||
}
|
||||
|
||||
function testBlendShape() {
|
||||
if (morphAdapter.getCacheSize() === 0) {
|
||||
showStatus("模型未加载", "error");
|
||||
|
||||
4776
examples/3d/预热数据.json
Normal file
4776
examples/3d/预热数据.json
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -48,7 +48,7 @@ class TextToBlendShapesService:
|
||||
output_dir, audio_path = self._prepare_output_paths(output_dir)
|
||||
|
||||
self.tts.text_to_audio(text, audio_path)
|
||||
csv_path = self.a2f.audio_to_csv(audio_path)
|
||||
csv_path, temp_dir = self.a2f.audio_to_csv(audio_path)
|
||||
frames = self.parser.csv_to_blend_shapes(csv_path)
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user