feat: resume fixes, OpenRouter templates, and project context

Delegate expected output filenames to modules so resume works for rembg
and composite; normalize G'MIC multi-frame output; add OpenRouter style
reference support with tests. Add Crusaders, orange, and team gallery
pipelines plus SOUL/AGENTS context files.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank Schwenk
2026-07-12 10:45:00 +02:00
parent 980c7f3b8b
commit ce431d7eec
22 changed files with 1616 additions and 31 deletions
+155
View File
@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""Crusaders pipeline: rembg variants composited over backgrounds and originals."""
from pathlib import Path
from imagepipeline import Pipeline
INPUT = Path("/home/frank/pics/20260620_Albershausen Crusaders - Montabaur Fighting Farmers/darktable_exported")
OUTPUT_BASE = Path.home() / "pipeline_output"
# Reuse outputs from a previous run or external folder (key = step id, e.g. rembg_01).
EXISTING_OUTPUTS: dict[str, Path] = {}
# Resume an aborted run: point to its output root folder (or None for a fresh run).
CONTINUE_FROM: Path | None = Path("/home/frank/pipeline_output/crusaders_260623085301")
COLOR1 = "#0064b0"
COLOR2 = "#00badf"
# COLOR1 = #0064b0 -> 0,100,176; COLOR2 = #00badf -> 0,186,223
GMIC_STEREO = "-gcd_stereo_img 0,0,2.028,1,1.714,3.06,4,1,0"
GMIC_DROP_SHADOW = "-fx_drop_shadow3d 0,0,0,10,1,1,2,0.5,0,186,223,200,0"
GMIC_BWRECOLOR = (
"-fx_bwrecolorize 0,0,0,0,0,1,0,2,0,186,223,255,0,100,176,0,255,"
"158,137,189,255,224,191,228,255,0,100,176,0,255,255,255,255,255,255,255,"
"255,255,0,100,176,0,255"
)
GMIC_GRADIENT_A = (
'-fx_custom_gradient 0,0,0,1,2,1,0,128,100,100,2,0,1,0,"",1,0,0,100,176,255,'
"0,186,223,255,255,255,0,255,255,255,255,255,0,255,255,255,0,255,0,255,0,0,"
"255,255,128,128,128,255,255,0,255,255,0,0,0,0"
)
GMIC_GRADIENT_B = (
'-fx_custom_gradient 0,0,0,1,2,1,0,128,100,100,2,0,1,0,"",1,0,0,186,223,255,'
"0,100,176,255,255,255,0,255,255,255,255,255,0,255,255,255,0,255,0,255,0,0,"
"255,255,128,128,128,255,255,0,255,255,0,0,0,0"
)
GMIC_JPR_SMOOTH = "-jpr_gradient_smooth 0,1.5"
def main() -> None:
with Pipeline(
name="crusaders",
input_dir=INPUT,
output_base=OUTPUT_BASE,
existing_outputs=EXISTING_OUTPUTS or None,
continue_from=CONTINUE_FROM,
) as p:
rembg_out = p.step("rembg", inputs="input")
white_bg = p.step("imagemagick_fill", inputs="input", color1="#ffffff")
black_bg = p.step("imagemagick_fill", inputs="input", color1="#000000")
gradient_45_bg = p.step(
"imagemagick_fill",
inputs="input",
color1=COLOR1,
color2=COLOR2,
gradient=True,
angle=45,
)
gradient_radial_bg = p.step(
"imagemagick_fill",
inputs="input",
color1=COLOR1,
color2=COLOR2,
gradient=True,
radial=True,
)
grayscale = p.step("gmic_grayscale", inputs="input")
rembg_stereo = p.step("gmic", inputs=rembg_out, command=GMIC_STEREO)
rembg_shadow = p.step("gmic", inputs=rembg_out, command=GMIC_DROP_SHADOW)
rembg_bwrecolor = p.step("gmic", inputs=rembg_out, command=GMIC_BWRECOLOR)
rembg_gradient_a = p.step("gmic", inputs=rembg_out, command=GMIC_GRADIENT_A)
rembg_gradient_b = p.step("gmic", inputs=rembg_out, command=GMIC_GRADIENT_B)
rembg_jpr_smooth = p.step("gmic", inputs=rembg_out, command=GMIC_JPR_SMOOTH)
rembg_jpr_smooth_sized = p.step(
"imagemagick_scale_crop",
inputs=rembg_jpr_smooth,
scale=1.05,
)
rembg_stereo_alpha = p.step(
"color_to_alpha", inputs=rembg_stereo, color="#000000"
)
color_bg = p.step("imagemagick_fill", inputs="input", color1=COLOR1)
rembg_smooth_alpha = p.step(
"color_to_alpha", inputs=rembg_jpr_smooth, color="#7f7f7f"
)
rembg_smooth_sized = p.step(
"imagemagick_scale_crop",
inputs=rembg_smooth_alpha,
scale=1.05,
)
# combine: white background, rembg
p.step("composite", inputs=[white_bg, rembg_out])
# combine: black background, rembg
p.step("composite", inputs=[black_bg, rembg_out])
# combine: linear gradient background, rembg
p.step("composite", inputs=[gradient_45_bg, rembg_out])
# combine: radial gradient background, rembg
p.step("composite", inputs=[gradient_radial_bg, rembg_out])
# combine: original, rembg (stereo), rembg
stereo_mid = p.step("composite", inputs=["input", rembg_stereo])
p.step("composite", inputs=[stereo_mid, rembg_out])
# combine: original, rembg (drop shadow), rembg
shadow_mid = p.step("composite", inputs=["input", rembg_shadow])
p.step("composite", inputs=[shadow_mid, rembg_out])
# combine: original, rembg (bw recolorize @ 50%), rembg
bw_mid = p.step(
"composite",
inputs=["input", rembg_bwrecolor],
foreground_opacity=0.5,
)
p.step("composite", inputs=[bw_mid, rembg_out])
# combine: rembg (custom gradient A), rembg
p.step("composite", inputs=[rembg_gradient_a, rembg_out])
# combine: rembg (custom gradient B), rembg
p.step("composite", inputs=[rembg_gradient_b, rembg_out])
# combine: original, rembg (jpr smooth, scaled), rembg
smooth_mid = p.step("composite", inputs=["input", rembg_jpr_smooth_sized])
p.step("composite", inputs=[smooth_mid, rembg_out])
# combine: original (grayscale), rembg
p.step("composite", inputs=[grayscale, rembg_out])
# combine: original, rembg (stereo, black to alpha), rembg
stereo_alpha_mid = p.step("composite", inputs=["input", rembg_stereo_alpha])
p.step("composite", inputs=[stereo_alpha_mid, rembg_out])
# combine: color background, rembg (drop shadow), rembg
shadow_color_mid = p.step("composite", inputs=[color_bg, rembg_shadow])
p.step("composite", inputs=[shadow_color_mid, rembg_out])
# combine: original, rembg (jpr smooth, #7f7f7f to alpha, scaled), rembg
smooth_alpha_mid = p.step("composite", inputs=["input", rembg_smooth_sized])
p.step("composite", inputs=[smooth_alpha_mid, rembg_out])
output_root = p.run()
print(f"Pipeline finished. Output: {output_root}")
if __name__ == "__main__":
main()
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Orange pipeline: rembg variants composited over backgrounds and originals."""
from pathlib import Path
from imagepipeline import Pipeline
INPUT = Path("/home/frank/pics/20260704_Hellraisers und The Shape Schlossplatz/darktable_exported")
OUTPUT_BASE = Path.home() / "pipeline_output"
# Reuse outputs from a previous run or external folder (key = step id, e.g. rembg_01).
EXISTING_OUTPUTS: dict[str, Path] = {}
# Resume an aborted run: point to its output root folder (or None for a fresh run).
CONTINUE_FROM: Path | None = None
COLOR1 = "#AA4E00"
COLOR2 = "#EDDD93"
# COLOR1 = #AA4E00 -> 170,78,0; COLOR2 = #EDDD93 -> 237,221,147
GMIC_DROP_SHADOW = "-fx_drop_shadow3d 0,0,0,10,1,1,2,0.5,237,221,147,200,0"
GMIC_BWRECOLOR = (
"-fx_bwrecolorize 0,0,0,0,0,1,0,2,237,221,147,255,170,78,0,255,"
"158,137,189,255,224,191,228,255,170,78,0,255,255,255,255,255,255,255,"
"255,255,170,78,0,255"
)
GMIC_GRADIENT_A = (
'-fx_custom_gradient 0,0,0,1,2,1,0,128,100,100,2,0,1,0,"",1,0,170,78,0,255,'
"237,221,147,255,255,255,0,255,255,255,255,255,0,255,255,255,0,255,0,255,0,0,"
"255,255,128,128,128,255,255,0,255,255,0,0,0,0"
)
GMIC_GRADIENT_B = (
'-fx_custom_gradient 0,0,0,1,2,1,0,128,100,100,2,0,1,0,"",1,0,237,221,147,255,'
"170,78,0,255,255,255,0,255,255,255,255,255,0,255,255,255,0,255,0,255,0,0,"
"255,255,128,128,128,255,255,0,255,255,0,0,0,0"
)
def main() -> None:
with Pipeline(
name="orange",
input_dir=INPUT,
output_base=OUTPUT_BASE,
existing_outputs=EXISTING_OUTPUTS or None,
continue_from=CONTINUE_FROM,
) as p:
rembg_out = p.step("rembg", inputs="input")
grayscale = p.step("gmic_grayscale", inputs="input")
rembg_shadow = p.step("gmic", inputs=rembg_out, command=GMIC_DROP_SHADOW)
rembg_bwrecolor = p.step("gmic", inputs=rembg_out, command=GMIC_BWRECOLOR)
color_bg = p.step("imagemagick_fill", inputs="input", color1=COLOR1)
# combine: original, rembg (drop shadow), rembg
shadow_mid = p.step("composite", inputs=["input", rembg_shadow])
p.step("composite", inputs=[shadow_mid, rembg_out])
# combine: original, rembg (bw recolorize @ 50%), rembg
bw_mid = p.step(
"composite",
inputs=["input", rembg_bwrecolor],
foreground_opacity=0.5,
)
p.step("composite", inputs=[bw_mid, rembg_out])
# combine: original (grayscale), rembg
p.step("composite", inputs=[grayscale, rembg_out])
# combine: color background, rembg (drop shadow), rembg
shadow_color_mid = p.step("composite", inputs=[color_bg, rembg_shadow])
p.step("composite", inputs=[shadow_color_mid, rembg_out])
output_root = p.run()
print(f"Pipeline finished. Output: {output_root}")
if __name__ == "__main__":
main()
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Match new team member photos to an existing player gallery style via OpenRouter."""
import os
from pathlib import Path
from imagepipeline import Pipeline
REPO_ROOT = Path(__file__).resolve().parents[1]
ENV_FILE = REPO_ROOT / ".env"
# Folder with new member photos to style-match.
INPUT = Path("/home/frank/tmp/spieler")
# One existing gallery player image as style reference.
TEMPLATE_IMAGE = Path("/home/frank/Downloads/2026_AHC_44_Julius-Nikolaus_Dittus-524-scaled.webp")
OUTPUT_BASE = Path.home() / "pipeline_output"
OPENROUTER_MODEL = "google/gemini-3-pro-image"
GALLERY_MATCH_PROMPT = (
"Match the second image to the first image's gallery style so it fits seamlessly "
"alongside the other players. Match color grading, white balance, contrast, "
"saturation, lighting direction, background treatment, sharpness, and overall "
"polish. "
"CRITICAL: Do not alter the person's face, identity, facial features, expression, "
"hair, pose, body shape, or clothing details. Do not crop, reframe, or change the "
"aspect ratio. Do not add or remove people or objects. "
"Keep the exact same image dimensions and composition — only adjust global style "
"and color to match the reference."
)
def _load_env_file(path: Path) -> None:
if not path.is_file():
return
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
key, sep, value = line.partition("=")
if not sep:
continue
key = key.strip()
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
value = value[1:-1]
os.environ.setdefault(key, value)
def main() -> None:
_load_env_file(ENV_FILE)
with Pipeline(
name="team_gallery_match",
input_dir=INPUT,
output_base=OUTPUT_BASE,
) as p:
p.step(
"openrouter_edit",
inputs="input",
prompt=GALLERY_MATCH_PROMPT,
model=OPENROUTER_MODEL,
template_image=TEMPLATE_IMAGE,
# Downscale for API, then upscale back to original dimensions.
# Use 0 only if you accept higher cost/latency for ~10 MB sources.
max_edge=4096,
skip_existing=True,
)
output_root = p.run()
print(f"Pipeline finished. Output: {output_root}")
if __name__ == "__main__":
main()