Spaces:
Running
Running
File size: 7,954 Bytes
e6404d0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | import re
SUBJECT_RE = re.compile(
r'\b(\d+\s*(?:girl|boy|woman|man|female|male|other)s?'
r'|multiple\s+girl(?:s)?|multiple\s+boy(?:s)?|no\s+humans?)\b',
re.IGNORECASE,
)
def _normalize_subject(token: str) -> str:
"""Canonicalize a matched subject token (handles spaced forms like '1 woman')."""
t = token.lower().replace(" ", "")
m = re.fullmatch(r"(\d+)(girl|boy|woman|man|female|male|other)s?", t)
if m:
n, kind = m.group(1), m.group(2)
if kind in ("girl", "woman", "female"):
base = "girl"
elif kind in ("boy", "man", "male"):
base = "boy"
else:
base = "other"
if n == "1":
return "1" + base
return n + ("girls" if base == "girl" else "boys" if base == "boy" else "others")
if t.startswith("multiple girl"):
return "multiple girls"
if t.startswith("multiple boy"):
return "multiple boys"
if t.startswith("no human"):
return "no humans"
return token.lower()
QUALITY_TAGS = frozenset({
"masterpiece", "best quality", "good quality", "normal quality",
"low quality", "worst quality", "high quality",
"score_9", "score_8", "score_7", "score_6", "score_5",
"score_4", "score_3", "score_2", "score_1",
})
META_TAGS = frozenset({
"highres", "absurdres", "incredibly absurdres", "ultra highres",
"anime screencap", "official art", "game_cg", "visual novel",
"light novel", "manga", "comic", "4koma", "doujinshi",
"jpeg artifacts", "lineart", "no lineart", "sketch",
"rough sketch", "clean sketch", "monochrome", "greyscale",
"full color", "colored", "monochrome lineart",
})
SAFETY_TAGS = frozenset({"safe", "sensitive", "questionable", "nsfw", "explicit"})
YEAR_RE = re.compile(r'\byear\s+(\d{4})\b', re.IGNORECASE)
PERIOD_TAGS = frozenset({"newest", "recent", "mid", "early", "old"})
ARTIST_RE = re.compile(r'(?:@|artist:)([^,]+?)(?:\s*,|\s*$)', re.IGNORECASE)
COPYRIGHT_PAREN_RE = re.compile(r'\(([^)]+)\)')
# SD emphasis / LoRA tokens are extracted BEFORE parsing so that e.g.
# "(blue eyes:1.2)" is not misread as a booru "(series)" parenthetical,
# and "<lora:name:0.8>" survives the comma-splitter intact.
WEIGHT_TOKEN_RE = re.compile(
r"\(\(([^()]+)\)\)" # ((tag)) strong emphasis
r"|\(([^()]+):\s*(-?\+?\d+(?:\.\d+)?)\s*\)" # (tag:1.2) explicit weight
)
LORA_RE = re.compile(r"<(?:lora|lyco):[^>]+>", re.IGNORECASE)
SERIES_IDENTIFIERS = frozenset({
"vocaloid", "touhou", "genshin impact", "honkai star rail",
"fate", "fate/grand order", "fgo", "azur lane", "kancolle",
"kantai collection", "blue archive", "arknights", "uma musume",
"love live", "idolmaster", "the idolmaster", "hololive",
"nijisanji", "碧蓝航线", "原神", "starrail",
})
class ParsedPrompt:
__slots__ = (
"subject", "character", "series", "artists",
"quality_tags", "meta_tags", "year_tag", "period_tag",
"safety_tag", "general_tags", "nl_text", "weighted_tokens",
)
def __init__(self):
self.subject: str = ""
self.character: str = ""
self.series: str = ""
self.artists: list[str] = []
self.quality_tags: list[str] = []
self.meta_tags: list[str] = []
self.year_tag: str = ""
self.period_tag: str = ""
self.safety_tag: str = ""
self.general_tags: list[str] = []
self.nl_text: str = ""
# Verbatim SD tokens extracted before parsing: "(tag:1.2)", "((tag))",
# "<lora:name:0.8>". They bypass tag processing and are appended to output.
self.weighted_tokens: list[str] = []
@property
def has_booru_structure(self) -> bool:
return bool(self.subject or self.quality_tags or self.general_tags)
def __repr__(self):
return (
f"ParsedPrompt(subject={self.subject!r}, character={self.character!r}, "
f"series={self.series!r}, artists={self.artists!r}, "
f"year={self.year_tag!r}, safety={self.safety_tag!r}, "
f"quality={self.quality_tags}, meta={self.meta_tags}, "
f"general={len(self.general_tags)} tags, nl={self.nl_text!r})"
)
def parse_prompt(raw: str) -> ParsedPrompt | None:
if not raw or not raw.strip():
return None
result = ParsedPrompt()
text = raw.strip()
# Extract explicit weight / emphasis / LoRA tokens FIRST so later stages
# (comma split, regex passes) cannot mangle or misclassify them.
# They ride along as verbatim strings and are appended at render time.
loras = LORA_RE.findall(text)
text = LORA_RE.sub(" ", text)
weighted: list[str] = []
def _stash_weight(m: re.Match) -> str:
if m.group(1) is not None: # ((tag)) strong emphasis
inner = m.group(1).strip()
if inner:
weighted.append(f"(({inner}))")
else: # (tag:1.2) explicit weight
inner, w = m.group(2).strip(), m.group(3)
if inner:
weighted.append(f"({inner}:{w})")
return " "
text = WEIGHT_TOKEN_RE.sub(_stash_weight, text)
result.weighted_tokens = weighted + loras
subject_m = SUBJECT_RE.search(text)
if subject_m:
result.subject = _normalize_subject(subject_m.group(1))
text = text[:subject_m.start()] + text[subject_m.end():]
for m in ARTIST_RE.finditer(text):
name = m.group(1).strip()
if name:
result.artists.append(name)
text = ARTIST_RE.sub("", text)
paren_m = COPYRIGHT_PAREN_RE.search(text)
if paren_m:
inner = paren_m.group(1).strip()
if inner and inner.lower() not in ("style", "medium", "artist", "parody"):
before = text[:paren_m.start()].rstrip()
if before:
last_comma = before.rfind(",")
char_candidate = before[last_comma + 1:].strip() if last_comma != -1 else before.strip()
if char_candidate:
result.character = char_candidate
result.series = inner
text = text[:paren_m.start()] + text[paren_m.end():]
year_m = YEAR_RE.search(text)
if year_m:
result.year_tag = f"year {year_m.group(1)}"
text = text[:year_m.start()] + text[year_m.end():]
parts = [p.strip() for p in text.split(",")]
general = []
for part in parts:
if not part:
continue
low = part.lower().replace("_", " ").strip()
low = re.sub(r"\s+", " ", low)
# Normalize spaced score tags ("score 9" / "score 9 up") into quality.
if low.startswith("score ") and low[6:].split()[0].isdigit():
rest = low[6:].split()
low = "score_" + rest[0] + ("_up" if len(rest) > 1 and rest[1] == "up" else "")
if low in QUALITY_TAGS:
result.quality_tags.append(low)
elif low in META_TAGS:
result.meta_tags.append(low)
elif low in SAFETY_TAGS:
result.safety_tag = low
elif low == part.lower() and low in PERIOD_TAGS:
# Guard against stealing common English words ("old church",
# "early morning") that are split at comma but mean something else.
# A single-word comma segment is far more likely to be a real
# period tag than a word embedded mid-phrase.
result.period_tag = low
elif low in SERIES_IDENTIFIERS and not result.series:
result.series = low
else:
general.append(part)
result.general_tags = general
# "solo" with no explicit count implies a single subject (usually 1girl).
if not result.subject and "solo" in [g.lower().strip() for g in general]:
result.subject = "1girl"
if not result.subject and not result.general_tags and not result.quality_tags:
result.nl_text = raw.strip()
return result
|