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"""
⚠ {msg}
""" 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("
") lines.append(f"
") lines.append(f"{t('analyzer_subject', lang)}: {html.escape(s['subject'])}") lines.append(f"{t('analyzer_character', lang)}: {html.escape(s['character'])}") lines.append(f"{t('analyzer_series', lang)}: {html.escape(s['series'])}") if s['artists']: lines.append(f"{t('analyzer_artists', lang)}: {html.escape(', '.join(s['artists']))}") lines.append("
") bar_color = {"good": "#34D399", "missing": "#F87171", "conflict": "#FBBF24", "low": "#FBBF24"}.get(q["status"], "#94A3B8") lines.append(f"
") lines.append(f"{t('analyzer_tags', lang)}: {s['total_tags']} ({t('analyzer_quality', lang)}: {s['num_quality']}, {t('analyzer_meta', lang)}: {s['num_meta']}, {t('analyzer_general', lang)}: {s['num_general']})") lines.append(f"{t('analyzer_quality', lang)}: {quality_label}") lines.append(f"{t('analyzer_tokens', lang)}: ≈{tok['clip']} CLIP {'⚠️' if tok['clip_warning'] else ''} / ≈{tok['t5']} T5 {'⚠️' if tok['t5_warning'] else ''}") if s['n_language']: lines.append(f"{t('analyzer_natural_lang', lang)}") lines.append("
") if conflicts or dups or recs: lines.append("
") for c1, c2 in conflicts[:3]: lines.append(f"
{html.escape(c1)}{html.escape(c2)} {t('analyzer_conflict', lang)}
") for d in dups[:3]: lines.append(f"
{html.escape(d)} {t('analyzer_duplicate', lang)}
") for overload in cat_overload[:3]: lines.append(f"
📊 {html.escape(overload)} {t('analyzer_category_overload', lang)}
") for overload in theme_overload[:3]: lines.append(f"
📊 {html.escape(overload)} {t('analyzer_theme_overload', lang)}
") for m in intent_mismatches[:2]: lines.append(f"
🎯 {html.escape(m)} {t('analyzer_intent_mismatch', lang)}
") if core_ratio == "low_core": lines.append(f"
💡 {t('rec_low_core_ratio', lang)}
") elif core_ratio == "high_core": lines.append(f"
💡 {t('rec_high_core_ratio', lang)}
") 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"
💡 {label}
") lines.append("
") lines.append("
") return "\n".join(lines)