diff --git a/nodes/web/__init__.py b/nodes/web/__init__.py
deleted file mode 100644
index ef41b57a..00000000
--- a/nodes/web/__init__.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""ComfyStream Web UI nodes"""
-import os
-import folder_paths
-
-# Define a simple Python class for the UI Preview node
-class ComfyStreamUIPreview:
- """
- This is a dummy Python class that corresponds to the JavaScript node.
- It's needed for ComfyUI to properly register and execute the node.
- The actual implementation is in the JavaScript file.
- """
- @classmethod
- def INPUT_TYPES(cls):
- return {
- "required": {},
- "optional": {}
- }
-
- RETURN_TYPES = ()
-
- FUNCTION = "execute"
- CATEGORY = "ComfyStream"
-
- def execute(self):
- # This function doesn't do anything as the real work is done in JavaScript
- # But we need to return something to satisfy the ComfyUI node execution system
- return ("UI Preview Node Executed",)
-
-# Register the node class
-NODE_CLASS_MAPPINGS = {
- "ComfyStreamUIPreview": ComfyStreamUIPreview
-}
-
-# Display names for the nodes
-NODE_DISPLAY_NAME_MAPPINGS = {
- "ComfyStreamUIPreview": "ComfyStream UI Preview"
-}
\ No newline at end of file
diff --git a/nodes/web/js/comfystream_ui_preview_node.js b/nodes/web/js/comfystream_ui_preview_node.js
deleted file mode 100644
index 50210313..00000000
--- a/nodes/web/js/comfystream_ui_preview_node.js
+++ /dev/null
@@ -1,142 +0,0 @@
-// ComfyStream Node - A custom JavaScript node for ComfyUI
-// This node displays the ComfyStream UI in an iframe
-
-const app = window.comfyAPI?.app?.app;
-
-// Register our extension
-app.registerExtension({
- name: "ComfyStream.Node",
-
- async beforeRegisterNodeDef(nodeType, nodeData, app) {
- if (nodeData.name === "ComfyStreamUIPreview") {
- // Set default size for the node type
- nodeType.size = [700, 800];
-
- // Make node resizable
- nodeType.resizable = true;
-
- // Save the original onNodeCreated method
- const onNodeCreated = nodeType.prototype.onNodeCreated;
-
- // Override the onNodeCreated method
- nodeType.prototype.onNodeCreated = function() {
- // Set node properties before calling original method
- this.title = "ComfyStream UI";
- this.color = "#4B9CD3"; // Blue color for the node
-
- // Set initial size
- this.size = [700, 800];
-
- // Make the node resizable
- this.resizable = true;
- this.flags.resizable = true;
-
- // Call the original onNodeCreated method if it exists
- const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
-
- // Create iframe element
- this.iframe = document.createElement("iframe");
- this.iframe.style.width = "100%";
- this.iframe.style.height = "100%";
- this.iframe.style.border = "none";
- this.iframe.style.borderRadius = "8px";
-
- // Function to load or refresh the iframe
- this.loadIframe = () => {
- fetch('/comfystream/extension_info')
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- // Use the current origin with the static route from extension_info
- this.iframe.src = `${window.location.origin}${data.static_route}/index.html`;
- } else {
- console.error("[ComfyStream] Error getting extension info:", data.error);
- // Fallback to hardcoded path
- const extensionName = "comfystream";
- this.iframe.src = `${window.location.origin}/extensions/${extensionName}/static/index.html`;
- }
- })
- .catch(error => {
- console.error("[ComfyStream] Error fetching extension info:", error);
- // Fallback to hardcoded path
- const extensionName = "comfystream";
- this.iframe.src = `${window.location.origin}/extensions/${extensionName}/static/index.html`;
- });
- };
-
- // Initial load of the iframe
- this.loadIframe();
-
- // Add the iframe as a DOM widget
- this.iframeWidget = this.addDOMWidget("iframe", "UI", this.iframe, {
- serialize: false,
- width: this.size[0],
- height: this.size[1] - 40
- });
-
- // Add a button to refresh the iframe
- this.addWidget("button", "Refresh UI", null, () => {
- console.log("[ComfyStream] Refreshing UI...");
- if (this.iframe) {
- // If iframe already has a src, we can just reload it
- if (this.iframe.src) {
- this.iframe.src = this.iframe.src;
- } else {
- // Otherwise load it using our function
- this.loadIframe();
- }
- }
- });
-
- // Add a button to launch the UI in a new tab
- this.addWidget("button", "Open in New Tab", null, () => {
- // Get extension info which contains the correct UI URL
- fetch('/comfystream/extension_info', {
- method: 'GET',
- headers: {
- 'Accept': 'application/json'
- }
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- // Use the current origin with the static route
- const uiUrl = `${window.location.origin}${data.static_route}/index.html`;
- // Open the URL in a new tab
- window.open(uiUrl, '_blank');
- }
- })
- .catch(error => {
- console.error("[ComfyStream] Error launching UI:", error);
- });
- });
-
-
- return result;
- };
-
- // Add a helper method to update iframe size
- nodeType.prototype.updateIframeSize = function() {
- if (this.iframeWidget) {
- this.iframeWidget.width = this.size[0];
- this.iframeWidget.height = this.size[1] - 40;
-
- // Also update the iframe element directly
- if (this.iframe) {
- this.iframe.style.width = this.size[0] + "px";
- this.iframe.style.height = (this.size[1] - 40) + "px";
- }
-
- // Force a canvas update
- this.setDirtyCanvas(true, true);
- }
- };
-
- // Override the onExecute method
- nodeType.prototype.onExecute = function() {
- // Trigger the output
- this.triggerSlot(0);
- };
- }
- }
-});
\ No newline at end of file
diff --git a/nodes/web/js/launcher.js b/nodes/web/js/launcher.js
deleted file mode 100644
index 7caf6e4f..00000000
--- a/nodes/web/js/launcher.js
+++ /dev/null
@@ -1,311 +0,0 @@
-// Wait for ComfyUI to be ready
-import { startStatusPolling, updateStatusIndicator, pollServerStatus } from './status-indicator.js';
-import { settingsManager, showSettingsModal } from './settings.js';
-
-const app = window.comfyAPI?.app?.app;
-
-// Store our indicator reference
-let statusIndicator = null;
-
-// Initialize the status indicator as soon as possible
-function initializeStatusIndicator() {
- if (!statusIndicator) {
- // Create the indicator with CSS variables for styling
- statusIndicator = startStatusPolling({
- size: '10px', // Slightly smaller to fit in menu
- showLabel: false, // No label needed since it's next to menu text
- runningLabel: '',
- stoppedLabel: '',
- startingLabel: '',
- stoppingLabel: '',
- minimalLabel: true
- });
-
- // Add a CSS class for styling
- statusIndicator.classList.add('comfystream-status-indicator');
-
- // Try to find the ComfyStream menu item label
- const findAndInjectIndicator = () => {
- // Don't use :contains() as it's not standard CSS - use the general approach instead
- const menuItems = document.querySelectorAll('.p-menubar-item-label');
- for (const item of menuItems) {
- if (item.textContent.includes('ComfyStream')) {
- // Insert the indicator after the menu label
- item.parentNode.insertBefore(statusIndicator, item.nextSibling);
- return true;
- }
- }
-
- return false;
- };
-
- // Try to inject immediately
- const injected = findAndInjectIndicator();
-
- // If not successful, set up a mutation observer to watch for menu changes
- if (!injected) {
- const observer = new MutationObserver((mutations) => {
- if (findAndInjectIndicator()) {
- observer.disconnect();
- }
- });
-
- observer.observe(document.body, {
- childList: true,
- subtree: true
- });
-
- // Also try again when the menu extension is registered
- document.addEventListener('comfy-extension-registered', (event) => {
- if (event.detail?.name === "ComfyStream.Menu") {
- setTimeout(findAndInjectIndicator, 100);
- }
- });
- }
-
- // Force an immediate status check
- pollServerStatus(true);
-
- // Create and inject CSS for positioning
- const style = document.createElement('style');
- style.textContent = `
- .comfystream-status-indicator {
- display: inline-block;
- margin-left: 2px;
- vertical-align: middle;
- position: relative;
- top: -1px;
- }
- `;
- document.head.appendChild(style);
- }
-}
-
-// Try to initialize immediately if app is available
-if (app) {
- initializeStatusIndicator();
-} else {
- // If app isn't ready yet, wait for DOMContentLoaded
- window.addEventListener('DOMContentLoaded', () => {
- initializeStatusIndicator();
- });
-}
-
-// Also initialize when the extension is registered
-document.addEventListener('comfy-extension-registered', (event) => {
- if (event.detail?.name === "ComfyStream.Menu") {
- initializeStatusIndicator();
- }
-});
-
-async function controlServer(action) {
- try {
- // Get settings from the settings manager
- const settings = settingsManager.getCurrentHostPort();
-
- // Set transitional state based on action
- if (action === 'start') {
- updateStatusIndicator({ starting: true, running: false, stopping: false });
- } else if (action === 'stop') {
- updateStatusIndicator({ stopping: true, running: true, starting: false });
- } else if (action === 'restart') {
- updateStatusIndicator({ stopping: true, running: true, starting: false });
- }
-
- const response = await fetch('/comfystream/control', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json'
- },
- body: JSON.stringify({
- action,
- settings
- })
- });
-
- if (!response.ok) {
- // Reset transitional states on error
- if (action === 'start') {
- updateStatusIndicator({ starting: false });
- } else if (action === 'stop' || action === 'restart') {
- updateStatusIndicator({ stopping: false });
- }
-
- const errorText = await response.text();
- console.error("[ComfyStream] Server returned error response:", response.status, errorText);
- try {
- const errorData = JSON.parse(errorText);
- throw new Error(errorData.error || `Server error: ${response.status}`);
- } catch (e) {
- throw new Error(`Server error: ${response.status} - ${errorText}`);
- }
- }
-
- const data = await response.json();
-
- // Update status indicator after control action
- if (data.status) {
- // Clear transitional states
- data.status.starting = false;
- data.status.stopping = false;
- updateStatusIndicator(data.status);
- }
-
- return data;
- } catch (error) {
- // Reset any transitional states on error
- if (action === 'start') {
- updateStatusIndicator({ starting: false });
- } else if (action === 'stop' || action === 'restart') {
- updateStatusIndicator({ stopping: false });
- }
-
- console.error('[ComfyStream] Error controlling server:', error);
- app.ui.dialog.show('Error', error.message || 'Failed to control ComfyStream server');
- throw error;
- }
-}
-
-async function openUI() {
- try {
- // Get extension info which contains the correct UI URL
- const response = await fetch('/comfystream/extension_info', {
- method: 'GET',
- headers: {
- 'Accept': 'application/json'
- }
- });
-
- if (!response.ok) {
- const errorText = await response.text();
- console.error("[ComfyStream] UI info returned error response:", response.status, errorText);
- try {
- const errorData = JSON.parse(errorText);
- throw new Error(errorData.error || `Server error: ${response.status}`);
- } catch (e) {
- throw new Error(`Server error: ${response.status} - ${errorText}`);
- }
- }
-
- const data = await response.json();
- if (!data.success) {
- throw new Error(data.error || 'Unknown error getting ComfyStream UI info');
- }
-
- // Use the current origin with the static route
- const uiUrl = `${window.location.origin}${data.static_route}/index.html`;
-
- // Open the URL in a new tab
- window.open(uiUrl, '_blank');
- } catch (error) {
- console.error('[ComfyStream] Error launching ComfyStream:', error);
- app.ui.dialog.show('Error', error.message || 'Failed to launch ComfyStream');
- throw error;
- }
-}
-
-// Function to open settings modal
-async function openSettings() {
- try {
- await showSettingsModal();
- } catch (error) {
- console.error("[ComfyStream] Error showing settings modal:", error);
- app.ui.dialog.show('Error', `Failed to show settings: ${error.message}`);
- }
-}
-
-// Register our extension
-const extension = {
- name: "ComfyStream.Menu",
-
- // Define commands that will be used by menu items
- commands: [
- {
- id: "ComfyStream.OpenUI",
- icon: "pi pi-external-link",
- label: "Open ComfyStream UI",
- function: openUI
- },
- {
- id: "ComfyStream.StartServer",
- icon: "pi pi-play",
- label: "Start ComfyStream Server",
- function: async () => {
- await controlServer('start');
- }
- },
- {
- id: "ComfyStream.StopServer",
- icon: "pi pi-stop",
- label: "Stop ComfyStream Server",
- function: async () => {
- await controlServer('stop');
- }
- },
- {
- id: "ComfyStream.RestartServer",
- icon: "pi pi-refresh",
- label: "Restart ComfyStream Server",
- function: async () => {
- await controlServer('restart');
- }
- },
- {
- id: "ComfyStream.Settings",
- icon: "pi pi-cog",
- label: "Server Settings",
- function: openSettings
- }
- ],
-
- // Define where these commands appear in the menu
- menuCommands: [
- {
- path: ["ComfyStream"],
- commands: [
- "ComfyStream.OpenUI",
- null, // Separator
- "ComfyStream.StartServer",
- "ComfyStream.StopServer",
- "ComfyStream.RestartServer",
- null, // Separator
- "ComfyStream.Settings"
- ]
- }
- ],
-
- // Setup function to handle menu registration based on settings
- setup() {
- let useNewMenu = "Enabled"; // Default to new menu system
-
- // Safely check if the settings store exists
- try {
- if (app.ui.settings && app.ui.settings.store && typeof app.ui.settings.store.get === 'function') {
- useNewMenu = app.ui.settings.store.get("Comfy.UseNewMenu") || "Enabled";
- }
- } catch (e) {
- console.log("[ComfyStream] Could not access settings store, defaulting to new menu system");
- }
-
- if (useNewMenu === "Disabled") {
- // Old menu system
- const menu = app.ui.menu;
- menu.addSeparator();
- const comfyStreamMenu = menu.addMenu("ComfyStream");
- comfyStreamMenu.addItem("Open UI", openUI, { icon: "pi pi-external-link" });
- comfyStreamMenu.addSeparator();
- comfyStreamMenu.addItem("Start Server", () => controlServer('start'), { icon: "pi pi-play" });
- comfyStreamMenu.addItem("Stop Server", () => controlServer('stop'), { icon: "pi pi-stop" });
- comfyStreamMenu.addItem("Restart Server", () => controlServer('restart'), { icon: "pi pi-refresh" });
- comfyStreamMenu.addSeparator();
- comfyStreamMenu.addItem("Server Settings", openSettings, { icon: "pi pi-cog" });
- }
- // New menu system is handled automatically by the menuCommands registration
-
- // Make sure the status indicator is initialized
- initializeStatusIndicator();
- }
-};
-
-app.registerExtension(extension);
\ No newline at end of file
diff --git a/nodes/web/js/settings.js b/nodes/web/js/settings.js
deleted file mode 100644
index 888917e8..00000000
--- a/nodes/web/js/settings.js
+++ /dev/null
@@ -1,860 +0,0 @@
-// ComfyStream Settings Manager
-console.log("[ComfyStream Settings] Initializing settings module");
-
-const DEFAULT_SETTINGS = {
- host: "0.0.0.0",
- port: 8889,
- configurations: [],
- selectedConfigIndex: -1 // -1 means no configuration is selected
-};
-
-class ComfyStreamSettings {
- constructor() {
- this.settings = DEFAULT_SETTINGS;
- this.loadSettings();
- }
-
- async loadSettings() {
- try {
- const response = await fetch('/comfystream/settings');
-
- if (!response.ok) {
- throw new Error(`Server returned ${response.status}: ${response.statusText}`);
- }
-
- this.settings = await response.json();
- return this.settings;
- } catch (error) {
- console.error("[ComfyStream Settings] Error loading settings from server:", error);
-
- // Try to load from localStorage as fallback
- try {
- const savedSettings = localStorage.getItem('comfystream_settings');
- if (savedSettings) {
- this.settings = { ...DEFAULT_SETTINGS, ...JSON.parse(savedSettings) };
-
- // Try to save these to the server
- this.saveSettings().catch(e => {
- console.error("[ComfyStream Settings] Failed to save localStorage settings to server:", e);
- });
- }
- } catch (localError) {
- console.error("[ComfyStream Settings] Error loading settings from localStorage:", localError);
- }
-
- return this.settings;
- }
- }
-
- async saveSettings() {
- try {
- const response = await fetch('/comfystream/settings', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(this.settings)
- });
-
- if (!response.ok) {
- throw new Error(`Server returned ${response.status}: ${response.statusText}`);
- }
-
- const result = await response.json();
-
- // Save to localStorage as fallback
- try {
- localStorage.setItem('comfystream_settings', JSON.stringify(this.settings));
- } catch (localError) {
- console.error("[ComfyStream Settings] Error saving to localStorage:", localError);
- }
-
- return true;
- } catch (error) {
- console.error("[ComfyStream Settings] Error saving settings to server:", error);
-
- // Try to save to localStorage as fallback
- try {
- localStorage.setItem('comfystream_settings', JSON.stringify(this.settings));
- } catch (localError) {
- console.error("[ComfyStream Settings] Error saving to localStorage:", localError);
- }
-
- return false;
- }
- }
-
- getSettings() {
- return this.settings;
- }
-
- async updateSettings(newSettings) {
- this.settings = { ...this.settings, ...newSettings };
- await this.saveSettings();
- return this.settings;
- }
-
- async addConfiguration(name, host, port) {
- try {
- const response = await fetch('/comfystream/settings/configuration', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- action: 'add',
- name,
- host,
- port
- })
- });
-
- if (!response.ok) {
- throw new Error(`Server returned ${response.status}: ${response.statusText}`);
- }
-
- const result = await response.json();
- if (result.success) {
- this.settings = result.settings;
- return { name, host, port };
- } else {
- throw new Error("Failed to add configuration");
- }
- } catch (error) {
- console.error("[ComfyStream Settings] Error adding configuration:", error);
-
- // Fallback to local operation
- const config = { name, host, port };
- this.settings.configurations.push(config);
- await this.saveSettings();
- return config;
- }
- }
-
- async removeConfiguration(index) {
- try {
- const response = await fetch('/comfystream/settings/configuration', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- action: 'remove',
- index
- })
- });
-
- if (!response.ok) {
- throw new Error(`Server returned ${response.status}: ${response.statusText}`);
- }
-
- const result = await response.json();
- if (result.success) {
- this.settings = result.settings;
- return true;
- } else {
- throw new Error("Failed to remove configuration");
- }
- } catch (error) {
- console.error("[ComfyStream Settings] Error removing configuration:", error);
-
- // Fallback to local operation
- if (index >= 0 && index < this.settings.configurations.length) {
- this.settings.configurations.splice(index, 1);
-
- // Update selectedConfigIndex if needed
- if (this.settings.selectedConfigIndex === index) {
- this.settings.selectedConfigIndex = -1;
- } else if (this.settings.selectedConfigIndex > index) {
- this.settings.selectedConfigIndex--;
- }
-
- await this.saveSettings();
- return true;
- }
- return false;
- }
- }
-
- async selectConfiguration(index) {
- try {
- const response = await fetch('/comfystream/settings/configuration', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- action: 'select',
- index
- })
- });
-
- if (!response.ok) {
- throw new Error(`Server returned ${response.status}: ${response.statusText}`);
- }
-
- const result = await response.json();
- if (result.success) {
- this.settings = result.settings;
- return true;
- } else {
- throw new Error("Failed to select configuration");
- }
- } catch (error) {
- console.error("[ComfyStream Settings] Error selecting configuration:", error);
-
- // Fallback to local operation
- if (index >= -1 && index < this.settings.configurations.length) {
- this.settings.selectedConfigIndex = index;
-
- // If a valid configuration is selected, update host and port
- if (index >= 0) {
- const config = this.settings.configurations[index];
- this.settings.host = config.host;
- this.settings.port = config.port;
- }
-
- await this.saveSettings();
- return true;
- }
- return false;
- }
- }
-
- getCurrentHostPort() {
- return {
- host: this.settings.host,
- port: this.settings.port
- };
- }
-
- getSelectedConfigName() {
- if (this.settings.selectedConfigIndex >= 0 &&
- this.settings.selectedConfigIndex < this.settings.configurations.length) {
- return this.settings.configurations[this.settings.selectedConfigIndex].name;
- }
- return null;
- }
-}
-
-// Create a single instance of the settings manager
-const settingsManager = new ComfyStreamSettings();
-
-// Show settings modal
-async function showSettingsModal() {
- // Ensure settings are loaded from server before showing modal
- await settingsManager.loadSettings();
-
- // Check if modal already exists and remove it
- const existingModal = document.getElementById("comfystream-settings-modal");
- if (existingModal) {
- existingModal.remove();
- }
-
- // Add CSS styles for the modal
- const styleId = "comfystream-settings-styles";
- if (!document.getElementById(styleId)) {
- const style = document.createElement("style");
- style.id = styleId;
- style.textContent = `
- #comfystream-settings-modal {
- position: fixed;
- z-index: 10000;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.5);
- display: flex;
- justify-content: center;
- align-items: center;
- backdrop-filter: blur(2px);
- animation: cs-fade-in 0.2s ease-out;
- }
-
- @keyframes cs-fade-in {
- from { opacity: 0; }
- to { opacity: 1; }
- }
-
- @keyframes cs-slide-in {
- from { transform: translateY(-20px); opacity: 0; }
- to { transform: translateY(0); opacity: 1; }
- }
-
- .cs-modal-content {
- background-color: var(--comfy-menu-bg, #202020);
- color: var(--comfy-text, #ffffff);
- border-radius: 8px;
- padding: 20px;
- box-shadow: 0 5px 25px rgba(0, 0, 0, 0.5);
- min-width: 450px;
- max-width: 80%;
- max-height: 80%;
- overflow: auto;
- position: relative;
- animation: cs-slide-in 0.2s ease-out;
- border: 1px solid var(--border-color, #444);
- }
-
- .cs-close-button {
- position: absolute;
- right: 10px;
- top: 10px;
- background: none;
- border: none;
- font-size: 20px;
- cursor: pointer;
- color: var(--comfy-text, #ffffff);
- width: 30px;
- height: 30px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- transition: background-color 0.2s;
- }
-
- .cs-close-button:hover {
- background-color: rgba(255, 255, 255, 0.1);
- }
-
- .cs-title {
- margin-top: 0;
- margin-bottom: 20px;
- border-bottom: 1px solid var(--border-color, #444);
- padding-bottom: 10px;
- font-size: 18px;
- font-weight: 500;
- }
-
- .cs-current-config {
- margin-bottom: 15px;
- padding: 10px;
- background-color: rgba(0, 0, 0, 0.2);
- border-radius: 6px;
- border: 1px solid var(--border-color, #444);
- display: flex;
- align-items: center;
- }
-
- .cs-current-config-label {
- font-weight: bold;
- margin-right: 5px;
- }
-
- .cs-input-group {
- margin-bottom: 15px;
- display: flex;
- align-items: center;
- }
-
- .cs-label {
- width: 80px;
- font-weight: 500;
- }
-
- .cs-input {
- flex: 1;
- padding: 8px 10px;
- background-color: var(--comfy-input-bg, #111);
- color: var(--comfy-text, #fff);
- border: 1px solid var(--border-color, #444);
- border-radius: 4px;
- transition: border-color 0.2s, box-shadow 0.2s;
- }
-
- .cs-input:focus {
- outline: none;
- border-color: var(--comfy-primary-color, #4b5563);
- box-shadow: 0 0 0 2px rgba(75, 85, 99, 0.3);
- }
-
- .cs-section {
- margin-top: 20px;
- }
-
- .cs-section-title {
- margin-bottom: 10px;
- font-size: 16px;
- font-weight: 500;
- }
-
- .cs-configs-list {
- max-height: 200px;
- overflow-y: auto;
- border: 1px solid var(--border-color, #444);
- border-radius: 6px;
- padding: 5px;
- margin-bottom: 10px;
- background-color: rgba(0, 0, 0, 0.1);
- }
-
- .cs-config-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 10px;
- border-bottom: 1px solid var(--border-color, #444);
- transition: background-color 0.2s;
- }
-
- .cs-config-item:last-child {
- border-bottom: none;
- }
-
- .cs-config-item:hover {
- background-color: rgba(255, 255, 255, 0.05);
- }
-
- .cs-config-item.selected {
- background-color: rgba(65, 105, 225, 0.2);
- border-radius: 4px;
- }
-
- .cs-config-info {
- font-weight: 500;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 250px;
- }
-
- .cs-buttons-group {
- display: flex;
- gap: 5px;
- }
-
- .cs-button {
- padding: 6px 12px;
- background-color: var(--comfy-menu-bg, #202020);
- color: var(--comfy-text, #fff);
- border: 1px solid var(--border-color, #444);
- border-radius: 4px;
- cursor: pointer;
- transition: background-color 0.2s, border-color 0.2s;
- font-size: 13px;
- }
-
- .cs-button:hover:not(:disabled) {
- background-color: rgba(255, 255, 255, 0.1);
- border-color: var(--comfy-primary-color, #4b5563);
- }
-
- .cs-button:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
-
- .cs-button.primary {
- background-color: var(--comfy-primary-color, #4b5563);
- border-color: var(--comfy-primary-color, #4b5563);
- }
-
- .cs-button.primary:hover {
- background-color: var(--comfy-primary-color-hover, #374151);
- }
-
- .cs-button.selected {
- background-color: rgba(65, 105, 225, 0.5);
- }
-
- .cs-button.delete:hover {
- background-color: rgba(220, 38, 38, 0.2);
- border-color: rgba(220, 38, 38, 0.5);
- }
-
- .cs-add-group {
- display: flex;
- gap: 10px;
- }
-
- .cs-footer {
- display: flex;
- justify-content: flex-end;
- margin-top: 20px;
- gap: 10px;
- }
-
- /* Scrollbar styling */
- .cs-configs-list::-webkit-scrollbar {
- width: 8px;
- }
-
- .cs-configs-list::-webkit-scrollbar-track {
- background: rgba(0, 0, 0, 0.1);
- border-radius: 4px;
- }
-
- .cs-configs-list::-webkit-scrollbar-thumb {
- background-color: rgba(255, 255, 255, 0.2);
- border-radius: 4px;
- }
-
- .cs-configs-list::-webkit-scrollbar-thumb:hover {
- background-color: rgba(255, 255, 255, 0.3);
- }
-
- /* Error state */
- .cs-input.error {
- border-color: #dc2626;
- animation: cs-shake 0.4s ease-in-out;
- }
-
- @keyframes cs-shake {
- 0%, 100% { transform: translateX(0); }
- 25% { transform: translateX(-5px); }
- 75% { transform: translateX(5px); }
- }
- `;
- document.head.appendChild(style);
- }
-
- // Create modal container
- const modal = document.createElement("div");
- modal.id = "comfystream-settings-modal";
-
- // Create modal content
- const modalContent = document.createElement("div");
- modalContent.className = "cs-modal-content";
-
- // Create close button
- const closeButton = document.createElement("button");
- closeButton.textContent = "×";
- closeButton.className = "cs-close-button";
- closeButton.onclick = () => {
- modal.remove();
- };
-
- // Create title
- const title = document.createElement("h3");
- title.textContent = "ComfyStream Server Settings";
- title.className = "cs-title";
-
- // Create settings form
- const form = document.createElement("div");
-
- // Current configuration indicator
- const currentConfigDiv = document.createElement("div");
- currentConfigDiv.className = "cs-current-config";
-
- const currentConfigLabel = document.createElement("span");
- currentConfigLabel.textContent = "Active Configuration: ";
- currentConfigLabel.className = "cs-current-config-label";
-
- const currentConfigName = document.createElement("span");
- const selectedName = settingsManager.getSelectedConfigName();
- currentConfigName.textContent = selectedName || "Custom (unsaved)";
- currentConfigName.style.fontStyle = selectedName ? "normal" : "italic";
-
- currentConfigDiv.appendChild(currentConfigLabel);
- currentConfigDiv.appendChild(currentConfigName);
-
- // Host setting
- const hostGroup = document.createElement("div");
- hostGroup.className = "cs-input-group";
-
- const hostLabel = document.createElement("label");
- hostLabel.textContent = "Host:";
- hostLabel.className = "cs-label";
-
- const hostInput = document.createElement("input");
- hostInput.id = "comfystream-host";
- hostInput.type = "text";
- hostInput.value = settingsManager.settings.host;
- hostInput.className = "cs-input";
-
- hostGroup.appendChild(hostLabel);
- hostGroup.appendChild(hostInput);
-
- // Port setting
- const portGroup = document.createElement("div");
- portGroup.className = "cs-input-group";
-
- const portLabel = document.createElement("label");
- portLabel.textContent = "Port:";
- portLabel.className = "cs-label";
-
- const portInput = document.createElement("input");
- portInput.id = "comfystream-port";
- portInput.type = "number";
- portInput.min = "1024";
- portInput.max = "65535";
- portInput.value = settingsManager.settings.port;
- portInput.className = "cs-input";
-
- portGroup.appendChild(portLabel);
- portGroup.appendChild(portInput);
-
- // Configurations section
- const configsSection = document.createElement("div");
- configsSection.className = "cs-section";
-
- const configsTitle = document.createElement("h4");
- configsTitle.textContent = "Saved Configurations";
- configsTitle.className = "cs-section-title";
-
- const configsList = document.createElement("div");
- configsList.id = "comfystream-configs-list";
- configsList.className = "cs-configs-list";
-
- const configsAddGroup = document.createElement("div");
- configsAddGroup.className = "cs-add-group";
-
- const configNameInput = document.createElement("input");
- configNameInput.id = "comfystream-config-name";
- configNameInput.type = "text";
- configNameInput.placeholder = "Configuration name";
- configNameInput.className = "cs-input";
-
- const addConfigButton = document.createElement("button");
- addConfigButton.id = "comfystream-add-config";
- addConfigButton.textContent = "Save Current";
- addConfigButton.className = "cs-button primary";
-
- configsAddGroup.appendChild(configNameInput);
- configsAddGroup.appendChild(addConfigButton);
-
- configsSection.appendChild(configsTitle);
- configsSection.appendChild(configsList);
- configsSection.appendChild(configsAddGroup);
-
- // Footer with buttons
- const footer = document.createElement("div");
- footer.className = "cs-footer";
-
- const cancelButton = document.createElement("button");
- cancelButton.textContent = "Cancel";
- cancelButton.className = "cs-button";
- cancelButton.onclick = () => {
- modal.remove();
- };
-
- const saveButton = document.createElement("button");
- saveButton.textContent = "Save";
- saveButton.className = "cs-button primary";
- saveButton.onclick = async () => {
- const host = hostInput.value;
- const port = parseInt(portInput.value);
-
- // If the current values match a saved configuration, select it
- let matchingConfigIndex = -1;
- settingsManager.settings.configurations.forEach((config, index) => {
- if (config.host === host && config.port === port) {
- matchingConfigIndex = index;
- }
- });
-
- if (matchingConfigIndex >= 0) {
- await settingsManager.selectConfiguration(matchingConfigIndex);
- } else {
- // No matching configuration, just update the settings
- await settingsManager.updateSettings({
- host,
- port,
- selectedConfigIndex: -1 // Reset selected config since we're using custom values
- });
- }
-
- modal.remove();
- };
-
- footer.appendChild(cancelButton);
- footer.appendChild(saveButton);
-
- // Assemble the modal
- form.appendChild(currentConfigDiv);
- form.appendChild(hostGroup);
- form.appendChild(portGroup);
- form.appendChild(configsSection);
-
- modalContent.appendChild(closeButton);
- modalContent.appendChild(title);
- modalContent.appendChild(form);
- modalContent.appendChild(footer);
-
- modal.appendChild(modalContent);
-
- // Add to document
- document.body.appendChild(modal);
-
- // Update configurations list
- async function updateConfigsList() {
- configsList.innerHTML = "";
-
- if (settingsManager.settings.configurations.length === 0) {
- const emptyMessage = document.createElement("div");
- emptyMessage.textContent = "No saved configurations";
- emptyMessage.style.padding = "10px";
- emptyMessage.style.color = "var(--comfy-text-muted, #aaa)";
- emptyMessage.style.fontStyle = "italic";
- emptyMessage.style.textAlign = "center";
- configsList.appendChild(emptyMessage);
- return;
- }
-
- settingsManager.settings.configurations.forEach((config, index) => {
- const configItem = document.createElement("div");
- configItem.className = `cs-config-item ${index === settingsManager.settings.selectedConfigIndex ? 'selected' : ''}`;
-
- const configInfo = document.createElement("span");
- configInfo.className = "cs-config-info";
- configInfo.textContent = `${config.name} (${config.host}:${config.port})`;
-
- const buttonsGroup = document.createElement("div");
- buttonsGroup.className = "cs-buttons-group";
-
- const selectButton = document.createElement("button");
- selectButton.textContent = index === settingsManager.settings.selectedConfigIndex ? "Selected" : "Select";
- selectButton.className = `cs-button comfystream-config-select ${index === settingsManager.settings.selectedConfigIndex ? 'selected' : ''}`;
- selectButton.dataset.index = index;
- selectButton.disabled = index === settingsManager.settings.selectedConfigIndex;
-
- const loadButton = document.createElement("button");
- loadButton.textContent = "Load";
- loadButton.className = "cs-button comfystream-config-load";
- loadButton.dataset.index = index;
-
- const deleteButton = document.createElement("button");
- deleteButton.textContent = "Delete";
- deleteButton.className = "cs-button delete comfystream-config-delete";
- deleteButton.dataset.index = index;
-
- buttonsGroup.appendChild(selectButton);
- buttonsGroup.appendChild(loadButton);
- buttonsGroup.appendChild(deleteButton);
-
- configItem.appendChild(configInfo);
- configItem.appendChild(buttonsGroup);
-
- configsList.appendChild(configItem);
- });
-
- // Add event listeners
- document.querySelectorAll(".comfystream-config-select").forEach(button => {
- if (!button.disabled) {
- button.addEventListener("click", async (e) => {
- const index = parseInt(e.target.dataset.index);
-
- // Select the configuration and update UI
- await settingsManager.selectConfiguration(index);
-
- // Update the current config display
- const selectedName = settingsManager.getSelectedConfigName();
- currentConfigName.textContent = selectedName || "Custom (unsaved)";
- currentConfigName.style.fontStyle = selectedName ? "normal" : "italic";
-
- // Update the input fields
- const config = settingsManager.settings.configurations[index];
- hostInput.value = config.host;
- portInput.value = config.port;
-
- // Refresh the list to update highlighting
- await updateConfigsList();
- });
- }
- });
-
- document.querySelectorAll(".comfystream-config-load").forEach(button => {
- button.addEventListener("click", (e) => {
- const index = parseInt(e.target.dataset.index);
- const config = settingsManager.settings.configurations[index];
- hostInput.value = config.host;
- portInput.value = config.port;
- });
- });
-
- document.querySelectorAll(".comfystream-config-delete").forEach(button => {
- button.addEventListener("click", async (e) => {
- const index = parseInt(e.target.dataset.index);
- await settingsManager.removeConfiguration(index);
-
- // Update the current config display if needed
- const selectedName = settingsManager.getSelectedConfigName();
- currentConfigName.textContent = selectedName || "Custom (unsaved)";
- currentConfigName.style.fontStyle = selectedName ? "normal" : "italic";
-
- await updateConfigsList();
- });
- });
- }
-
- // Add event listener for the add config button
- addConfigButton.addEventListener("click", async () => {
- const name = configNameInput.value.trim();
- const host = hostInput.value;
- const port = parseInt(portInput.value);
-
- if (name) {
- // Add the configuration
- await settingsManager.addConfiguration(name, host, port);
-
- // Select the newly added configuration
- const newIndex = settingsManager.settings.configurations.length - 1;
- await settingsManager.selectConfiguration(newIndex);
-
- // Update the current config display
- currentConfigName.textContent = name;
- currentConfigName.style.fontStyle = "normal";
-
- // Update the list
- await updateConfigsList();
- configNameInput.value = "";
- } else {
- console.warn("[ComfyStream Settings] Cannot add config without a name");
- // Show a brief error message
- configNameInput.classList.add("error");
- setTimeout(() => {
- configNameInput.classList.remove("error");
- }, 2000);
- }
- });
-
- // Add event listeners for input changes
- hostInput.addEventListener("input", () => {
- // When user changes the input, check if it still matches the selected config
- const selectedIndex = settingsManager.settings.selectedConfigIndex;
- if (selectedIndex >= 0) {
- const config = settingsManager.settings.configurations[selectedIndex];
- if (hostInput.value !== config.host || parseInt(portInput.value) !== config.port) {
- // Values no longer match the selected config
- currentConfigName.textContent = "Custom (unsaved)";
- currentConfigName.style.fontStyle = "italic";
- } else {
- // Values match the selected config again
- currentConfigName.textContent = config.name;
- currentConfigName.style.fontStyle = "normal";
- }
- }
- });
-
- portInput.addEventListener("input", () => {
- // Same check for port changes
- const selectedIndex = settingsManager.settings.selectedConfigIndex;
- if (selectedIndex >= 0) {
- const config = settingsManager.settings.configurations[selectedIndex];
- if (hostInput.value !== config.host || parseInt(portInput.value) !== config.port) {
- currentConfigName.textContent = "Custom (unsaved)";
- currentConfigName.style.fontStyle = "italic";
- } else {
- currentConfigName.textContent = config.name;
- currentConfigName.style.fontStyle = "normal";
- }
- }
- });
-
- // Initial update of configurations list
- await updateConfigsList();
-
- // Focus the host input
- hostInput.focus();
-}
-
-// Export for use in other modules
-export { settingsManager, showSettingsModal };
-
-// Also keep the global for backward compatibility
-window.comfyStreamSettings = {
- settingsManager,
- showSettingsModal
-};
\ No newline at end of file
diff --git a/nodes/web/js/status-indicator.js b/nodes/web/js/status-indicator.js
deleted file mode 100644
index a38fc47f..00000000
--- a/nodes/web/js/status-indicator.js
+++ /dev/null
@@ -1,454 +0,0 @@
-// ComfyStream Status Indicator
-// This module provides a web component for the ComfyStream server status indicator
-
-// Define the custom element
-class ComfyStreamStatusIndicator extends HTMLElement {
- constructor() {
- super();
-
- // Create a shadow DOM for encapsulation
- this.attachShadow({ mode: 'open' });
-
- // Initialize state
- this.state = {
- running: false,
- starting: false,
- stopping: false,
- host: null,
- port: null,
- polling: false,
- pollInterval: null
- };
-
- // Initial render
- this.render();
-
- // Start polling when created
- this.startPolling();
- }
-
- // Lifecycle callbacks
- connectedCallback() {
- // Listen for theme changes
- this.observeThemeChanges();
-
- // Make sure we're visible
- this.style.display = 'inline-block';
- }
-
- disconnectedCallback() {
- this.stopPolling();
-
- // Clean up any observers
- if (this.themeObserver) {
- this.themeObserver.disconnect();
- }
- }
-
- // Observe theme changes to adapt colors
- observeThemeChanges() {
- // Use MutationObserver to detect theme changes (light/dark mode)
- this.themeObserver = new MutationObserver((mutations) => {
- // Check for class changes on body or html that might indicate theme changes
- for (const mutation of mutations) {
- if (mutation.type === 'attributes' &&
- (mutation.attributeName === 'class' || mutation.attributeName === 'data-theme')) {
- this.updateThemeColors();
- }
- }
- });
-
- // Start observing
- this.themeObserver.observe(document.documentElement, { attributes: true });
- this.themeObserver.observe(document.body, { attributes: true });
-
- // Initial theme check
- this.updateThemeColors();
- }
-
- // Update colors based on current theme
- updateThemeColors() {
- // Check if we're in dark mode
- const isDarkMode = document.documentElement.classList.contains('dark') ||
- document.body.classList.contains('dark') ||
- window.matchMedia('(prefers-color-scheme: dark)').matches;
-
- // Update CSS variables based on theme
- if (isDarkMode) {
- this.style.setProperty('--indicator-border-color', '#888');
- } else {
- this.style.setProperty('--indicator-border-color', '#444');
- }
- }
-
- // Get label text based on attributes or default
- getLabelText() {
- // Check if custom labels are provided via attributes
- const runningLabel = this.getAttribute('running-label') || '';
- const stoppedLabel = this.getAttribute('stopped-label') || '';
- const startingLabel = this.getAttribute('starting-label') || '';
- const stoppingLabel = this.getAttribute('stopping-label') || '';
-
- // Use custom labels if provided, otherwise use minimal default
- if (this.state.starting) {
- return startingLabel || (this.hasAttribute('minimal-label') ? 'Starting' : '');
- } else if (this.state.stopping) {
- return stoppingLabel || (this.hasAttribute('minimal-label') ? 'Stopping' : '');
- } else if (this.state.running) {
- return runningLabel || (this.hasAttribute('minimal-label') ? 'Running' : '');
- } else {
- return stoppedLabel || (this.hasAttribute('minimal-label') ? 'Stopped' : '');
- }
- }
-
- // Render the component
- render() {
- // Get label text
- const labelText = this.getLabelText();
-
- // Determine indicator color based on state
- let indicatorColor, indicatorShadowColor;
-
- if (this.state.starting) {
- indicatorColor = 'var(--indicator-color-starting, #FFA500)'; // Orange for starting
- indicatorShadowColor = 'var(--indicator-shadow-color-starting, rgba(255, 165, 0, 0.6))';
- } else if (this.state.stopping) {
- indicatorColor = 'var(--indicator-color-stopping, #FFC107)'; // Amber for stopping
- indicatorShadowColor = 'var(--indicator-shadow-color-stopping, rgba(255, 193, 7, 0.6))';
- } else if (this.state.running) {
- indicatorColor = 'var(--indicator-color-running)';
- indicatorShadowColor = 'var(--indicator-shadow-color-running)';
- } else {
- indicatorColor = 'var(--indicator-color-stopped)';
- indicatorShadowColor = 'var(--indicator-shadow-color-stopped)';
- }
-
- this.shadowRoot.innerHTML = `
-
-
- `;
- }
-
- // Update the status
- updateStatus(status) {
- const wasRunning = this.state.running;
- const willBeRunning = status.running;
-
- // Update state
- this.state = { ...this.state, ...status };
-
- // Re-render
- this.render();
-
- // Add pulse animation if status changed
- if (wasRunning !== willBeRunning) {
- const indicator = this.shadowRoot.querySelector('.indicator');
- indicator.classList.add('pulse');
- setTimeout(() => {
- indicator.classList.remove('pulse');
- }, 300);
- }
- }
-
- // Get the title text
- getTitle() {
- if (this.state.starting) {
- return 'ComfyStream Server: Starting...';
- } else if (this.state.stopping) {
- return 'ComfyStream Server: Stopping...';
- } else if (this.state.running) {
- return `ComfyStream Server: Running on ${this.state.host || 'localhost'}:${this.state.port}`;
- } else {
- return 'ComfyStream Server: Stopped';
- }
- }
-
- // Start polling
- startPolling() {
- // Poll immediately
- this.pollStatus(true);
-
- // Set up interval polling (every 5 seconds)
- if (!this.state.pollInterval) {
- this.state.pollInterval = setInterval(() => this.pollStatus(), 5000);
- }
- }
-
- // Stop polling
- stopPolling() {
- if (this.state.pollInterval) {
- clearInterval(this.state.pollInterval);
- this.state.pollInterval = null;
- }
- }
-
- // Poll for status
- async pollStatus(immediate = false) {
- // Prevent multiple polling processes
- if (this.state.polling && !immediate) return;
-
- this.state.polling = true;
-
- try {
- const response = await fetch('/comfystream/control', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json'
- },
- body: JSON.stringify({ action: 'status' })
- });
-
- if (response.ok) {
- const data = await response.json();
- const wasRunning = this.state.running;
- const willBeRunning = data.status.running;
- if (wasRunning !== willBeRunning) {
- console.log('[ComfyStream] Server status changed:', willBeRunning ? 'Running' : 'Stopped');
- }
-
- this.updateStatus(data.status);
-
- // Dispatch an event that other components can listen for
- this.dispatchEvent(new CustomEvent('status-changed', {
- detail: data.status,
- bubbles: true,
- composed: true
- }));
- } else {
- if (this.state.running) {
- console.log('[ComfyStream] Server status error, assuming stopped');
- }
- this.updateStatus({ running: false });
- }
- } catch (error) {
- if (this.state.running) {
- console.log('[ComfyStream] Server connection error:', error.message);
- }
- this.updateStatus({ running: false });
- } finally {
- this.state.polling = false;
- }
- }
-
- // Static get observedAttributes
- static get observedAttributes() {
- return ['show-label', 'running-label', 'stopped-label', 'starting-label', 'stopping-label', 'minimal-label'];
- }
-
- // Attribute changed callback
- attributeChangedCallback(name, oldValue, newValue) {
- // Re-render when any of our observed attributes change
- this.render();
- }
-}
-
-// Register the custom element
-customElements.define('comfystream-status-indicator', ComfyStreamStatusIndicator);
-
-// Global registry for indicators
-const indicatorRegistry = {
- indicators: [],
- mainIndicator: null
-};
-
-/**
- * Create a status indicator element
- * @param {Object} options - Configuration options (CSS properties)
- * @returns {HTMLElement} The created indicator element
- */
-function createStatusIndicator(options = {}) {
- // Create the indicator element
- const indicator = document.createElement('comfystream-status-indicator');
-
- // Apply any custom CSS properties and attributes
- Object.entries(options).forEach(([key, value]) => {
- if (key === 'showLabel' && value) {
- indicator.setAttribute('show-label', '');
- } else if (key === 'runningLabel' && value) {
- indicator.setAttribute('running-label', value);
- } else if (key === 'stoppedLabel' && value) {
- indicator.setAttribute('stopped-label', value);
- } else if (key === 'startingLabel' && value) {
- indicator.setAttribute('starting-label', value);
- } else if (key === 'stoppingLabel' && value) {
- indicator.setAttribute('stopping-label', value);
- } else if (key === 'minimalLabel' && value) {
- indicator.setAttribute('minimal-label', '');
- } else if (key.startsWith('--')) {
- // CSS variables that start with --
- indicator.style.setProperty(key, value);
- } else {
- // Regular CSS variables
- indicator.style.setProperty(`--indicator-${key}`, value);
- }
- });
-
- // Add to registry
- indicatorRegistry.indicators.push(indicator);
-
- return indicator;
-}
-
-/**
- * Start status polling and create a default indicator
- * @param {Object} options - Options for the indicator
- * @param {HTMLElement} container - Container to append the indicator to
- * @returns {HTMLElement} The created indicator element
- */
-function startStatusPolling(options = {}, container = document.body) {
- // If we already have a main indicator, return it
- if (indicatorRegistry.mainIndicator) {
- return indicatorRegistry.mainIndicator;
- }
-
- // Create the indicator
- const indicator = createStatusIndicator(options);
-
- // Add to container
- container.appendChild(indicator);
-
- // Store as main indicator
- indicatorRegistry.mainIndicator = indicator;
-
- return indicator;
-}
-
-/**
- * Update all status indicators
- * @param {Object} status - Status information
- */
-function updateStatusIndicator(status) {
- // Dispatch a custom event that all indicators will listen for
- document.dispatchEvent(new CustomEvent('comfystream-status-update', {
- detail: status
- }));
-
- // Also update each indicator directly
- indicatorRegistry.indicators.forEach(indicator => {
- if (indicator instanceof ComfyStreamStatusIndicator) {
- indicator.updateStatus(status);
- }
- });
-}
-
-/**
- * Poll server status
- * @param {boolean} immediate - Whether to poll immediately
- */
-function pollServerStatus(immediate = false) {
- // If we have a main indicator, use its polling method
- if (indicatorRegistry.mainIndicator) {
- indicatorRegistry.mainIndicator.pollStatus(immediate);
- }
-}
-
-/**
- * Remove a status indicator
- * @param {HTMLElement} indicator - The indicator to remove
- */
-function removeStatusIndicator(indicator) {
- const index = indicatorRegistry.indicators.indexOf(indicator);
- if (index !== -1) {
- indicatorRegistry.indicators.splice(index, 1);
-
- // If it's the main indicator, clear that reference
- if (indicator === indicatorRegistry.mainIndicator) {
- indicatorRegistry.mainIndicator = null;
- }
-
- // Remove from DOM
- indicator.remove();
- }
-}
-
-/**
- * Stop status polling and remove all indicators
- */
-function stopStatusPolling() {
- // Remove all indicators
- indicatorRegistry.indicators.forEach(indicator => {
- indicator.remove();
- });
-
- indicatorRegistry.indicators = [];
- indicatorRegistry.mainIndicator = null;
-}
-
-// Export the functions that will be used by other modules
-export {
- startStatusPolling,
- createStatusIndicator,
- updateStatusIndicator,
- pollServerStatus,
- removeStatusIndicator,
- stopStatusPolling
-};
\ No newline at end of file
diff --git a/nodes/web/static/.gitkeep b/nodes/web/static/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/pyproject.toml b/pyproject.toml
index 9ba244e2..0d9a147d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "comfystream"
description = "Build Live AI Video with ComfyUI"
-version = "0.0.5"
+version = "0.0.4"
license = { file = "LICENSE" }
dependencies = [
"asyncio",
@@ -15,11 +15,10 @@ dependencies = [
"toml",
"twilio",
"prometheus_client",
- "librosa"
]
[project.optional-dependencies]
-dev = ["pytest", "pytest-cov"]
+dev = ["pytest"]
[project.urls]
repository = "https://github.com/yondonfu/comfystream"
@@ -34,4 +33,4 @@ package-dir = {"" = "src"}
packages = {find = {where = ["src", "nodes"]}}
[tool.setuptools.dynamic]
-dependencies = {file = ["requirements.txt"]}
\ No newline at end of file
+dependencies = {file = ["requirements.txt"]}
diff --git a/requirements.txt b/requirements.txt
index f94f3345..4a7e68ad 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,4 +5,3 @@ aiohttp
toml
twilio
prometheus_client
-librosa
diff --git a/server/app.py b/server/app.py
index e1e9150f..dcf18c66 100644
--- a/server/app.py
+++ b/server/app.py
@@ -4,6 +4,7 @@
import logging
import os
import sys
+import time
import torch
@@ -24,9 +25,8 @@
from aiortc.rtcrtpsender import RTCRtpSender
from pipeline import Pipeline
from twilio.rest import Client
-from utils import patch_loop_datagram, add_prefix_to_app_routes, FPSMeter
-from metrics import MetricsManager, StreamStatsManager
-import time
+from utils import patch_loop_datagram, add_prefix_to_app_routes
+from metrics import MetricsManager, StreamStatsManager, StreamStats
logger = logging.getLogger(__name__)
logging.getLogger("aiortc.rtcrtpsender").setLevel(logging.WARNING)
@@ -38,7 +38,7 @@
class VideoStreamTrack(MediaStreamTrack):
- """video stream track that processes video frames using a pipeline.
+ """Video stream track that processes video frames using a pipeline.
Attributes:
kind (str): The kind of media, which is "video" for this class.
@@ -54,17 +54,21 @@ def __init__(self, track: MediaStreamTrack, pipeline: Pipeline):
Args:
track: The underlying media stream track.
pipeline: The processing pipeline to apply to each video frame.
+ stats: The stream statistics.
"""
+ self._start_time = time.monotonic()
super().__init__()
self.track = track
self.pipeline = pipeline
- self.fps_meter = FPSMeter(
- metrics_manager=app["metrics_manager"], track_id=track.id
+ self.stats = StreamStats(
+ track_id=track.id,
+ metrics_manager=app.get("metrics_manager", None),
)
- self.running = True
- self.collect_task = asyncio.create_task(self.collect_frames())
-
- # Add cleanup when track ends
+ self._running = True
+
+ asyncio.create_task(self.collect_frames())
+
+ # Add cleanup when track ends.
@track.on("ended")
async def on_ended():
logger.info("Source video track ended, stopping collection")
@@ -75,7 +79,7 @@ async def collect_frames(self):
the processing pipeline. Stops when track ends or connection closes.
"""
try:
- while self.running:
+ while self._running:
try:
frame = await self.track.recv()
await self.pipeline.put_video_frame(frame)
@@ -87,9 +91,9 @@ async def collect_frames(self):
logger.info("Media stream ended")
else:
logger.error(f"Error collecting video frames: {str(e)}")
- self.running = False
+ self._running = False
break
-
+
# Perform cleanup outside the exception handler
logger.info("Video frame collection stopped")
except asyncio.CancelledError:
@@ -100,28 +104,54 @@ async def collect_frames(self):
await self.pipeline.cleanup()
async def recv(self):
- """Receive a processed video frame from the pipeline, increment the frame
- count for FPS calculation and return the processed frame to the client.
+ """Receive a processed video frame from the pipeline and return it to the
+ client, while collecting statistics about the stream.
"""
+ if self.stats.startup_time is None:
+ self.stats.start_timestamp = time.monotonic()
+ self.stats.startup_time = self.stats.start_timestamp - self._start_time
+ self.stats.pipeline.warmup_time = self.pipeline.stats.warmup_time
+
processed_frame = await self.pipeline.get_processed_video_frame()
# Increment the frame count to calculate FPS.
- await self.fps_meter.increment_frame_count()
+ await self.stats.fps_meter.increment_frame_count()
return processed_frame
class AudioStreamTrack(MediaStreamTrack):
+ """Audio stream track that processes audio frames using a pipeline.
+
+ Attributes:
+ kind (str): The kind of media, which is "audio" for this class.
+ track (MediaStreamTrack): The underlying media stream track.
+ pipeline (Pipeline): The processing pipeline to apply to each audio frame.
+ """
+
kind = "audio"
def __init__(self, track: MediaStreamTrack, pipeline):
+ """Initialize the AudioStreamTrack.
+
+ Args:
+ track: The underlying media stream track.
+ pipeline: The processing pipeline to apply to each audio frame.
+ stats: The stream statistics.
+ """
+ self._start_time = time.monotonic()
super().__init__()
self.track = track
self.pipeline = pipeline
- self.running = True
- self.collect_task = asyncio.create_task(self.collect_frames())
-
- # Add cleanup when track ends
+ self.stats = StreamStats(
+ track_id=track.id,
+ metrics_manager=app.get("metrics_manager", None),
+ )
+ self._running = True
+
+ asyncio.create_task(self.collect_frames())
+
+ # Add cleanup when track ends.
@track.on("ended")
async def on_ended():
logger.info("Source audio track ended, stopping collection")
@@ -132,7 +162,7 @@ async def collect_frames(self):
the processing pipeline. Stops when track ends or connection closes.
"""
try:
- while self.running:
+ while self._running:
try:
frame = await self.track.recv()
await self.pipeline.put_audio_frame(frame)
@@ -144,9 +174,9 @@ async def collect_frames(self):
logger.info("Media stream ended")
else:
logger.error(f"Error collecting audio frames: {str(e)}")
- self.running = False
+ self._running = False
break
-
+
# Perform cleanup outside the exception handler
logger.info("Audio frame collection stopped")
except asyncio.CancelledError:
@@ -157,7 +187,20 @@ async def collect_frames(self):
await self.pipeline.cleanup()
async def recv(self):
- return await self.pipeline.get_processed_audio_frame()
+ """Receive a processed audio frame from the pipeline and return it to the
+ client, while collecting statistics about the stream.
+ """
+ if self.stats.startup_time is None:
+ self.stats.start_timestamp = time.monotonic()
+ self.stats.startup_time = self.stats.start_timestamp - self._start_time
+ self.stats.pipeline.warmup_time = self.pipeline.stats.warmup_time
+
+ processed_frame = await self.pipeline.get_processed_audio_frame()
+
+ # Increment the frame count to calculate FPS.
+ await self.stats.fps_meter.increment_frame_count()
+
+ return processed_frame
def force_codec(pc, sender, forced_codec):
@@ -318,15 +361,17 @@ async def on_connectionstatechange():
),
)
+
async def cancel_collect_frames(track):
track.running = False
- if hasattr(track, 'collect_task') is not None and not track.collect_task.done():
+ if hasattr(track, "collect_task") is not None and not track.collect_task.done():
try:
track.collect_task.cancel()
await track.collect_task
- except (asyncio.CancelledError):
+ except asyncio.CancelledError:
pass
+
async def set_prompt(request):
pipeline = request.app["pipeline"]
@@ -410,11 +455,9 @@ async def on_shutdown(app: web.Application):
# Add routes for getting stream statistics.
stream_stats_manager = StreamStatsManager(app)
+ app.router.add_get("/streams/stats", stream_stats_manager.collect_all_stream_stats)
app.router.add_get(
- "/streams/stats", stream_stats_manager.collect_all_stream_metrics
- )
- app.router.add_get(
- "/stream/{stream_id}/stats", stream_stats_manager.collect_stream_metrics_by_id
+ "/stream/{stream_id}/stats", stream_stats_manager.collect_stream_stats_by_id
)
# Add Prometheus metrics endpoint.
diff --git a/server/metrics/__init__.py b/server/metrics/__init__.py
index 5fb1a2ba..dc329383 100644
--- a/server/metrics/__init__.py
+++ b/server/metrics/__init__.py
@@ -1,2 +1,3 @@
+from .pipeline_stats import PipelineStats
from .prometheus_metrics import MetricsManager
-from .stream_stats import StreamStatsManager
+from .stream_stats import StreamStatsManager, StreamStats
diff --git a/server/metrics/prometheus_metrics.py b/server/metrics/prometheus_metrics.py
index 080bc294..458e697a 100644
--- a/server/metrics/prometheus_metrics.py
+++ b/server/metrics/prometheus_metrics.py
@@ -1,4 +1,4 @@
-"""Prometheus metrics utilities."""
+"""Manages Prometheus metrics collection and exposure."""
from prometheus_client import Gauge, generate_latest
from aiohttp import web
@@ -12,7 +12,8 @@ def __init__(self, include_stream_id: bool = False):
"""Initializes the MetricsManager class.
Args:
- include_stream_id: Whether to include the stream ID as a label in the metrics.
+ include_stream_id: Whether to include the stream ID as a label in the
+ metrics.
"""
self._enabled = False
self._include_stream_id = include_stream_id
@@ -21,13 +22,28 @@ def __init__(self, include_stream_id: bool = False):
self._fps_gauge = Gauge(
"stream_fps", "Frames per second of the stream", base_labels
)
+ self._startup_time_gauge = Gauge(
+ "stream_startup_time",
+ "Time taken to start the stream",
+ base_labels,
+ )
+ self._pipeline_audio_warmup_time_gauge = Gauge(
+ "stream_pipeline_audio_warmup_time",
+ "Time taken to warm up the audio pipeline",
+ base_labels,
+ )
+ self._pipeline_video_warmup_time_gauge = Gauge(
+ "stream_pipeline_video_warmup_time",
+ "Time taken to warm up the video pipeline",
+ base_labels,
+ )
def enable(self):
"""Enable Prometheus metrics collection."""
self._enabled = True
- def update_fps_metrics(self, fps: float, stream_id: Optional[str] = None):
- """Update Prometheus metrics for a given stream.
+ def update_fps(self, fps: float, stream_id: Optional[str] = None):
+ """Update fps metrics for a given stream.
Args:
fps: The current frames per second.
@@ -39,6 +55,55 @@ def update_fps_metrics(self, fps: float, stream_id: Optional[str] = None):
else:
self._fps_gauge.set(fps)
+ def update_startup_time(self, startup_time: float, stream_id: Optional[str] = None):
+ """Update startup time metrics for a given stream.
+
+ Args:
+ startup_time: The time taken to start the stream.
+ stream_id: The ID of the stream.
+ """
+ if self._enabled:
+ if self._include_stream_id:
+ self._startup_time_gauge.labels(stream_id=stream_id or "").set(
+ startup_time
+ )
+ else:
+ self._startup_time_gauge.set(startup_time)
+
+ def update_video_warmup_time(
+ self, warmup_time: float, stream_id: Optional[str] = None
+ ):
+ """Update video pipeline warmup time metrics for a given stream.
+
+ Args:
+ warmup_time: The time taken to warm up the video pipeline.
+ stream_id: The ID of the stream.
+ """
+ if self._enabled:
+ if self._include_stream_id:
+ self._pipeline_video_warmup_time_gauge.labels(
+ stream_id=stream_id or ""
+ ).set(warmup_time)
+ else:
+ self._pipeline_video_warmup_time_gauge.set(warmup_time)
+
+ def update_audio_warmup_time(
+ self, warmup_time: float, stream_id: Optional[str] = None
+ ):
+ """Update audio pipeline warmup time metrics for a given stream.
+
+ Args:
+ warmup_time: The time taken to warm up the audio pipeline.
+ stream_id: The ID of the stream.
+ """
+ if self._enabled:
+ if self._include_stream_id:
+ self._pipeline_audio_warmup_time_gauge.labels(
+ stream_id=stream_id or ""
+ ).set(warmup_time)
+ else:
+ self._pipeline_audio_warmup_time_gauge.set(warmup_time)
+
async def metrics_handler(self, _):
"""Handle Prometheus metrics endpoint."""
return web.Response(body=generate_latest(), content_type="text/plain")
diff --git a/server/metrics/stream_stats.py b/server/metrics/stream_stats.py
index 8dc2ab19..f5722c1c 100644
--- a/server/metrics/stream_stats.py
+++ b/server/metrics/stream_stats.py
@@ -1,23 +1,120 @@
-"""Handles real-time video stream statistics (non-Prometheus, JSON API)."""
+"""Handles real-time video stream statistics for JSON API publishing."""
-from typing import Any, Dict
+from typing import Any, Dict, Optional
import json
from aiohttp import web
from aiortc import MediaStreamTrack
+from utils.fps_meter import FPSMeter
+from .prometheus_metrics import MetricsManager
+from .pipeline_stats import PipelineStats
+import time
+
+
+class StreamStats:
+ """Tracks real-time statistics of the stream.
+
+ Attributes:
+ fps_meter: The FPSMeter instance for the stream.
+ start_timestamp: The timestamp when the stream started.
+ pipeline_stats: The PipelineStats instance for the stream.
+ """
+
+ def __init__(self, track_id: str, metrics_manager: Optional[MetricsManager] = None):
+ """Initializes the StreamStats class.
+
+ Args:
+ track_id: The ID of the stream track.
+ metrics_manager: The Prometheus metrics manager instance.
+ """
+ update_metrics_callback = (
+ metrics_manager.update_fps if metrics_manager else None
+ )
+ self.fps_meter = FPSMeter(
+ track_id=track_id,
+ update_metrics_callback=update_metrics_callback,
+ )
+ self.pipeline = PipelineStats(
+ metrics_manager=metrics_manager, track_id=track_id
+ )
+
+ self.start_timestamp = None
+
+ self._metrics_manager = metrics_manager
+ self._startup_time = None
+
+ @property
+ def startup_time(self) -> float:
+ """Time taken to start the stream."""
+ return self._startup_time
+
+ @startup_time.setter
+ def startup_time(self, value: float):
+ """Sets the time taken to start the stream."""
+ if self._metrics_manager:
+ self._metrics_manager.update_startup_time(value, self.fps_meter.track_id)
+ self._startup_time = value
+
+ async def get_fps(self) -> float:
+ """Current frames per second (FPS) of the stream.
+
+ Alias for FPSMeter's get_fps method.
+ """
+ return await self.fps_meter.get_fps()
+
+ async def get_fps_measurements(self) -> list:
+ """List of FPS measurements over time.
+
+ Alias for FPSMeter's get_fps_measurements method.
+ """
+ return await self.fps_meter.get_fps_measurements()
+
+ async def get_average_fps(self) -> float:
+ """Average FPS over the last minute.
+
+ Alias for FPSMeter's get_average_fps method.
+ """
+ return await self.fps_meter.get_average_fps()
+
+ async def get_last_fps_calculation_time(self) -> float:
+ """Timestamp of the last FPS calculation.
+
+ Alias for FPSMeter's get_last_fps_calculation_time method.
+ """
+ return await self.fps_meter.get_last_fps_calculation_time()
+
+ @property
+ def time(self) -> float:
+ """Elapsed time since the stream started."""
+ return (
+ 0.0
+ if self.start_timestamp is None
+ else time.monotonic() - self.start_timestamp
+ )
+
+ async def to_dict(self) -> Dict[str, Any]:
+ """Convert stats to dictionary for easy JSON serialization."""
+ return {
+ "timestamp": self.time,
+ "startup_time": self.startup_time,
+ "pipeline": self.pipeline.to_dict(),
+ "fps": await self.get_fps(),
+ "minute_avg_fps": await self.get_average_fps(),
+ "minute_fps_array": await self.get_fps_measurements(),
+ }
class StreamStatsManager:
- """Handles real-time video stream statistics collection."""
+ """Handles real-time stream statistics collection."""
def __init__(self, app: web.Application):
- """Initializes the StreamMetrics class.
+ """Initializes the StreamStatsManager class.
Args:
app: The web application instance storing stream tracks.
"""
self._app = app
- async def collect_video_metrics(
+ async def collect_video_stats(
self, video_track: MediaStreamTrack
) -> Dict[str, Any]:
"""Collects real-time statistics for a video track.
@@ -28,23 +125,38 @@ async def collect_video_metrics(
Returns:
A dictionary containing FPS-related statistics.
"""
- return {
- "timestamp": await video_track.fps_meter.last_fps_calculation_time,
- "fps": await video_track.fps_meter.fps,
- "minute_avg_fps": await video_track.fps_meter.average_fps,
- "minute_fps_array": await video_track.fps_meter.fps_measurements,
- }
+ return {"type": video_track.kind, **await video_track.stats.to_dict()}
+
+ async def collect_audio_stats(
+ self, audio_track: MediaStreamTrack
+ ) -> Dict[str, Any]:
+ """Collects real-time statistics for an audio track.
+
+ Args:
+ audio_track: The audio stream track instance.
+
+ Returns:
+ A dictionary containing audio-related statistics.
+ """
+ return {"type": audio_track.kind, **await audio_track.stats.to_dict()}
- async def collect_all_stream_metrics(self, _) -> web.Response:
- """Retrieves real-time metrics for all active video streams.
+ async def collect_all_stream_stats(self, _) -> web.Response:
+ """Retrieves real-time statistics for all active video and audio streams.
Returns:
A JSON response containing FPS statistics for all streams.
"""
- video_tracks = self._app.get("video_tracks", {})
+ tracks = {
+ **self._app.get("video_tracks", {}),
+ **self._app.get("audio_tracks", {}),
+ }
all_stats = {
- stream_id: await self.collect_video_metrics(track)
- for stream_id, track in video_tracks.items()
+ stream_id: await (
+ self.collect_video_stats(track)
+ if track.kind == "video"
+ else self.collect_audio_stats(track)
+ )
+ for stream_id, track in tracks.items()
}
return web.Response(
@@ -52,24 +164,35 @@ async def collect_all_stream_metrics(self, _) -> web.Response:
text=json.dumps(all_stats),
)
- async def collect_stream_metrics_by_id(self, request: web.Request) -> web.Response:
- """Retrieves real-time metrics for a specific video stream by ID.
+ async def collect_stream_stats_by_id(self, request: web.Request) -> web.Response:
+ """Retrieves real-time statistics for a specific video or audio stream by ID.
Args:
request: The HTTP request containing the stream ID.
Returns:
- A JSON response with stream metrics or an error message.
+ A JSON response with stream statistics or an error message.
"""
stream_id = request.match_info.get("stream_id")
- video_tracks = self._app.get("video_tracks", {})
- video_track = video_tracks.get(stream_id)
-
- if video_track:
- stats = await self.collect_video_metrics(video_track)
- else:
- stats = {"error": "Stream not found"}
-
+ tracks = {
+ **self._app.get("video_tracks", {}),
+ **self._app.get("audio_tracks", {}),
+ }
+ track = tracks.get(stream_id)
+
+ if not track:
+ error_response = {"error": "Stream not found"}
+ return web.Response(
+ status=404,
+ content_type="application/json",
+ text=json.dumps(error_response),
+ )
+
+ stats = await (
+ self.collect_video_stats(track)
+ if track.kind == "video"
+ else self.collect_audio_stats(track)
+ )
return web.Response(
content_type="application/json",
text=json.dumps(stats),
diff --git a/server/pipeline.py b/server/pipeline.py
index 26270923..5a85114b 100644
--- a/server/pipeline.py
+++ b/server/pipeline.py
@@ -2,7 +2,9 @@
import torch
import numpy as np
import asyncio
+import time
+from metrics import PipelineStats
from typing import Any, Dict, Union, List
from comfystream.client import ComfyStreamClient
@@ -11,13 +13,26 @@
class Pipeline:
def __init__(self, **kwargs):
+ """Initialize the pipeline with the given configuration.
+
+ Attributes:
+ client: The client to communicate with the ComfyStream server.
+ video_incoming_frames: Queue to store incoming video frames.
+ audio_incoming_frames: Queue to store incoming audio frames.
+ processed_audio_buffer: Buffer to store processed audio frames.
+ stats: Statistics about the pipeline.
+ """
self.client = ComfyStreamClient(**kwargs)
self.video_incoming_frames = asyncio.Queue()
self.audio_incoming_frames = asyncio.Queue()
self.processed_audio_buffer = np.array([], dtype=np.int16)
+ self.stats = PipelineStats()
+
async def warm_video(self):
+ start_time = time.monotonic()
+
dummy_frame = av.VideoFrame()
dummy_frame.side_data.input = torch.randn(1, 512, 512, 3)
@@ -25,22 +40,32 @@ async def warm_video(self):
self.client.put_video_input(dummy_frame)
await self.client.get_video_output()
+ self.stats.video_warmup_time = time.monotonic() - start_time
+
async def warm_audio(self):
+ start_time = time.monotonic()
+
dummy_frame = av.AudioFrame()
- dummy_frame.side_data.input = np.random.randint(-32768, 32767, int(48000 * 0.5), dtype=np.int16) # TODO: adds a lot of delay if it doesn't match the buffer size, is warmup needed?
+ dummy_frame.side_data.input = np.random.randint(
+ -32768, 32767, int(48000 * 0.5), dtype=np.int16
+ ) # TODO: adds a lot of delay if it doesn't match the buffer size, is warmup needed?
dummy_frame.sample_rate = 48000
for _ in range(WARMUP_RUNS):
self.client.put_audio_input(dummy_frame)
await self.client.get_audio_output()
+ self.stats.audio_warmup_time = time.monotonic() - start_time
+
async def set_prompts(self, prompts: Union[Dict[Any, Any], List[Dict[Any, Any]]]):
if isinstance(prompts, list):
await self.client.set_prompts(prompts)
else:
await self.client.set_prompts([prompts])
- async def update_prompts(self, prompts: Union[Dict[Any, Any], List[Dict[Any, Any]]]):
+ async def update_prompts(
+ self, prompts: Union[Dict[Any, Any], List[Dict[Any, Any]]]
+ ):
if isinstance(prompts, list):
await self.client.update_prompts(prompts)
else:
@@ -61,18 +86,27 @@ async def put_audio_frame(self, frame: av.AudioFrame):
def video_preprocess(self, frame: av.VideoFrame) -> Union[torch.Tensor, np.ndarray]:
frame_np = frame.to_ndarray(format="rgb24").astype(np.float32) / 255.0
return torch.from_numpy(frame_np).unsqueeze(0)
-
+
def audio_preprocess(self, frame: av.AudioFrame) -> Union[torch.Tensor, np.ndarray]:
return frame.to_ndarray().ravel().reshape(-1, 2).mean(axis=1).astype(np.int16)
-
- def video_postprocess(self, output: Union[torch.Tensor, np.ndarray]) -> av.VideoFrame:
+
+ def video_postprocess(
+ self, output: Union[torch.Tensor, np.ndarray]
+ ) -> av.VideoFrame:
return av.VideoFrame.from_ndarray(
- (output * 255.0).clamp(0, 255).to(dtype=torch.uint8).squeeze(0).cpu().numpy()
+ (output * 255.0)
+ .clamp(0, 255)
+ .to(dtype=torch.uint8)
+ .squeeze(0)
+ .cpu()
+ .numpy()
)
- def audio_postprocess(self, output: Union[torch.Tensor, np.ndarray]) -> av.AudioFrame:
+ def audio_postprocess(
+ self, output: Union[torch.Tensor, np.ndarray]
+ ) -> av.AudioFrame:
return av.AudioFrame.from_ndarray(np.repeat(output, 2).reshape(1, -1))
-
+
async def get_processed_video_frame(self):
# TODO: make it generic to support purely generative video cases
out_tensor = await self.client.get_video_output()
@@ -80,10 +114,10 @@ async def get_processed_video_frame(self):
while frame.side_data.skipped:
frame = await self.video_incoming_frames.get()
- processed_frame = self.video_postprocess(out_tensor)
+ processed_frame = self.video_postprocess(out_tensor)
processed_frame.pts = frame.pts
processed_frame.time_base = frame.time_base
-
+
return processed_frame
async def get_processed_audio_frame(self):
@@ -91,21 +125,23 @@ async def get_processed_audio_frame(self):
frame = await self.audio_incoming_frames.get()
if frame.samples > len(self.processed_audio_buffer):
out_tensor = await self.client.get_audio_output()
- self.processed_audio_buffer = np.concatenate([self.processed_audio_buffer, out_tensor])
- out_data = self.processed_audio_buffer[:frame.samples]
- self.processed_audio_buffer = self.processed_audio_buffer[frame.samples:]
+ self.processed_audio_buffer = np.concatenate(
+ [self.processed_audio_buffer, out_tensor]
+ )
+ out_data = self.processed_audio_buffer[: frame.samples]
+ self.processed_audio_buffer = self.processed_audio_buffer[frame.samples :]
processed_frame = self.audio_postprocess(out_data)
processed_frame.pts = frame.pts
processed_frame.time_base = frame.time_base
processed_frame.sample_rate = frame.sample_rate
-
+
return processed_frame
-
+
async def get_nodes_info(self) -> Dict[str, Any]:
"""Get information about all nodes in the current prompt including metadata."""
nodes_info = await self.client.get_available_nodes()
return nodes_info
-
+
async def cleanup(self):
- await self.client.cleanup()
\ No newline at end of file
+ await self.client.cleanup()
diff --git a/server/utils/fps_meter.py b/server/utils/fps_meter.py
index ce94317b..7ae14d67 100644
--- a/server/utils/fps_meter.py
+++ b/server/utils/fps_meter.py
@@ -4,16 +4,31 @@
import logging
import time
from collections import deque
-from metrics import MetricsManager
+from typing import Callable, Optional
logger = logging.getLogger(__name__)
class FPSMeter:
- """Class to calculate and store the framerate of a stream by counting frames."""
-
- def __init__(self, metrics_manager: MetricsManager, track_id: str):
- """Initializes the FPSMeter class."""
+ """Class to calculate and store the framerate of a stream by counting frames.
+
+ Attributes:
+ track_id: The ID of the track.
+ """
+
+ def __init__(
+ self,
+ track_id: str,
+ update_metrics_callback: Optional[Callable[[float, str], None]] = None,
+ ):
+ """Initializes the FPSMeter class.
+
+ Args:
+ track_id: The ID of the track.
+ update_metrics_callback: An optional callback function to update Prometheus
+ metrics with FPS data.
+ """
+ self.track_id = track_id
self._lock = asyncio.Lock()
self._fps_interval_frame_count = 0
self._last_fps_calculation_time = None
@@ -21,8 +36,7 @@ def __init__(self, metrics_manager: MetricsManager, track_id: str):
self._fps = 0.0
self._fps_measurements = deque(maxlen=60)
self._running_event = asyncio.Event()
- self._metrics_manager = metrics_manager
- self.track_id = track_id
+ self._update_metrics_callback = update_metrics_callback
asyncio.create_task(self._calculate_fps_loop())
@@ -51,8 +65,9 @@ async def _calculate_fps_loop(self):
self._last_fps_calculation_time = current_time
self._fps_interval_frame_count = 0
- # Update Prometheus metrics if enabled.
- self._metrics_manager.update_fps_metrics(self._fps, self.track_id)
+ # Update Prometheus metrics using the callback if provided.
+ if self._update_metrics_callback:
+ self._update_metrics_callback(self._fps, self.track_id)
await asyncio.sleep(1) # Calculate FPS every second.
@@ -63,8 +78,7 @@ async def increment_frame_count(self):
if not self._running_event.is_set():
self._running_event.set()
- @property
- async def fps(self) -> float:
+ async def get_fps(self) -> float:
"""Get the current output frames per second (FPS).
Returns:
@@ -73,8 +87,7 @@ async def fps(self) -> float:
async with self._lock:
return self._fps
- @property
- async def fps_measurements(self) -> list:
+ async def get_fps_measurements(self) -> list:
"""Get the array of FPS measurements for the last minute.
Returns:
@@ -83,8 +96,7 @@ async def fps_measurements(self) -> list:
async with self._lock:
return list(self._fps_measurements)
- @property
- async def average_fps(self) -> float:
+ async def get_average_fps(self) -> float:
"""Calculate the average FPS from the measurements taken in the last minute.
Returns:
@@ -98,8 +110,7 @@ async def average_fps(self) -> float:
else self._fps
)
- @property
- async def last_fps_calculation_time(self) -> float:
+ async def get_last_fps_calculation_time(self) -> float:
"""Get the elapsed time since the last FPS calculation.
Returns:
diff --git a/src/comfystream/client.py b/src/comfystream/client.py
index 47e995a5..6e544eb3 100644
--- a/src/comfystream/client.py
+++ b/src/comfystream/client.py
@@ -45,8 +45,7 @@ async def run_prompt(self, prompt_index: int):
async def cleanup(self):
async with self.cleanup_lock:
- tasks_to_cancel = list(self.running_prompts.values())
- for task in tasks_to_cancel:
+ for task in self.running_prompts.values():
task.cancel()
try:
await task
@@ -55,11 +54,7 @@ async def cleanup(self):
self.running_prompts.clear()
if self.comfy_client.is_running:
- try:
- await self.comfy_client.__aexit__()
- except Exception as e:
- logger.error(f"Error during ComfyClient cleanup: {e}")
-
+ await self.comfy_client.__aexit__()
await self.cleanup_queues()
logger.info("Client cleanup complete")
@@ -115,105 +110,77 @@ async def get_available_nodes(self):
for node_id, node in prompt.items()
}
nodes_info = {}
-
+
# Only process nodes until we've found all the ones we need
for class_type, node_class in nodes.NODE_CLASS_MAPPINGS.items():
if not remaining_nodes: # Exit early if we've found all needed nodes
break
-
+
if class_type not in needed_class_types:
continue
-
+
# Get metadata for this node type (same as original get_node_metadata)
input_data = node_class.INPUT_TYPES() if hasattr(node_class, 'INPUT_TYPES') else {}
input_info = {}
-
+
# Process required inputs
if 'required' in input_data:
for name, value in input_data['required'].items():
- if isinstance(value, tuple):
- if len(value) == 1 and isinstance(value[0], list):
- # Handle combo box case where value is ([option1, option2, ...],)
- input_info[name] = {
- 'type': 'combo',
- 'value': value[0], # The list of options becomes the value
- }
- elif len(value) == 2:
- input_type, config = value
- input_info[name] = {
- 'type': input_type,
- 'required': True,
- 'min': config.get('min', None),
- 'max': config.get('max', None),
- 'widget': config.get('widget', None)
- }
- elif len(value) == 1:
- # Handle simple type case like ('IMAGE',)
- input_info[name] = {
- 'type': value[0]
- }
+ if isinstance(value, tuple) and len(value) == 2:
+ input_type, config = value
+ input_info[name] = {
+ 'type': input_type,
+ 'required': True,
+ 'min': config.get('min', None),
+ 'max': config.get('max', None),
+ 'widget': config.get('widget', None)
+ }
else:
logger.error(f"Unexpected structure for required input {name}: {value}")
-
- # Process optional inputs with same logic
+
+ # Process optional inputs
if 'optional' in input_data:
for name, value in input_data['optional'].items():
- if isinstance(value, tuple):
- if len(value) == 1 and isinstance(value[0], list):
- # Handle combo box case where value is ([option1, option2, ...],)
- input_info[name] = {
- 'type': 'combo',
- 'value': value[0], # The list of options becomes the value
- }
- elif len(value) == 2:
- input_type, config = value
- input_info[name] = {
- 'type': input_type,
- 'required': False,
- 'min': config.get('min', None),
- 'max': config.get('max', None),
- 'widget': config.get('widget', None)
- }
- elif len(value) == 1:
- # Handle simple type case like ('IMAGE',)
- input_info[name] = {
- 'type': value[0]
- }
+ if isinstance(value, tuple) and len(value) == 2:
+ input_type, config = value
+ input_info[name] = {
+ 'type': input_type,
+ 'required': False,
+ 'min': config.get('min', None),
+ 'max': config.get('max', None),
+ 'widget': config.get('widget', None)
+ }
else:
logger.error(f"Unexpected structure for optional input {name}: {value}")
-
+
# Now process any nodes in our prompt that use this class_type
for node_id in list(remaining_nodes):
node = prompt[node_id]
if node.get('class_type') != class_type:
continue
-
+
node_info = {
'class_type': class_type,
'inputs': {}
}
-
+
if 'inputs' in node:
for input_name, input_value in node['inputs'].items():
- input_metadata = input_info.get(input_name, {})
node_info['inputs'][input_name] = {
'value': input_value,
- 'type': input_metadata.get('type', 'unknown'),
- 'min': input_metadata.get('min', None),
- 'max': input_metadata.get('max', None),
- 'widget': input_metadata.get('widget', None)
+ 'type': input_info.get(input_name, {}).get('type', 'unknown'),
+ 'min': input_info.get(input_name, {}).get('min', None),
+ 'max': input_info.get(input_name, {}).get('max', None),
+ 'widget': input_info.get(input_name, {}).get('widget', None)
}
- # For combo type inputs, include the list of options
- if input_metadata.get('type') == 'combo':
- node_info['inputs'][input_name]['value'] = input_metadata.get('value', [])
-
+
nodes_info[node_id] = node_info
remaining_nodes.remove(node_id)
all_prompts_nodes_info[prompt_index] = nodes_info
-
+
return all_prompts_nodes_info
-
+
except Exception as e:
logger.error(f"Error getting node info: {str(e)}")
return {}
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 52d53057..d26fdca3 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ui",
- "version": "0.0.5",
+ "version": "0.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ui",
- "version": "0.0.5",
+ "version": "0.0.4",
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-dialog": "^1.1.6",
diff --git a/ui/package.json b/ui/package.json
index 6be2b1c5..1e6a4e14 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "ui",
- "version": "0.0.5",
+ "version": "0.0.4",
"private": true,
"scripts": {
"dev": "cross-env NEXT_PUBLIC_DEV=true next dev",
diff --git a/ui/src/components/control-panel.tsx b/ui/src/components/control-panel.tsx
index 3d5f458c..8d9a90f6 100644
--- a/ui/src/components/control-panel.tsx
+++ b/ui/src/components/control-panel.tsx
@@ -4,7 +4,7 @@ import React, { useState, useEffect } from "react";
import { usePeerContext } from "@/context/peer-context";
import { usePrompt } from "./settings";
-type InputValue = string | number | boolean | string[];
+type InputValue = string | number | boolean;
interface InputInfo {
value: InputValue;
@@ -12,7 +12,6 @@ interface InputInfo {
min?: number;
max?: number;
widget?: string;
- options?: string[];
}
interface NodeInfo {
@@ -46,32 +45,19 @@ const InputControl = ({
value: string;
onChange: (value: string) => void;
}) => {
- if (input.widget === "combo" || input.type === "combo") {
- // Get options from either the options field or value field
- const options = input.options
- ? input.options
- : Array.isArray(input.value)
- ? input.value
- : typeof input.value === 'string'
- ? [input.value]
- : [];
-
- // If no value is selected, select the first option by default
- const currentValue = value || options[0] || '';
-
+ if (input.widget === "combo") {
return (
);
}
@@ -114,9 +100,6 @@ const InputControl = ({
className="p-2 border rounded w-full"
/>
);
- // Handle combo in the main combo block above
- case "combo":
- return InputControl({ input: { ...input, widget: "combo" }, value, onChange });
default:
console.warn(`Unhandled input type: ${input.type}`); // Debug log
return (
@@ -213,33 +196,12 @@ export const ControlPanel = ({
]
: null;
if (!currentInput || !currentPrompts) return;
-
- // Don't send updates if this is a combo and we haven't selected a value yet
- if (currentInput.widget === "combo" && !panelState.value) return;
let isValidValue = true;
let processedValue: InputValue = panelState.value;
- // For combo inputs, use the value directly
- if (currentInput.widget === "combo" || currentInput.type === "combo") {
- // Get options from either the options field or value field
- const options = currentInput.options
- ? currentInput.options
- : Array.isArray(currentInput.value)
- ? currentInput.value as string[]
- : typeof currentInput.value === 'string'
- ? [currentInput.value as string]
- : [];
-
- // If no value is selected and we have options, use the first option
- const validValue = panelState.value || options[0] || '';
-
- // Validate that the value is in the options list
- isValidValue = options.includes(validValue);
- processedValue = validValue;
- } else {
- // Validate and process value based on type
- switch (currentInput.type.toLowerCase()) {
+ // Validate and process value based on type
+ switch (currentInput.type.toLowerCase()) {
case "number":
isValidValue =
/^-?\d*\.?\d*$/.test(panelState.value) && panelState.value !== "";
@@ -255,9 +217,13 @@ export const ControlPanel = ({
processedValue = panelState.value;
break;
default:
- isValidValue = panelState.value !== "";
- processedValue = panelState.value;
- }
+ if (currentInput.widget === "combo") {
+ isValidValue = panelState.value !== "";
+ processedValue = panelState.value;
+ } else {
+ isValidValue = panelState.value !== "";
+ processedValue = panelState.value;
+ }
}
const hasRequiredFields =
@@ -295,32 +261,9 @@ export const ControlPanel = ({
}
const updatedPrompt = JSON.parse(JSON.stringify(prompt)); // Deep clone
if (updatedPrompt[panelState.nodeId]?.inputs) {
- // Ensure we're not overwriting with an invalid value
- const currentVal = updatedPrompt[panelState.nodeId].inputs[panelState.fieldName];
- const input = availableNodes[promptIdxToUpdate][panelState.nodeId]?.inputs[panelState.fieldName];
-
- if (input?.widget === 'combo' || input?.type === 'combo') {
- // Get options from either the options field or value field
- const options = input.options
- ? input.options
- : Array.isArray(input.value)
- ? input.value as string[]
- : typeof input.value === 'string'
- ? [input.value as string]
- : [];
-
- // If no value is selected and we have options, use the first option
- const validValue = (processedValue as string) || options[0] || '';
-
- // Only update if it's a valid combo value
- if (options.includes(validValue)) {
- updatedPrompt[panelState.nodeId].inputs[panelState.fieldName] = validValue;
- hasUpdated = true;
- }
- } else {
- updatedPrompt[panelState.nodeId].inputs[panelState.fieldName] = processedValue;
- hasUpdated = true;
- }
+ updatedPrompt[panelState.nodeId].inputs[panelState.fieldName] =
+ processedValue;
+ hasUpdated = true;
}
return updatedPrompt;
},
@@ -369,13 +312,8 @@ export const ControlPanel = ({
if (input.type.toLowerCase() === "boolean") {
return (!!input.value).toString();
}
- if (input.widget === "combo") {
- const options = Array.isArray(input.value)
- ? input.value as string[]
- : typeof input.value === 'string'
- ? [input.value as string]
- : [];
- return options[0] || "";
+ if (input.widget === "combo" && Array.isArray(input.value)) {
+ return input.value[0]?.toString() || "";
}
return input.value?.toString() || "0";
};
@@ -389,19 +327,11 @@ export const ControlPanel = ({
selectedField
];
if (input) {
- // For combo fields, don't set an initial value to prevent auto-update from firing
- if (input.widget === "combo") {
- onStateChange({
- fieldName: selectedField,
- value: "",
- });
- } else {
- const initialValue = getInitialValue(input);
- onStateChange({
- fieldName: selectedField,
- value: initialValue,
- });
- }
+ const initialValue = getInitialValue(input);
+ onStateChange({
+ fieldName: selectedField,
+ value: initialValue,
+ });
} else {
onStateChange({ fieldName: selectedField });
}
@@ -427,7 +357,7 @@ export const ControlPanel = ({
onStateChange({
nodeId: e.target.value,
fieldName: "",
- value: "", // Start with empty value to prevent auto-update from firing
+ value: "0",
});
}}
className="p-2 border rounded"
@@ -457,20 +387,16 @@ export const ControlPanel = ({
typeof info.type === "string"
? info.type.toLowerCase()
: String(info.type).toLowerCase();
- return [
- "boolean",
- "number",
- "float",
- "int",
- "string",
- "combo",
- ].includes(
- type,
- ) || info.widget === "combo";
+ return (
+ ["boolean", "number", "float", "int", "string"].includes(
+ type,
+ ) || info.widget === "combo"
+ );
})
.map(([field, info]) => (
))}
diff --git a/ui/src/components/webcam.tsx b/ui/src/components/webcam.tsx
index f48ab938..4f64ac54 100644
--- a/ui/src/components/webcam.tsx
+++ b/ui/src/components/webcam.tsx
@@ -170,7 +170,7 @@ export function Webcam({
width: { ideal: 512 },
height: { ideal: 512 },
aspectRatio: { ideal: 1 },
- frameRate: { ideal: frameRate },
+ frameRate: { ideal: frameRate, max: frameRate },
},
audio:
selectedAudioDeviceId === "none"