Files
Damkohler-CaptionForge/jlc_captionforge_claim_extractor_CUI_node.py
T

464 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
JLC CaptionForge Claim Extractor — ComfyUI Node Wrapper
- CaptionForge
- This node is part of **CaptionForge**, a model-agnostic captioning
framework for ComfyUI developed by **J. L. Córdova**.
- Repository
https://github.com/Damkohler/CaptionForge
- CaptionForge focuses on practical dataset-captioning infrastructure for:
• LoRA dataset preparation
• multi-engine caption generation
• JSONL audit trails
• claim extraction and refinement
• consensus-oriented caption improvement
- Node Purpose
- The **JLC CaptionForge Claim Extractor** is the ComfyUI-facing wrapper for
CaptionForge Pass B claim extraction.
- It consumes the shared Pass A caption JSONL produced by CaptionForge
caption engines, groups records by `image_key`, extracts atomic visual
claims, normalizes rough equivalents, preserves source references, flags
simple conflicts, and writes one Pass B claim JSONL record per image.
- This node is intentionally text-only:
• It does not load Qwen models
• It does not load Joy models
• It does not perform image captioning
• It does not inspect image pixels directly
• It operates only on Pass A caption audit records
- The underlying engine module is:
captionforge_claim_engine.py
- CaptionForge Pass Role
- This node participates in **Pass B** of the CaptionForge pipeline.
- Pass A generates caption evidence from one or more caption engines.
- Pass B decomposes that caption evidence into structured claim records that
can be inspected, compared, normalized, scored, and refined by later
CaptionForge passes.
- The node exposes Pass B controls for:
• input Pass A JSONL path
• output Pass B JSONL path
• overwrite / append behavior
• dry-run mode
• image limit
• error-record inclusion
• low-confidence claim inclusion
• conflict detection
• maximum claims per caption
• backend selection
• image-key filtering
• future-LLM prompt bundle export
• raw LLM/manual-response preservation
- LLM-Ready Infrastructure
- v0.4.0 exposes the current Pass B backend contract through ComfyUI.
- Supported backends:
• heuristic
Deterministic rule-based extraction for immediate local use.
• manual_json
Development backend for parser and schema testing using
precomputed JSON/JSONL responses.
- The node can optionally export one future-LLM prompt bundle per image key,
allowing the prompt contract to be tested before live runtime integration
with a local text-only LLM backend.
- Future backends may include systems such as Ollama, Transformers,
llama.cpp, or other local providers.
- Audit and Output Behavior
- The node returns:
• a human-readable summary string
• JSONL record text
• resolved output JSONL path
- Pass B records preserve the evidence chain from normalized claims back to
the originating Pass A caption records.
- v0.4.0 supports parser-audit fields from the engine:
• llm_parse_result
• parser_warnings
• rejected_claims
• raw_llm_response
- Design Philosophy
- This node treats caption text as evidence rather than final truth.
- The goal is not to replace human judgment with a single model output, but
to expose intermediate caption claims so later passes can compare support,
identify contradictions, normalize language, and produce more reliable
LoRA-ready captions.
- The node preserves CaptionForge’s engine-democratic design by consuming
Pass A records from any compatible captioning engine rather than assuming
a single canonical caption source.
- ⚠️ Development Status
- This is Pass B v0.4.0 ComfyUI wrapper infrastructure.
- The default backend is deterministic and heuristic.
- Live LLM claim extraction is not yet integrated.
- The UI, backend list, and output schema may evolve as future LLM-powered
and consensus-based refinement passes are added.
- Attribution & License
- Concept and implementation by **J. L. Córdova**
with development assistance from **ChatGPT (OpenAI)**.
- Copyright (c) 2026 J. L. Córdova
- Released under the **MIT License**.
"""
from __future__ import annotations
MANIFEST = {
"name": "JLC CaptionForge Claim Extractor",
"version": (0, 4, 0),
"author": "J. L. Córdova",
"description": (
"ComfyUI wrapper for CaptionForge Pass B text-only claim extraction. Consumes "
"shared Pass A caption JSONL records, groups captions by image_key, extracts "
"atomic visual claims, normalizes rough equivalents, preserves source references, "
"flags simple conflicts, and writes one Pass B claim JSONL record per image. "
"Exposes v0.4 LLM-ready infrastructure including heuristic and manual_json "
"backends, optional future-LLM prompt bundle export, parser-audit fields, "
"image-key filtering, dry-run output, and raw response preservation for local "
"claim parser development."
),
}
from pathlib import Path
try:
import folder_paths
except Exception:
folder_paths = None
try:
from .captionforge_claim_engine import (
BatchClaimConfig,
ClaimExtractionConfig,
default_output_path,
default_prompt_output_path,
extract_claims_batch,
)
except ImportError:
from captionforge_claim_engine import (
BatchClaimConfig,
ClaimExtractionConfig,
default_output_path,
default_prompt_output_path,
extract_claims_batch,
)
class JLC_CaptionForgeClaimExtractor:
"""
ComfyUI wrapper for CaptionForge Pass B claim extraction.
This node is intentionally text-only. It does not load image models and does
not depend on the Qwen/Joy engines.
"""
@classmethod
def INPUT_TYPES(cls):
default_output = ""
default_prompts = ""
if folder_paths is not None:
try:
out_dir = Path(folder_paths.get_output_directory())
default_output = str(out_dir / "captionforge_claims_pass_b.jsonl")
default_prompts = str(out_dir / "captionforge_pass_b_llm_prompts.jsonl")
except Exception:
default_output = ""
default_prompts = ""
return {
"required": {
"input_jsonl": (
"STRING",
{
"default": "",
"multiline": False,
"tooltip": (
"Path to the Pass A captions JSONL file produced by the Qwen/Joy "
"caption nodes, e.g. C:\\\\...\\\\sample_pics3\\\\captions.jsonl. "
"This must be the JSONL file, not the image folder."
),
},
),
"output_jsonl": (
"STRING",
{
"default": default_output,
"multiline": False,
"tooltip": "Path for the Pass B claim JSONL. Leave blank to write beside input_jsonl as *_claims_pass_b.jsonl.",
},
),
"write_jsonl": (
"BOOLEAN",
{
"default": True,
"tooltip": "Write Pass B claim records to output_jsonl.",
},
),
"overwrite": (
"BOOLEAN",
{
"default": True,
"tooltip": "Overwrite output_jsonl. Disable to append.",
},
),
"dry_run": (
"BOOLEAN",
{
"default": False,
"tooltip": "Run extraction and return JSONL text without writing files.",
},
),
"limit_images": (
"INT",
{
"default": 0,
"min": 0,
"max": 100000,
"step": 1,
"tooltip": "Maximum grouped image_key records to process. 0 means no limit.",
},
),
"include_error_pass_a_records": (
"BOOLEAN",
{
"default": False,
"tooltip": "Include Pass A records with status != ok. Normally disabled.",
},
),
"include_low_confidence": (
"BOOLEAN",
{
"default": True,
"tooltip": "Keep lower-confidence heuristic claims. Useful during early audit development.",
},
),
"detect_conflicts": (
"BOOLEAN",
{
"default": True,
"tooltip": "Flag simple mutually exclusive attribute conflicts, such as multiple hair colors.",
},
),
"max_claims_per_caption": (
"INT",
{
"default": 80,
"min": 1,
"max": 500,
"step": 1,
"tooltip": "Maximum atomic claims extracted from each source caption.",
},
),
"text_llm_backend": (
["heuristic", "manual_json"],
{
"default": "heuristic",
"tooltip": (
"Text-only extraction backend. heuristic uses the deterministic scaffold. "
"manual_json reads precomputed JSON/JSONL responses using the same schema expected from a future LLM."
),
},
),
"manual_json_path": (
"STRING",
{
"default": "",
"multiline": False,
"tooltip": (
"Optional JSON/JSONL file containing manual/future-LLM claim responses. "
"Required only when text_llm_backend is manual_json."
),
},
),
"write_llm_prompt_jsonl": (
"BOOLEAN",
{
"default": False,
"tooltip": "Export one future-LLM prompt bundle per image_key for backend testing/auditing.",
},
),
"llm_prompt_jsonl": (
"STRING",
{
"default": default_prompts,
"multiline": False,
"tooltip": "Output path for prompt bundle JSONL. Leave blank to write beside input_jsonl as *_pass_b_llm_prompts.jsonl.",
},
),
"image_key_filter": (
"STRING",
{
"default": "",
"multiline": False,
"tooltip": (
"Optional debug filter. Process/export only image_keys that exactly match or contain this text. "
"Useful for testing one LLM prompt/response at a time."
),
},
),
"preserve_raw_llm_response": (
"BOOLEAN",
{
"default": False,
"tooltip": (
"For manual_json/future LLM backends, store the raw response text inside the Pass B record. "
"Useful for debugging, but can make JSONL large."
),
},
),
"text_llm_model": (
"STRING",
{
"default": "",
"multiline": False,
"tooltip": "Reserved model identifier/path for future runtime LLM backends. Stored in Pass B params.",
},
),
"text_llm_prompt": (
"STRING",
{
"default": (
"Extract atomic visible visual claims from the provided image captions. "
"Return strict JSON only. Preserve source references. Do not add facts "
"that are not supported by the captions."
),
"multiline": True,
"tooltip": "Prompt instruction stored in Pass B params and used in exported future-LLM prompt bundles.",
},
),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING")
RETURN_NAMES = ("summary", "jsonl_records", "output_jsonl")
FUNCTION = "extract_claims"
CATEGORY = "JLC/Captioning"
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
def extract_claims(
self,
input_jsonl,
output_jsonl,
write_jsonl,
overwrite,
dry_run,
limit_images,
include_error_pass_a_records,
include_low_confidence,
detect_conflicts,
max_claims_per_caption,
text_llm_backend,
manual_json_path,
write_llm_prompt_jsonl,
llm_prompt_jsonl,
image_key_filter,
preserve_raw_llm_response,
text_llm_model,
text_llm_prompt,
):
input_jsonl = (input_jsonl or "").strip()
if not input_jsonl:
raise RuntimeError("input_jsonl is required. Select the Pass A captions.jsonl file, not the image folder.")
input_path = Path(input_jsonl)
if input_path.is_dir():
raise RuntimeError(
"input_jsonl points to a folder, not a JSONL file:\n"
f" {input_path}\n"
"Select the Pass A captions JSONL file, for example captions.jsonl."
)
resolved_output = (output_jsonl or "").strip()
if not resolved_output:
resolved_output = str(default_output_path(input_jsonl))
resolved_prompt_jsonl = (llm_prompt_jsonl or "").strip()
if not resolved_prompt_jsonl:
try:
resolved_prompt_jsonl = str(default_prompt_output_path(input_jsonl))
except Exception:
resolved_prompt_jsonl = ""
config = ClaimExtractionConfig(
include_low_confidence=bool(include_low_confidence),
detect_conflicts=bool(detect_conflicts),
max_claims_per_caption=int(max_claims_per_caption),
text_llm_backend=(text_llm_backend or "heuristic").strip(),
text_llm_model=(text_llm_model or "").strip(),
text_llm_prompt=(text_llm_prompt or "").strip(),
preserve_raw_llm_response=bool(preserve_raw_llm_response),
)
batch = BatchClaimConfig(
input_jsonl=input_jsonl,
output_jsonl=resolved_output,
write_jsonl=bool(write_jsonl),
dry_run=bool(dry_run),
limit_images=int(limit_images),
include_error_pass_a_records=bool(include_error_pass_a_records),
overwrite=bool(overwrite),
manual_json_path=(manual_json_path or "").strip(),
write_llm_prompt_jsonl=bool(write_llm_prompt_jsonl),
llm_prompt_jsonl=resolved_prompt_jsonl,
image_key_filter=(image_key_filter or "").strip(),
)
result = extract_claims_batch(batch, config)
total_atomic = sum(len(r.atomic_claims) for r in result.records)
total_normalized = sum(len(r.normalized_claims) for r in result.records)
total_conflicts = sum(len(r.conflicts) for r in result.records)
total_rejected = sum(len(getattr(r, "rejected_claims", []) or []) for r in result.records)
total_parser_warnings = sum(len(getattr(r, "parser_warnings", []) or []) for r in result.records)
prompt_line = ""
if write_llm_prompt_jsonl:
prompt_line = f"LLM prompt JSONL: {resolved_prompt_jsonl}\n"
summary = (
"CaptionForge Pass B claim extraction complete.\n"
f"Node version: 0.4.0\n"
f"Engine version: {config.engine_version}\n"
f"Input JSONL: {input_jsonl}\n"
f"Output JSONL: {resolved_output if write_jsonl and not dry_run else '(not written)'}\n"
f"{prompt_line}"
f"Images processed: {result.processed}\n"
f"Failed groups: {result.failed}\n"
f"Atomic claims: {total_atomic}\n"
f"Normalized claims: {total_normalized}\n"
f"Conflict records: {total_conflicts}\n"
f"Backend: {config.text_llm_backend}"
)
return (summary, result.jsonl_text, resolved_output)
NODE_CLASS_MAPPINGS = {
"JLC_CaptionForgeClaimExtractor": JLC_CaptionForgeClaimExtractor,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"JLC_CaptionForgeClaimExtractor": "\u2003JLC CaptionForge Claim Extractor",
}