cluster4npu/core/nodes/exact_nodes.py

644 lines
26 KiB
Python

"""
Exact node implementations matching the original UI.py properties.
This module provides node implementations that exactly match the original
properties and behavior from the monolithic UI.py file.
"""
try:
from NodeGraphQt import BaseNode
NODEGRAPH_AVAILABLE = True
except ImportError:
NODEGRAPH_AVAILABLE = False
# Create a mock base class
class BaseNode:
def __init__(self):
pass
class ExactInputNode(BaseNode):
"""Input data source node - exact match to original."""
__identifier__ = 'com.cluster.input_node.ExactInputNode'
NODE_NAME = 'Input Node'
def __init__(self):
super().__init__()
if NODEGRAPH_AVAILABLE:
# Setup node connections - exact match
self.add_output('output', color=(0, 255, 0))
self.set_color(83, 133, 204)
# Original properties - exact match
self.create_property('source_type', 'Camera')
self.create_property('device_id', 0)
self.create_property('source_path', '')
self.create_property('resolution', '1920x1080')
self.create_property('fps', 30)
# Original property options - exact match
self._property_options = {
'source_type': ['Camera', 'Microphone', 'File', 'RTSP Stream', 'HTTP Stream'],
'device_id': {'min': 0, 'max': 10},
'resolution': ['640x480', '1280x720', '1920x1080', '3840x2160', 'Custom'],
'fps': {'min': 1, 'max': 120},
'source_path': {'type': 'file_path', 'filter': 'Media files (*.mp4 *.avi *.mov *.mkv *.wav *.mp3)'}
}
# Create custom properties dictionary for UI compatibility
self._populate_custom_properties()
def _populate_custom_properties(self):
"""Populate the custom properties dictionary for UI compatibility."""
if not NODEGRAPH_AVAILABLE:
return
# Get all business properties defined in _property_options
business_props = list(self._property_options.keys())
# Create custom dictionary containing current property values
custom_dict = {}
for prop_name in business_props:
try:
# Skip 'custom' property to avoid infinite recursion
if prop_name != 'custom':
custom_dict[prop_name] = self.get_property(prop_name)
except:
# If property doesn't exist, skip it
pass
# Create the custom property that contains all business properties
self.create_property('custom', custom_dict)
def get_business_properties(self):
"""Get all business properties for serialization."""
if not NODEGRAPH_AVAILABLE:
return {}
properties = {}
for prop_name in self._property_options.keys():
try:
properties[prop_name] = self.get_property(prop_name)
except:
pass
return properties
def get_display_properties(self):
"""Return properties that should be displayed in the UI panel."""
# Customize which properties appear in the properties panel
# You can reorder, filter, or modify this list
return ['source_type', 'resolution', 'fps'] # Only show these 3 properties
class ExactModelNode(BaseNode):
"""Model node for ML inference - exact match to original."""
__identifier__ = 'com.cluster.model_node.ExactModelNode'
NODE_NAME = 'Model Node'
def __init__(self):
super().__init__()
if NODEGRAPH_AVAILABLE:
# Setup node connections - exact match
self.add_input('input', multi_input=False, color=(255, 140, 0))
self.add_output('output', color=(0, 255, 0))
self.set_color(65, 84, 102)
# Original properties - exact match
self.create_property('model_path', '')
self.create_property('scpu_fw_path', '')
self.create_property('ncpu_fw_path', '')
self.create_property('dongle_series', '520')
self.create_property('num_dongles', 1)
self.create_property('port_id', '')
self.create_property('upload_fw', True)
# Multi-series properties
self.create_property('multi_series_mode', False)
self.create_property('assets_folder', '')
self.create_property('enabled_series', ['520', '720'])
self.create_property('max_queue_size', 100)
self.create_property('result_buffer_size', 1000)
self.create_property('batch_size', 1)
self.create_property('enable_preprocessing', False)
self.create_property('enable_postprocessing', False)
# Original property options - exact match
self._property_options = {
'dongle_series': ['520', '720', '1080', 'Custom'],
'num_dongles': {'min': 1, 'max': 16},
'model_path': {'type': 'file_path', 'filter': 'NEF Model files (*.nef)'},
'scpu_fw_path': {'type': 'file_path', 'filter': 'SCPU Firmware files (*.bin)'},
'ncpu_fw_path': {'type': 'file_path', 'filter': 'NCPU Firmware files (*.bin)'},
'port_id': {'placeholder': 'e.g., 8080 or auto'},
'upload_fw': {'type': 'bool', 'default': True, 'description': 'Upload firmware to dongle if needed'},
# Multi-series property options
'multi_series_mode': {'type': 'bool', 'default': False, 'description': 'Enable multi-series dongle support'},
'assets_folder': {'type': 'file_path', 'filter': 'Folder', 'mode': 'directory'},
'enabled_series': {'type': 'list', 'options': ['520', '720', '630', '730', '540'], 'default': ['520', '720']},
'max_queue_size': {'min': 1, 'max': 1000, 'default': 100},
'result_buffer_size': {'min': 100, 'max': 10000, 'default': 1000},
'batch_size': {'min': 1, 'max': 32, 'default': 1},
'enable_preprocessing': {'type': 'bool', 'default': False},
'enable_postprocessing': {'type': 'bool', 'default': False}
}
# Create custom properties dictionary for UI compatibility
self._populate_custom_properties()
# Set up custom property handlers for folder selection
if NODEGRAPH_AVAILABLE:
self._setup_custom_property_handlers()
def _populate_custom_properties(self):
"""Populate the custom properties dictionary for UI compatibility."""
if not NODEGRAPH_AVAILABLE:
return
# Get all business properties defined in _property_options
business_props = list(self._property_options.keys())
# Create custom dictionary containing current property values
custom_dict = {}
for prop_name in business_props:
try:
# Skip 'custom' property to avoid infinite recursion
if prop_name != 'custom':
custom_dict[prop_name] = self.get_property(prop_name)
except:
# If property doesn't exist, skip it
pass
# Create the custom property that contains all business properties
self.create_property('custom', custom_dict)
def get_business_properties(self):
"""Get all business properties for serialization."""
if not NODEGRAPH_AVAILABLE:
return {}
properties = {}
for prop_name in self._property_options.keys():
try:
properties[prop_name] = self.get_property(prop_name)
except:
pass
return properties
def get_display_properties(self):
"""Return properties that should be displayed in the UI panel."""
if not NODEGRAPH_AVAILABLE:
return []
# Base properties that are always shown
base_props = ['multi_series_mode']
try:
# Check if we're in multi-series mode
multi_series_mode = self.get_property('multi_series_mode')
if multi_series_mode:
# Multi-series mode: show multi-series specific properties
return base_props + [
'assets_folder', 'enabled_series',
'max_queue_size', 'result_buffer_size', 'batch_size',
'enable_preprocessing', 'enable_postprocessing'
]
else:
# Single-series mode: show traditional properties
return base_props + [
'model_path', 'scpu_fw_path', 'ncpu_fw_path',
'dongle_series', 'num_dongles', 'port_id', 'upload_fw'
]
except:
# Fallback to single-series mode if property access fails
return base_props + [
'model_path', 'scpu_fw_path', 'ncpu_fw_path',
'dongle_series', 'num_dongles', 'port_id', 'upload_fw'
]
def get_inference_config(self):
"""Get configuration for inference pipeline"""
if not NODEGRAPH_AVAILABLE:
return {}
try:
multi_series_mode = self.get_property('multi_series_mode')
if multi_series_mode:
# Multi-series configuration
return {
'multi_series_mode': True,
'assets_folder': self.get_property('assets_folder'),
'enabled_series': self.get_property('enabled_series'),
'max_queue_size': self.get_property('max_queue_size'),
'result_buffer_size': self.get_property('result_buffer_size'),
'batch_size': self.get_property('batch_size'),
'enable_preprocessing': self.get_property('enable_preprocessing'),
'enable_postprocessing': self.get_property('enable_postprocessing')
}
else:
# Single-series configuration
return {
'multi_series_mode': False,
'model_path': self.get_property('model_path'),
'scpu_fw_path': self.get_property('scpu_fw_path'),
'ncpu_fw_path': self.get_property('ncpu_fw_path'),
'dongle_series': self.get_property('dongle_series'),
'num_dongles': self.get_property('num_dongles'),
'port_id': self.get_property('port_id'),
'upload_fw': self.get_property('upload_fw')
}
except:
# Fallback to single-series configuration
return {
'multi_series_mode': False,
'model_path': self.get_property('model_path', ''),
'scpu_fw_path': self.get_property('scpu_fw_path', ''),
'ncpu_fw_path': self.get_property('ncpu_fw_path', ''),
'dongle_series': self.get_property('dongle_series', '520'),
'num_dongles': self.get_property('num_dongles', 1),
'port_id': self.get_property('port_id', ''),
'upload_fw': self.get_property('upload_fw', True)
}
def get_hardware_requirements(self):
"""Get hardware requirements for this node"""
if not NODEGRAPH_AVAILABLE:
return {}
try:
multi_series_mode = self.get_property('multi_series_mode')
if multi_series_mode:
enabled_series = self.get_property('enabled_series')
return {
'multi_series_mode': True,
'required_series': enabled_series,
'estimated_dongles': len(enabled_series) * 2 # Assume 2 dongles per series
}
else:
dongle_series = self.get_property('dongle_series')
num_dongles = self.get_property('num_dongles')
return {
'multi_series_mode': False,
'required_series': [f'KL{dongle_series}'],
'estimated_dongles': num_dongles
}
except:
return {'multi_series_mode': False, 'required_series': ['KL520'], 'estimated_dongles': 1}
def _setup_custom_property_handlers(self):
"""Setup custom property handlers, especially for folder selection."""
try:
# For assets_folder, we want to trigger folder selection dialog
# This might require custom widget or property handling
# For now, we'll use the standard approach but add validation
# You can override the property widget here if needed
# This is a placeholder for custom folder selection implementation
pass
except Exception as e:
print(f"Warning: Could not setup custom property handlers: {e}")
def select_assets_folder(self):
"""Method to open folder selection dialog for assets folder using tkinter."""
if not NODEGRAPH_AVAILABLE:
return ""
try:
import tkinter as tk
from tkinter import filedialog
# Create a root window but keep it hidden
root = tk.Tk()
root.withdraw() # Hide the main window
root.attributes('-topmost', True) # Bring dialog to front
# Open folder selection dialog
folder_path = filedialog.askdirectory(
title="Select Assets Folder",
initialdir="",
mustexist=True
)
# Destroy the root window
root.destroy()
if folder_path:
# Validate the selected folder structure
if self._validate_assets_folder(folder_path):
# Set the property
if NODEGRAPH_AVAILABLE:
self.set_property('assets_folder', folder_path)
print(f"Assets folder set to: {folder_path}")
return folder_path
else:
print(f"Warning: Selected folder does not have the expected structure")
print("Expected structure: Assets/Firmware/ and Assets/Models/ with series subfolders")
# Still set it, but warn user
if NODEGRAPH_AVAILABLE:
self.set_property('assets_folder', folder_path)
return folder_path
except ImportError:
print("tkinter not available, falling back to simple input")
# Fallback to manual input
folder_path = input("Enter Assets folder path: ").strip()
if folder_path and NODEGRAPH_AVAILABLE:
self.set_property('assets_folder', folder_path)
return folder_path
except Exception as e:
print(f"Error selecting assets folder: {e}")
return ""
def _validate_assets_folder(self, folder_path):
"""Validate that the assets folder has the expected structure."""
try:
import os
# Check if Firmware and Models folders exist
firmware_path = os.path.join(folder_path, 'Firmware')
models_path = os.path.join(folder_path, 'Models')
has_firmware = os.path.exists(firmware_path) and os.path.isdir(firmware_path)
has_models = os.path.exists(models_path) and os.path.isdir(models_path)
if not (has_firmware and has_models):
return False
# Check for at least one series subfolder
expected_series = ['KL520', 'KL720', 'KL630', 'KL730', 'KL540']
firmware_series = [d for d in os.listdir(firmware_path)
if os.path.isdir(os.path.join(firmware_path, d)) and d in expected_series]
models_series = [d for d in os.listdir(models_path)
if os.path.isdir(os.path.join(models_path, d)) and d in expected_series]
# At least one series should exist in both firmware and models
return len(firmware_series) > 0 and len(models_series) > 0
except Exception as e:
print(f"Error validating assets folder: {e}")
return False
def get_assets_folder_info(self):
"""Get information about the configured assets folder."""
if not NODEGRAPH_AVAILABLE:
return {}
try:
folder_path = self.get_property('assets_folder')
if not folder_path:
return {'status': 'not_set', 'message': 'No assets folder selected'}
if not os.path.exists(folder_path):
return {'status': 'invalid', 'message': 'Selected folder does not exist'}
info = {'status': 'valid', 'path': folder_path, 'series': []}
# Get available series
firmware_path = os.path.join(folder_path, 'Firmware')
models_path = os.path.join(folder_path, 'Models')
if os.path.exists(firmware_path):
firmware_series = [d for d in os.listdir(firmware_path)
if os.path.isdir(os.path.join(firmware_path, d))]
info['firmware_series'] = firmware_series
if os.path.exists(models_path):
models_series = [d for d in os.listdir(models_path)
if os.path.isdir(os.path.join(models_path, d))]
info['models_series'] = models_series
# Find common series
if 'firmware_series' in info and 'models_series' in info:
common_series = list(set(info['firmware_series']) & set(info['models_series']))
info['available_series'] = common_series
if not common_series:
info['status'] = 'incomplete'
info['message'] = 'No series found with both firmware and models'
return info
except Exception as e:
return {'status': 'error', 'message': f'Error reading assets folder: {e}'}
class ExactPreprocessNode(BaseNode):
"""Preprocessing node - exact match to original."""
__identifier__ = 'com.cluster.preprocess_node.ExactPreprocessNode'
NODE_NAME = 'Preprocess Node'
def __init__(self):
super().__init__()
if NODEGRAPH_AVAILABLE:
# Setup node connections - exact match
self.add_input('input', multi_input=False, color=(255, 140, 0))
self.add_output('output', color=(0, 255, 0))
self.set_color(45, 126, 72)
# Original properties - exact match
self.create_property('resize_width', 640)
self.create_property('resize_height', 480)
self.create_property('normalize', True)
self.create_property('crop_enabled', False)
self.create_property('operations', 'resize,normalize')
# Original property options - exact match
self._property_options = {
'resize_width': {'min': 64, 'max': 4096},
'resize_height': {'min': 64, 'max': 4096},
'operations': {'placeholder': 'comma-separated: resize,normalize,crop'}
}
# Create custom properties dictionary for UI compatibility
self._populate_custom_properties()
def _populate_custom_properties(self):
"""Populate the custom properties dictionary for UI compatibility."""
if not NODEGRAPH_AVAILABLE:
return
# Get all business properties defined in _property_options
business_props = list(self._property_options.keys())
# Create custom dictionary containing current property values
custom_dict = {}
for prop_name in business_props:
try:
# Skip 'custom' property to avoid infinite recursion
if prop_name != 'custom':
custom_dict[prop_name] = self.get_property(prop_name)
except:
# If property doesn't exist, skip it
pass
# Create the custom property that contains all business properties
self.create_property('custom', custom_dict)
def get_business_properties(self):
"""Get all business properties for serialization."""
if not NODEGRAPH_AVAILABLE:
return {}
properties = {}
for prop_name in self._property_options.keys():
try:
properties[prop_name] = self.get_property(prop_name)
except:
pass
return properties
class ExactPostprocessNode(BaseNode):
"""Postprocessing node - exact match to original."""
__identifier__ = 'com.cluster.postprocess_node.ExactPostprocessNode'
NODE_NAME = 'Postprocess Node'
def __init__(self):
super().__init__()
if NODEGRAPH_AVAILABLE:
# Setup node connections - exact match
self.add_input('input', multi_input=False, color=(255, 140, 0))
self.add_output('output', color=(0, 255, 0))
self.set_color(153, 51, 51)
# Original properties - exact match
self.create_property('output_format', 'JSON')
self.create_property('confidence_threshold', 0.5)
self.create_property('nms_threshold', 0.4)
self.create_property('max_detections', 100)
# Original property options - exact match
self._property_options = {
'output_format': ['JSON', 'XML', 'CSV', 'Binary'],
'confidence_threshold': {'min': 0.0, 'max': 1.0, 'step': 0.1},
'nms_threshold': {'min': 0.0, 'max': 1.0, 'step': 0.1},
'max_detections': {'min': 1, 'max': 1000}
}
# Create custom properties dictionary for UI compatibility
self._populate_custom_properties()
def _populate_custom_properties(self):
"""Populate the custom properties dictionary for UI compatibility."""
if not NODEGRAPH_AVAILABLE:
return
# Get all business properties defined in _property_options
business_props = list(self._property_options.keys())
# Create custom dictionary containing current property values
custom_dict = {}
for prop_name in business_props:
try:
# Skip 'custom' property to avoid infinite recursion
if prop_name != 'custom':
custom_dict[prop_name] = self.get_property(prop_name)
except:
# If property doesn't exist, skip it
pass
# Create the custom property that contains all business properties
self.create_property('custom', custom_dict)
def get_business_properties(self):
"""Get all business properties for serialization."""
if not NODEGRAPH_AVAILABLE:
return {}
properties = {}
for prop_name in self._property_options.keys():
try:
properties[prop_name] = self.get_property(prop_name)
except:
pass
return properties
class ExactOutputNode(BaseNode):
"""Output data sink node - exact match to original."""
__identifier__ = 'com.cluster.output_node.ExactOutputNode'
NODE_NAME = 'Output Node'
def __init__(self):
super().__init__()
if NODEGRAPH_AVAILABLE:
# Setup node connections - exact match
self.add_input('input', multi_input=False, color=(255, 140, 0))
self.set_color(255, 140, 0)
# Original properties - exact match
self.create_property('output_type', 'File')
self.create_property('destination', '')
self.create_property('format', 'JSON')
self.create_property('save_interval', 1.0)
# Original property options - exact match
self._property_options = {
'output_type': ['File', 'API Endpoint', 'Database', 'Display', 'MQTT'],
'format': ['JSON', 'XML', 'CSV', 'Binary'],
'destination': {'type': 'file_path', 'filter': 'Output files (*.json *.xml *.csv *.txt)'},
'save_interval': {'min': 0.1, 'max': 60.0, 'step': 0.1}
}
# Create custom properties dictionary for UI compatibility
self._populate_custom_properties()
def _populate_custom_properties(self):
"""Populate the custom properties dictionary for UI compatibility."""
if not NODEGRAPH_AVAILABLE:
return
# Get all business properties defined in _property_options
business_props = list(self._property_options.keys())
# Create custom dictionary containing current property values
custom_dict = {}
for prop_name in business_props:
try:
# Skip 'custom' property to avoid infinite recursion
if prop_name != 'custom':
custom_dict[prop_name] = self.get_property(prop_name)
except:
# If property doesn't exist, skip it
pass
# Create the custom property that contains all business properties
self.create_property('custom', custom_dict)
def get_business_properties(self):
"""Get all business properties for serialization."""
if not NODEGRAPH_AVAILABLE:
return {}
properties = {}
for prop_name in self._property_options.keys():
try:
properties[prop_name] = self.get_property(prop_name)
except:
pass
return properties
# Export the exact nodes
EXACT_NODE_TYPES = {
'Input Node': ExactInputNode,
'Model Node': ExactModelNode,
'Preprocess Node': ExactPreprocessNode,
'Postprocess Node': ExactPostprocessNode,
'Output Node': ExactOutputNode
}