Spaces:
Running
Running
| 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) | |