Enhance video handling and performance optimizations

- Added environment variables to prevent CPU thread pools from busy-waiting.
- Deferred loading of video models until first use to reduce VRAM footprint.
- Implemented streaming of speaking clips for improved responsiveness.
- Introduced a queue for managing speaking clips to handle multiple requests smoothly.
- Updated video playback logic to ensure proper handling of clip generation.
This commit is contained in:
2026-04-24 00:36:18 -04:00
parent 129df7d1fa
commit 44a10667c2
7 changed files with 234 additions and 69 deletions
+52 -31
View File
@@ -24,6 +24,8 @@ let videoModeName = "off"; // "off" | "library" | "reflective"
let idleClipUrl = null; // URL string (server-served) or null
let pendingSpeakingClipMeta = null; // {chunk_id, duration_ms, text} waiting for MP4 binary
let currentSpeakingClipBlobUrl = null;
let speakingClipQueue = []; // [{blobUrl, meta}] clips waiting to play
let currentClipGeneration = 0; // incremented each clip start; guards stale onended handlers
const chatArea = document.getElementById("chat-area");
const statusBadge = document.getElementById("status-badge");
@@ -131,67 +133,86 @@ function refreshStage() {
if (videoModeEnabled && idleClipUrl) {
stageEl.classList.add("active");
if (avatarVideo.src !== location.origin + idleClipUrl) {
avatarVideo.src = idleClipUrl;
avatarVideo.loop = true;
avatarVideo.muted = true;
avatarVideo.play().catch(() => {});
_returnToIdle();
}
} else {
stageEl.classList.remove("active");
}
}
function _returnToIdle() {
if (!idleClipUrl) return;
avatarVideo.onended = null;
avatarVideo.loop = false;
avatarVideo.muted = true;
avatarVideo.src = idleClipUrl;
avatarVideo.play().catch(() => {});
}
function playSpeakingClip(arrayBuffer, meta) {
// Replace the idle loop with the speaking clip.
stopSpeakingClip();
const blob = new Blob([arrayBuffer], { type: "video/mp4" });
currentSpeakingClipBlobUrl = URL.createObjectURL(blob);
const blobUrl = URL.createObjectURL(blob);
if (currentSpeakingClipBlobUrl !== null) {
// A clip is already playing — queue this one.
speakingClipQueue.push({ blobUrl, meta });
} else {
_startSpeakingClip(blobUrl, meta);
}
}
function _startSpeakingClip(blobUrl, meta) {
const gen = ++currentClipGeneration;
if (currentSpeakingClipBlobUrl) {
URL.revokeObjectURL(currentSpeakingClipBlobUrl);
}
currentSpeakingClipBlobUrl = blobUrl;
avatarVideo.loop = false;
avatarVideo.muted = false;
avatarVideo.src = currentSpeakingClipBlobUrl;
avatarVideo.src = blobUrl;
// Show the full reply text now — the MP4 plays it in one shot so there's
// no per-chunk sync to do.
if (meta && meta.text) {
appendAssistantText(meta.text);
}
isPlaying = true;
avatarVideo.onended = () => {
isPlaying = false;
finalizeAssistantMessage(false);
// Return to idle loop.
if (idleClipUrl) {
avatarVideo.loop = true;
avatarVideo.muted = true;
avatarVideo.src = idleClipUrl;
avatarVideo.play().catch(() => {});
}
if (currentSpeakingClipBlobUrl) {
URL.revokeObjectURL(currentSpeakingClipBlobUrl);
currentSpeakingClipBlobUrl = null;
if (currentClipGeneration !== gen) return; // stale handler from a replaced clip
URL.revokeObjectURL(currentSpeakingClipBlobUrl);
currentSpeakingClipBlobUrl = null;
const next = speakingClipQueue.shift();
if (next) {
_startSpeakingClip(next.blobUrl, next.meta);
} else {
isPlaying = false;
finalizeAssistantMessage(false);
_returnToIdle();
}
};
avatarVideo.play().catch((e) => {
console.error("speaking clip play failed:", e);
});
}
function stopSpeakingClip() {
// Discard any queued clips.
for (const { blobUrl } of speakingClipQueue) {
URL.revokeObjectURL(blobUrl);
}
speakingClipQueue = [];
currentClipGeneration++; // invalidate any in-flight onended handlers
if (!currentSpeakingClipBlobUrl) return;
try {
avatarVideo.pause();
} catch (_) {}
try { avatarVideo.pause(); } catch (_) {}
avatarVideo.onended = null;
URL.revokeObjectURL(currentSpeakingClipBlobUrl);
currentSpeakingClipBlobUrl = null;
if (idleClipUrl) {
avatarVideo.loop = true;
avatarVideo.muted = true;
avatarVideo.src = idleClipUrl;
avatarVideo.play().catch(() => {});
}
isPlaying = false;
_returnToIdle();
}
async function uploadAvatar() {