831 lines
21 KiB
Markdown
831 lines
21 KiB
Markdown
# Smart Metadata Reader Phase 1 Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Build the Python-only MVP for `Smart Metadata Reader`, a ComfyUI image-loader node that loads an image, reads metadata, extracts real positive and negative prompt text through ComfyUI graph traversal, and outputs common image and settings ports.
|
|
|
|
**Architecture:** The ComfyUI node in `nodes.py` will stay thin. It will call a testable parser package under `smart_metadata_reader/`. Metadata reading, graph indexing, sampler selection, conditioning resolution, prompt merging, LoRA extraction, settings formatting, and A1111 fallback will be separate modules with dataclass-based interfaces.
|
|
|
|
**Tech Stack:** Python 3, Pillow, torch tensors for ComfyUI `IMAGE` and `MASK` outputs, pytest for parser tests, standard-library `json`, `dataclasses`, `pathlib`, and `typing`.
|
|
|
|
---
|
|
|
|
## Phase 1 Scope
|
|
|
|
Phase 1 includes:
|
|
|
|
- Python-only `Smart Metadata Reader` node.
|
|
- Image selection and upload through ComfyUI `image_upload=True`.
|
|
- Metadata reading from PNG, JPEG, and WEBP.
|
|
- Image and mask tensor output.
|
|
- ComfyUI `prompt` and `workflow` parsing.
|
|
- Final sampler selection.
|
|
- Positive and negative conditioning graph traversal.
|
|
- Initial adapters for CLIP text, pysssss text nodes, generic text nodes, conditioning passthrough nodes, samplers, loaders, and LoRA loaders.
|
|
- Human-readable `setting` output.
|
|
- A1111/Forge `parameters` fallback for common fields.
|
|
- pytest coverage for the parser core and node contract.
|
|
|
|
Phase 1 excludes:
|
|
|
|
- Full frontend JavaScript node layout.
|
|
- Embedded large read-only positive, negative, and setting text boxes.
|
|
- Civitai hash calculation.
|
|
- Metadata writing.
|
|
- Exhaustive custom node support.
|
|
|
|
## File Map
|
|
|
|
Create these files:
|
|
|
|
```text
|
|
__init__.py
|
|
nodes.py
|
|
requirements.txt
|
|
pyproject.toml
|
|
smart_metadata_reader/__init__.py
|
|
smart_metadata_reader/models.py
|
|
smart_metadata_reader/image_io.py
|
|
smart_metadata_reader/metadata_reader.py
|
|
smart_metadata_reader/graph.py
|
|
smart_metadata_reader/sampler_selector.py
|
|
smart_metadata_reader/resolver.py
|
|
smart_metadata_reader/prompt_merge.py
|
|
smart_metadata_reader/lora_extractor.py
|
|
smart_metadata_reader/settings_formatter.py
|
|
smart_metadata_reader/a1111_parser.py
|
|
smart_metadata_reader/trace.py
|
|
smart_metadata_reader/adapters/__init__.py
|
|
smart_metadata_reader/adapters/base.py
|
|
smart_metadata_reader/adapters/clip_text.py
|
|
smart_metadata_reader/adapters/text_sources.py
|
|
smart_metadata_reader/adapters/pysssss.py
|
|
smart_metadata_reader/adapters/conditioning.py
|
|
smart_metadata_reader/adapters/samplers.py
|
|
smart_metadata_reader/adapters/loaders.py
|
|
tests/conftest.py
|
|
tests/fixtures.py
|
|
tests/test_metadata_reader.py
|
|
tests/test_graph.py
|
|
tests/test_sampler_selector.py
|
|
tests/test_conditioning_resolver.py
|
|
tests/test_text_adapters.py
|
|
tests/test_lora_extractor.py
|
|
tests/test_node_id_independence.py
|
|
tests/test_node_contract.py
|
|
```
|
|
|
|
## Core Interfaces
|
|
|
|
`smart_metadata_reader/models.py` will define:
|
|
|
|
```python
|
|
@dataclass
|
|
class MetadataBundle:
|
|
filename: str
|
|
width: int
|
|
height: int
|
|
prompt_raw: str | None
|
|
workflow_raw: str | None
|
|
parameters_raw: str | None
|
|
source_format: str
|
|
|
|
@dataclass
|
|
class NodeRecord:
|
|
node_id: str
|
|
class_type: str
|
|
inputs: dict[str, Any]
|
|
widgets_values: list[Any] | dict[str, Any] | None = None
|
|
|
|
@dataclass
|
|
class PromptSegment:
|
|
text: str
|
|
node_id: str | None
|
|
class_type: str | None
|
|
field: str | None
|
|
path: list[str]
|
|
confidence: float = 1.0
|
|
|
|
@dataclass
|
|
class LoraInfo:
|
|
node_id: str
|
|
class_type: str
|
|
lora_name: str
|
|
strength_model: float | None
|
|
strength_clip: float | None
|
|
path: list[str]
|
|
|
|
@dataclass
|
|
class ParseResult:
|
|
positive: str
|
|
negative: str
|
|
seed: int
|
|
steps: int
|
|
cfg: float
|
|
width: int
|
|
height: int
|
|
model_name: str
|
|
filename: str
|
|
setting: str
|
|
vae_name: str
|
|
sampler_name: str
|
|
scheduler: str
|
|
loras: list[LoraInfo]
|
|
raw_prompt_json: str
|
|
raw_workflow_json: str
|
|
partial_result: dict[str, Any]
|
|
debug_trace: str
|
|
confidence: float
|
|
status_message: str
|
|
```
|
|
|
|
`smart_metadata_reader/graph.py` will define:
|
|
|
|
- `GraphIndex.__init__(prompt: dict[str, Any] | None, workflow: dict[str, Any] | None)`: normalize prompt and workflow nodes.
|
|
- `GraphIndex.get_node(node_id: str) -> NodeRecord | None`: return a normalized node by string ID.
|
|
- `GraphIndex.is_link(value: Any) -> bool`: detect ComfyUI link references such as `[node_id, output_index]`.
|
|
- `GraphIndex.link_target(value: Any) -> tuple[str, int] | None`: return the linked node ID and output index.
|
|
- `GraphIndex.upstream_link_inputs(node_id: str) -> list[tuple[str, Any]]`: list linked inputs for a node.
|
|
- `GraphIndex.downstream_nodes(node_id: str) -> list[NodeRecord]`: list nodes that consume the node.
|
|
- `GraphIndex.workflow_cache_for(node_id: str, field_names: list[str]) -> str | None`: read supplemental cache text from workflow metadata.
|
|
|
|
`smart_metadata_reader/resolver.py` will define:
|
|
|
|
- `ConditioningResolver.__init__(graph: GraphIndex, adapters: AdapterRegistry, max_depth: int, prefer_cached_text: bool)`: store resolver dependencies and traversal limits.
|
|
- `ConditioningResolver.resolve_positive(sampler: NodeRecord) -> list[PromptSegment]`: resolve `sampler.inputs.positive`.
|
|
- `ConditioningResolver.resolve_negative(sampler: NodeRecord) -> list[PromptSegment]`: resolve `sampler.inputs.negative`.
|
|
- `ConditioningResolver.resolve_value(value: Any, role: str, path: list[str], depth: int) -> list[PromptSegment]`: resolve strings, links, text nodes, and conditioning passthrough nodes.
|
|
|
|
`nodes.py` will define:
|
|
|
|
```python
|
|
class SmartMetadataReader:
|
|
CATEGORY = "metadata/image"
|
|
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "STRING", "INT", "INT", "FLOAT", "INT", "INT", "STRING", "STRING", "STRING")
|
|
RETURN_NAMES = ("image", "mask", "positive", "negative", "seed", "steps", "cfg", "width", "height", "model_name", "filename", "setting")
|
|
FUNCTION = "read_metadata"
|
|
```
|
|
|
|
## Task 1: Project Bootstrap
|
|
|
|
**Files:**
|
|
|
|
- Create: `requirements.txt`
|
|
- Create: `pyproject.toml`
|
|
- Create: `__init__.py`
|
|
- Create: `smart_metadata_reader/__init__.py`
|
|
- Create: `smart_metadata_reader/adapters/__init__.py`
|
|
|
|
- [ ] **Step 1: Initialize Git when implementation starts**
|
|
|
|
Run from `F:\AI\AIProjects\ComfyUI-Smart-Metadata-Reader`:
|
|
|
|
```powershell
|
|
git init
|
|
git branch -M main
|
|
git remote add origin https://github.com/CyberShadowX/ComfyUI-Smart-Metadata-Reader.git
|
|
git status --short --branch
|
|
```
|
|
|
|
Expected: repository on branch `main` with untracked files only.
|
|
|
|
- [ ] **Step 2: Create package metadata**
|
|
|
|
`requirements.txt`:
|
|
|
|
```text
|
|
Pillow>=10.0.0
|
|
```
|
|
|
|
`pyproject.toml`:
|
|
|
|
```toml
|
|
[project]
|
|
name = "comfyui-smart-metadata-reader"
|
|
version = "0.1.0"
|
|
description = "ComfyUI image loader node that reads AI image metadata and traces real prompt conditioning."
|
|
requires-python = ">=3.10"
|
|
dependencies = ["Pillow>=10.0.0"]
|
|
|
|
[tool.pytest.ini_options]
|
|
testpaths = ["tests"]
|
|
pythonpath = ["."]
|
|
```
|
|
|
|
- [ ] **Step 3: Register the node package**
|
|
|
|
`__init__.py` will export:
|
|
|
|
```python
|
|
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
|
|
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
|
```
|
|
|
|
- [ ] **Step 4: Run bootstrap check**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m compileall .
|
|
```
|
|
|
|
Expected: Python files compile.
|
|
|
|
## Task 2: Data Models
|
|
|
|
**Files:**
|
|
|
|
- Create: `smart_metadata_reader/models.py`
|
|
- Test: `tests/test_graph.py`
|
|
|
|
- [ ] **Step 1: Write model import test**
|
|
|
|
Create a test that imports `MetadataBundle`, `NodeRecord`, `PromptSegment`, `LoraInfo`, and `ParseResult`.
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_graph.py -q
|
|
```
|
|
|
|
Expected before implementation: import failure.
|
|
|
|
- [ ] **Step 2: Add dataclasses**
|
|
|
|
Create the dataclasses listed in the Core Interfaces section. Use explicit defaults for unresolved scalar values:
|
|
|
|
```python
|
|
seed = -1
|
|
steps = 0
|
|
cfg = 0.0
|
|
width = 0
|
|
height = 0
|
|
```
|
|
|
|
- [ ] **Step 3: Run model test**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_graph.py -q
|
|
```
|
|
|
|
Expected: imports pass.
|
|
|
|
## Task 3: Image and Metadata Reading
|
|
|
|
**Files:**
|
|
|
|
- Create: `smart_metadata_reader/image_io.py`
|
|
- Create: `smart_metadata_reader/metadata_reader.py`
|
|
- Test: `tests/test_metadata_reader.py`
|
|
|
|
- [ ] **Step 1: Write PNG metadata test**
|
|
|
|
Create a synthetic PNG in the test using Pillow with text chunks:
|
|
|
|
```python
|
|
pnginfo.add_text("prompt", json.dumps(prompt_graph))
|
|
pnginfo.add_text("workflow", json.dumps(workflow_graph))
|
|
pnginfo.add_text("parameters", "positive\nNegative prompt: negative\nSteps: 20, Sampler: Euler, CFG scale: 7, Seed: 123, Size: 512x768")
|
|
```
|
|
|
|
Assert that `read_metadata(path)` returns `prompt_raw`, `workflow_raw`, `parameters_raw`, `width`, `height`, and `filename`.
|
|
|
|
- [ ] **Step 2: Implement `read_metadata`**
|
|
|
|
`metadata_reader.py` will expose:
|
|
|
|
Signature: `read_metadata(image_path: str | Path) -> MetadataBundle`.
|
|
|
|
Use Pillow `Image.open`. Read `image.info` keys `prompt`, `workflow`, and `parameters`.
|
|
|
|
- [ ] **Step 3: Implement image tensor conversion**
|
|
|
|
`image_io.py` will expose:
|
|
|
|
Signature: `load_image_and_mask(image_path: str | Path) -> tuple[Any, Any, int, int]`.
|
|
|
|
Return ComfyUI-compatible image tensor shape `[1, H, W, 3]` and mask shape `[1, H, W]`.
|
|
|
|
- [ ] **Step 4: Run metadata tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_metadata_reader.py -q
|
|
```
|
|
|
|
Expected: metadata and dimensions are read from the synthetic image.
|
|
|
|
## Task 4: Graph Index
|
|
|
|
**Files:**
|
|
|
|
- Create: `smart_metadata_reader/graph.py`
|
|
- Test: `tests/test_graph.py`
|
|
|
|
- [ ] **Step 1: Write graph tests**
|
|
|
|
Cover:
|
|
|
|
- API prompt format with node IDs as dictionary keys.
|
|
- UI workflow format with `nodes` array.
|
|
- Link detection for `["6", 0]` and `[6, 0]`.
|
|
- `workflow_cache_for` reading cached `text_0` from workflow node inputs or widget values.
|
|
|
|
- [ ] **Step 2: Implement `GraphIndex`**
|
|
|
|
Normalize all node IDs to strings. Store prompt nodes as primary records. Attach workflow supplemental data by matching node ID.
|
|
|
|
- [ ] **Step 3: Run graph tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_graph.py -q
|
|
```
|
|
|
|
Expected: graph lookup, link parsing, and workflow cache lookup pass.
|
|
|
|
## Task 5: Prompt Merge and Trace
|
|
|
|
**Files:**
|
|
|
|
- Create: `smart_metadata_reader/prompt_merge.py`
|
|
- Create: `smart_metadata_reader/trace.py`
|
|
- Test: `tests/test_text_adapters.py`
|
|
|
|
- [ ] **Step 1: Write prompt merge tests**
|
|
|
|
Assert:
|
|
|
|
- Empty segments are removed.
|
|
- Extra commas from joins are collapsed.
|
|
- LoRA trigger words and tags remain unchanged.
|
|
- Repeated meaningful tokens remain.
|
|
|
|
- [ ] **Step 2: Implement `merge_prompt_segments`**
|
|
|
|
Expose:
|
|
|
|
Signature: `merge_prompt_segments(segments: list[PromptSegment]) -> str`.
|
|
|
|
Only perform formatting cleanup allowed by `docs/spec.md`.
|
|
|
|
- [ ] **Step 3: Implement trace collector**
|
|
|
|
Expose:
|
|
|
|
`TraceCollector` methods:
|
|
|
|
- `add(section: str, line: str) -> None`
|
|
- `render() -> str`
|
|
|
|
- [ ] **Step 4: Run merge tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_text_adapters.py -q
|
|
```
|
|
|
|
Expected: formatting cleanup passes without deleting trigger words.
|
|
|
|
## Task 6: Adapter Registry and Text Adapters
|
|
|
|
**Files:**
|
|
|
|
- Create: `smart_metadata_reader/adapters/base.py`
|
|
- Create: `smart_metadata_reader/adapters/clip_text.py`
|
|
- Create: `smart_metadata_reader/adapters/text_sources.py`
|
|
- Create: `smart_metadata_reader/adapters/pysssss.py`
|
|
- Test: `tests/test_text_adapters.py`
|
|
|
|
- [ ] **Step 1: Write adapter tests**
|
|
|
|
Cover:
|
|
|
|
- `CLIPTextEncode.inputs.text` direct string.
|
|
- `CLIPTextEncode.inputs.text` linked to generic text node.
|
|
- `CLIPTextEncodeSDXL` with multiple text fields.
|
|
- `StringFunction|pysssss` append from `text_a`, `text_b`, `text_c`.
|
|
- `ShowText|pysssss` prefers `text_0` cache.
|
|
- `DeepTranslatorTextNode.inputs.text`.
|
|
|
|
- [ ] **Step 2: Implement base adapter protocol**
|
|
|
|
Expose:
|
|
|
|
`AdapterRegistry` methods:
|
|
|
|
- `register(adapter: NodeAdapter) -> None`
|
|
- `adapter_for(node: NodeRecord) -> NodeAdapter | None`
|
|
|
|
`NodeAdapter` protocol methods:
|
|
|
|
- `matches(node: NodeRecord) -> bool`
|
|
|
|
- [ ] **Step 3: Implement text adapters**
|
|
|
|
Text adapters return `PromptSegment` instances and recursively call the resolver for linked input values.
|
|
|
|
- [ ] **Step 4: Run text adapter tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_text_adapters.py -q
|
|
```
|
|
|
|
Expected: all text source cases pass.
|
|
|
|
## Task 7: Conditioning Resolver
|
|
|
|
**Files:**
|
|
|
|
- Create: `smart_metadata_reader/adapters/conditioning.py`
|
|
- Create: `smart_metadata_reader/resolver.py`
|
|
- Test: `tests/test_conditioning_resolver.py`
|
|
|
|
- [ ] **Step 1: Write resolver tests**
|
|
|
|
Cover:
|
|
|
|
- Simple `KSampler -> CLIPTextEncode`.
|
|
- `ControlNetApply` passthrough.
|
|
- `ControlNetApplyAdvanced` passthrough.
|
|
- `ConditioningCombine` two-branch merge.
|
|
- Multiple `CLIPTextEncode` nodes entering positive.
|
|
- Unknown passthrough produces partial result and trace entry.
|
|
|
|
- [ ] **Step 2: Implement conditioning adapter**
|
|
|
|
Support fields:
|
|
|
|
```python
|
|
[
|
|
"conditioning",
|
|
"positive",
|
|
"negative",
|
|
"positive_conditioning",
|
|
"negative_conditioning",
|
|
"base_positive",
|
|
"base_negative",
|
|
]
|
|
```
|
|
|
|
Branch adapters must resolve all conditioning-like linked inputs.
|
|
|
|
- [ ] **Step 3: Implement depth and cycle guards**
|
|
|
|
`ConditioningResolver` must stop at `max_depth` and track visited `(node_id, field, role)` keys.
|
|
|
|
- [ ] **Step 4: Run resolver tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_conditioning_resolver.py -q
|
|
```
|
|
|
|
Expected: passthrough and branch resolution pass, unknown nodes produce partial data.
|
|
|
|
## Task 8: Sampler Selection and Parameter Extraction
|
|
|
|
**Files:**
|
|
|
|
- Create: `smart_metadata_reader/adapters/samplers.py`
|
|
- Create: `smart_metadata_reader/sampler_selector.py`
|
|
- Test: `tests/test_sampler_selector.py`
|
|
|
|
- [ ] **Step 1: Write sampler selector tests**
|
|
|
|
Cover:
|
|
|
|
- `SaveImage <- VAEDecode <- KSampler`.
|
|
- `PreviewImage <- VAEDecode <- KSampler`.
|
|
- Multiple samplers where only one reaches final output.
|
|
- Fallback when no output node exists.
|
|
|
|
- [ ] **Step 2: Implement sampler candidates**
|
|
|
|
Initial candidate classes:
|
|
|
|
```python
|
|
{"KSampler", "KSamplerAdvanced", "SamplerCustom", "SamplerCustomAdvanced"}
|
|
```
|
|
|
|
- [ ] **Step 3: Implement output-chain selection**
|
|
|
|
Walk backward from image output nodes through image, latent, sample, and VAE decode fields. Select nearest upstream sampler.
|
|
|
|
- [ ] **Step 4: Extract sampler values**
|
|
|
|
Expose:
|
|
|
|
Signature: `extract_sampler_settings(sampler: NodeRecord) -> dict[str, Any]`.
|
|
|
|
Return seed, steps, cfg, sampler_name, scheduler, and denoise when present.
|
|
|
|
- [ ] **Step 5: Run sampler tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_sampler_selector.py -q
|
|
```
|
|
|
|
Expected: final output sampler is selected and fallback is flagged with reduced confidence.
|
|
|
|
## Task 9: Loader and LoRA Extraction
|
|
|
|
**Files:**
|
|
|
|
- Create: `smart_metadata_reader/adapters/loaders.py`
|
|
- Create: `smart_metadata_reader/lora_extractor.py`
|
|
- Test: `tests/test_lora_extractor.py`
|
|
|
|
- [ ] **Step 1: Write loader tests**
|
|
|
|
Cover:
|
|
|
|
- `CheckpointLoaderSimple` model name.
|
|
- `VAELoader` VAE name.
|
|
- One `LoraLoader`.
|
|
- Multiple chained `LoraLoader` nodes.
|
|
- LoRA trigger words in text remain in positive prompt.
|
|
|
|
- [ ] **Step 2: Implement loader extraction**
|
|
|
|
Expose:
|
|
|
|
Required functions:
|
|
|
|
- `extract_model_name(graph: GraphIndex, sampler: NodeRecord) -> str`
|
|
- `extract_vae_name(graph: GraphIndex) -> str`
|
|
- `extract_loras(graph: GraphIndex, sampler: NodeRecord) -> list[LoraInfo]`
|
|
|
|
LoRA extraction follows model and clip chains but never filters prompt text.
|
|
|
|
- [ ] **Step 3: Run LoRA tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_lora_extractor.py -q
|
|
```
|
|
|
|
Expected: LoRA loader info is exported and trigger words remain in prompt output.
|
|
|
|
## Task 10: A1111 and Forge Fallback
|
|
|
|
**Files:**
|
|
|
|
- Create: `smart_metadata_reader/a1111_parser.py`
|
|
- Test: `tests/test_metadata_reader.py`
|
|
|
|
- [ ] **Step 1: Write fallback tests**
|
|
|
|
Input:
|
|
|
|
```text
|
|
masterpiece, character trigger
|
|
Negative prompt: blurry, low quality
|
|
Steps: 25, Sampler: Euler a, CFG scale: 7.5, Seed: 12345, Size: 768x1024, Model: demo_model
|
|
```
|
|
|
|
Assert positive, negative, seed, steps, cfg, sampler, size, and model.
|
|
|
|
- [ ] **Step 2: Implement `parse_a1111_parameters`**
|
|
|
|
Expose:
|
|
|
|
Signature: `parse_a1111_parameters(parameters: str, parameter_index: int = 0) -> ParseResult`.
|
|
|
|
- [ ] **Step 3: Run fallback tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_metadata_reader.py -q
|
|
```
|
|
|
|
Expected: common A1111 fields parse correctly.
|
|
|
|
## Task 11: Settings Formatter
|
|
|
|
**Files:**
|
|
|
|
- Create: `smart_metadata_reader/settings_formatter.py`
|
|
- Test: `tests/test_node_contract.py`
|
|
|
|
- [ ] **Step 1: Write setting formatter tests**
|
|
|
|
Assert the setting text includes:
|
|
|
|
- Filename.
|
|
- Source.
|
|
- Model.
|
|
- VAE.
|
|
- LoRA list.
|
|
- Seed.
|
|
- Steps.
|
|
- CFG.
|
|
- Sampler.
|
|
- Scheduler.
|
|
- Size.
|
|
- Status.
|
|
- Confidence.
|
|
- Unresolved nodes for partial results.
|
|
|
|
- [ ] **Step 2: Implement `format_setting`**
|
|
|
|
Expose:
|
|
|
|
Signature: `format_setting(result: ParseResult) -> str`.
|
|
|
|
The output is human-readable text, not JSON.
|
|
|
|
- [ ] **Step 3: Run formatter tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_node_contract.py -q
|
|
```
|
|
|
|
Expected: required setting lines are present.
|
|
|
|
## Task 12: Parser Orchestration
|
|
|
|
**Files:**
|
|
|
|
- Create: `smart_metadata_reader/metadata_reader.py`
|
|
- Modify: parser-facing modules from earlier tasks
|
|
- Test: `tests/test_conditioning_resolver.py`
|
|
- Test: `tests/test_node_id_independence.py`
|
|
|
|
- [ ] **Step 1: Write end-to-end parser tests**
|
|
|
|
Cover a ComfyUI prompt graph with:
|
|
|
|
- Final `KSampler`.
|
|
- Positive through `ControlNetApply`.
|
|
- `ConditioningCombine` with two branches.
|
|
- `StringFunction|pysssss` append.
|
|
- `ShowText|pysssss.text_0`.
|
|
- `DeepTranslatorTextNode`.
|
|
- Negative through a separate `CLIPTextEncode`.
|
|
- LoRA loaders.
|
|
|
|
- [ ] **Step 2: Write node ID independence test**
|
|
|
|
Create a helper that remaps all node IDs and link references to random strings. Assert that positive, negative, sampler values, and LoRA values match the original result.
|
|
|
|
- [ ] **Step 3: Implement orchestration**
|
|
|
|
Expose:
|
|
|
|
Signature:
|
|
|
|
```python
|
|
def parse_metadata_bundle(
|
|
bundle: MetadataBundle,
|
|
parameter_index: int = 0,
|
|
prefer_cached_text: bool = True,
|
|
include_raw_json: bool = True,
|
|
max_depth: int = 40,
|
|
) -> ParseResult
|
|
```
|
|
|
|
Use ComfyUI graph parsing first. Use A1111 fallback when ComfyUI graph parsing cannot start.
|
|
|
|
- [ ] **Step 4: Run end-to-end tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_conditioning_resolver.py tests/test_node_id_independence.py -q
|
|
```
|
|
|
|
Expected: graph traversal succeeds and random node IDs do not change results.
|
|
|
|
## Task 13: ComfyUI Node Contract
|
|
|
|
**Files:**
|
|
|
|
- Create: `nodes.py`
|
|
- Modify: `__init__.py`
|
|
- Test: `tests/test_node_contract.py`
|
|
|
|
- [ ] **Step 1: Write node contract tests**
|
|
|
|
Assert:
|
|
|
|
- `NODE_CLASS_MAPPINGS` contains `SmartMetadataReader`.
|
|
- `NODE_DISPLAY_NAME_MAPPINGS` contains `Smart Metadata Reader`.
|
|
- `INPUT_TYPES()` includes `image` with `image_upload=True`.
|
|
- `RETURN_TYPES` and `RETURN_NAMES` match the spec.
|
|
|
|
- [ ] **Step 2: Implement node class**
|
|
|
|
`SmartMetadataReader.INPUT_TYPES()` uses ComfyUI `folder_paths.get_input_directory()` and lists image files from the input directory.
|
|
|
|
`SmartMetadataReader.read_metadata(image, parameter_index, prefer_cached_text, include_raw_json, max_depth)`:
|
|
|
|
1. Resolves the selected image path.
|
|
2. Loads image and mask.
|
|
3. Reads metadata.
|
|
4. Parses metadata bundle.
|
|
5. Returns `(image, mask, positive, negative, seed, steps, cfg, width, height, model_name, filename, setting)`.
|
|
|
|
- [ ] **Step 3: Run node contract tests**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_node_contract.py -q
|
|
```
|
|
|
|
Expected: node mappings, inputs, and outputs match the spec.
|
|
|
|
## Task 14: Phase 1 Verification
|
|
|
|
**Files:**
|
|
|
|
- All Phase 1 files
|
|
|
|
- [ ] **Step 1: Run full test suite**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m pytest -q
|
|
```
|
|
|
|
Expected: all tests pass.
|
|
|
|
- [ ] **Step 2: Compile all Python files**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m compileall .
|
|
```
|
|
|
|
Expected: all Python files compile.
|
|
|
|
- [ ] **Step 3: Check Git status**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
git status --short --branch
|
|
```
|
|
|
|
Expected: only intended project files are changed or untracked.
|
|
|
|
- [ ] **Step 4: Commit Phase 1**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
git add .
|
|
git commit -m "feat: add python smart metadata reader mvp"
|
|
```
|
|
|
|
Expected: one commit containing the Python-only MVP.
|
|
|
|
## Test Execution Order
|
|
|
|
Run tests in this order during implementation:
|
|
|
|
```powershell
|
|
python -m pytest tests/test_graph.py -q
|
|
python -m pytest tests/test_metadata_reader.py -q
|
|
python -m pytest tests/test_text_adapters.py -q
|
|
python -m pytest tests/test_conditioning_resolver.py -q
|
|
python -m pytest tests/test_sampler_selector.py -q
|
|
python -m pytest tests/test_lora_extractor.py -q
|
|
python -m pytest tests/test_node_id_independence.py -q
|
|
python -m pytest tests/test_node_contract.py -q
|
|
python -m pytest -q
|
|
```
|
|
|
|
## Phase 1 Completion Criteria
|
|
|
|
Phase 1 is complete when:
|
|
|
|
- `Smart Metadata Reader` appears in ComfyUI node mappings.
|
|
- The node can select or upload an image.
|
|
- The node returns `IMAGE` and `MASK`.
|
|
- The node returns common ports defined in `docs/spec.md`.
|
|
- ComfyUI `prompt` metadata is parsed before `workflow` fallback.
|
|
- Positive and negative prompts are derived from selected sampler conditioning traversal.
|
|
- LoRA loader data is separated from prompt text.
|
|
- LoRA trigger words remain in prompt text when they enter conditioning.
|
|
- Unknown passthrough nodes create partial results instead of fabricated text.
|
|
- The full pytest suite passes.
|