首次播放不掉帧
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
data/
|
data/
|
||||||
a2f_venv/
|
a2f_venv/
|
||||||
external/
|
external/
|
||||||
|
nul
|
||||||
@ -12,6 +12,7 @@ class BlendShapeAnimator {
|
|||||||
this.idleAnimations = {};
|
this.idleAnimations = {};
|
||||||
this.blendShapeScale = config.blendShapeScale || 1.0;
|
this.blendShapeScale = config.blendShapeScale || 1.0;
|
||||||
this.dataFps = config.dataFps || 30;
|
this.dataFps = config.dataFps || 30;
|
||||||
|
this.playbackSpeed = config.playbackSpeed || 1.0; // 播放倍速:0.5=慢速, 1.0=正常, 2.0=快速
|
||||||
this.isStreaming = false;
|
this.isStreaming = false;
|
||||||
this.streamingComplete = true;
|
this.streamingComplete = true;
|
||||||
this.streamingWaitStart = null;
|
this.streamingWaitStart = null;
|
||||||
@ -333,7 +334,7 @@ class BlendShapeAnimator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const frameDuration = 1000 / this.dataFps;
|
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 exactFrame = elapsed / frameDuration;
|
||||||
const targetFrameIndex = Math.floor(exactFrame);
|
const targetFrameIndex = Math.floor(exactFrame);
|
||||||
const frameProgress = exactFrame - targetFrameIndex;
|
const frameProgress = exactFrame - targetFrameIndex;
|
||||||
@ -710,6 +711,8 @@ class BlendShapeAnimator {
|
|||||||
updateConfig(key, value) {
|
updateConfig(key, value) {
|
||||||
if (key === 'blendShapeScale') {
|
if (key === 'blendShapeScale') {
|
||||||
this.blendShapeScale = value;
|
this.blendShapeScale = value;
|
||||||
|
} else if (key === 'playbackSpeed') {
|
||||||
|
this.playbackSpeed = value;
|
||||||
} else if (this[key]) {
|
} else if (this[key]) {
|
||||||
this[key] = value;
|
this[key] = value;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,13 @@
|
|||||||
style="width: 100%; cursor: pointer;">
|
style="width: 100%; cursor: pointer;">
|
||||||
</div>
|
</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 id="generateBtn" onclick="generateAnimation()">生成动画</button>
|
||||||
<button onclick="playAnimation()">播放动画</button>
|
<button onclick="playAnimation()">播放动画</button>
|
||||||
<button onclick="stopAnimation()">停止动画</button>
|
<button onclick="stopAnimation()">停止动画</button>
|
||||||
|
|||||||
@ -54,36 +54,13 @@ function init() {
|
|||||||
} else {
|
} else {
|
||||||
console.log(`✓ 构建缓存完成,共 ${totalTargets} 个形态键`);
|
console.log(`✓ 构建缓存完成,共 ${totalTargets} 个形态键`);
|
||||||
|
|
||||||
// 预热:直接调用生成动画流程(和用户点击按钮完全一样)
|
// 预热:使用写死的预热数据,不请求接口
|
||||||
showStatus("预热中...", "info");
|
showStatus("预热中...", "info");
|
||||||
const apiUrl = document.getElementById('apiUrl').value;
|
try {
|
||||||
const streamEnabled = document.getElementById('streamEnabled')?.checked;
|
await warmupWithLocalData();
|
||||||
console.log('预热 apiUrl:', apiUrl, 'stream:', streamEnabled);
|
console.log('✓ 预热完成');
|
||||||
if (apiUrl) {
|
} catch (e) {
|
||||||
try {
|
console.warn('预热失败:', e);
|
||||||
// 和 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();
|
|
||||||
console.log('✓ 预热完成');
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('预热失败:', e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('预热跳过: apiUrl 为空');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showStatus("就绪", "success");
|
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() {
|
async function warmupWithApi() {
|
||||||
console.log('=== 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 text = document.getElementById('textInput').value.trim();
|
||||||
const apiUrl = document.getElementById('apiUrl').value;
|
const apiUrl = document.getElementById('apiUrl').value;
|
||||||
const btn = document.getElementById('generateBtn');
|
const btn = document.getElementById('generateBtn');
|
||||||
@ -176,6 +194,19 @@ async function generateAnimationBatch(text, apiUrl) {
|
|||||||
|
|
||||||
animator.loadAnimationFrames(data.frames);
|
animator.loadAnimationFrames(data.frames);
|
||||||
console.log("动画数据:", 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");
|
showStatus(`动画生成成功!共 ${data.frames.length} 帧`, "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,6 +386,11 @@ function updateFps(value) {
|
|||||||
document.getElementById('fpsValue').textContent = value;
|
document.getElementById('fpsValue').textContent = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updatePlaybackSpeed(value) {
|
||||||
|
animator.updateConfig('playbackSpeed', parseFloat(value));
|
||||||
|
document.getElementById('speedValue').textContent = value + 'x';
|
||||||
|
}
|
||||||
|
|
||||||
function testBlendShape() {
|
function testBlendShape() {
|
||||||
if (morphAdapter.getCacheSize() === 0) {
|
if (morphAdapter.getCacheSize() === 0) {
|
||||||
showStatus("模型未加载", "error");
|
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)
|
output_dir, audio_path = self._prepare_output_paths(output_dir)
|
||||||
|
|
||||||
self.tts.text_to_audio(text, audio_path)
|
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)
|
frames = self.parser.csv_to_blend_shapes(csv_path)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user