test: add E2E integration test + example ComfyUI workflow

Add test_integration_e2e.py that exercises the full loader → generator
→ composer pipeline with GreyBackend, asserting 2 pages, 4 panels, and
cache-hit behaviour on second run (PNG round-trip tolerance atol=1/255).

Add examples/workflows/m1_basic_vertical_stack.json as a reference
ComfyUI workflow JSON wiring CheckpointLoaderSimple → ComicScriptLoader
→ ComicBatchGenerator → ComicPageComposer → SaveImage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tack
2026-04-23 12:48:28 +08:00
co-authored by Claude Sonnet 4.6
parent 9d3a587cf6
commit 1e4ccca032
2 changed files with 119 additions and 0 deletions
@@ -0,0 +1,53 @@
{
"last_node_id": 7,
"last_link_id": 8,
"nodes": [
{ "id": 1, "type": "CheckpointLoaderSimple", "pos": [40, 100],
"inputs": [], "outputs": [{"name":"MODEL","type":"MODEL","links":[1]},
{"name":"CLIP","type":"CLIP","links":[2]},
{"name":"VAE","type":"VAE","links":[3]}],
"widgets_values": ["realistic_vision.safetensors"] },
{ "id": 2, "type": "ComicScriptLoader", "pos": [40, 300],
"inputs": [],
"outputs": [{"name":"comic_script","type":"COMIC_SCRIPT","links":[4]},
{"name":"summary","type":"STRING","links":null}],
"widgets_values": ["file", "comics/basic.json", "", ""] },
{ "id": 3, "type": "ComicBatchGenerator", "pos": [400, 150],
"inputs": [
{"name":"comic_script","type":"COMIC_SCRIPT","link":4},
{"name":"model","type":"MODEL","link":1},
{"name":"clip","type":"CLIP","link":2},
{"name":"vae","type":"VAE","link":3}
],
"outputs": [
{"name":"generated_panels","type":"IMAGE","links":[5]},
{"name":"comic_script","type":"COMIC_SCRIPT","links":[6]},
{"name":"generation_log","type":"STRING","links":null}
],
"widgets_values": ["euler","normal",25,7.0,1.0,"retry_then_skip",2,"",""] },
{ "id": 4, "type": "ComicPageComposer", "pos": [800, 150],
"inputs": [
{"name":"generated_panels","type":"IMAGE","link":5},
{"name":"comic_script","type":"COMIC_SCRIPT","link":6}
],
"outputs": [
{"name":"composed_pages","type":"IMAGE","links":[7]},
{"name":"panel_bboxes_json","type":"STRING","links":null},
{"name":"individual_panels","type":"IMAGE","links":null}
],
"widgets_values": [false] },
{ "id": 5, "type": "SaveImage", "pos": [1200, 150],
"inputs": [{"name":"images","type":"IMAGE","link":7}],
"widgets_values": ["comic/page"] }
],
"links": [
[1,1,0,3,1,"MODEL"],
[2,1,1,3,2,"CLIP"],
[3,1,2,3,3,"VAE"],
[4,2,0,3,0,"COMIC_SCRIPT"],
[5,3,0,4,0,"IMAGE"],
[6,3,1,4,1,"COMIC_SCRIPT"],
[7,4,0,5,0,"IMAGE"]
],
"version": 0.4
}
@@ -0,0 +1,66 @@
"""End-to-end: loader → generator → composer with the basic.json fixture."""
from pathlib import Path
import numpy as np
import torch
from tk_comfyui_batch_image.core.sampler_runner import SamplerBackend
from tk_comfyui_batch_image.nodes.batch_generator import ComicBatchGenerator
from tk_comfyui_batch_image.nodes.page_composer import ComicPageComposer
from tk_comfyui_batch_image.nodes.script_loader import ComicScriptLoader
FIXTURES = Path(__file__).parent / "fixtures" / "scripts"
class GreyBackend(SamplerBackend):
def encode(self, clip, text):
return ("c", text)
def empty_latent(self, w, h, batch_size=1):
return {"w": w, "h": h}
def sample(self, model, seed, steps, cfg, sampler_name, scheduler,
cond_pos, cond_neg, latent, denoise):
return latent
def vae_decode(self, vae, samples):
h, w = samples["h"], samples["w"]
return np.full((h, w, 3), 0.5, dtype=np.float32)
def test_full_pipeline_basic_script(tmp_path: Path):
loader = ComicScriptLoader()
text = (FIXTURES / "basic.json").read_text(encoding="utf-8")
script, summary = loader.load(mode="inline", json_text=text,
json_file="", json_path="")
gen = ComicBatchGenerator()
panels, pass_script, log = gen.generate(
comic_script=script, model="M", clip="C", vae="V",
sampler_name="euler", scheduler="normal", steps=10, cfg=7.0, denoise=1.0,
on_failure="halt", retries=0,
positive_suffix="", negative_suffix="",
_backend=GreyBackend(), _out_dir=tmp_path,
)
composer = ComicPageComposer()
pages, bboxes_json, pass_panels = composer.compose(
generated_panels=panels, comic_script=pass_script, debug_overlay=False,
)
# 2 pages of same size
assert pages.shape[0] == 2
# all panels generated
assert panels.shape[0] == 4
# rerun without changes → everything cached
gen2 = ComicBatchGenerator()
panels2, _, log2 = gen2.generate(
comic_script=script, model="M", clip="C", vae="V",
sampler_name="euler", scheduler="normal", steps=10, cfg=7.0, denoise=1.0,
on_failure="halt", retries=0,
positive_suffix="", negative_suffix="",
_backend=GreyBackend(), _out_dir=tmp_path,
)
assert "cached" in log2
# PNG round-trip introduces ≤1/255 quantisation; use atol accordingly
assert torch.allclose(panels, panels2, atol=1.0 / 255)