- Enhanced geometric feature extraction for comprehensive mouth shape analysis - Implemented vowel classification (A, E, I, O, U) based on lip geometry - Added new SRT format option to display detected viseme sequences - New toggle for enabling/disabling viseme detection - Caching system supports viseme detection state - Viseme sequences grouped for readability (A3-E2-I-O4) Technical implementation: - Extract 8 geometric features: lip width/height, mouth area, roundedness, curvature - Rule-based vowel classification with confidence scoring - Frame-by-frame viseme detection integrated with existing movement analysis - ~20% performance impact when enabled, fully cached for repeated analysis
187 lines
8.0 KiB
Plaintext
187 lines
8.0 KiB
Plaintext
/**
|
|
* Mouth Movement Analyzer In-Node Video Preview
|
|
* Uses ComfyUI's addDOMWidget to display video directly inside the node
|
|
*/
|
|
|
|
import { app } from "../../scripts/app.js";
|
|
|
|
app.registerExtension({
|
|
name: "TTS_Audio_Suite.MouthMovementPreview",
|
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData.name === "MouthMovementAnalyzer") {
|
|
|
|
// Override onNodeCreated to add DOM widget when preview mode is enabled
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function() {
|
|
const result = onNodeCreated?.apply(this, arguments);
|
|
|
|
// Find the preview_mode widget
|
|
const previewWidget = this.widgets?.find(w => w.name === "preview_mode");
|
|
if (previewWidget) {
|
|
const originalCallback = previewWidget.callback;
|
|
previewWidget.callback = (value) => {
|
|
// Call original callback
|
|
if (originalCallback) originalCallback.call(this, value);
|
|
|
|
// Handle preview widget visibility
|
|
if (value) {
|
|
this.showVideoWidget();
|
|
} else {
|
|
this.hideVideoWidget();
|
|
}
|
|
};
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
// Method to create and show video DOM widget
|
|
nodeType.prototype.showVideoWidget = function() {
|
|
if (this.videoWidget) return; // Already exists
|
|
|
|
// Create container for video
|
|
const container = document.createElement("div");
|
|
container.style.width = "100%";
|
|
container.style.minHeight = "200px";
|
|
container.style.backgroundColor = "#1a1a1a";
|
|
container.style.borderRadius = "4px";
|
|
container.style.border = "1px solid #444";
|
|
container.style.padding = "8px";
|
|
container.style.marginTop = "8px";
|
|
|
|
// Add title
|
|
const title = document.createElement("div");
|
|
title.textContent = "👄 Mouth Movement Preview";
|
|
title.style.color = "#fff";
|
|
title.style.fontSize = "12px";
|
|
title.style.marginBottom = "8px";
|
|
title.style.fontWeight = "bold";
|
|
title.style.textAlign = "center";
|
|
|
|
// Add placeholder
|
|
const placeholder = document.createElement("div");
|
|
placeholder.textContent = "Preview will appear after analysis...";
|
|
placeholder.style.color = "#888";
|
|
placeholder.style.textAlign = "center";
|
|
placeholder.style.padding = "40px 20px";
|
|
placeholder.style.fontSize = "11px";
|
|
|
|
container.appendChild(title);
|
|
container.appendChild(placeholder);
|
|
|
|
// Create DOM widget using ComfyUI's addDOMWidget
|
|
this.videoWidget = this.addDOMWidget(
|
|
"video_preview",
|
|
"div",
|
|
container,
|
|
{
|
|
hideOnZoom: false,
|
|
getValue: () => container.innerHTML,
|
|
setValue: (v) => { /* Not used */ }
|
|
}
|
|
);
|
|
|
|
// Store references
|
|
this.videoContainer = container;
|
|
this.videoTitle = title;
|
|
this.videoPlaceholder = placeholder;
|
|
|
|
// Resize node to accommodate widget
|
|
this.setSize([Math.max(400, this.size[0]), this.size[1] + 250]);
|
|
};
|
|
|
|
// Method to hide video widget
|
|
nodeType.prototype.hideVideoWidget = function() {
|
|
if (this.videoWidget) {
|
|
this.removeWidget(this.videoWidget);
|
|
this.videoWidget = null;
|
|
this.videoContainer = null;
|
|
this.videoTitle = null;
|
|
this.videoPlaceholder = null;
|
|
|
|
// Resize node back
|
|
this.setSize([this.size[0], this.size[1] - 250]);
|
|
}
|
|
};
|
|
|
|
// Method to update video content
|
|
nodeType.prototype.updateVideoPreview = function(videoPath) {
|
|
if (!this.videoContainer || !videoPath) return;
|
|
|
|
if (videoPath === "Preview mode disabled" || videoPath.startsWith("Error:") || videoPath.startsWith("No preview")) {
|
|
this.videoTitle.textContent = "❌ " + videoPath;
|
|
this.videoTitle.style.color = "#ff6b6b";
|
|
this.videoContainer.removeChild(this.videoContainer.querySelector("video")); // Remove if exists
|
|
return;
|
|
}
|
|
|
|
// Remove existing video if any
|
|
const existingVideo = this.videoContainer.querySelector("video");
|
|
if (existingVideo) {
|
|
this.videoContainer.removeChild(existingVideo);
|
|
}
|
|
|
|
// Remove placeholder
|
|
if (this.videoPlaceholder && this.videoPlaceholder.parentNode) {
|
|
this.videoContainer.removeChild(this.videoPlaceholder);
|
|
}
|
|
|
|
// Create video element
|
|
const videoElement = document.createElement("video");
|
|
videoElement.controls = true;
|
|
videoElement.loop = true;
|
|
videoElement.muted = true;
|
|
videoElement.style.width = "100%";
|
|
videoElement.style.maxHeight = "200px";
|
|
videoElement.style.borderRadius = "4px";
|
|
videoElement.style.backgroundColor = "#000";
|
|
|
|
// Set video source
|
|
if (videoPath.includes('/output/') || videoPath.includes('\\output\\')) {
|
|
const filename = videoPath.split(/[/\\]/).pop();
|
|
videoElement.src = `/view?filename=${encodeURIComponent(filename)}&type=output`;
|
|
} else {
|
|
videoElement.src = videoPath;
|
|
}
|
|
|
|
// Handle load events
|
|
videoElement.onloadeddata = () => {
|
|
this.videoTitle.textContent = "✅ Mouth Movement Preview";
|
|
this.videoTitle.style.color = "#51cf66";
|
|
};
|
|
|
|
videoElement.onerror = () => {
|
|
this.videoTitle.textContent = "❌ Failed to load preview";
|
|
this.videoTitle.style.color = "#ff6b6b";
|
|
};
|
|
|
|
// Add video to container
|
|
this.videoContainer.appendChild(videoElement);
|
|
};
|
|
|
|
// Override onExecuted to update video when analysis completes
|
|
const onExecuted = nodeType.prototype.onExecuted;
|
|
nodeType.prototype.onExecuted = function(message) {
|
|
const result = onExecuted?.apply(this, arguments);
|
|
|
|
// Check if we have a video widget and preview data
|
|
if (this.videoWidget && message && message.ui && message.ui.preview_path) {
|
|
const previewPath = message.ui.preview_path[0];
|
|
this.updateVideoPreview(previewPath);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
// Cleanup when node is removed
|
|
const onRemoved = nodeType.prototype.onRemoved;
|
|
nodeType.prototype.onRemoved = function() {
|
|
this.hideVideoWidget();
|
|
return onRemoved?.apply(this, arguments);
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log("Mouth Movement Analyzer in-node video preview loaded"); |