import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; import { AudioAnalyzerInterface } from "./audio_analyzer_core.js"; // Simple execution tracking let audioAnalyzerNodes = new Map(); let analyzerRefreshTimers = []; // Hook into ComfyUI's global execution completion if (window.app && window.app.ui && window.app.ui.queue) { const originalProcessComplete = window.app.ui.queue.processComplete || function() {}; window.app.ui.queue.processComplete = function(prompt_id, results) { console.log('🎵 Queue execution completed:', prompt_id, results); // Check all audio analyzer nodes for fresh data for (const [nodeId, node] of audioAnalyzerNodes) { if (node.lastExecutionTime && Date.now() - node.lastExecutionTime < 5000) { console.log('🎵 Checking node', nodeId, 'for execution results'); // Try to get the result data if (results && results[nodeId] && results[nodeId].length > 1) { console.log('🎉 Found execution result for AudioAnalyzer node!'); const vizData = results[nodeId][1]; // visualization_data output if (node.audioAnalyzerInterface) { handleNodeExecution.call(node, [null, vizData]); } } } } return originalProcessComplete.call(this, prompt_id, results); }; } function refreshAnalyzerSidecars() { for (const node of audioAnalyzerNodes.values()) { if (!node.audioAnalyzerInterface || typeof node.tryWebFileData !== 'function') continue; node.tryWebFileData(); } } function scheduleAnalyzerSidecarRefresh() { analyzerRefreshTimers.forEach(timer => clearTimeout(timer)); analyzerRefreshTimers = [250, 1000, 2000].map(delay => setTimeout(refreshAnalyzerSidecars, delay)); } api.addEventListener("executed", () => { scheduleAnalyzerSidecarRefresh(); }); api.addEventListener("execution_success", () => { scheduleAnalyzerSidecarRefresh(); }); // Basic execution handler function handleNodeExecution(message) { if (!message || !Array.isArray(message) || message.length < 2) return; let vizData = message[1]; if (typeof vizData === 'string') { try { vizData = JSON.parse(vizData); } catch (e) { return; } } // Mark that we received data and call updateVisualization this.lastDataReceived = Date.now(); if (this.audioAnalyzerInterface) { this.audioAnalyzerInterface.updateVisualization(vizData); // Setup audio playback - Connected audio has priority over file path const audioFileWidget = this.widgets?.find(w => w.name === 'audio_file'); console.log(`🎵 Playback setup: audioFileWidget=${audioFileWidget?.value || 'empty'}, web_audio_filename=${vizData.web_audio_filename || 'not provided'}`); if (vizData.web_audio_filename) { // Connected audio with saved temporary file (has priority) console.log('🎵 Setting up connected audio playback from temp file (priority)'); this.setupAudioPlayback(vizData.web_audio_filename); } else if (audioFileWidget && audioFileWidget.value) { console.log('🎵 Setting up file-based audio playback'); this.setupAudioPlayback(audioFileWidget.value); } else { console.log('🎵 No audio source for playback setup'); } } } // ComfyUI Extension Registration app.registerExtension({ name: "chatterbox.audio_analyzer", async setup() { // Patch graphToPrompt to inject node ID right before prompt is sent to backend const onGraphToPrompt = app.graphToPrompt; app.graphToPrompt = async function() { // Get the original prompt const prompt = await onGraphToPrompt.apply(this, arguments); // Find our node in the prompt's output and inject the ID if (prompt.output) { for (const nodeId in prompt.output) { const nodeData = prompt.output[nodeId]; if (nodeData.class_type === "ChatterBoxAudioAnalyzer") { // Inject the node_id into the inputs object nodeData.inputs.node_id = nodeId; const graphNode = app.graph?.getNodeById?.(Number(nodeId)) || app.graph?._nodes_by_id?.[nodeId]; if (graphNode?.audioAnalyzerForceRun) { nodeData.inputs.force_run_id = String(graphNode.audioAnalyzerForceRun); graphNode.audioAnalyzerForceRun = ""; } } } } return prompt; }; // console.log("🌊 Audio Wave Analyzer: Patched graphToPrompt to inject node IDs."); // Debug: patch confirmation }, async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name === "ChatterBoxAudioAnalyzer") { const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; this.setSize([800, 600]); try { const analyzerInterface = new AudioAnalyzerInterface(this); this.audioAnalyzerInterface = analyzerInterface; audioAnalyzerNodes.set(String(this.id), this); // No need for hidden widget - node_id is now injected via graphToPrompt patch } catch (error) { console.error('❌ Audio Wave Analyzer: Failed to create interface:', error); } return result; }; // Add widget value persistence fix for ComfyUI reloads const onConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function(info) { const result = onConfigure ? onConfigure.apply(this, arguments) : undefined; // Ensure widget values are properly restored from saved data if (info && info.widgets_values && this.widgets) { setTimeout(() => { try { // Find export_format widget and ensure it has correct value const exportFormatWidget = this.widgets.find(w => w.name === 'export_format'); if (exportFormatWidget && info.widgets_values) { // Find the export_format value index in widgets_values const exportFormatIndex = this.widgets.findIndex(w => w.name === 'export_format'); if (exportFormatIndex >= 0 && exportFormatIndex < info.widgets_values.length) { const savedValue = info.widgets_values[exportFormatIndex]; if (savedValue && ['f5tts', 'json', 'csv'].includes(savedValue)) { exportFormatWidget.value = savedValue; // console.log(`🌊 Audio Wave Analyzer: Restored export_format to: ${savedValue}`); // Debug: format restoration } } } // Also ensure all other dropdowns are properly restored const dropdownWidgets = ['analysis_method', 'precision_level']; dropdownWidgets.forEach(widgetName => { const widget = this.widgets.find(w => w.name === widgetName); if (widget) { const widgetIndex = this.widgets.findIndex(w => w.name === widgetName); if (widgetIndex >= 0 && widgetIndex < info.widgets_values.length) { const savedValue = info.widgets_values[widgetIndex]; if (savedValue !== undefined) { widget.value = savedValue; } } } }); } catch (error) { console.warn('🌊 Audio Wave Analyzer: Widget restoration warning:', error); } }, 100); // Small delay to ensure widgets are fully initialized } return result; }; // Additional widget persistence backup - ensure values persist on workflow load const onSerialize = nodeType.prototype.onSerialize; nodeType.prototype.onSerialize = function(info) { if (onSerialize) { onSerialize.apply(this, arguments); } // Force widget values to be saved correctly if (this.widgets && this.widgets.length > 0) { info.widgets_values = this.widgets.map(w => w.value); } }; // Override onExecuted to capture data immediately const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function(message) { console.log(`🎵 onExecuted called for node ${this.id} with message:`, message); // Store the execution result for later access this.lastExecutionResult = message; this.lastExecutionTime = Date.now(); // Check if message contains array with our data if (Array.isArray(message) && message.length > 1) { if (typeof message[1] === 'string') { try { const parsedData = JSON.parse(message[1]); if (parsedData.duration) { // Immediately update visualization with real data if (this.audioAnalyzerInterface) { this.audioAnalyzerInterface.updateVisualization(parsedData); // Setup audio playback - Connected audio has priority const audioFileWidget = this.widgets?.find(w => w.name === 'audio_file'); if (parsedData.web_audio_filename) { // Connected audio with saved temporary file (has priority) console.log('🎵 Setting up connected audio playback from temp file in onExecuted (priority)'); this.setupAudioPlayback(parsedData.web_audio_filename); } else if (audioFileWidget && audioFileWidget.value) { this.setupAudioPlayback(audioFileWidget.value); } else { console.log('🎵 No audio source for playback in onExecuted'); } } return onExecuted ? onExecuted.apply(this, arguments) : undefined; } } catch (e) { // Failed to parse execution data } } } const result = onExecuted ? onExecuted.apply(this, arguments) : undefined; if (this.audioAnalyzerInterface) { handleNodeExecution.call(this, message); setTimeout(() => this.tryWebFileData?.(), 250); setTimeout(() => this.tryWebFileData?.(), 1000); } return result; }; // Hook into ComfyUI's execution flow const originalOnDrawBackground = nodeType.prototype.onDrawBackground; nodeType.prototype.onDrawBackground = function(ctx) { const result = originalOnDrawBackground ? originalOnDrawBackground.apply(this, arguments) : undefined; if (this.audioAnalyzerInterface) { const canvasChanged = this.audioAnalyzerInterface.resizeCanvas(false); if (canvasChanged) { requestAnimationFrame(() => this.audioAnalyzerInterface?.visualization?.redraw()); } } // Check if we just executed and have new output data if (this.lastExecutionTime && Date.now() - this.lastExecutionTime < 1000) { // Recently executed, try to get fresh output data if (this.outputs && this.outputs[1] && this.outputs[1].value) { console.log('🎉 Fresh execution detected, found output data!'); this.lastExecutionTime = 0; // Prevent repeat processing handleNodeExecution.call(this, [null, this.outputs[1].value]); } } return result; }; // Refresh functionality is now integrated into the Analyze button // Manual data refresh nodeType.prototype.manualDataRefresh = function() { if (!this.audioAnalyzerInterface) return; const audioFileWidget = this.widgets?.find(w => w.name === 'audio_file'); if (!audioFileWidget || !audioFileWidget.value) { if (this.audioAnalyzerInterface.ui.showMessage) { this.audioAnalyzerInterface.ui.showMessage('Please specify an audio file first'); } return; } if (this.audioAnalyzerInterface.ui.showMessage) { this.audioAnalyzerInterface.ui.showMessage('Analyzing audio file...'); } // Clear current audio setup to allow new setup this.currentAudioFile = null; // Mark execution time for tracking this.lastExecutionTime = Date.now(); // Queue execution and check for results window.app.queuePrompt(); // Check for results after execution setTimeout(() => this.checkForResults(), 3000); setTimeout(() => this.checkForResults(), 6000); }; // Check for results in node outputs nodeType.prototype.checkForResults = function() { if (this.lastDataReceived && Date.now() - this.lastDataReceived < 5000) { return; // Already received data } // Check for output data // Try multiple ways to get the output data let vizData = null; // Method 1: getOutputData if (this.getOutputData) { try { vizData = this.getOutputData(1); if (vizData) { // Found data via getOutputData } } catch (e) { // getOutputData failed } } // Method 2: Check outputs array if (!vizData && this.outputs && this.outputs[1]) { const output = this.outputs[1]; const possibleDataFields = ['value', 'data', '_data', 'content', 'result']; for (const field of possibleDataFields) { if (output[field] && typeof output[field] === 'string' && output[field].includes('duration')) { vizData = output[field]; break; } } } if (vizData && this.audioAnalyzerInterface) { // Check if this looks like real data (duration != 10.79) let parsedData = vizData; if (typeof vizData === 'string') { try { parsedData = JSON.parse(vizData); } catch (e) {} } // Process visualization data handleNodeExecution.call(this, [null, vizData]); } else { // Try web file approach this.tryWebFileData(); } }; // Simple temp file data fetch with fallback (restore original working approach) nodeType.prototype.tryWebFileData = function() { // Try multiple ComfyUI temp directory paths const possibleTempUrls = [ `/view?filename=audio_data_${this.id}.json&type=temp&t=${Date.now()}`, `/temp/audio_data_${this.id}.json?t=${Date.now()}`, `/api/view?filename=audio_data_${this.id}.json&type=temp&t=${Date.now()}` ]; let urlIndex = 0; const tryNextTempUrl = () => { if (urlIndex >= possibleTempUrls.length) { console.log('⚠️ All temp file URLs failed, checking persistent cache'); this.tryFetchTempData(); return; } const tempFileUrl = possibleTempUrls[urlIndex++]; // console.log(`🎵 Attempting to fetch temp file (${urlIndex}/${possibleTempUrls.length}): ${tempFileUrl}`); // Debug: fetch attempts fetch(tempFileUrl) .then(response => { if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); }) .then(vizData => { // console.log(`🎉 Successfully fetched temp file data! Duration: ${vizData.duration}`); // Debug: fetch success if (this.audioAnalyzerInterface) { this.audioAnalyzerInterface.updateVisualization(vizData); // Setup audio playback - Connected audio has priority const audioFileWidget = this.widgets?.find(w => w.name === 'audio_file'); if (vizData.web_audio_filename) { // Connected audio with saved temporary file (has priority) // console.log('🎵 Setting up connected audio playback from temp file fetch (priority)'); // Debug: audio priority this.setupAudioPlayback(vizData.web_audio_filename); } else if (audioFileWidget && audioFileWidget.value) { this.setupAudioPlayback(audioFileWidget.value); } else { console.log('🎵 No audio source for playback in temp file fetch'); } } }) .catch(error => { console.log(`⚠️ Temp file URL failed: ${tempFileUrl} - ${error.message}`); tryNextTempUrl(); // Try next URL }); }; tryNextTempUrl(); }; // Try to fetch cached visualization data nodeType.prototype.tryFetchTempData = function() { console.log('💾 Checking for cached visualization data...'); // Focus on output directory since it's web-accessible const possibleCacheUrls = [ `/output/audio_analyzer_cache_${this.id}.json`, `/view?filename=audio_analyzer_cache_${this.id}.json&type=output`, `/api/view?filename=audio_analyzer_cache_${this.id}.json&type=output`, // Backup patterns in case of different ComfyUI configurations `/temp/audio_analyzer_cache_${this.id}.json`, `/view?filename=audio_analyzer_cache_${this.id}.json&type=temp` ]; let urlIndex = 0; const tryNextUrl = () => { if (urlIndex >= possibleCacheUrls.length) { console.warn('💾 All cache URLs failed; no real analyzer data is available yet'); if (this.audioAnalyzerInterface) { this.audioAnalyzerInterface.ui.updateStatus('No cached analysis data'); this.audioAnalyzerInterface.showMessage('No cached analysis data found. Queue the workflow once to regenerate analyzer data.'); this.audioAnalyzerInterface.visualization.redraw(); } return; } const cacheUrl = `${possibleCacheUrls[urlIndex++]}?t=${Date.now()}`; console.log(`💾 Trying cache URL: ${cacheUrl}`); fetch(cacheUrl) .then(response => { if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); }) .then(data => { console.log('🎉 Cache file loaded successfully!'); console.log('🎉 Cache data keys:', Object.keys(data)); const vizData = data.visualization_data || data; if (vizData?.waveform?.samples?.length) { console.log('🎉 REAL DATA FOUND! Duration:', vizData.duration); if (this.audioAnalyzerInterface) { handleNodeExecution.call(this, [null, JSON.stringify(vizData)]); // Setup audio playback with the original file path if (data.file_path) { this.setupAudioPlayback(data.file_path); } return; } } else { throw new Error('No visualization_data in cache'); } }) .catch(error => { console.log(`💾 Cache URL failed: ${error.message}`); tryNextUrl(); }); }; tryNextUrl(); }; // Setup audio playback nodeType.prototype.setupAudioPlayback = function(filePath) { if (!this.audioAnalyzerInterface) return; // Prevent duplicate audio setup for the same file if (this.currentAudioFile === filePath) { // console.log(`🎵 Audio already set up for: ${filePath}`); // Debug: duplicate setup prevention return; } try { if (this.audioAnalyzerInterface.audioElement) { this.audioAnalyzerInterface.audioElement.pause(); } // Mark this file as the current audio this.currentAudioFile = filePath; // Extract filename and try multiple URL formats let fileName = filePath.split('\\').pop().split('/').pop(); let possibleUrls = [ `/view?filename=${fileName}&type=input`, `/api/view?filename=${fileName}&type=input`, `/input/${fileName}`, `./input/${fileName}`, filePath // Direct path as last resort ]; let urlIndex = 0; const tryNextUrl = () => { if (urlIndex >= possibleUrls.length) { if (this.audioAnalyzerInterface.ui.showMessage) { this.audioAnalyzerInterface.ui.showMessage('Audio playback not available - file not accessible via web'); } return; } let webUrl = possibleUrls[urlIndex++]; // console.log(`🎵 Trying audio URL: ${webUrl}`); // Debug: URL attempts this.audioAnalyzerInterface.audioElement = new Audio(); this.audioAnalyzerInterface.audioElement.src = webUrl; this.audioAnalyzerInterface.audioElement.preload = 'metadata'; this.audioAnalyzerInterface.audioElement.addEventListener('loadedmetadata', () => { // console.log('✅ Audio playback ready with URL:', webUrl); // Debug: playback ready }); this.audioAnalyzerInterface.audioElement.addEventListener('ended', () => { // console.log('🔊 Audio ended - stopping all animations (from interface)'); // Debug: audio end event if (this.audioAnalyzerInterface.core) { // Set isPlaying to false IMMEDIATELY to stop any pending animation frames this.audioAnalyzerInterface.core.isPlaying = false; // Stop animations explicitly this.audioAnalyzerInterface.core.stopPlayheadAnimation(); this.audioAnalyzerInterface.core.visualization.stopAnimation(); // Update UI after a small delay to ensure no race conditions setTimeout(() => { this.audioAnalyzerInterface.core.ui.playButton.textContent = '▶️ Play'; this.audioAnalyzerInterface.core.currentTime = 0; this.audioAnalyzerInterface.core.ui.updateTimeDisplay(); this.audioAnalyzerInterface.core.visualization.redraw(); console.log('🔊 All animations stopped from interface, isPlaying:', this.audioAnalyzerInterface.core.isPlaying); }, 10); } }); this.audioAnalyzerInterface.audioElement.addEventListener('error', () => { console.log(`❌ Failed URL: ${webUrl}`); tryNextUrl(); // Try next URL }); }; tryNextUrl(); } catch (error) { console.error('❌ Audio setup failed:', error); } }; } } });