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:
+52
-31
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user