流式传输

This commit is contained in:
yinsx
2025-12-25 15:36:35 +08:00
parent e56f47076c
commit 14bfdcbf51
19 changed files with 1191 additions and 65 deletions

View File

@ -60,6 +60,7 @@ async function generateAnimation() {
const text = document.getElementById('textInput').value.trim();
const apiUrl = document.getElementById('apiUrl').value;
const btn = document.getElementById('generateBtn');
const streamEnabled = document.getElementById('streamEnabled')?.checked;
if (!text) {
showStatus("请输入文字", "error");
@ -70,22 +71,11 @@ async function generateAnimation() {
showStatus("生成中...", "info");
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text, language: 'zh-CN' })
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || '请求失败');
if (streamEnabled) {
await generateAnimationStream(text, apiUrl);
} else {
await generateAnimationBatch(text, apiUrl);
}
animator.loadAnimationFrames(data.frames);
console.log("动画数据:", data.frames);
showStatus(`动画生成成功!共 ${data.frames.length}`, "success");
} catch (err) {
showStatus("错误: " + err.message, "error");
} finally {
@ -93,6 +83,190 @@ async function generateAnimation() {
}
}
async function generateAnimationBatch(text, apiUrl) {
const url = normalizeApiUrl(apiUrl, false);
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 })
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || '请求失败');
}
animator.loadAnimationFrames(data.frames);
console.log("动画数据:", data.frames);
showStatus(`动画生成成功!共 ${data.frames.length}`, "success");
}
async function generateAnimationStream(text, apiUrl) {
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.loadAnimationFrames([]);
animator.startStreaming();
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let started = false;
const pendingFrames = [];
const streamBufferMs = 300;
const flushBatchMs = 50;
const minStartFrames = Math.max(1, Math.round(animator.dataFps * (streamBufferMs / 1000)));
const frameBatchSize = Math.max(1, Math.round(animator.dataFps * (flushBatchMs / 1000)));
const flushFrames = (force = false) => {
if (pendingFrames.length === 0) {
return;
}
if (!force && pendingFrames.length < frameBatchSize) {
return;
}
const framesToFlush = pendingFrames.splice(0, pendingFrames.length);
animator.appendAnimationFrames(framesToFlush);
console.log(`Flushed ${framesToFlush.length} frames, total: ${animator.animationFrames.length}`);
if (!started && animator.animationFrames.length >= minStartFrames) {
console.log(`Starting animation with ${animator.animationFrames.length} frames (min: ${minStartFrames})`);
animator.playAnimation();
started = true;
}
};
const handleMessage = (message) => {
if (message.type === 'frame') {
pendingFrames.push(message.frame);
flushFrames();
return;
}
if (message.type === 'status') {
const stageMessage = message.message || 'Streaming';
showStatus(stageMessage, 'info');
console.log('Stream status:', message);
return;
}
if (message.type === 'error') {
throw new Error(message.message || 'Streaming error');
}
if (message.type === 'end') {
console.log('Stream ended, flushing remaining frames');
flushFrames(true);
animator.endStreaming();
if (!started && animator.animationFrames.length > 0) {
animator.playAnimation();
started = true;
}
const totalFrames = message.frames ?? animator.animationFrames.length;
console.log(`Total frames received: ${totalFrames}, in animator: ${animator.animationFrames.length}`);
showStatus(`流式动画接收完成,共 ${totalFrames}`, "success");
}
};
let streamError = null;
try {
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;
}
let message;
try {
message = JSON.parse(line);
} catch (err) {
continue;
}
handleMessage(message);
}
}
if (buffer.trim()) {
try {
handleMessage(JSON.parse(buffer));
} catch (err) {
// ignore trailing parse errors
}
}
} catch (err) {
streamError = err;
throw err;
} finally {
if (!streamError) {
flushFrames(true);
if (!started && animator.animationFrames.length > 0) {
animator.playAnimation();
}
}
animator.endStreaming();
}
}
function normalizeApiUrl(apiUrl, streamEnabled) {
if (!apiUrl) {
return apiUrl;
}
const trimmed = apiUrl.replace(/\/+$/, '');
const basePath = '/text-to-blendshapes';
const streamPath = `${basePath}/stream`;
if (streamEnabled) {
if (trimmed.endsWith(streamPath)) {
return trimmed;
}
if (trimmed.endsWith(basePath)) {
return trimmed + '/stream';
}
return trimmed + streamPath;
}
if (trimmed.endsWith(streamPath)) {
return trimmed.slice(0, -'/stream'.length);
}
if (trimmed.endsWith(basePath)) {
return trimmed;
}
return trimmed + basePath;
}
function playAnimation() {
animator.playAnimation();
}