Whyx-PROmpTea / src /prompt_analyzer.py
ArtShumov's picture
feat(prod): rewrite pipeline + NoobAI + ensemble tagger (3xWD14+DeepDanbooru-ready) + 1000 artists + negative templates + history ext + scoring config
e6404d0
Raw
History Blame
14.3 kB
from src.prompt_parser import parse_prompt, QUALITY_TAGS
from src.tag_warehouse import TagWarehouse
from src.variation_engine import _find_conflict_group
from src.semantic_coherence import detect_intent, _THEME_GROUPS, _INTENT_ALIGNMENT, _INTENT_OPPOSITION
from src.i18n import t
import html
CLIP_TOKEN_ESTIMATE = 75
T5_TOKEN_ESTIMATE = 256
def _estimate_clip_tokens(text: str) -> int:
"""CLIP BPE estimate: roughly 1 token per 4 latin chars + separators.
Comma-separated tags each carry an overhead (separator + start/end churn),
so count tags explicitly on top of character volume.
"""
tags = [t for t in text.split(",") if t.strip()]
char_tokens = max(0, len(text) // 4)
return max(1, tags.__len__() + char_tokens // 2)
def _estimate_t5_tokens(text: str) -> int:
"""T5 sentencepiece estimate: roughly 1 token per 3.5 chars."""
return max(1, int(len(text) / 3.5))
def _estimate_tokens(text: str) -> int:
"""Backwards-compatible alias kept for callers that expect one number."""
return _estimate_clip_tokens(text)
def _quality_status(quality_tags: list[str]) -> tuple[str, str]:
has_positive = any(t in quality_tags for t in
["masterpiece", "best quality", "high quality", "good quality"])
has_negative = any(t in quality_tags for t in
["low quality", "worst quality", "normal quality"])
positive_list = [t for t in quality_tags if t in {
"masterpiece", "best quality", "high quality", "good quality",
"score_9", "score_8", "score_7", "score_6", "score_5"}]
if not quality_tags:
return "missing", ""
if has_positive and has_negative:
return "conflict", ", ".join(positive_list[:3])
if not has_positive:
return "low", ""
return "good", ", ".join(positive_list[:3])
def _find_internal_conflicts(general_tags: list[str]) -> list[tuple[str, str]]:
conflicts = []
for i, t1 in enumerate(general_tags):
group = _find_conflict_group(t1)
if group is None:
continue
for t2 in general_tags[i + 1:]:
if t2.lower() in group:
conflicts.append((t1, t2))
return conflicts
def _find_category_overload_warnings(tags: list[str]) -> list[str]:
from src.prompt_rewriter import get_tag_categories
cat_counts: dict[str, int] = {}
for tag in tags:
for cat in get_tag_categories(tag):
cat_counts[cat] = cat_counts.get(cat, 0) + 1
return [f"{cat}: {count}" for cat, count in sorted(cat_counts.items(), key=lambda x: -x[1]) if count > 3]
def _find_theme_overload_warnings(tags: list[str]) -> list[str]:
from src.prompt_rewriter import get_tag_categories
theme_counts: dict[str, int] = {}
cat_to_theme: dict[str, str] = {}
for theme, cats in _THEME_GROUPS.items():
for cat in cats:
cat_to_theme[cat] = theme
for tag in tags:
for cat in get_tag_categories(tag):
theme = cat_to_theme.get(cat, "misc")
theme_counts[theme] = theme_counts.get(theme, 0) + 1
return [f"{theme}: {count}" for theme, count in sorted(theme_counts.items(), key=lambda x: -x[1]) if count > 5]
def _intent_mismatch_suggestions(tags: list[str]) -> list[str]:
from src.prompt_rewriter import get_tag_categories
if not tags:
return []
suggestions = []
dummy_parsed = type("obj", (object,), {"subject": "", "general_tags": tags, "character": ""})()
intent = detect_intent(dummy_parsed)
if intent == "general":
return []
opposed = _INTENT_OPPOSITION.get(intent, set())
for tag in tags:
tl = tag.lower().strip()
for cat in get_tag_categories(tl):
if cat in opposed:
suggestions.append(f"{tag} ({intent} intent)")
break
return suggestions[:3]
def _core_decorative_ratio(tags: list[str]) -> str | None:
from src.semantic_coherence import split_core_decorative
core, deco = split_core_decorative(tags)
total = len(core) + len(deco)
if total == 0:
return None
ratio = len(core) / total
if ratio < 0.2:
return "low_core"
if ratio > 0.8:
return "high_core"
return None
def _detect_duplicates(tags: list[str]) -> list[str]:
seen = {}
dups = []
for t in tags:
k = t.lower().strip()
if k in seen:
dups.append(t)
seen[k] = t
return dups
def analyze_prompt(raw: str, warehouse: TagWarehouse) -> dict:
result = {
"raw": raw,
"parsed": None,
"error": None,
"structure": {},
"token_estimate": {},
"quality": {},
"conflicts": [],
"duplicates": [],
"recommendations": [],
}
if not raw or not raw.strip():
result["error"] = "empty"
return result
parsed = parse_prompt(raw)
if parsed is None:
result["error"] = "parse"
return result
result["parsed"] = parsed
structure = {
"subject": parsed.subject or "β€”",
"character": parsed.character or "β€”",
"series": parsed.series or "β€”",
"artists": parsed.artists,
"n_language": bool(parsed.nl_text),
"num_quality": len(parsed.quality_tags),
"num_meta": len(parsed.meta_tags),
"num_general": len(parsed.general_tags),
"num_artists": len(parsed.artists),
"total_tags": len(parsed.quality_tags) + len(parsed.meta_tags)
+ len(parsed.general_tags) + len(parsed.artists)
+ (1 if parsed.subject else 0)
+ (1 if parsed.character else 0)
+ (1 if parsed.series else 0),
"has_safety": bool(parsed.safety_tag),
"has_year": bool(parsed.year_tag),
}
result["structure"] = structure
tokens = {
"clip": _estimate_clip_tokens(raw),
"t5": _estimate_t5_tokens(raw),
"clip_warning": _estimate_clip_tokens(raw) > CLIP_TOKEN_ESTIMATE,
"t5_warning": _estimate_t5_tokens(raw) > T5_TOKEN_ESTIMATE,
}
result["token_estimate"] = tokens
q_status, q_examples = _quality_status(parsed.quality_tags)
result["quality"] = {"status": q_status, "examples": q_examples}
all_tags = (parsed.quality_tags + parsed.meta_tags + parsed.general_tags)
result["conflicts"] = _find_internal_conflicts(all_tags)
result["duplicates"] = _detect_duplicates(all_tags)
result["category_overload"] = _find_category_overload_warnings(all_tags)
result["theme_overload"] = _find_theme_overload_warnings(all_tags)
result["intent_mismatches"] = _intent_mismatch_suggestions(parsed.general_tags)
result["core_ratio"] = _core_decorative_ratio(parsed.general_tags)
recs = []
if not parsed.subject:
recs.append("missing_subject")
if not parsed.has_booru_structure and not parsed.nl_text:
recs.append("unrecognized")
elif parsed.nl_text and not parsed.has_booru_structure:
recs.append("nl_only")
if q_status == "missing":
recs.append("quality_missing")
elif q_status == "conflict":
recs.append("quality_conflict")
elif q_status == "low":
recs.append("quality_low")
if not parsed.safety_tag:
recs.append("safety_missing")
if not parsed.artists and parsed.has_booru_structure:
recs.append("artist_missing")
if tokens["clip_warning"]:
recs.append("token_clip")
if tokens["t5_warning"]:
recs.append("token_t5")
if result["conflicts"]:
recs.append("tag_conflicts")
if result["duplicates"]:
recs.append("duplicate_tags")
if structure["num_general"] > 20:
recs.append("too_many_tags")
if not parsed.character and not parsed.series and parsed.has_booru_structure:
recs.append("character_series_missing")
if len(parsed.artists) > 3:
recs.append("too_many_artists")
if result.get("theme_overload"):
recs.append("theme_overload")
if result.get("intent_mismatches"):
recs.append("intent_mismatches")
if result.get("core_ratio") == "low_core":
recs.append("low_core_ratio")
elif result.get("core_ratio") == "high_core":
recs.append("high_core_ratio")
result["recommendations"] = recs
return result
def format_analysis_html(data: dict, lang: str) -> str:
if data.get("error") == "empty":
return ""
if data.get("error") == "parse":
msg = t("analyzer_parse_error", lang)
return f"""
<div style="background:rgba(15,23,42,0.5);border:1px solid rgba(239,68,68,0.25);border-radius:10px;padding:10px 14px;margin-top:4px;">
<div style="color:#F87171;font-size:12px;">⚠ {msg}</div>
</div>
"""
s = data["structure"]
q = data["quality"]
tok = data["token_estimate"]
recs = data["recommendations"]
conflicts = data["conflicts"]
dups = data["duplicates"]
cat_overload = data.get("category_overload", [])
theme_overload = data.get("theme_overload", [])
intent_mismatches = data.get("intent_mismatches", [])
core_ratio = data.get("core_ratio")
quality_label = {
"good": t("analyzer_status_good", lang),
"missing": t("analyzer_status_missing", lang),
"conflict": t("analyzer_status_conflict", lang),
"low": t("analyzer_status_low", lang),
}.get(q["status"], q["status"])
lines = []
lines.append("<div style='background:rgba(15,23,42,0.5);border:1px solid rgba(56,189,248,0.12);border-radius:10px;padding:10px 14px;margin-top:4px;'>")
lines.append(f"<div style='display:flex;gap:12px;flex-wrap:wrap;font-size:11px;color:#94A3B8;margin-bottom:6px;'>")
lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_subject', lang)}:</strong> {html.escape(s['subject'])}</span>")
lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_character', lang)}:</strong> {html.escape(s['character'])}</span>")
lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_series', lang)}:</strong> {html.escape(s['series'])}</span>")
if s['artists']:
lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_artists', lang)}:</strong> {html.escape(', '.join(s['artists']))}</span>")
lines.append("</div>")
bar_color = {"good": "#34D399", "missing": "#F87171", "conflict": "#FBBF24", "low": "#FBBF24"}.get(q["status"], "#94A3B8")
lines.append(f"<div style='display:flex;gap:10px;flex-wrap:wrap;font-size:11px;color:#94A3B8;margin-bottom:6px;'>")
lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_tags', lang)}:</strong> {s['total_tags']} ({t('analyzer_quality', lang)}: {s['num_quality']}, {t('analyzer_meta', lang)}: {s['num_meta']}, {t('analyzer_general', lang)}: {s['num_general']})</span>")
lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_quality', lang)}:</strong> <span style='color:{bar_color};'>{quality_label}</span></span>")
lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_tokens', lang)}:</strong> β‰ˆ{tok['clip']} CLIP {'⚠️' if tok['clip_warning'] else ''} / β‰ˆ{tok['t5']} T5 {'⚠️' if tok['t5_warning'] else ''}</span>")
if s['n_language']:
lines.append(f"<span style='color:#818CF8;'>{t('analyzer_natural_lang', lang)}</span>")
lines.append("</div>")
if conflicts or dups or recs:
lines.append("<div style='border-top:1px solid rgba(56,189,248,0.08);margin-top:6px;padding-top:6px;'>")
for c1, c2 in conflicts[:3]:
lines.append(f"<div style='font-size:11px;color:#FBBF24;'>⚠ <strong>{html.escape(c1)}</strong> ↔ <strong>{html.escape(c2)}</strong> {t('analyzer_conflict', lang)}</div>")
for d in dups[:3]:
lines.append(f"<div style='font-size:11px;color:#FBBF24;'>⚠ <strong>{html.escape(d)}</strong> {t('analyzer_duplicate', lang)}</div>")
for overload in cat_overload[:3]:
lines.append(f"<div style='font-size:11px;color:#FBBF24;'>πŸ“Š {html.escape(overload)} {t('analyzer_category_overload', lang)}</div>")
for overload in theme_overload[:3]:
lines.append(f"<div style='font-size:11px;color:#FBBF24;'>πŸ“Š {html.escape(overload)} {t('analyzer_theme_overload', lang)}</div>")
for m in intent_mismatches[:2]:
lines.append(f"<div style='font-size:11px;color:#FBBF24;'>🎯 {html.escape(m)} {t('analyzer_intent_mismatch', lang)}</div>")
if core_ratio == "low_core":
lines.append(f"<div style='font-size:11px;color:#38BDF8;'>πŸ’‘ {t('rec_low_core_ratio', lang)}</div>")
elif core_ratio == "high_core":
lines.append(f"<div style='font-size:11px;color:#38BDF8;'>πŸ’‘ {t('rec_high_core_ratio', lang)}</div>")
rec_labels = {
"missing_subject": t("rec_missing_subject", lang),
"unrecognized": t("rec_unrecognized", lang),
"nl_only": t("rec_nl_only", lang),
"quality_missing": t("rec_quality_missing", lang),
"quality_conflict": t("rec_quality_conflict", lang),
"quality_low": t("rec_quality_low", lang),
"safety_missing": t("rec_safety_missing", lang),
"artist_missing": t("rec_artist_missing", lang),
"token_clip": t("rec_token_clip", lang).format(n=CLIP_TOKEN_ESTIMATE),
"token_t5": t("rec_token_t5", lang).format(n=T5_TOKEN_ESTIMATE),
"tag_conflicts": t("rec_tag_conflicts", lang),
"duplicate_tags": t("rec_duplicate_tags", lang),
"too_many_tags": t("rec_too_many_tags", lang),
"character_series_missing": t("rec_character_series_missing", lang),
"too_many_artists": t("rec_too_many_artists", lang),
"theme_overload": t("rec_theme_overload", lang),
"intent_mismatches": t("rec_intent_mismatches", lang),
"low_core_ratio": t("rec_low_core_ratio", lang),
"high_core_ratio": t("rec_high_core_ratio", lang),
}
for r in recs[:5]:
label = rec_labels.get(r, r)
lines.append(f"<div style='font-size:11px;color:#38BDF8;margin-top:2px;'>πŸ’‘ {label}</div>")
lines.append("</div>")
lines.append("</div>")
return "\n".join(lines)