DageBjorne
Package project as pip-installable augmenator.
7025ca1
Raw
History Blame Contribute Delete
12 kB
import re
from augmenator.background_replace import parse_background_query
from augmenator.embedding_planner import select_keywords, warmup as warmup_embeddings
from augmenator.keyword_catalog import AugmentKeyword
from augmenator.spatial import infer_cover_count
from augmenator.spatial_triggers import allows_spatial_keyword
SPATIAL_OPS = {
"cover_avoid_text",
"cover_random",
"cover_and_cutout",
"cover_and_cutout_avoid_text",
"add_cutout",
"replace_background",
}
GENERATIVE_PATTERNS = (
"put ",
"place ",
"insert ",
"behind",
"in front of",
"table",
"chair",
"person",
"object",
"scene",
)
UNSUPPORTED_REASON = (
"This instruction needs generative editing (adding or replacing objects/scenes). "
"v1 supports color transforms, classical edits, procedural cutouts, text-aware covering, "
"and background replacement — not object insertion."
)
def warmup():
warmup_embeddings()
def _instruction_mentions_text(instruction: str) -> bool:
lowered = instruction.lower()
return any(
phrase in lowered
for phrase in ("not text", "avoid text", "except text", "not on text", "without text")
)
def _infer_fill(instruction: str) -> str:
lowered = instruction.lower()
if any(word in lowered for word in ("blur", "soft", "soften")):
return "blur"
return "solid"
def _infer_cutout_style(instruction: str, explicit: str | None = None) -> str:
if explicit in ("transparent", "solid"):
return explicit
lowered = instruction.lower()
if any(w in lowered for w in ("transparent", "clear cutout", "cut a hole", "punch hole")):
return "transparent"
if any(w in lowered for w in ("solid", "opaque", "colored cutout", "red cutout", "blue cutout", "green cutout")):
return "solid"
return "solid"
def _infer_cutout_color(instruction: str) -> tuple[int, int, int]:
lowered = instruction.lower()
if "red" in lowered:
return (220, 60, 60)
if "blue" in lowered:
return (60, 100, 220)
if "green" in lowered:
return (60, 180, 80)
if "yellow" in lowered:
return (230, 200, 50)
return (255, 200, 80)
def _is_cutout_op(op: str) -> bool:
return op in ("add_cutout", "cutout") or op.startswith("cover_and_cutout")
def _cutout_spatial_defaults(instruction: str) -> dict:
return {
"cutout_count": None,
"varied_shapes": True,
}
def _has_generative_intent(instruction: str) -> bool:
lowered = instruction.lower()
return any(pattern in lowered for pattern in GENERATIVE_PATTERNS)
def _build_spatial_entry(keyword: AugmentKeyword, instruction: str) -> dict:
extra = keyword.spatial_extra or {}
op = keyword.spatial_op or keyword.id
avoid_text = extra.get("avoid_text", False) or _instruction_mentions_text(instruction)
entry = {
"op": op,
"fill": _infer_fill(instruction),
"cutout_style": extra.get("cutout_style") or _infer_cutout_style(instruction),
"cutout_color": _infer_cutout_color(instruction),
"avoid_text": avoid_text,
"varied_shapes": extra.get("varied_shapes", _is_cutout_op(op)),
"query": None,
}
if _is_cutout_op(op):
entry.update(_cutout_spatial_defaults(instruction))
if extra.get("avoid_text_cutouts"):
entry["avoid_text_cutouts"] = True
if op in ("cover_and_cutout", "cover_and_cutout_avoid_text"):
entry["cover_count"] = infer_cover_count(instruction)
entry["varied_shapes"] = True
if op == "cover_and_cutout_avoid_text":
entry["avoid_text"] = True
entry["avoid_text_cutouts"] = True
if op in ("add_cutout", "cutout"):
entry["varied_shapes"] = True
if avoid_text or _instruction_mentions_text(instruction):
entry["avoid_text"] = True
entry["avoid_text_cutouts"] = True
if op == "cover_avoid_text":
entry["avoid_text"] = True
if op == "replace_background":
parsed = parse_background_query(instruction)
entry["query"] = None if parsed is False else parsed
if avoid_text and op in ("add_cutout", "cover_random"):
entry["avoid_text"] = True
if op == "cover_random":
entry["op"] = "cover_avoid_text"
return entry
def _correct_spatial_for_instruction(spatial: list[dict], instruction: str) -> list[dict]:
lowered = instruction.lower()
mentions_cover = any(
phrase in lowered
for phrase in ("cover", "hide part", "hide a", "hide section", "hide region")
)
mentions_cutout = any(
phrase in lowered
for phrase in ("cutout", "cut out", "hole", "sticker", "overlay", "punch")
)
compound_ops = {"cover_and_cutout", "cover_and_cutout_avoid_text"}
ops = {s["op"] for s in spatial}
if not (ops & compound_ops):
return spatial
entry = next(s for s in spatial if s["op"] in compound_ops)
if mentions_cutout and not mentions_cover:
avoid = entry.get("avoid_text") or _instruction_mentions_text(instruction)
return [
{
"op": "add_cutout",
"fill": entry.get("fill") or _infer_fill(instruction),
"cutout_style": entry.get("cutout_style", "solid"),
"cutout_color": entry.get("cutout_color", (255, 200, 80)),
"avoid_text": avoid,
"avoid_text_cutouts": avoid,
"cutout_count": entry.get("cutout_count"),
"varied_shapes": True,
"query": None,
}
]
if mentions_cover and not mentions_cutout:
op = "cover_avoid_text" if entry.get("avoid_text") else "cover_random"
return [
{
"op": op,
"fill": entry.get("fill") or _infer_fill(instruction),
"cutout_style": entry.get("cutout_style", "solid"),
"cutout_color": entry.get("cutout_color", (255, 200, 80)),
"avoid_text": entry.get("avoid_text", False),
"varied_shapes": False,
"query": None,
}
]
return spatial
def _finalize_spatial(spatial: list[dict]) -> list[dict]:
compound_ops = {"cover_and_cutout", "cover_and_cutout_avoid_text"}
simple_cover = {"cover_random", "cover_avoid_text", "cover"}
simple_cutout = {"add_cutout", "cutout"}
ops = {s["op"] for s in spatial}
if ops & compound_ops:
return [
s for s in spatial
if s["op"] not in simple_cover and s["op"] not in simple_cutout
]
if (ops & simple_cutout) and not (ops & simple_cover):
return [s for s in spatial if s["op"] not in compound_ops]
return spatial
def _consolidate_cover_cutout(spatial: list[dict], instruction: str) -> list[dict]:
"""Merge separate cover + cutout ops into a combined cover_and_cutout op."""
has_cover = any(s["op"] in ("cover_random", "cover", "cover_avoid_text") for s in spatial)
has_cutout = any(s["op"] == "add_cutout" for s in spatial)
if not (has_cover and has_cutout):
return spatial
avoid_text = any(
s.get("avoid_text") or s["op"] in ("cover_avoid_text", "cover_and_cutout_avoid_text")
for s in spatial
)
cutout_style = "solid"
cutout_color = (255, 200, 80)
for s in spatial:
if s["op"] == "add_cutout":
cutout_style = s.get("cutout_style", cutout_style)
cutout_color = s.get("cutout_color", cutout_color)
combined_op = "cover_and_cutout_avoid_text" if avoid_text else "cover_and_cutout"
combined = {
"op": combined_op,
"fill": _infer_fill(instruction),
"cutout_style": cutout_style,
"cutout_color": cutout_color,
"avoid_text": avoid_text,
"avoid_text_cutouts": avoid_text,
"varied_shapes": True,
"cover_count": infer_cover_count(instruction),
"cutout_count": None,
"query": None,
}
remaining = [
s
for s in spatial
if s["op"] not in ("cover_random", "cover", "cover_avoid_text", "add_cutout", "cutout")
]
if not any(s["op"] == combined_op for s in remaining):
remaining.append(combined)
return remaining
def _matches_to_plan(
matches: list[tuple[AugmentKeyword, float]],
instruction: str,
) -> tuple[list[str], list[dict], list[tuple[str, float]]]:
tags: list[str] = []
spatial: list[dict] = []
scores: list[tuple[str, float]] = []
for keyword, score in matches:
scores.append((keyword.id, score))
if keyword.kind == "tag":
if keyword.id not in tags:
tags.append(keyword.id)
elif keyword.kind == "spatial":
entry = _build_spatial_entry(keyword, instruction)
dup_key = (entry.get("op"), entry.get("cutout_style"), entry.get("query"))
if not any(
(e.get("op"), e.get("cutout_style"), e.get("query")) == dup_key for e in spatial
):
spatial.append(entry)
spatial = _consolidate_cover_cutout(spatial, instruction)
spatial = _finalize_spatial(spatial)
spatial = _correct_spatial_for_instruction(spatial, instruction)
return tags, spatial, scores
def plan_from_instruction(instruction: str) -> dict:
instruction = instruction.strip()
if not instruction:
return {
"tags": [],
"spatial": [],
"scores": [],
"supported": False,
"reason": "Please enter an instruction.",
}
matches = select_keywords(instruction)
matches = [
(keyword, score)
for keyword, score in matches
if keyword.kind != "spatial" or allows_spatial_keyword(keyword.id, instruction)
]
tags, spatial, scores = _matches_to_plan(matches, instruction)
if not tags and not spatial and _has_generative_intent(instruction):
return {
"tags": [],
"spatial": [],
"scores": scores,
"supported": False,
"reason": UNSUPPORTED_REASON,
}
if tags or spatial:
normalized_spatial = []
for entry in spatial:
op = entry.get("op", "")
if op in SPATIAL_OPS:
normalized_spatial.append(
{
"op": op,
"fill": entry.get("fill") or _infer_fill(instruction),
"cutout_style": entry.get("cutout_style", "solid"),
"cutout_color": entry.get("cutout_color", (255, 200, 80)),
"avoid_text": entry.get("avoid_text", op in ("cover_avoid_text", "cover_and_cutout_avoid_text")),
"avoid_text_cutouts": entry.get(
"avoid_text_cutouts",
entry.get("avoid_text", False) or op == "cover_and_cutout_avoid_text",
),
"varied_shapes": entry.get(
"varied_shapes",
op in ("cover_and_cutout", "cover_and_cutout_avoid_text", "add_cutout", "cutout"),
),
"cover_count": entry.get("cover_count"),
"cutout_count": entry.get("cutout_count"),
"query": entry.get("query"),
}
)
return {
"tags": tags,
"spatial": normalized_spatial,
"scores": scores,
"supported": True,
"reason": None,
}
return {
"tags": [],
"spatial": [],
"scores": scores,
"supported": False,
"reason": (
f'Could not map "{instruction}" to a supported transform. '
"Try describing an effect more clearly, or use one of the example prompts below."
),
}