From 2351d4f323f4f9cba9cd98915d9ad28faadbccbc Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sun, 30 Mar 2025 09:40:01 +0200 Subject: [PATCH 1/2] feat: add startup metrics --- .editorconfig | 17 +- .github/codeql-config.yaml | 2 - .github/workflows/comfyui-base.yaml | 14 +- .github/workflows/test.yaml | 86 -- configs/nodes.yaml | 2 +- nodes/web/__init__.py | 37 - nodes/web/js/comfystream_ui_preview_node.js | 142 ---- nodes/web/js/launcher.js | 311 ------- nodes/web/js/settings.js | 860 -------------------- nodes/web/js/status-indicator.js | 454 ----------- nodes/web/static/.gitkeep | 0 prometheus.yml | 8 + pyproject.toml | 7 +- requirements.txt | 1 - server/app.py | 103 ++- server/metrics/__init__.py | 3 +- server/metrics/pipeline_stats.py | 76 ++ server/metrics/prometheus_metrics.py | 73 +- server/metrics/stream_stats.py | 177 +++- server/pipeline.py | 70 +- server/utils/fps_meter.py | 45 +- src/comfystream/client.py | 105 +-- ui/package-lock.json | 4 +- ui/package.json | 2 +- ui/src/components/control-panel.tsx | 148 +--- ui/src/components/webcam.tsx | 2 +- 26 files changed, 552 insertions(+), 2197 deletions(-) delete mode 100644 .github/codeql-config.yaml delete mode 100644 .github/workflows/test.yaml delete mode 100644 nodes/web/__init__.py delete mode 100644 nodes/web/js/comfystream_ui_preview_node.js delete mode 100644 nodes/web/js/launcher.js delete mode 100644 nodes/web/js/settings.js delete mode 100644 nodes/web/js/status-indicator.js delete mode 100644 nodes/web/static/.gitkeep create mode 100644 prometheus.yml create mode 100644 server/metrics/pipeline_stats.py diff --git a/.editorconfig b/.editorconfig index c39bef28..e53f3c8d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,19 +9,6 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[.gitignore] -insert_final_newline = unset - -[*.py] -indent_size = 4 - -[workflows/comfy*/*.json] -insert_final_newline = unset - [Dockerfile*] -indent_size = 4 - -[*.{md,txt,mkdn}] -indent_size = 4 -indent_size = unset -trim_trailing_whitespace = false +indent_style = tab +indent_size = 8 diff --git a/.github/codeql-config.yaml b/.github/codeql-config.yaml deleted file mode 100644 index 53a66416..00000000 --- a/.github/codeql-config.yaml +++ /dev/null @@ -1,2 +0,0 @@ -paths-ignore: - - "docker/" diff --git a/.github/workflows/comfyui-base.yaml b/.github/workflows/comfyui-base.yaml index 82281031..b1971e32 100644 --- a/.github/workflows/comfyui-base.yaml +++ b/.github/workflows/comfyui-base.yaml @@ -2,13 +2,19 @@ name: Build and push comfyui-base docker image on: pull_request: - paths-ignore: - - "ui/*" + paths: + - docker/Dockerfile.base + - src/comfystream/scripts/ + - configs/ + - .github/workflows/comfyui-base.yaml branches: - main push: - paths-ignore: - - "ui/*" + paths: + - docker/Dockerfile.base + - src/comfystream/scripts/ + - configs/ + - .github/workflows/comfyui-base.yaml branches: - main tags: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 69e06608..00000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,86 +0,0 @@ -name: Test project - -on: - pull_request: - branches: - - "main" - push: - branches: - - "main" - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - codeql: - name: Perform CodeQL analysis - if: ${{ github.repository == 'livepeer/comfystream' }} - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: typescript,javascript,python - config-file: ./.github/codeql-config.yaml - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - - - editorconfig: - name: Run editorconfig checker - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v4 - with: - # Check https://github.com/livepeer/go-livepeer/pull/1891 - # for ref value discussion - ref: ${{ github.event.pull_request.head.sha }} - - - name: Install editorconfig-checker - uses: editorconfig-checker/action-editorconfig-checker@main - - - name: Run editorconfig checker against the repo - if: false - # disabled editorconfig lint rule for now - run: editorconfig-checker --format github-actions - - test: - name: Run Tests - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v4 - with: - # Check https://github.com/livepeer/go-livepeer/pull/1891 - # for ref value discussion - ref: ${{ github.event.pull_request.head.sha }} - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: pip - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install '.[dev]' - - - name: Run tests - run: pytest --cov --verbose --showlocals - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CI_CODECOV_TOKEN }} - name: ${{ github.event.repository.name }} diff --git a/configs/nodes.yaml b/configs/nodes.yaml index 055aa096..460843a6 100644 --- a/configs/nodes.yaml +++ b/configs/nodes.yaml @@ -73,5 +73,5 @@ nodes: comfyui-stream-pack: name: "ComfyUI Stream Pack" url: "https://github.com/livepeer/ComfyUI-Stream-Pack" - branch: "main" + branch: "expose_feature_bank_to_comfyui" type: "utility" \ No newline at end of file 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 = ` - -
-
- ${labelText} -
- `; - } - - // 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/prometheus.yml b/prometheus.yml new file mode 100644 index 00000000..ef3c2533 --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 5s # Scrape metrics every 5 seconds + +scrape_configs: + - job_name: "comfystream_metrics" + metrics_path: "/metrics" + static_configs: + - targets: ["localhost:8889"] # Change this to "localhost" if on Linux 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/pipeline_stats.py b/server/metrics/pipeline_stats.py new file mode 100644 index 00000000..99ab0fe7 --- /dev/null +++ b/server/metrics/pipeline_stats.py @@ -0,0 +1,76 @@ +"""Contains a class for real-time pipeline statistics.""" + +from typing import Optional, Dict, Any +from dataclasses import dataclass +from .prometheus_metrics import MetricsManager + + +class PipelineStats: + """Tracks real-time statistics of the pipeline. + + Attributes: + metrics_manager: The Prometheus metrics manager instance. + track_id: The ID of the stream track. + """ + + def __init__( + self, + metrics_manager: Optional[MetricsManager] = None, + track_id: Optional[str] = None, + ): + """Initializes the PipelineStats class. + + Args: + metrics_manager: The Prometheus metrics manager instance. + track_id: The ID of the stream track. + """ + self.metrics_manager = metrics_manager + self.track_id = track_id + + self._video_warmup_time = 0.0 + self._audio_warmup_time = 0.0 + self._startup_time = 0.0 + + @property + def video_warmup_time(self) -> float: + """Time taken to warm up the video pipeline.""" + return self._video_warmup_time + + @video_warmup_time.setter + def video_warmup_time(self, value: float): + """Sets the time taken to warm up the video pipeline.""" + self._video_warmup_time = value + if self.metrics_manager: + self.metrics_manager.update_video_warmup_time(value, self.track_id) + + @property + def audio_warmup_time(self) -> float: + """Time taken to warm up the audio pipeline.""" + return self._audio_warmup_time + + @audio_warmup_time.setter + def audio_warmup_time(self, value: float): + """Sets the time taken to warm up the audio pipeline.""" + self._audio_warmup_time = value + if self.metrics_manager: + self.metrics_manager.update_audio_warmup_time(value, self.track_id) + + @property + def startup_time(self) -> float: + """Time taken to start up the entire pipeline.""" + return self._startup_time + + @startup_time.setter + def startup_time(self, value: float): + """Sets the time taken to start up the entire pipeline.""" + self._startup_time = value + if self.metrics_manager: + self.metrics_manager.update_startup_time(value, self.track_id) + + def to_dict(self) -> Dict[str, Any]: + """Convert stats to a dictionary for easy JSON serialization.""" + return { + "video_warmup_time": self._video_warmup_time, + "audio_warmup_time": self._audio_warmup_time, + "startup_time": self._startup_time, + } 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" From 4dd52fed5aa005be9f049a345801929e4c88e831 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sun, 30 Mar 2025 09:41:53 +0200 Subject: [PATCH 2/2] other --- .editorconfig | 17 +++++- .github/codeql-config.yaml | 2 + .github/workflows/comfyui-base.yaml | 14 ++--- .github/workflows/test.yaml | 86 +++++++++++++++++++++++++++++ configs/nodes.yaml | 2 +- prometheus.yml | 8 --- server/metrics/pipeline_stats.py | 76 ------------------------- 7 files changed, 108 insertions(+), 97 deletions(-) create mode 100644 .github/codeql-config.yaml create mode 100644 .github/workflows/test.yaml delete mode 100644 prometheus.yml delete mode 100644 server/metrics/pipeline_stats.py diff --git a/.editorconfig b/.editorconfig index e53f3c8d..c39bef28 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,19 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +[.gitignore] +insert_final_newline = unset + +[*.py] +indent_size = 4 + +[workflows/comfy*/*.json] +insert_final_newline = unset + [Dockerfile*] -indent_style = tab -indent_size = 8 +indent_size = 4 + +[*.{md,txt,mkdn}] +indent_size = 4 +indent_size = unset +trim_trailing_whitespace = false diff --git a/.github/codeql-config.yaml b/.github/codeql-config.yaml new file mode 100644 index 00000000..53a66416 --- /dev/null +++ b/.github/codeql-config.yaml @@ -0,0 +1,2 @@ +paths-ignore: + - "docker/" diff --git a/.github/workflows/comfyui-base.yaml b/.github/workflows/comfyui-base.yaml index b1971e32..82281031 100644 --- a/.github/workflows/comfyui-base.yaml +++ b/.github/workflows/comfyui-base.yaml @@ -2,19 +2,13 @@ name: Build and push comfyui-base docker image on: pull_request: - paths: - - docker/Dockerfile.base - - src/comfystream/scripts/ - - configs/ - - .github/workflows/comfyui-base.yaml + paths-ignore: + - "ui/*" branches: - main push: - paths: - - docker/Dockerfile.base - - src/comfystream/scripts/ - - configs/ - - .github/workflows/comfyui-base.yaml + paths-ignore: + - "ui/*" branches: - main tags: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..69e06608 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,86 @@ +name: Test project + +on: + pull_request: + branches: + - "main" + push: + branches: + - "main" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + codeql: + name: Perform CodeQL analysis + if: ${{ github.repository == 'livepeer/comfystream' }} + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: typescript,javascript,python + config-file: ./.github/codeql-config.yaml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + + + editorconfig: + name: Run editorconfig checker + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + # Check https://github.com/livepeer/go-livepeer/pull/1891 + # for ref value discussion + ref: ${{ github.event.pull_request.head.sha }} + + - name: Install editorconfig-checker + uses: editorconfig-checker/action-editorconfig-checker@main + + - name: Run editorconfig checker against the repo + if: false + # disabled editorconfig lint rule for now + run: editorconfig-checker --format github-actions + + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + # Check https://github.com/livepeer/go-livepeer/pull/1891 + # for ref value discussion + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install '.[dev]' + + - name: Run tests + run: pytest --cov --verbose --showlocals + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CI_CODECOV_TOKEN }} + name: ${{ github.event.repository.name }} diff --git a/configs/nodes.yaml b/configs/nodes.yaml index 460843a6..055aa096 100644 --- a/configs/nodes.yaml +++ b/configs/nodes.yaml @@ -73,5 +73,5 @@ nodes: comfyui-stream-pack: name: "ComfyUI Stream Pack" url: "https://github.com/livepeer/ComfyUI-Stream-Pack" - branch: "expose_feature_bank_to_comfyui" + branch: "main" type: "utility" \ No newline at end of file diff --git a/prometheus.yml b/prometheus.yml deleted file mode 100644 index ef3c2533..00000000 --- a/prometheus.yml +++ /dev/null @@ -1,8 +0,0 @@ -global: - scrape_interval: 5s # Scrape metrics every 5 seconds - -scrape_configs: - - job_name: "comfystream_metrics" - metrics_path: "/metrics" - static_configs: - - targets: ["localhost:8889"] # Change this to "localhost" if on Linux diff --git a/server/metrics/pipeline_stats.py b/server/metrics/pipeline_stats.py deleted file mode 100644 index 99ab0fe7..00000000 --- a/server/metrics/pipeline_stats.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Contains a class for real-time pipeline statistics.""" - -from typing import Optional, Dict, Any -from dataclasses import dataclass -from .prometheus_metrics import MetricsManager - - -class PipelineStats: - """Tracks real-time statistics of the pipeline. - - Attributes: - metrics_manager: The Prometheus metrics manager instance. - track_id: The ID of the stream track. - """ - - def __init__( - self, - metrics_manager: Optional[MetricsManager] = None, - track_id: Optional[str] = None, - ): - """Initializes the PipelineStats class. - - Args: - metrics_manager: The Prometheus metrics manager instance. - track_id: The ID of the stream track. - """ - self.metrics_manager = metrics_manager - self.track_id = track_id - - self._video_warmup_time = 0.0 - self._audio_warmup_time = 0.0 - self._startup_time = 0.0 - - @property - def video_warmup_time(self) -> float: - """Time taken to warm up the video pipeline.""" - return self._video_warmup_time - - @video_warmup_time.setter - def video_warmup_time(self, value: float): - """Sets the time taken to warm up the video pipeline.""" - self._video_warmup_time = value - if self.metrics_manager: - self.metrics_manager.update_video_warmup_time(value, self.track_id) - - @property - def audio_warmup_time(self) -> float: - """Time taken to warm up the audio pipeline.""" - return self._audio_warmup_time - - @audio_warmup_time.setter - def audio_warmup_time(self, value: float): - """Sets the time taken to warm up the audio pipeline.""" - self._audio_warmup_time = value - if self.metrics_manager: - self.metrics_manager.update_audio_warmup_time(value, self.track_id) - - @property - def startup_time(self) -> float: - """Time taken to start up the entire pipeline.""" - return self._startup_time - - @startup_time.setter - def startup_time(self, value: float): - """Sets the time taken to start up the entire pipeline.""" - self._startup_time = value - if self.metrics_manager: - self.metrics_manager.update_startup_time(value, self.track_id) - - def to_dict(self) -> Dict[str, Any]: - """Convert stats to a dictionary for easy JSON serialization.""" - return { - "video_warmup_time": self._video_warmup_time, - "audio_warmup_time": self._audio_warmup_time, - "startup_time": self._startup_time, - }