Rewrite README with node documentation, remove .pyc files and spec/planning docs

This commit is contained in:
2026-04-22 20:10:09 -07:00
parent a6b1d1027c
commit 7c27eed171
6 changed files with 80 additions and 468 deletions

View File

@@ -1,240 +0,0 @@
# Implementation Plan: Compass Image Loader
This document provides a step-by-step implementation plan for the `CompassImageLoader` ComfyUI custom node. Each task is a concrete, actionable item with a checkbox.
---
## Phase 1: Project Bootstrap & File Structure
**Goal:** Set up the custom node package so ComfyUI recognizes and registers the node.
- [ ] **Create `__init__.py`**
- [ ] Import `CompassImageLoader` from `compass_image_loader`.
- [ ] Define `NODE_CLASS_MAPPINGS = {"CompassImageLoader": CompassImageLoader}`.
- [ ] Define `NODE_DISPLAY_NAME_MAPPINGS = {"CompassImageLoader": "Compass Image Loader"}`.
- [ ] **Create `compass_image_loader.py`**
- [ ] Define the `CompassImageLoader` class.
- [ ] Set `CATEGORY = "image/loaders"`.
---
## Phase 2: Backend Node Implementation
**Goal:** Implement the Python class that ComfyUI executes.
### 2.1 Input Types & Discovery
- [ ] Implement `CompassImageLoader.INPUT_TYPES(cls)` classmethod.
- [ ] Implement `_discover_directories()` helper that:
- [ ] Gets the base path via `folder_paths.get_input_directory()`.
- [ ] Recursively walks the base path using `os.walk`.
- [ ] Identifies valid direction names: `{"n", "ne", "e", "se", "s", "sw", "w", "nw"}`.
- [ ] Identifies valid modality names: `{"image", "depth", "openpose"}`.
- [ ] For each directory, if it contains **any** valid direction subfolder **or** any valid modality subfolder, add its relative path (from `input/`) to the candidate list.
- [ ] Return sorted, deduplicated list of strings.
- [ ] Return dictionary with:
- `required`:
- `directory`: `(directories_list,)`
- `direction`: `(["", "n", "ne", "e", "se", "s", "sw", "w", "nw"],)`
- `modality`: `(["image", "depth", "openpose"],)`
- `frame`: `("STRING", {"default": ""})`
- `width`: `("INT", {"default": 0, "min": 0, "max": 16384, "step": 1})`
- `height`: `("INT", {"default": 0, "min": 0, "max": 16384, "step": 1})`
### 2.2 Return Types & Function Signature
- [ ] Set `RETURN_TYPES = ("IMAGE", "STRING", "INT", "INT", "INT")`.
- [ ] Set `RETURN_NAMES = ("IMAGE", "path", "width", "height", "frame_count")`.
- [ ] Set `FUNCTION = "load_images"`.
- [ ] Set `OUTPUT_NODE = False`.
### 2.3 Core Loading Logic
- [ ] Implement `load_images(self, directory, direction, modality, frame, width, height)`.
#### Path Resolution
- [ ] Resolve `base_dir = folder_paths.get_input_directory()`.
- [ ] Construct `target_dir`:
- [ ] If `direction.strip()`: `os.path.join(base_dir, directory, direction, modality)`.
- [ ] Else: `os.path.join(base_dir, directory, modality)`.
- [ ] Verify `target_dir` exists; if not, raise `RuntimeError`.
#### Image Discovery
- [ ] List files in `target_dir` with `os.listdir`.
- [ ] Filter to supported image extensions: `png`, `jpg`, `jpeg`, `webp`, `bmp`, `gif`, `tiff`.
- [ ] Sort alphabetically: `sorted(files)`.
- [ ] If list is empty, raise `RuntimeError`.
#### Frame Selection
- [ ] If `frame` is `None` or `frame.strip() == ""`:
- [ ] Select all images (`selected_files = all_files`).
- [ ] Set `output_path = target_dir`.
- [ ] Else:
- [ ] Try `index = int(frame.strip())`.
- [ ] If `index < 0` or `index >= len(all_files)`, raise `RuntimeError`.
- [ ] Select single file: `selected_files = [all_files[index]]`.
- [ ] Set `output_path = os.path.join(target_dir, all_files[index])`.
#### Image Processing Loop
- [ ] Initialize empty list `tensors = []`.
- [ ] For each file in `selected_files`:
- [ ] Open with `Image.open(path)`.
- [ ] Convert to RGB: `image = image.convert("RGB")`.
- [ ] Read original dimensions: `orig_w, orig_h = image.size`.
- [ ] Determine final dimensions and resize:
- [ ] If `width == 0 and height == 0`:
- [ ] `final_w, final_h = orig_w, orig_h` (no resize).
- [ ] Elif `width > 0 and height == 0`:
- [ ] `final_w = width`.
- [ ] `final_h = int(orig_h * (width / orig_w))`.
- [ ] `image = image.resize((final_w, final_h), Image.Resampling.LANCZOS)`.
- [ ] Elif `height > 0 and width == 0`:
- [ ] `final_h = height`.
- [ ] `final_w = int(orig_w * (height / orig_h))`.
- [ ] `image = image.resize((final_w, final_h), Image.Resampling.LANCZOS)`.
- [ ] Else (both non-zero):
- [ ] `scale = max(width / orig_w, height / orig_h)`.
- [ ] `new_w = int(orig_w * scale)`.
- [ ] `new_h = int(orig_h * scale)`.
- [ ] `image = image.resize((new_w, new_h), Image.Resampling.LANCZOS)`.
- [ ] Compute crop box:
- [ ] `left = (new_w - width) // 2`
- [ ] `top = (new_h - height) // 2`
- [ ] `right = left + width`
- [ ] `bottom = top + height`
- [ ] `image = image.crop((left, top, right, bottom))`.
- [ ] `final_w, final_h = width, height`.
- [ ] Convert to NumPy array:
- [ ] `np_image = np.array(image).astype(np.float32) / 255.0`
- [ ] Convert to Torch tensor:
- [ ] `tensor = torch.from_numpy(np_image)[None,]` # shape [1, H, W, 3]
- [ ] Append to `tensors`.
#### Batch Assembly
- [ ] If `len(tensors) == 1`:
- [ ] `image_batch = tensors[0]`
- [ ] Else:
- [ ] `image_batch = torch.cat(tensors, dim=0)`
#### Return
- [ ] Return `(image_batch, output_path, final_w, final_h, len(selected_files))`.
### 2.4 Change Detection
- [ ] Implement `IS_CHANGED(self, directory, direction, modality, frame, width, height)`:
- [ ] Compute `target_dir` as above.
- [ ] If frame is empty, hash the sorted filenames list + mtime.
- [ ] If frame is specified, hash the single file content.
- [ ] Include `width` and `height` in the hash (so resizing triggers re-execution).
---
## Phase 3: Frontend Extension
**Goal:** Provide an enhanced UX for the `directory` combo widget (typing to filter / autocomplete).
- [ ] **Create `js/compass_image_loader.js`**
- [ ] Import `app` from `scripts/app.js`.
- [ ] Import `api` from `scripts/api.js` (for event listeners if needed).
- [ ] **Register Extension**
- [ ] Call `app.registerExtension({ name: "CompassImageLoader", ... })`.
- [ ] **Override `beforeRegisterNodeDef`**
- [ ] Check `passedNodeData.name === "CompassImageLoader"`.
- [ ] Save reference to original `onNodeCreated`.
- [ ] Replace with a wrapper that:
- [ ] Calls the original `onNodeCreated`.
- [ ] Finds the `directory` widget by name.
- [ ] Stores the original full list of combo values on the widget instance (e.g., `widget._all_values`).
- [ ] Attaches a keyup/input listener to the widget's DOM element (`widget.inputEl` or equivalent) to implement filtering.
- [ ] When the user types, filter `_all_values` case-insensitively and update `widget.options.values`.
- [ ] If the current `widget.value` is not in the filtered list, leave it as-is (allow free-form or preserve selection).
- [ ] **Handle Refresh**
- [ ] Listen for `fresh-node-defs` event from `api`.
- [ ] On event, iterate over all nodes in `app.graph._nodes`.
- [ ] For each `CompassImageLoader` node:
- [ ] Find the `directory` widget.
- [ ] Update `widget._all_values` and `widget.options.values` with the new list from the refreshed node definition.
- [ ] If the currently selected value still exists in the new list, keep it.
- [ ] If it does not exist, clear it to `""` (or first available).
- [ ] **State Preservation**
- [ ] Ensure the directory value is correctly serialized in the workflow JSON (ComfyUI handles this automatically for widgets, but verify the filtered list does not override the saved value on `configure`).
---
## Phase 4: Integration & Packaging
- [ ] Verify file layout:
```
ComfyUI/custom_nodes/ComfyUI-compass-paths/
__init__.py
compass_image_loader.py
js/
compass_image_loader.js
```
- [ ] Ensure `__init__.py` is minimal and correct.
- [ ] Restart ComfyUI and verify the node appears in the node search under `image/loaders`.
---
## Phase 5: Testing & Validation
Run the following manual tests in the ComfyUI interface.
- [ ] **Test 1: Single Frame Load**
- [ ] Set `directory`, `direction=s`, `modality=depth`, `frame=0`, `width=0`, `height=0`.
- [ ] Queue. Verify output `IMAGE` shape is `[1, H, W, 3]`, `frame_count=1`, `path` is the full file path.
- [ ] **Test 2: Batch Load**
- [ ] Leave `frame` blank.
- [ ] Queue. Verify `IMAGE` shape is `[N, H, W, 3]` where `N > 1`, `frame_count=N`, `path` is the directory path.
- [ ] **Test 3: Cover + Crop Resize**
- [ ] Set `width=512`, `height=768` on a non-square source image.
- [ ] Verify output `width=512`, `height=768`, and image appears center-cropped.
- [ ] **Test 4: Proportional Resize (Width Only)**
- [ ] Set `width=512`, `height=0`.
- [ ] Verify output width=512 and height is proportionally scaled (no cropping).
- [ ] **Test 5: Proportional Resize (Height Only)**
- [ ] Set `width=0`, `height=768`.
- [ ] Verify output height=768 and width is proportionally scaled (no cropping).
- [ ] **Test 6: No Direction**
- [ ] Set `direction` to blank.
- [ ] Verify node loads from `input/<directory>/<modality>/` (no direction folder).
- [ ] **Test 7: Error — Missing Directory**
- [ ] Set `directory` to a non-existent path.
- [ ] Queue. Verify a clear `RuntimeError` is raised.
- [ ] **Test 8: Error — Empty Image Folder**
- [ ] Point to a valid directory/modality path that has no images.
- [ ] Queue. Verify a clear `RuntimeError` is raised.
- [ ] **Test 9: Error — Out of Bounds Frame**
- [ ] Set `frame` to a number larger than the number of images minus one.
- [ ] Queue. Verify a clear `RuntimeError` is raised.
- [ ] **Test 10: Refresh Updates Combo**
- [ ] Add a new valid folder under `input/` while ComfyUI is running.
- [ ] Click the ComfyUI Refresh button.
- [ ] Open the node's `directory` dropdown. Verify the new folder appears.
- [ ] **Test 11: Autocomplete Filtering**
- [ ] Click the `directory` combo and type a substring (e.g., `walk`).
- [ ] Verify the dropdown filters to show only matching entries.
---
## Future Enhancements (Out of Scope for Initial Implementation)
- Support for recursive image loading across subdirectories.
- Caching of directory scans to improve performance on very large `input/` trees.
- Support for additional modalities beyond `image`, `depth`, `openpose` via a configuration file.
- Drag-and-drop folder path into the directory widget.