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." ), }