# 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.