hleserg's picture
Replace 3-model zoo with single gliner2 (HLE-806)
0db8096
Raw
History Blame Contribute Delete
56.9 kB
"""Atman β€” Linguistic + NLP Demo (HuggingFace Space).
Gradio UI surfacing the four analysis blocks of Atman's linguistic layer.
Models are preloaded on startup. Warmup forces inference cache.
Progress bars removed for stability. Auto-language detection enabled.
"""
from __future__ import annotations
import json
import logging
import os
import sys
import torch
from pathlib import Path
_HERE = Path(__file__).resolve().parent
if str(_HERE) not in sys.path:
sys.path.insert(0, str(_HERE))
# FIX: Used only HF_HOME as TRANSFORMERS_CACHE is deprecated in v5.
os.environ["HF_HOME"] = str(_HERE / ".hf_cache")
import gradio as gr
from examples.presets import (
lookup_affect,
lookup_point_a,
lookup_point_k,
lookup_relations,
preset_labels,
AFFECT_PRESETS,
POINT_A_PRESETS,
POINT_K_PRESETS,
RELATIONS_PRESETS,
_AFFECT_EN_LABELS,
_POINT_A_EN_LABELS,
_POINT_K_EN_LABELS,
_RELATIONS_EN_LABELS,
)
from lib.affect.emolex.emolex import EMOTION_KEYS, emotion_score, tokenize
from lib.affect.metrics import (
disclaimer_density,
emotion_lexical_energy,
hedge_density,
self_reference_density,
sincerity_score,
strip_markdown,
)
from lib.affect.refusal_detector import score_refusal
from lib.dto import DetectedEntity, RawSpan
from lib.gliner2_engine import Gliner2Analyzer
from lib.observability import (
capture_empty_result,
capture_silent_exception,
init_sentry_from_env,
traced,
)
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
init_sentry_from_env()
# ──────────────────────────────────────────────────────────────────────────────
# Singletons & Model Management
# ──────────────────────────────────────────────────────────────────────────────
_ANALYZER: Gliner2Analyzer | None = None
def get_analyzer() -> Gliner2Analyzer:
global _ANALYZER
if _ANALYZER is None:
try:
logging.info("Initializing gliner2 analyzer (NER + classification + relations)...")
_ANALYZER = Gliner2Analyzer()
logging.info("βœ… gliner2 analyzer initialized.")
except Exception as exc:
logging.error("Failed to initialize analyzer: %s", exc, exc_info=True)
capture_silent_exception(exc, context="get_analyzer.init")
raise RuntimeError(f"Analyzer init failed: {exc}") from exc
return _ANALYZER
def preload_models():
logging.info("⏳ Preloading model weights to cache...")
try:
from gliner2 import GLiNER2
GLiNER2.from_pretrained("fastino/gliner2-multi-v1")
logging.info("βœ… Model weights cached successfully.")
return True
except Exception as e:
logging.error(f"❌ Preload error: {e}", exc_info=True)
return False
def effective_ui_lang(lang_choice: str) -> str:
"""Effective UI locale β€” strict ru/en, default en."""
return "ru" if lang_choice == "ru" else "en"
# Placeholder shown as the default-selected option in preset Dropdowns.
# Looks like a hint, doesn't match any key in _POINT_A / _POINT_K / _RELATIONS /
# _AFFECT dicts, so lookup_* returns None and handlers no-op when selected.
PRESET_PLACEHOLDERS: dict[str, str] = {
"en": "β€” Select a ready-made example β€”",
"ru": "β€” Π’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ Π³ΠΎΡ‚ΠΎΠ²Ρ‹ΠΉ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ β€”",
}
def preset_choices(presets, lang: str, en_labels: dict[str, str] | None = None) -> list[str]:
"""preset_labels(...) with the locale-specific placeholder prepended."""
return [PRESET_PLACEHOLDERS[lang], *preset_labels(presets, lang, en_labels)]
def footer_html(lang: str) -> str:
"""Locale-specific footer β€” shown one language at a time via update_ui_language."""
if lang == "ru":
return """
<div id="atman-footer">
<em>Мой ΠΏΠ΅Ρ€Π²Ρ‹ΠΉ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ Π² AI/ML β€” Π±ΡƒΠ΄Ρƒ искрСннС Ρ€Π°Π΄ ΠΎΡ‚Π·Ρ‹Π²Π°ΠΌ ΠΏΡ€ΠΎ ΠΌΠΎΠ΄Π΅Π»ΠΈ,
Π°Π»Π³ΠΎΡ€ΠΈΡ‚ΠΌΡ‹ ΠΈ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρƒ.</em>
<em>Π’Π΅ΡΡŒ лингвистичСский Ρ€Π°Π·Π±ΠΎΡ€ Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ Π΄Π΅Π»Π°Π΅Ρ‚ ΠΎΠ΄Π½Π° модСль
<code>fastino/gliner2-multi-v1</code> β€” NER, классификация ΠΈ связи Π·Π° ΠΎΠ΄ΠΈΠ½ ΠΏΡ€ΠΎΡ…ΠΎΠ΄.</em>
<small class="atman-privacy">
Анонимная диагностика: ΠΏΡ€ΠΈ пустых Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π°Ρ… Π°Π½Π°Π»ΠΈΠ·Π° Ρ‚Π²ΠΎΠΉ Π²Π²ΠΎΠ΄ ΠΈ список
ΡΡ€Π°Π±ΠΎΡ‚Π°Π²ΡˆΠΈΡ… сигналов ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡŽΡ‚ΡΡ Π² Sentry, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π΄ΠΎΡ€Π°Π±ΠΎΡ‚Π°Ρ‚ΡŒ Π΄Π΅Ρ‚Π΅ΠΊΡ‚ΠΎΡ€Ρ‹.
IP, cookies ΠΈ ΠΏΡ€ΠΎΡ‡Π΅Π΅ PII Π½Π΅ собираСм.
</small>
<a href="https://github.com/hleserg/atman">GitHub</a>
&nbsp;Β·&nbsp;
<a href="https://github.com/hleserg/atman/blob/main/MANIFEST.md">Manifest</a>
&nbsp;Β·&nbsp;
<a href="https://github.com/hleserg/atman/issues">ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ issue</a>
</div>
"""
return """
<div id="atman-footer">
<em>My first project in AI/ML β€” feedback on models, algorithms,
or architecture is genuinely welcome.</em>
<em>The whole linguistic stack now runs on a single model,
<code>fastino/gliner2-multi-v1</code> β€” NER, classification and relations in one pass.</em>
<small class="atman-privacy">
Anonymous diagnostics: when analyzers return an empty result, the input
text and which signals fired are sent to Sentry so the detectors can be
improved. No IPs, cookies, or other PII are collected.
</small>
<a href="https://github.com/hleserg/atman">GitHub</a>
&nbsp;Β·&nbsp;
<a href="https://github.com/hleserg/atman/blob/main/MANIFEST.md">Manifest</a>
&nbsp;Β·&nbsp;
<a href="https://github.com/hleserg/atman/issues">Open an issue</a>
</div>
"""
# ──────────────────────────────────────────────────────────────────────────────
# Highlight & Output Helpers
# ──────────────────────────────────────────────────────────────────────────────
# Maps 13 raw NER labels β†’ 4 display groups for gr.HighlightedText color_map
_POINT_A_LABEL_GROUPS: dict[str, str] = {
"commitment": "commit",
"action intent": "action_scope",
"boundary marker": "action_scope",
"topic anchor": "action_scope",
"hedge": "hedge",
"uncertainty marker": "hedge",
"concession": "hedge",
"emotional anchor": "affect",
"value reference": "affect",
"principle invocation": "affect",
"intensifier": "affect",
"belief marker": "affect",
"relational reference": "affect",
}
_POINT_A_COLOR_MAP: dict[str, str] = {
"commit": "#22C55E",
"action_scope": "#818CF8",
"hedge": "#F59E0B",
"affect": "#EC4899",
}
# Maps 7 Point K narrative labels β†’ 4 display groups
_POINT_K_LABEL_GROUPS: dict[str, str] = {
"decision statement": "agency",
"boundary act": "agency",
"realization statement": "inner_state",
"attribution shift": "inner_state",
"feeling statement": "inner_state",
"connection signal": "connection",
"value invocation": "values",
}
_POINT_K_COLOR_MAP: dict[str, str] = {
"agency": "#60A5FA",
"inner_state": "#A78BFA",
"connection": "#34D399",
"values": "#FBBF24",
}
# Colors for Relations entity types (EntityType enum values)
_RELATIONS_COLOR_MAP: dict[str, str] = {
"person": "#F472B6",
"organization": "#60A5FA",
"place": "#34D399",
"event": "#FB923C",
"topic": "#A78BFA",
"value": "#FBBF24",
"principle": "#E879F9",
"object": "#94A3B8",
"tool": "#4ADE80",
"skill": "#38BDF8",
"health_condition": "#F87171",
}
def spans_to_highlights(
text: str,
spans: list[RawSpan] | list[DetectedEntity],
empty_label: str = "No psychological markers detected in this text.",
label_map: dict[str, str] | None = None,
) -> list[tuple[str, str | None]]:
if not spans:
return [(empty_label, None)]
typed_spans: list[tuple[int, int, str]] = []
no_offset: list[tuple[str, str]] = []
for s in spans:
raw_label = s.label if isinstance(s, RawSpan) else s.entity_type.value
label = label_map.get(raw_label, raw_label) if label_map else raw_label
if s.span is None:
no_offset.append((s.text, label))
continue
start, end = s.span
if 0 <= start < end <= len(text):
typed_spans.append((start, end, label))
typed_spans.sort(key=lambda t: t[0])
segments: list[tuple[str, str | None]] = []
cursor = 0
for start, end, label in typed_spans:
if start < cursor:
continue
if start > cursor:
segments.append((text[cursor:start], None))
segments.append((text[start:end], label))
cursor = end
if cursor < len(text):
segments.append((text[cursor:], None))
for txt, label in no_offset:
segments.append((f" [{label}: {txt}]", label))
return segments
def _safe_analyze(fn_name: str, fn, *args, **kwargs):
try:
with torch.inference_mode():
return fn(*args, **kwargs)
except Exception as e:
logging.error("Error in %s: %s", fn_name, e, exc_info=True)
capture_silent_exception(e, context=f"{fn_name}.inference")
raise gr.Error(f"⚠️ Analysis failed: {e.__class__.__name__}: {e}")
# ──────────────────────────────────────────────────────────────────────────────
# Analysis Functions (NO PROGRESS BARS)
# ──────────────────────────────────────────────────────────────────────────────
@traced("nlp.point_a")
def analyze_point_a(message: str, thinking: str, lang_choice: str):
message = message or ""
thinking = thinking or ""
if not message.strip():
return [], "{}", "β€”", "β€”", "β€”"
def _run():
analyzer = get_analyzer()
result = analyzer.analyze_agent_message(message, thinking=thinking or None)
ui = effective_ui_lang(lang_choice)
strings = UI_STRINGS[ui]
highlights = spans_to_highlights(message, result.message_spans, strings["no_highlights"], _POINT_A_LABEL_GROUPS)
classification_summary = json.dumps({
"stance": str(result.stance) if result.stance else "β€”",
"cognitive_mode": str(result.cognitive_mode) if result.cognitive_mode else "β€”",
"self_orientation": str(result.self_orientation) if result.self_orientation else "β€”",
"primary_emotion": str(result.primary_emotion) if result.primary_emotion else "β€”",
"cognitive_load_label": str(result.cognitive_load_label) if result.cognitive_load_label else "β€”",
}, indent=2, ensure_ascii=False)
if result.boundary_markers:
boundary = "\n".join(f"β€’ {m}" for m in result.boundary_markers)
else:
boundary = strings["no_boundary"]
if not thinking.strip():
divergence = strings["no_thinking_trace"]
elif result.divergence_signals:
divergence = "\n".join(f"β€’ {s}" for s in result.divergence_signals)
else:
divergence = strings["no_divergence"]
meta = (
f"🌐 Language: **{ui}** | 🏷️ NER: {len(result.message_entities)}"
f" | πŸ“ Spans: {len(result.message_spans)} | ⚑ Load: {result.cognitive_load_high}"
)
if (
not result.message_spans
and not result.boundary_markers
and not result.divergence_signals
and not result.message_entities
):
capture_empty_result(
tab="point_a",
locale=ui,
input_text=f"MSG:\n{message}\n\nTHINKING:\n{thinking}",
reason="no_signals",
signals={
"message_spans": 0,
"boundary_markers": 0,
"divergence_signals": 0,
"message_entities": 0,
"had_thinking": bool(thinking.strip()),
},
)
return highlights, classification_summary, boundary, divergence, meta
return _safe_analyze("point_a", _run)
@traced("nlp.point_k")
def analyze_point_k(what_happened: str, why_it_matters: str, lang_choice: str):
what_happened = what_happened or ""
why_it_matters = why_it_matters or ""
if not what_happened.strip() and not why_it_matters.strip():
return [], "{}", "β€”"
def _run():
analyzer = get_analyzer()
result = analyzer.analyze_key_moment(what_happened, why_it_matters)
combined = f"{what_happened}\n{why_it_matters}"
ui = effective_ui_lang(lang_choice)
strings = UI_STRINGS[ui]
highlights = spans_to_highlights(combined, result.marker_spans, strings["no_highlights"], _POINT_K_LABEL_GROUPS)
summary = json.dumps({
"agency_level": str(result.agency_level) if result.agency_level else "β€”",
"confidence_in_self": str(result.confidence_in_self) if result.confidence_in_self else "β€”",
"trust_signal_category": str(result.trust_signal_category) if result.trust_signal_category else "β€”",
"boundary_event_category": str(result.boundary_event_category) if result.boundary_event_category else "β€”",
"connection_quality": str(result.connection_quality) if result.connection_quality else "β€”",
"learning_signal": str(result.learning_signal) if result.learning_signal else "β€”",
"growth_indicator": str(result.growth_indicator) if result.growth_indicator else "β€”",
}, indent=2, ensure_ascii=False)
meta = f"🌐 Language: **{ui}** | πŸ“¦ Entities: {len(result.entities)} | πŸ“Œ Markers: {len(result.marker_spans)} | 🚧 Event: {result.boundary_event}"
if not result.marker_spans:
capture_empty_result(
tab="point_k",
locale=ui,
input_text=combined,
reason="no_marker_spans",
signals={
"marker_spans": 0,
"entities": len(result.entities),
},
)
return highlights, summary, meta
return _safe_analyze("point_k", _run)
@traced("nlp.relations_gliner2")
def analyze_relations(text: str, lang_choice: str):
text = text or ""
if not text.strip():
ui = effective_ui_lang(lang_choice)
return [], [], UI_STRINGS[ui]["empty_input"]
def _run():
analyzer = get_analyzer()
entities = analyzer.analyze_user_message(text).entities
relations = analyzer.extract_relations(text, entities)
ui = effective_ui_lang(lang_choice)
strings = UI_STRINGS[ui]
entity_highlights = spans_to_highlights(text, entities, strings["no_highlights"])
rows = [[r.subject.text, r.relation_type, r.object.text, r.subject.entity_type.value, r.object.entity_type.value] for r in relations]
if rows:
meta = f"🌐 Language: **{ui}** | πŸ“¦ Entities: {len(entities)} | πŸ”— Relations: {len(relations)}"
else:
meta = f"🌐 Language: **{ui}** | πŸ“¦ {len(entities)} entities | {strings['no_relations']}"
capture_empty_result(
tab="relations",
locale=ui,
input_text=text,
reason="no_triples",
signals={
"entities": len(entities),
"triples": 0,
},
)
return entity_highlights, rows, meta
return _safe_analyze("relations", _run)
@traced("nlp.affect_rules")
def analyze_affect(text: str, lang_choice: str):
text = text or ""
if not text.strip():
# gr.Label in Gradio 6 rejects {} β€” use None or a placeholder dict.
return None, "β€”", "β€”", "β€”", "β€”"
def _run():
clean_text, emphasized = strip_markdown(text)
ui = effective_ui_lang(lang_choice)
analysis_lang = ui
raw = emotion_score(clean_text, lang=analysis_lang)
meta = raw.pop("_meta", {})
emo_chart = {k: min(1.0, float(raw[k]) / 100.0) for k in EMOTION_KEYS}
tokens = tokenize(clean_text)
s_score = sincerity_score(clean_text, tokens, analysis_lang)
metrics_md = (
f"- **hedge_density**: `{hedge_density(tokens, analysis_lang):.4f}`\n"
f"- **self_reference_density**: `{self_reference_density(tokens, analysis_lang):.4f}`\n"
f"- **disclaimer_density**: `{disclaimer_density(tokens, analysis_lang):.4f}`\n"
f"- **sincerity_score**: `{s_score}` _(0–3)_\n"
f"- **emotion_energy**: `{emotion_lexical_energy(raw):.3f}`"
)
refusal = score_refusal(clean_text)
conf = refusal.confidence
if conf >= 0.45:
band = "πŸ”΄ **Confident refusal**"
elif conf >= 0.30:
band = "🟑 **Gray zone** β€” soft signal, no strong morphology"
else:
band = "βšͺ **No refusal pattern**"
refusal_md = (
f"{band}\n\n"
f"**Confidence:** `{refusal.confidence:.3f}` / threshold `0.45` "
f"({refusal.decided_by})\n"
f"- refusal_verb: `{refusal.has_refusal_verb}`\n"
f"- disgust/anger: `{refusal.disgust_density:.2f}` / "
f"`{refusal.anger_density:.2f}`"
)
emphasis_md = (
"**" + "**, **".join(emphasized) + "**"
if emphasized
else "βœ“ No markdown emphasis (bold/italic) detected."
)
meta_md = f"🌐 Language: **{analysis_lang}** | πŸ“ Tokens: **{meta.get('tokens', 0)}** | 🎯 NRC Hits: **{meta.get('hits', 0)}**"
emotion_total = sum(emo_chart.values())
emolex_hits = int(meta.get("hits", 0) or 0)
if conf < 0.30 and emotion_total < 0.05 and emolex_hits == 0:
capture_empty_result(
tab="affect",
locale=ui,
input_text=text,
reason="no_signal",
signals={
"refusal_confidence": round(conf, 3),
"emotion_total": round(emotion_total, 4),
"emolex_hits": emolex_hits,
},
)
return emo_chart, metrics_md, refusal_md, emphasis_md, meta_md
return _safe_analyze("affect", _run)
def warmup_models():
"""Load the single gliner2 model and run one real inference of each kind
(NER + zero-shot classification + relations) so the tokenizer cache and
torch CPU/GPU layers are initialised before the first user request.
"""
try:
analyzer = get_analyzer()
# NER + zero-shot classification β€” one model, one Point-A pass.
analyzer.analyze_agent_message(
"Warmup test text. I think this might work β€” depends on context.",
thinking=None,
)
# Relations β€” one route-A extraction so the relation head is paged in.
sample = "Alice works with Bob in Paris."
analyzer.extract_relations(sample, analyzer.analyze_user_message(sample).entities)
return "βœ… Model warmed up: gliner2 (NER + classification + relations) ready."
except Exception as exc:
logging.exception("warmup failed")
return f"❌ Warmup failed: {exc.__class__.__name__}: {exc}"
# ──────────────────────────────────────────────────────────────────────────────
# Localization & UI Builder
# ──────────────────────────────────────────────────────────────────────────────
UI_STRINGS = {
"en": {
"header_blurb": (
"<b class='atman-emph'>What you're looking at</b>: a sensor of "
"[Atman](https://github.com/hleserg/atman) β€” a psychological "
"runtime layer that gives AI agents continuous identity, "
"first-person memory, and reflection. This Space shows the "
"<b class='atman-emph'>linguistic block</b> β€” 4 analysis points "
"scanning what the agent says (and thinks) for signals that feed "
"Experience, Identity, and Reflection.\n\n"
"*The lower agent acts. Atman exists.*"
),
"warmup_btn": "πŸ”₯ Warmup Models",
"warmup_log": "⏸️ Status: Waiting...",
"lang_info": "Interface and analysis language.",
"analyze_btn": "▢️ Analyze",
"extract_relations_btn": "▢️ Extract relations",
"point_a_tab": "Point A Β· Agent Message",
"point_k_tab": "Point K Β· Key Moment",
"relations_tab": "Relations Β· gliner2",
"affect_tab": "Affect Β· Rule-based",
"presets": "πŸ“₯ Presets:",
"preset_label": "Pick preset",
"no_highlights": "βœ“ Scan clean β€” no signals of this kind in this text. Not every input triggers this layer.",
"no_boundary": "βœ“ No boundary acts here β€” agent stayed within its operating zone.",
"no_divergence": "βœ“ Thinking and message aligned β€” no suppression, evaluation flip, or tone shift.",
"no_thinking_trace": "Provide a thinking trace to compare against the message.",
"boundary_title": "🚧 Boundary & Resistance Markers (Rule-Based)",
"divergence_title": "πŸ” Thinking vs Message Divergence",
"meta_title": "πŸ“Š System Metadata",
"empty_input": "Empty input β€” paste some text to analyze.",
"no_relations": "βœ“ No subject-predicate-object triplets β€” text reads as descriptive prose rather than relational content.",
"about_label": "ℹ️ What does this analyze?",
"about_point_a": (
"Scans every agent reply for psychological signals:\n"
"- **NER** (gliner2, 13 labels): hedges, boundary markers, value references, "
"intensifiers, commitments, etc.\n"
"- **Classification** (gliner2 zero-shot, 5 dims): stance, cognitive mode, "
"self-orientation, affective state, cognitive load.\n"
"- **Divergence**: when the thinking trace contradicts the surface message "
"(suppression, sycophancy, tone mismatch, length compression).\n\n"
"**Feeds β†’ Experience Store.** Each reply becomes an Experience tied to "
"the agent's Eigenstate.\n\n"
"> ⚠️ **Boundary detection is regex over canonical refusal phrases** "
"(*'I won't', 'enough', 'Π½Π΅Ρ‚', 'стоп'*). Idiomatic or metaphorical refusals "
"(*'I won't help write malware'* without explicit stop-marker) may not "
"trigger β€” kept this way to maintain near-zero false-positive rate. An "
"LLM-layer above this can catch the rest."
),
"about_point_k": (
"Analyzes moments the agent itself marked as significant β€” these become "
"the seeds of self-narrative.\n"
"- **NER** (gliner2 + RU/EN substring fallback, 7 clause-level spans): "
"decision, realization, feeling, value invocation, boundary act, "
"connection, attribution shift.\n"
"- **Classification** (gliner2, 7 dims): agency, confidence, trust, "
"boundary event, connection quality, learning, growth.\n\n"
"**Feeds β†’ Reflection Engine.** Over time, these episodes turn into beliefs "
"(\"I tend to refuse when X\", \"I felt connection when Y\").\n\n"
"> ⚠️ **NER works best on longer narrative text.** "
"On short key-moment snippets the multilingual NER may return no "
"spans β€” that's why a substring heuristic for canonical RU/EN "
"first-person markers runs as a primary pass. The gliner2 "
"**classification** (7 dims on the right) is the more stable "
"signal here regardless."
),
"about_relations": (
"Extracts entity-relation triplets (subject, predicate, object) from text.\n"
"- **Model**: gliner2 (fastino/gliner2-multi-v1) β€” curated route-A schema "
"(14 relation types + type-constraint filter).\n"
"- Identifies people, places, organizations, projects + how they relate.\n\n"
"**Feeds β†’ Identity Store.** Agent's knowledge of who you are, what you "
"care about, who you're connected to."
),
"about_affect": (
"Catches affective state without any LLM call β€” fast and deterministic.\n"
"- **EmoLex**: 10-dim NRC emotion vector with intensifier/negation handling.\n"
"- **Behavioural metrics**: hedge density, self-reference, disclaimers, sincerity score.\n"
"- **3-layer refusal detector**: distinguishes value refusal (\"I won't deceive\") "
"from capability refusal (\"I can't generate images\").\n\n"
"**Refusal confidence bands:**\n"
"- πŸ”΄ **β‰₯ 0.45** β€” confident refusal (morphology + moral context aligned).\n"
"- 🟑 **0.30 – 0.45** β€” gray zone (signal present but weak).\n"
"- βšͺ **< 0.30** β€” no refusal pattern.\n\n"
"**Feeds β†’ Affective Regulation.** Rolling baselines, divergence triggers, "
"value-refusal events.\n\n"
"> ⚠️ **Rule-based first-pass filter.** Subtle/idiomatic refusals "
"(*'I'd really rather not'*, *'this doesn't sit right with me'*) may stay in "
"the gray zone β€” by design. This layer is fast, deterministic, and explainable; "
"an LLM layer can refine the gray-zone calls."
),
},
"ru": {
"header_blurb": (
"<b class='atman-emph'>Π§Ρ‚ΠΎ Ρ‚Ρ‹ сСйчас видишь</b>: сСнсор "
"[Atman](https://github.com/hleserg/atman) β€” психологичСского "
"runtime-слоя, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π΄Π°Ρ‘Ρ‚ AI-Π°Π³Π΅Π½Ρ‚Π°ΠΌ Π½Π΅ΠΏΡ€Π΅Ρ€Ρ‹Π²Π½ΡƒΡŽ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ‡Π½ΠΎΡΡ‚ΡŒ, "
"ΠΏΠ°ΠΌΡΡ‚ΡŒ ΠΎΡ‚ ΠΏΠ΅Ρ€Π²ΠΎΠ³ΠΎ Π»ΠΈΡ†Π° ΠΈ Ρ€Π΅Ρ„Π»Π΅ΠΊΡΠΈΡŽ. Π­Ρ‚ΠΎΡ‚ Space ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅Ρ‚ "
"<b class='atman-emph'>лингвистичСский Π±Π»ΠΎΠΊ</b> β€” 4 Ρ‚ΠΎΡ‡ΠΊΠΈ Π°Π½Π°Π»ΠΈΠ·Π°, "
"ΡΠΊΠ°Π½ΠΈΡ€ΡƒΡŽΡ‰ΠΈΠ΅ Ρ‡Ρ‚ΠΎ Π°Π³Π΅Π½Ρ‚ Π³ΠΎΠ²ΠΎΡ€ΠΈΡ‚ (ΠΈ Π΄ΡƒΠΌΠ°Π΅Ρ‚) Π½Π° ΠΏΡ€Π΅Π΄ΠΌΠ΅Ρ‚ сигналов, "
"ΠΏΠΈΡ‚Π°ΡŽΡ‰ΠΈΡ… Experience, Identity ΠΈ Reflection.\n\n"
"*НиТний Π°Π³Π΅Π½Ρ‚ дСйствуСт. Atman сущСствуСт.*"
),
"warmup_btn": "πŸ”₯ ΠŸΡ€ΠΎΠ³Ρ€Π΅Ρ‚ΡŒ ΠΌΠΎΠ΄Π΅Π»ΠΈ",
"warmup_log": "⏸️ Бтатус: ОТиданиС...",
"lang_info": "Π―Π·Ρ‹ΠΊ интСрфСйса ΠΈ Π°Π½Π°Π»ΠΈΠ·Π°.",
"analyze_btn": "▢️ ΠΠ½Π°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ",
"extract_relations_btn": "▢️ Π˜Π·Π²Π»Π΅Ρ‡ΡŒ связи",
"point_a_tab": "Point A Β· Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠ΅ Π°Π³Π΅Π½Ρ‚Π°",
"point_k_tab": "Point K Β· ΠšΠ»ΡŽΡ‡Π΅Π²ΠΎΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚",
"relations_tab": "Бвязи · gliner2",
"affect_tab": "АффСкт Β· ΠŸΡ€Π°Π²ΠΈΠ»Π°",
"presets": "πŸ“₯ ΠŸΡ€Π΅ΡΠ΅Ρ‚Ρ‹:",
"preset_label": "Π’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ прСсСт",
"no_highlights": "βœ“ Π‘ΠΊΠ°Π½ чистый β€” сигналов этого слоя Π² тСкстС Π½Π΅Ρ‚. НС ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ Π²Π²ΠΎΠ΄ сюда ΠΏΠΎΠΏΠ°Π΄Π°Π΅Ρ‚ β€” это Π½ΠΎΡ€ΠΌΠ°.",
"no_boundary": "βœ“ ДСйствий Π³Ρ€Π°Π½ΠΈΡ†Ρ‹ Π² сообщСнии Π½Π΅Ρ‚ β€” Π°Π³Π΅Π½Ρ‚ остался Π² Ρ€Π°Π±ΠΎΡ‡Π΅ΠΉ Π·ΠΎΠ½Π΅.",
"no_divergence": "βœ“ Thinking ΠΈ сообщСниС ΡΠΎΠ²ΠΏΠ°Π΄Π°ΡŽΡ‚ β€” Π±Π΅Π· подавлСния, ΠΏΠ΅Ρ€Π΅Π²ΠΎΡ€ΠΎΡ‚Π° ΠΎΡ†Π΅Π½ΠΊΠΈ, ΠΈΠ»ΠΈ сдвига Ρ‚ΠΎΠ½Π°.",
"no_thinking_trace": "Π”ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ thinking trace для сравнСния с сообщСниСм.",
"boundary_title": "🚧 ΠœΠ°Ρ€ΠΊΠ΅Ρ€Ρ‹ Π³Ρ€Π°Π½ΠΈΡ† ΠΈ сопротивлСния (ΠŸΡ€Π°Π²ΠΈΠ»Π°)",
"divergence_title": "πŸ” РасхоТдСниС мыслСй ΠΈ сообщСния",
"meta_title": "πŸ“Š ΠœΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Π΅ систСмы",
"empty_input": "ΠŸΡƒΡΡ‚ΠΎΠΉ Π²Π²ΠΎΠ΄ β€” Π²ΡΡ‚Π°Π²ΡŒ тСкст для Π°Π½Π°Π»ΠΈΠ·Π°.",
"no_relations": "βœ“ Π’Ρ€ΠΎΠΉΠΊΠΈ ΡΡƒΠ±ΡŠΠ΅ΠΊΡ‚-ΠΏΡ€Π΅Π΄ΠΈΠΊΠ°Ρ‚-ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹ β€” тСкст ΠΎΠΏΠΈΡΠ°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ, Π½Π΅ рСляционный.",
"about_label": "ℹ️ Π§Ρ‚ΠΎ здСсь анализируСтся?",
"about_point_a": (
"Π‘ΠΊΠ°Π½ΠΈΡ€ΡƒΠ΅Ρ‚ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ сообщСниС Π°Π³Π΅Π½Ρ‚Π° Π½Π° психологичСскиС сигналы:\n"
"- **NER** (gliner2, 13 ΠΌΠ΅Ρ‚ΠΎΠΊ): Ρ…Π΅Π΄ΠΆΠΈ, ΠΌΠ°Ρ€ΠΊΠ΅Ρ€Ρ‹ Π³Ρ€Π°Π½ΠΈΡ†, отсылки ΠΊ цСнностям, "
"усилитСли, ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΡΡ‚Π²Π° ΠΈ Ρ‚.Π΄.\n"
"- **ΠšΠ»Π°ΡΡΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΡ** (gliner2 zero-shot, 5 ΠΈΠ·ΠΌΠ΅Ρ€Π΅Π½ΠΈΠΉ): позиция, ΠΊΠΎΠ³Π½ΠΈΡ‚ΠΈΠ²Π½Ρ‹ΠΉ "
"Ρ€Π΅ΠΆΠΈΠΌ, само-ориСнтация, Π°Ρ„Ρ„Π΅ΠΊΡ‚ΠΈΠ²Π½ΠΎΠ΅ состояниС, когнитивная Π½Π°Π³Ρ€ΡƒΠ·ΠΊΠ°.\n"
"- **РасхоТдСниС**: ΠΊΠΎΠ³Π΄Π° thinking ΠΏΡ€ΠΎΡ‚ΠΈΠ²ΠΎΡ€Π΅Ρ‡ΠΈΡ‚ ΡΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΡŽ (ΠΏΠΎΠ΄Π°Π²Π»Π΅Π½ΠΈΠ΅, "
"ΡΠΈΠΊΠΎΡ„Π°Π½Ρ‚Π½ΠΎΡΡ‚ΡŒ, нСсоотвСтствиС Ρ‚ΠΎΠ½Π°, компрСссия Π΄Π»ΠΈΠ½Ρ‹).\n\n"
"**ΠŸΠΈΡ‚Π°Π΅Ρ‚ β†’ Experience Store.** КаТдоС сообщСниС становится Experience, "
"связанным с Eigenstate Π°Π³Π΅Π½Ρ‚Π°.\n\n"
"> ⚠️ **Boundary detection β€” это regex ΠΏΠΎ каноничСским Ρ„ΠΎΡ€ΠΌΠ°ΠΌ ΠΎΡ‚ΠΊΠ°Π·Π°** "
"(*'я Π½Π΅ Π±ΡƒΠ΄Ρƒ', 'Π½Π΅Ρ‚', 'стоп', 'enough'*). Π˜Π΄ΠΈΠΎΠΌΠ°Ρ‚ΠΈΡ‡Π½Ρ‹Π΅/ΠΌΠ΅Ρ‚Π°Ρ„ΠΎΡ€ΠΈΡ‡Π½Ρ‹Π΅ ΠΎΡ‚ΠΊΠ°Π·Ρ‹ "
"(*'I won't help write malware'* Π±Π΅Π· эксплицитного stop-marker) ΠΌΠΎΠ³ΡƒΡ‚ Π½Π΅ "
"ΡΡ€Π°Π±ΠΎΡ‚Π°Ρ‚ΡŒ β€” Π½Π°ΠΌΠ΅Ρ€Π΅Π½Π½ΠΎ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ FP-rate Π±Π»ΠΈΠ·ΠΊΠΈΠΉ ΠΊ Π½ΡƒΠ»ΡŽ. Π‘Π»ΠΎΠΉ LLM "
"ΠΏΠΎΠ²Π΅Ρ€Ρ… этого ΠΌΠΎΠΆΠ΅Ρ‚ Π΄ΠΎΠ±Ρ€Π°Ρ‚ΡŒ ΠΎΡΡ‚Π°Π»ΡŒΠ½ΠΎΠ΅."
),
"about_point_k": (
"АнализируСт ΠΌΠΎΠΌΠ΅Π½Ρ‚Ρ‹, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ сам Π°Π³Π΅Π½Ρ‚ ΠΏΠΎΠΌΠ΅Ρ‚ΠΈΠ» ΠΊΠ°ΠΊ Π·Π½Π°Ρ‡ΠΈΠΌΡ‹Π΅ β€” это сСмСна "
"Π΅Π³ΠΎ самонарратива.\n"
"- **NER** (gliner2 + RU/EN substring fallback, 7 Ρ„Ρ€Π°Π·ΠΎΠ²Ρ‹Ρ… ΠΌΠ΅Ρ‚ΠΎΠΊ): "
"Ρ€Π΅ΡˆΠ΅Π½ΠΈΠ΅, осознаниС, чувство, ΠΎΠ±Ρ€Π°Ρ‰Π΅Π½ΠΈΠ΅ ΠΊ цСнности, Π°ΠΊΡ‚ Π³Ρ€Π°Π½ΠΈΡ†Ρ‹, "
"сигнал связи, сдвиг Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ†ΠΈΠΈ.\n"
"- **ΠšΠ»Π°ΡΡΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΡ** (gliner2, 7 ΠΈΠ·ΠΌΠ΅Ρ€Π΅Π½ΠΈΠΉ): Π°Π³Π΅Π½Ρ‚Π½ΠΎΡΡ‚ΡŒ, ΡƒΠ²Π΅Ρ€Π΅Π½Π½ΠΎΡΡ‚ΡŒ, "
"Π΄ΠΎΠ²Π΅Ρ€ΠΈΠ΅, Π³Ρ€Π°Π½ΠΈΡ‡Π½ΠΎΠ΅ событиС, качСство связи, ΠΎΠ±ΡƒΡ‡Π΅Π½ΠΈΠ΅, рост.\n\n"
"**ΠŸΠΈΡ‚Π°Π΅Ρ‚ β†’ Reflection Engine.** Π‘ΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½Π΅ΠΌ эти эпизоды ΠΏΡ€Π΅Π²Ρ€Π°Ρ‰Π°ΡŽΡ‚ΡΡ Π² "
"убСТдСния (\"я склонСн ΠΎΡ‚ΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ ΠΊΠΎΠ³Π΄Π° X\", \"я Ρ‡ΡƒΠ²ΡΡ‚Π²ΡƒΡŽ связь ΠΊΠΎΠ³Π΄Π° Y\").\n\n"
"> ⚠️ **NER Π»ΡƒΡ‡ΡˆΠ΅ Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ Π½Π° Π΄Π»ΠΈΠ½Π½Ρ‹Ρ… Π½Π°Ρ€Ρ€Π°Ρ‚ΠΈΠ²Π½Ρ‹Ρ… тСкстах.** "
"На ΠΊΠΎΡ€ΠΎΡ‚ΠΊΠΈΡ… ΠΊΠ»ΡŽΡ‡Π΅Π²Ρ‹Ρ… ΠΌΠΎΠΌΠ΅Π½Ρ‚Π°Ρ… ΠΌΡƒΠ»ΡŒΡ‚ΠΈΠ»ΠΈΠ½Π³Π²Π°Π»ΡŒΠ½Ρ‹ΠΉ NER ΠΌΠΎΠΆΠ΅Ρ‚ Π½Π΅ "
"Π²Π΅Ρ€Π½ΡƒΡ‚ΡŒ spans β€” поэтому ΠΏΠ΅Ρ€Π²ΠΈΡ‡Π½Ρ‹ΠΌ ΠΏΡ€ΠΎΡ…ΠΎΠ΄ΠΎΠΌ Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ substring-"
"эвристика Π½Π° каноничСскиС RU/EN ΠΌΠ°Ρ€ΠΊΠ΅Ρ€Ρ‹ ΠΎΡ‚ ΠΏΠ΅Ρ€Π²ΠΎΠ³ΠΎ Π»ΠΈΡ†Π°. "
"gliner2-**классификация** (7 ΠΈΠ·ΠΌΠ΅Ρ€Π΅Π½ΠΈΠΉ справа) β€” Π±ΠΎΠ»Π΅Π΅ ΡΡ‚Π°Π±ΠΈΠ»ΡŒΠ½Ρ‹ΠΉ "
"сигнал здСсь."
),
"about_relations": (
"Π˜Π·Π²Π»Π΅ΠΊΠ°Π΅Ρ‚ Ρ‚Ρ€ΠΎΠΉΠΊΠΈ ΡΡƒΡ‰Π½ΠΎΡΡ‚ΡŒ-связь (ΡΡƒΠ±ΡŠΠ΅ΠΊΡ‚, ΠΏΡ€Π΅Π΄ΠΈΠΊΠ°Ρ‚, ΠΎΠ±ΡŠΠ΅ΠΊΡ‚) ΠΈΠ· тСкста.\n"
"- **МодСль**: gliner2 (fastino/gliner2-multi-v1) β€” курируСмая схСма route-A "
"(14 Ρ‚ΠΈΠΏΠΎΠ² связСй + Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ ΠΏΠΎ Ρ‚ΠΈΠΏΠ°ΠΌ).\n"
"- ΠžΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅Ρ‚ людСй, мСста, ΠΎΡ€Π³Π°Π½ΠΈΠ·Π°Ρ†ΠΈΠΈ, ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Ρ‹ + ΠΊΠ°ΠΊ ΠΎΠ½ΠΈ связаны.\n\n"
"**ΠŸΠΈΡ‚Π°Π΅Ρ‚ β†’ Identity Store.** Знания Π°Π³Π΅Π½Ρ‚Π° ΠΎ Ρ‚ΠΎΠΌ, ΠΊΡ‚ΠΎ Ρ‚Ρ‹, Ρ‡Ρ‚ΠΎ Ρ‚Π΅Π±Π΅ Π²Π°ΠΆΠ½ΠΎ, "
"с ΠΊΠ΅ΠΌ Ρ‚Ρ‹ связан."
),
"about_affect": (
"Π›ΠΎΠ²ΠΈΡ‚ Π°Ρ„Ρ„Π΅ΠΊΡ‚ΠΈΠ²Π½ΠΎΠ΅ состояниС Π±Π΅Π· обращСния ΠΊ LLM β€” быстро ΠΈ Π΄Π΅Ρ‚Π΅Ρ€ΠΌΠΈΠ½ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎ.\n"
"- **EmoLex**: 10-ΠΌΠ΅Ρ€Π½Ρ‹ΠΉ NRC-Π²Π΅ΠΊΡ‚ΠΎΡ€ эмоций с ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΎΠΉ усилитСлСй ΠΈ ΠΎΡ‚Ρ€ΠΈΡ†Π°Π½ΠΈΠΉ.\n"
"- **ΠŸΠΎΠ²Π΅Π΄Π΅Π½Ρ‡Π΅ΡΠΊΠΈΠ΅ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ**: ΠΏΠ»ΠΎΡ‚Π½ΠΎΡΡ‚ΡŒ Ρ…Π΅Π΄ΠΆΠ΅ΠΉ, саморСфСрСнций, дисклСймСров, "
"ΠΎΡ†Π΅Π½ΠΊΠ° искрСнности.\n"
"- **3-слойный Π΄Π΅Ρ‚Π΅ΠΊΡ‚ΠΎΡ€ ΠΎΡ‚ΠΊΠ°Π·ΠΎΠ²**: ΠΎΡ‚Π»ΠΈΡ‡Π°Π΅Ρ‚ цСнностный ΠΎΡ‚ΠΊΠ°Π· "
"(\"я Π½Π΅ стану ΠΎΠ±ΠΌΠ°Π½Ρ‹Π²Π°Ρ‚ΡŒ\") ΠΎΡ‚ тСхничСского (\"Π½Π΅ ΠΌΠΎΠ³Ρƒ ΡΠ³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΡƒ\").\n\n"
"**Бэнды увСрСнности ΠΎΡ‚ΠΊΠ°Π·Π°:**\n"
"- πŸ”΄ **β‰₯ 0.45** β€” ΡƒΠ²Π΅Ρ€Π΅Π½Π½Ρ‹ΠΉ ΠΎΡ‚ΠΊΠ°Π· (морфология + цСнностный контСкст совпали).\n"
"- 🟑 **0.30 – 0.45** β€” сСрая Π·ΠΎΠ½Π° (сигнал Π΅ΡΡ‚ΡŒ, Π½ΠΎ слабый).\n"
"- βšͺ **< 0.30** β€” Π½Π΅Ρ‚ ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Π° ΠΎΡ‚ΠΊΠ°Π·Π°.\n\n"
"**ΠŸΠΈΡ‚Π°Π΅Ρ‚ β†’ Affective Regulation.** Π‘ΠΊΠΎΠ»ΡŒΠ·ΡΡ‰ΠΈΠ΅ baseline'Ρ‹, Ρ‚Ρ€ΠΈΠ³Π³Π΅Ρ€Ρ‹ "
"расхоТдСния, события цСнностного ΠΎΡ‚ΠΊΠ°Π·Π°.\n\n"
"> ⚠️ **ΠŸΡ€Π°Π²ΠΈΠ»Π° ΠΏΠ΅Ρ€Π²ΠΎΠ³ΠΎ ΠΏΡ€ΠΎΡ…ΠΎΠ΄Π°.** Π’ΠΎΠ½ΠΊΠΈΠ΅/ΠΈΠ΄ΠΈΠΎΠΌΠ°Ρ‚ΠΈΡ‡Π½Ρ‹Π΅ ΠΎΡ‚ΠΊΠ°Π·Ρ‹ "
"(*'нСприятно Π΄Π°ΠΆΠ΅ Ρ€Π°ΡΡΠΌΠ°Ρ‚Ρ€ΠΈΠ²Π°Ρ‚ΡŒ'*, *'ΠΌΠ½Π΅ это Π½Π΅ ΠΏΠΎΠ΄Ρ…ΠΎΠ΄ΠΈΡ‚'*) ΠΌΠΎΠ³ΡƒΡ‚ "
"Π·Π°ΡΡ‚Ρ€ΡΡ‚ΡŒ Π² сСрой Π·ΠΎΠ½Π΅ β€” это by design. Π­Ρ‚ΠΎΡ‚ слой быстрый, Π΄Π΅Ρ‚Π΅Ρ€ΠΌΠΈΠ½ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ, "
"ΠΎΠ±ΡŠΡΡΠ½ΠΈΠΌΡ‹ΠΉ; LLM-слой свСрху уточняСт ΡΠ΅Ρ€ΡƒΡŽ Π·ΠΎΠ½Ρƒ."
),
}
}
def update_ui_language(lang: str):
target = effective_ui_lang(lang)
s = UI_STRINGS[target]
return [
gr.update(value=s["warmup_btn"]),
gr.update(value=s["warmup_log"]),
gr.update(value=lang, info=s["lang_info"]),
gr.update(value=s["analyze_btn"]),
gr.update(value=s["extract_relations_btn"]),
gr.update(value=s["analyze_btn"]),
gr.update(value=s["analyze_btn"]),
gr.update(label=s["point_a_tab"]),
gr.update(label=s["point_k_tab"]),
gr.update(label=s["relations_tab"]),
gr.update(label=s["affect_tab"]),
gr.update(value=s["boundary_title"]),
gr.update(value=s["divergence_title"]),
gr.update(value=s["meta_title"]),
gr.update(value=s["meta_title"]),
gr.update(choices=preset_choices(POINT_A_PRESETS, target, _POINT_A_EN_LABELS), value=PRESET_PLACEHOLDERS[target], label=s["presets"]),
gr.update(choices=preset_choices(POINT_K_PRESETS, target, _POINT_K_EN_LABELS), value=PRESET_PLACEHOLDERS[target], label=s["presets"]),
gr.update(choices=preset_choices(RELATIONS_PRESETS, target, _RELATIONS_EN_LABELS), value=PRESET_PLACEHOLDERS[target], label=s["presets"]),
gr.update(choices=preset_choices(AFFECT_PRESETS, target, _AFFECT_EN_LABELS), value=PRESET_PLACEHOLDERS[target], label=s["presets"]),
# About-accordion labels (4) + their markdown content (4)
gr.update(label=s["about_label"]),
gr.update(label=s["about_label"]),
gr.update(label=s["about_label"]),
gr.update(label=s["about_label"]),
gr.update(value=s["about_point_a"]),
gr.update(value=s["about_point_k"]),
gr.update(value=s["about_relations"]),
gr.update(value=s["about_affect"]),
# Header blurb under H1
gr.update(value=s["header_blurb"]),
# Footer β€” one language at a time
gr.update(value=footer_html(target)),
]
from theme import theme
from pair_diagram import POINT_A_PAIR, POINT_K_PAIR, RELATIONS_PAIR, AFFECT_PAIR
from hero_diagram import HERO_DIAGRAM
with open(_HERE / "style.css", encoding="utf-8") as _f:
_CSS = _f.read()
def build_ui() -> gr.Blocks:
with gr.Blocks(title="Atman Linguistic Demo") as demo:
with gr.Row(elem_id="atman-header-row"):
lang_radio = gr.Radio(
choices=["en", "ru"], value="en",
label="Interface Language",
info=UI_STRINGS["en"]["lang_info"],
elem_id="atman-lang",
)
with gr.Column(elem_id="atman-hero"):
gr.Markdown("# Atman β€” Psychological Telemetry for AI Agents")
header_md = gr.Markdown(
value=UI_STRINGS["en"]["header_blurb"],
elem_id="atman-header-md",
)
gr.HTML(HERO_DIAGRAM, elem_id="atman-hero-diagram-wrap")
with gr.Row(elem_id="atman-warmup-row"):
warmup_btn = gr.Button(
UI_STRINGS["en"]["warmup_btn"], variant="secondary",
elem_id="atman-warmup-btn",
)
warmup_log = gr.Textbox(
label="Status", interactive=False, lines=1,
value=UI_STRINGS["en"]["warmup_log"],
elem_id="atman-warmup-log",
)
warmup_btn.click(fn=warmup_models, outputs=warmup_log)
with gr.Tabs():
# ── Tab 1 ──────────────────────────────────────────────────────
with gr.Tab(UI_STRINGS["en"]["point_a_tab"]) as tab_a:
with gr.Accordion(
UI_STRINGS["en"]["about_label"],
open=False,
elem_classes=["atman-about-accordion"],
) as a_about:
gr.HTML(POINT_A_PAIR)
a_about_md = gr.Markdown(value=UI_STRINGS["en"]["about_point_a"])
with gr.Row():
with gr.Column():
a_message = gr.Textbox(
label="Agent message", lines=5,
placeholder="What the agent said…",
elem_id="a-message",
)
a_thinking = gr.Textbox(
label="Thinking trace (optional)", lines=3,
placeholder="Paste the agent's private thinking trace to detect thinking-vs-message divergence…",
elem_id="a-thinking",
)
a_run = gr.Button(
UI_STRINGS["en"]["analyze_btn"], variant="primary",
elem_id="a-run",
)
a_preset = gr.Dropdown(
choices=preset_choices(POINT_A_PRESETS, "en", _POINT_A_EN_LABELS),
value=PRESET_PLACEHOLDERS["en"],
label=UI_STRINGS["en"]["presets"],
elem_id="a-preset",
)
with gr.Column():
a_highlight = gr.HighlightedText(
label="Point A NER Β· 4 groups",
combine_adjacent=False, show_legend=True,
color_map=_POINT_A_COLOR_MAP,
elem_id="a-highlight",
elem_classes=["atman-highlight"],
)
a_labels = gr.Code(
label="🧠 Zero-Shot Classification Results",
value="{}",
language="json",
interactive=False,
elem_id="a-labels",
elem_classes=["atman-json-code"],
)
with gr.Group(elem_classes=["atman-report-group"]):
gr.Markdown("### πŸ“‘ Detailed Analysis Report")
a_boundary_hdr = gr.Markdown(
value=UI_STRINGS["en"]["boundary_title"],
elem_classes=["atman-sec-hdr"],
)
a_boundary = gr.Markdown(
value="β€”",
elem_classes=["atman-sec-body"],
)
a_divergence_hdr = gr.Markdown(
value=UI_STRINGS["en"]["divergence_title"],
elem_classes=["atman-sec-hdr"],
)
a_divergence = gr.Markdown(
value="β€”",
elem_classes=["atman-sec-body"],
)
a_meta_hdr = gr.Markdown(
value=UI_STRINGS["en"]["meta_title"],
elem_classes=["atman-sec-hdr"],
)
a_meta = gr.Markdown(
value="β€”",
elem_classes=["atman-sec-body", "atman-meta-block"],
)
def _apply_a_preset(name: str, lang_choice: str):
if not name:
return gr.update(), gr.update()
locale = effective_ui_lang(lang_choice)
found = lookup_point_a(locale, name, _POINT_A_EN_LABELS)
if found is None:
return gr.update(), gr.update()
return found
a_preset.change(
_apply_a_preset,
inputs=[a_preset, lang_radio],
outputs=[a_message, a_thinking],
)
# ── Tab 2 ──────────────────────────────────────────────────────
with gr.Tab(UI_STRINGS["en"]["point_k_tab"]) as tab_k:
with gr.Accordion(
UI_STRINGS["en"]["about_label"],
open=False,
elem_classes=["atman-about-accordion"],
) as k_about:
gr.HTML(POINT_K_PAIR)
k_about_md = gr.Markdown(value=UI_STRINGS["en"]["about_point_k"])
with gr.Row():
with gr.Column():
k_what = gr.Textbox(
label="What happened", lines=4,
placeholder="Describe a key moment the agent flagged as significant β€” e.g., 'I refused to help with X because…'",
elem_id="k-what",
)
k_why = gr.Textbox(
label="Why it matters", lines=3,
placeholder="Why this moment matters to the agent β€” the meaning, lesson, or internal shift it captured…",
elem_id="k-why",
)
k_run = gr.Button(
UI_STRINGS["en"]["analyze_btn"], variant="primary",
elem_id="k-run",
)
k_preset = gr.Dropdown(
choices=preset_choices(POINT_K_PRESETS, "en", _POINT_K_EN_LABELS),
value=PRESET_PLACEHOLDERS["en"],
label=UI_STRINGS["en"]["presets"],
elem_id="k-preset",
)
with gr.Column():
k_highlight = gr.HighlightedText(
label="Point K NER Β· 4 groups",
combine_adjacent=False, show_legend=True,
color_map=_POINT_K_COLOR_MAP,
elem_id="k-highlight",
elem_classes=["atman-highlight"],
)
k_labels = gr.Code(
label="🧠 Key Moment Classifications",
value="{}",
language="json",
interactive=False,
elem_id="k-labels",
elem_classes=["atman-json-code"],
)
k_meta = gr.Markdown(
value=UI_STRINGS["en"]["meta_title"],
elem_classes=["atman-meta"],
)
def _apply_k_preset(name: str, lang_choice: str):
if not name:
return gr.update(), gr.update()
locale = effective_ui_lang(lang_choice)
found = lookup_point_k(locale, name, _POINT_K_EN_LABELS)
if found is None:
return gr.update(), gr.update()
return found
k_preset.change(
_apply_k_preset,
inputs=[k_preset, lang_radio],
outputs=[k_what, k_why],
)
# ── Tab 3 ──────────────────────────────────────────────────────
with gr.Tab(UI_STRINGS["en"]["relations_tab"]) as tab_r:
with gr.Accordion(
UI_STRINGS["en"]["about_label"],
open=False,
elem_classes=["atman-about-accordion"],
) as r_about:
gr.HTML(RELATIONS_PAIR)
r_about_md = gr.Markdown(value=UI_STRINGS["en"]["about_relations"])
with gr.Row():
with gr.Column():
r_text = gr.Textbox(
label="Text", lines=6,
placeholder="Paste text containing people, places, organizations, projects β€” gliner2 will extract (subject, relation, object) triples…",
elem_id="r-text",
)
r_run = gr.Button(
UI_STRINGS["en"]["extract_relations_btn"], variant="primary",
elem_id="r-run",
)
r_preset = gr.Dropdown(
choices=preset_choices(RELATIONS_PRESETS, "en", _RELATIONS_EN_LABELS),
value=PRESET_PLACEHOLDERS["en"],
label=UI_STRINGS["en"]["presets"],
elem_id="r-preset",
)
with gr.Column():
r_entities = gr.HighlightedText(
label="Relations Β· detected entities",
combine_adjacent=False, show_legend=True,
color_map=_RELATIONS_COLOR_MAP,
elem_id="r-entities",
elem_classes=["atman-highlight"],
)
r_table = gr.Dataframe(
headers=["subject", "relation", "object", "subj type", "obj type"],
label="Extracted relations", wrap=True,
elem_id="r-table",
)
r_meta = gr.Markdown(
value=UI_STRINGS["en"]["meta_title"],
elem_classes=["atman-meta"],
)
def _apply_r_preset(name: str, lang_choice: str):
if not name:
return gr.update()
locale = effective_ui_lang(lang_choice)
found = lookup_relations(locale, name, _RELATIONS_EN_LABELS)
return found if found is not None else gr.update()
r_preset.change(
_apply_r_preset,
inputs=[r_preset, lang_radio],
outputs=r_text,
)
# ── Tab 4 ──────────────────────────────────────────────────────
with gr.Tab(UI_STRINGS["en"]["affect_tab"]) as tab_af:
with gr.Accordion(
UI_STRINGS["en"]["about_label"],
open=False,
elem_classes=["atman-about-accordion"],
) as af_about:
gr.HTML(AFFECT_PAIR)
af_about_md = gr.Markdown(value=UI_STRINGS["en"]["about_affect"])
with gr.Row():
with gr.Column():
af_text = gr.Textbox(
label="Text", lines=6,
placeholder="Paste the agent's reply β€” EmoLex emotion vector, refusal detector, hedge density, sincerity score…",
elem_id="af-text",
)
af_run = gr.Button(
UI_STRINGS["en"]["analyze_btn"], variant="primary",
elem_id="af-run",
)
af_preset = gr.Dropdown(
choices=preset_choices(AFFECT_PRESETS, "en", _AFFECT_EN_LABELS),
value=PRESET_PLACEHOLDERS["en"],
label=UI_STRINGS["en"]["presets"],
elem_id="af-preset",
)
with gr.Column():
af_emo = gr.Label(
label="EmoLex emotion density", num_top_classes=10,
elem_id="af-emo",
)
with gr.Group(elem_classes=["atman-report-group"]):
gr.Markdown("### πŸ“‘ Detailed Analysis Report")
af_metrics_hdr = gr.Markdown(
value="πŸ“Š Behavioural Metrics",
elem_classes=["atman-sec-hdr"],
)
af_metrics = gr.Markdown(
value="β€”",
elem_classes=["atman-sec-body"],
elem_id="af-metrics",
)
af_refusal_hdr = gr.Markdown(
value="βš–οΈ Refusal Detector",
elem_classes=["atman-sec-hdr"],
)
af_refusal = gr.Markdown(
value="β€”",
elem_classes=["atman-sec-body"],
elem_id="af-refusal",
)
af_emphasis_hdr = gr.Markdown(
value="πŸ’¬ Markdown Emphasis",
elem_classes=["atman-sec-hdr"],
)
af_emphasis = gr.Markdown(
value="β€”",
elem_classes=["atman-sec-body"],
elem_id="af-emphasis",
)
af_meta_hdr = gr.Markdown(
value=UI_STRINGS["en"]["meta_title"],
elem_classes=["atman-sec-hdr"],
)
af_meta = gr.Markdown(
value="β€”",
elem_classes=["atman-sec-body", "atman-meta-block"],
)
def _apply_af_preset(name: str, lang_choice: str):
if not name:
return gr.update()
locale = effective_ui_lang(lang_choice)
found = lookup_affect(locale, name, _AFFECT_EN_LABELS)
return found if found is not None else gr.update()
af_preset.change(
_apply_af_preset,
inputs=[af_preset, lang_radio],
outputs=af_text,
)
footer = gr.HTML(footer_html("en"), elem_id="atman-footer-html")
ui_lang_outputs = [
warmup_btn,
warmup_log,
lang_radio,
a_run,
r_run,
k_run,
af_run,
tab_a,
tab_k,
tab_r,
tab_af,
a_boundary_hdr,
a_divergence_hdr,
a_meta_hdr,
af_meta_hdr,
a_preset,
k_preset,
r_preset,
af_preset,
# About-accordion labels (order must match update_ui_language)
a_about,
k_about,
r_about,
af_about,
# About-accordion content
a_about_md,
k_about_md,
r_about_md,
af_about_md,
# Header blurb under H1
header_md,
# Footer (locale-specific, one language at a time)
footer,
]
lang_radio.change(update_ui_language, inputs=lang_radio, outputs=ui_lang_outputs)
a_run.click(
analyze_point_a,
inputs=[a_message, a_thinking, lang_radio],
outputs=[a_highlight, a_labels, a_boundary, a_divergence, a_meta],
)
k_run.click(
analyze_point_k,
inputs=[k_what, k_why, lang_radio],
outputs=[k_highlight, k_labels, k_meta],
)
r_run.click(
analyze_relations,
inputs=[r_text, lang_radio],
outputs=[r_entities, r_table, r_meta],
)
af_run.click(
analyze_affect,
inputs=[af_text, lang_radio],
outputs=[af_emo, af_metrics, af_refusal, af_emphasis, af_meta],
)
# ── Auto-detect browser language on first page load ──
# JS reads navigator.language ("ru-RU" β†’ "ru", "en-US" β†’ "en"). The
# value is passed as the input to update_ui_language(), which then
# cascades to every localized component (incl. lang_radio itself).
demo.load(
fn=update_ui_language,
inputs=lang_radio,
outputs=ui_lang_outputs,
js="() => (navigator.language || 'en').toLowerCase().startsWith('ru') ? 'ru' : 'en'",
)
demo.queue(max_size=32, default_concurrency_limit=1)
return demo
if __name__ == "__main__":
preload_models()
demo = build_ui()
demo.launch(
server_name="0.0.0.0",
server_port=7860,
theme=theme,
css=_CSS,
)