diff --git a/README.md b/README.md index 9f3058d..a6fe975 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ This project implements an AI image/video generation tool inspired by the node-based workflow editor of Stable Diffusion Web UI. Built as a modern web application using: - **Backend**: Pure Rust (Actix-web framework) -- **Frontend**: React + TypeScript with Node graph visualization +- **Frontend**: React + TypeScript with Node graph visualization (currently having TypeScript version conflict) +- **New Frontend Alternative**: Native Rust frontend using egui - **GPU Acceleration**: ROCm integration for AMD RX 9070 XT GPU ### Key Features: @@ -50,7 +51,7 @@ rocminfo # Should show your GPU info including "gfx900" or similar architectu ### Backend Setup (Rust): ```bash -cd ComfyUI-Rust/backend +cd backend cargo build --release # For production builds, use release mode for better performance on AMD GPUs # Run the backend server: @@ -60,10 +61,10 @@ Backend runs at: http://localhost:[PORT] API endpoints available after starting. ``` -### Frontend Setup (React): +### Frontend Setup (React - currently has TypeScript conflict): ```bash -cd ComfyUI-Rust/frontend +cd frontend npm install # Install dependencies Run dev mode with hot reload and AMD GPU preview support: @@ -73,6 +74,16 @@ yarn start RUST_AMD_ROCM_PATH=/usr/local/AMDROCmlib yarn run prod-build && npm run serve ``` +### New Rust Frontend Setup (egui): + +```bash +cd rust-frontend +cargo run # Run the egui-based frontend + +# Build for release: +cargo build --release +``` + ## Project Structure Overview: ### Rust Backend (`backend/src`): @@ -96,7 +107,7 @@ src/ ``` -### Frontend Web App (`frontend/src`): +### Original Frontend Web App (`frontend/src`): ```typescript/react src/ ├─ components/node-editor.tsx // Node-based workflow editor (graph canvas) @@ -109,6 +120,17 @@ src/ ``` +### New Rust Frontend (`rust-frontend/src`): +```rust +src/ +├── main.rs # Main application entry point +├── node_editor.rs # Node-based workflow editor implementation +├── node_panel.rs # Panel for selecting and managing nodes +├── preview_pane.rs # Preview pane for image results +└── api_client.rs # Backend API communication layer + +``` + ## ROCm Integration Notes: ### Key Components: @@ -116,7 +138,7 @@ src/ - Parallelism configured based on AMD GPU thread count (RX9070XT) ```rust -// Example configuration from rust/backend/src/config.rs +// Example configuration from backend/src/config.rs pub struct Config { pub gpu_backend_config = RocmConfig { // ROCm detection for RX900 series GPUs diff --git a/frontend/package.json b/frontend/package.json index 0fe4e74..c241c59 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,7 @@ "@types/node": "^20.11.0", "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", - "typescript": "^5.3.3" + "typescript": "^4.9.5" }, "browserslist": { "production": [ diff --git a/rust-frontend/Cargo.toml b/rust-frontend/Cargo.toml new file mode 100644 index 0000000..10e22a5 --- /dev/null +++ b/rust-frontend/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "comfyui-rust-frontend" +version = "0.1.0" +edition = "2021" + +[dependencies] +eframe = "0.24" +egui = "0.24" +egui_extras = "0.24" +reqwest = { version = "0.11", features = ["json"] } +tokio = { version = "1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +image = "0.24" +thiserror = "1.0" + +[dependencies.comfyui-backend] +path = "../backend" \ No newline at end of file diff --git a/rust-frontend/README.md b/rust-frontend/README.md new file mode 100644 index 0000000..530e02a --- /dev/null +++ b/rust-frontend/README.md @@ -0,0 +1,59 @@ +# ComfyUI Rust Frontend + +A native Rust frontend for ComfyUI-like AI image generation tool using egui. + +## Features + +- Node-based workflow editor +- Real-time preview pane +- Integration with backend API +- ROCm support through Rust bindings + +## Architecture + +This frontend is built with: +- `eframe` and `egui` for the UI framework +- `reqwest` for HTTP communication with the backend +- `serde` for JSON serialization/deserialization + +## Project Structure + +``` +src/ +├── main.rs # Main application entry point +├── node_editor.rs # Node-based workflow editor implementation +├── node_panel.rs # Panel for selecting and managing nodes +├── preview_pane.rs # Preview pane for image results +└── api_client.rs # Backend API communication layer +``` + +## Running the Application + +1. Make sure you have Rust installed (`rustup` or `curl https://sh.rustup.org/rustup.sh -sSf | sh`) +2. Start the backend server: + ```bash + cd ../backend && cargo run + ``` +3. In another terminal, run the frontend: + ```bash + cd rust-frontend && cargo run + ``` + +## Building + +To build for release: +```bash +cd rust-frontend && cargo build --release +``` + +The executable will be located at `target/release/comfyui-rust-frontend`. + +## Integration with Backend + +This frontend communicates with the backend through HTTP API endpoints. The API client handles: + +- Fetching available node types from `/nodes` +- Executing workflows via POST to `/execute` +- Getting execution results and preview images + +The backend must be running on `http://localhost:8080` by default, but this can be configured in `src/main.rs`. \ No newline at end of file diff --git a/rust-frontend/src/api_client.rs b/rust-frontend/src/api_client.rs new file mode 100644 index 0000000..8329ae8 --- /dev/null +++ b/rust-frontend/src/api_client.rs @@ -0,0 +1,74 @@ +use reqwest; +use serde::{Deserialize, Serialize}; +use std::error::Error; + +#[derive(Debug, Clone)] +pub struct ApiClient { + base_url: String, + client: reqwest::Client, +} + +impl ApiClient { + pub fn new(base_url: String) -> Self { + Self { + base_url, + client: reqwest::Client::new(), + } + } + + // Example API call - in a real implementation, this would be connected to your backend endpoints + pub async fn get_nodes(&self) -> Result, Box> { + let response = self.client + .get(&format!("{}/nodes", self.base_url)) + .send() + .await?; + + let nodes: Vec = response.json().await?; + Ok(nodes) + } + + pub async fn execute_workflow(&self, workflow: &Workflow) -> Result> { + let response = self.client + .post(&format!("{}/execute", self.base_url)) + .json(workflow) + .send() + .await?; + + let result: ExecutionResult = response.json().await?; + Ok(result) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NodeInfo { + pub id: String, + pub name: String, + pub description: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Workflow { + pub nodes: Vec, + pub connections: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct WorkflowNode { + pub id: String, + pub node_type: String, + pub parameters: serde_json::Value, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Connection { + pub from_node: String, + pub from_output: String, + pub to_node: String, + pub to_input: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ExecutionResult { + pub status: String, + pub output: Option, +} \ No newline at end of file diff --git a/rust-frontend/src/lib.rs b/rust-frontend/src/lib.rs new file mode 100644 index 0000000..2e2fbff --- /dev/null +++ b/rust-frontend/src/lib.rs @@ -0,0 +1,12 @@ +//! A Rust frontend for ComfyUI using egui + +pub mod node_editor; +pub mod node_panel; +pub mod preview_pane; +pub mod api_client; + +// Re-export key types for easier access +pub use node_editor::{NodeEditor, Node}; +pub use node_panel::NodePanel; +pub use preview_pane::PreviewPane; +pub use api_client::{ApiClient, Workflow, ExecutionResult}; \ No newline at end of file diff --git a/rust-frontend/src/main.rs b/rust-frontend/src/main.rs new file mode 100644 index 0000000..64762b0 --- /dev/null +++ b/rust-frontend/src/main.rs @@ -0,0 +1,69 @@ +use eframe::egui; +use std::sync::Arc; + +mod node_editor; +mod node_panel; +mod preview_pane; +mod api_client; + +use node_editor::NodeEditor; +use node_panel::NodePanel; +use preview_pane::PreviewPane; +use api_client::ApiClient; + +struct ComfyUIApp { + node_editor: NodeEditor, + node_panel: NodePanel, + preview_pane: PreviewPane, + api_client: ApiClient, +} + +impl ComfyUIApp { + fn new(_cc: &eframe::CreationContext) -> Self { + Self { + node_editor: NodeEditor::new(), + node_panel: NodePanel::new(), + preview_pane: PreviewPane::new(), + api_client: ApiClient::new("http://localhost:8080".to_string()), + } + } +} + +impl eframe::App for ComfyUIApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + // Main layout with three panes + ui.horizontal(|ui| { + // Left panel - Node selection + ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { + self.node_panel.ui(ui); + }); + + // Center panel - Node editor + ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { + self.node_editor.ui(ui, &self.api_client); + }); + + // Right panel - Preview pane + ui.with_layout(egui::Layout::top_down_justified(egui::Align::RIGHT), |ui| { + self.preview_pane.ui(ui, &self.api_client); + }); + }); + }); + } +} + +fn main() -> eframe::Result { + let native_options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_title("ComfyUI Rust Frontend") + .with_inner_size([1200.0, 800.0]), + ..Default::default() + }; + + eframe::run_native( + "ComfyUI Rust Frontend", + native_options, + Box::new(|cc| Box::new(ComfyUIApp::new(cc))), + ) +} \ No newline at end of file diff --git a/rust-frontend/src/node_editor.rs b/rust-frontend/src/node_editor.rs new file mode 100644 index 0000000..fc8ddfa --- /dev/null +++ b/rust-frontend/src/node_editor.rs @@ -0,0 +1,67 @@ +use eframe::egui; +use crate::api_client::ApiClient; + +pub struct NodeEditor { + nodes: Vec, +} + +#[derive(Debug, Clone)] +pub struct Node { + pub id: String, + pub name: String, + pub x: f32, + pub y: f32, + pub inputs: Vec, + pub outputs: Vec, +} + +#[derive(Debug, Clone)] +pub struct Input { + pub id: String, + pub name: String, + pub node_id: String, +} + +#[derive(Debug, Clone)] +pub struct Output { + pub id: String, + pub name: String, + pub node_id: String, +} + +impl NodeEditor { + pub fn new() -> Self { + Self { + nodes: vec![], + } + } + + pub fn ui(&mut self, ui: &mut egui::Ui, api_client: &ApiClient) { + ui.heading("Node Editor"); + + // Create a scroll area for the node editor + egui::ScrollArea::vertical().show(ui, |ui| { + // Placeholder for actual node rendering logic + ui.label("This is where the node-based workflow would be rendered."); + ui.label("Nodes can be dragged and connected here."); + + // Simple example of a node + if ui.button("Add Node").clicked() { + let new_node = Node { + id: format!("node_{}", self.nodes.len()), + name: "New Node".to_string(), + x: 100.0 + (self.nodes.len() as f32 * 50.0), + y: 100.0, + inputs: vec![], + outputs: vec![], + }; + self.nodes.push(new_node); + } + + // Display existing nodes + for node in &self.nodes { + ui.label(format!("Node: {} at ({}, {})", node.name, node.x, node.y)); + } + }); + } +} \ No newline at end of file diff --git a/rust-frontend/src/node_panel.rs b/rust-frontend/src/node_panel.rs new file mode 100644 index 0000000..53d5b8e --- /dev/null +++ b/rust-frontend/src/node_panel.rs @@ -0,0 +1,47 @@ +use eframe::egui; + +pub struct NodePanel { + selected_node_type: Option, +} + +impl NodePanel { + pub fn new() -> Self { + Self { + selected_node_type: None, + } + } + + pub fn ui(&mut self, ui: &mut egui::Ui) { + ui.set_min_width(200.0); + ui.heading("Node Panel"); + + ui.separator(); + + // Available node types + ui.label("Available Nodes:"); + + let node_types = vec![ + "Image Loader", + "Text Input", + "Image Generator", + "Image Save", + "Conditional Node", + "Loop Node" + ]; + + for node_type in node_types { + if ui.button(node_type).clicked() { + self.selected_node_type = Some(node_type.to_string()); + } + } + + ui.separator(); + + // Selected node info + if let Some(selected) = &self.selected_node_type { + ui.label(format!("Selected: {}", selected)); + } else { + ui.label("No node selected"); + } + } +} \ No newline at end of file diff --git a/rust-frontend/src/preview_pane.rs b/rust-frontend/src/preview_pane.rs new file mode 100644 index 0000000..ec0bb3f --- /dev/null +++ b/rust-frontend/src/preview_pane.rs @@ -0,0 +1,53 @@ +use eframe::egui; +use crate::api_client::ApiClient; + +pub struct PreviewPane { + image_data: Option>, + image_name: String, +} + +impl PreviewPane { + pub fn new() -> Self { + Self { + image_data: None, + image_name: "No image".to_string(), + } + } + + pub fn ui(&mut self, ui: &mut egui::Ui, api_client: &ApiClient) { + ui.set_min_width(300.0); + ui.heading("Preview Pane"); + + ui.separator(); + + // Display current preview info + ui.label(format!("Image: {}", self.image_name)); + + if let Some(data) = &self.image_data { + // Try to display the image (simplified) + ui.label("Image would be displayed here"); + + // Placeholder for actual image display logic + ui.horizontal(|ui| { + if ui.button("Load Sample Image").clicked() { + self.image_name = "sample_output.png".to_string(); + // In a real implementation, this would load actual image data + self.image_data = Some(vec![0; 1024]); + } + }); + } else { + ui.label("No preview available"); + + if ui.button("Generate Preview").clicked() { + self.image_name = "generated_output.png".to_string(); + // In a real implementation, this would fetch actual image data from backend + self.image_data = Some(vec![0; 1024]); + } + } + + ui.separator(); + + // Status info + ui.label("Status: Ready"); + } +} \ No newline at end of file