File size: 8,812 Bytes
aa9de32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import sys
import time
import pickle
import numpy as np
import pandas as pd

from tanglish_sentiment_pipeline import (
    resolve_path,
    load_dataset,
    normalize_text,
    CongMoeClassifier,
    TARGET_CLASSES,
    EMOJI_MAP
)

MODEL_FILE = resolve_path("tanglish_congmoe_model.pkl")
DATASET_FILE = resolve_path("tanglish_test_20000_with_labels.csv")

STATE_EMOJIS = {
    "Positive": "๐ŸŒŸ POSITIVE (Praise / High Excitement)",
    "Negative": "๐Ÿ’” NEGATIVE (Disappointment / Criticism)",
    "Mixed_feelings": "โš–๏ธ  MIXED FEELINGS (Conflicted / Nuanced Review)",
    "Unknown_state": "โ“ UNKNOWN STATE (Question / Inquiry / Neutral)"
}

def get_or_train_model():
    """Loads the pre-trained model or trains and saves it if not already present."""
    if os.path.exists(MODEL_FILE):
        print(f"[*] Loading pre-trained ConG-MoE v2 model from '{MODEL_FILE}'...")
        t0 = time.perf_counter()
        with open(MODEL_FILE, "rb") as f:
            model = pickle.load(f)
        print(f"[*] Model loaded in {(time.perf_counter() - t0)*1000:.1f} ms!\n")
        return model

    print(f"[*] Pre-trained model not found. Training once on {DATASET_FILE}...")
    df, text_col, label_col = load_dataset(DATASET_FILE)
    X_train_raw = df[text_col]
    y_train = df[label_col]

    print("[*] Normalizing text representations...")
    X_train_norm = X_train_raw.apply(normalize_text)

    print("[*] Training ConG-MoE v2 architecture...")
    t0 = time.perf_counter()
    model = CongMoeClassifier(confidence_threshold=0.65, C=0.5, random_state=42)
    model.fit(X_train_norm, y_train, X_raw=X_train_raw)
    train_time = time.perf_counter() - t0
    print(f"[*] Training finished in {train_time:.2f} seconds.")

    print(f"[*] Saving model to '{MODEL_FILE}' for instant future startup...")
    with open(MODEL_FILE, "wb") as f:
        pickle.dump(model, f)
    print("[*] Model successfully cached!\n")
    return model

def predict_comment(raw_text: str, model: CongMoeClassifier):
    """Predicts the sentiment state of a user comment using ConG-MoE v2."""
    t0 = time.perf_counter()

    clean_text = normalize_text(raw_text)

    # 1. Base statistical pipeline prediction & probabilities
    probs = model.base_pipeline.predict_proba([clean_text])[0]
    classes = model.base_pipeline.classes_
    prob_dict = {cls: float(p) for cls, p in zip(classes, probs)}

    base_pred = classes[probs.argmax()]
    confidence = float(probs.max())

    # 2. Check Expert Heads & Linguistic Markers
    # Extended pivot check
    extended_pivots = {"but", "aana", "aanal", "aanaa", "anaa", "ana", "irundhalum", "irunthalum", "yet", "though", "however", "analum"}
    has_pivot = any(p in clean_text.split() for p in extended_pivots)

    # Counterfactual regret markers ('could have been better')
    regret_markers = {"irunthurukalam", "irunthirukalam", "irundhurukalam", "irundhirukalam", "irukalam", "irunthrukalam", "panniirukalam"}
    has_regret = any(m in clean_text for m in regret_markers) or ("inno" in clean_text and "best" in clean_text) or ("innum" in clean_text and "nalla" in clean_text)

    pivot_pred = model.pivot_head.predict_sample(raw_text)
    emoji_pred = model.emoji_head.predict_sample(raw_text)

    # Affective terms
    pos_terms = {"sema", "mass", "super", "nalla", "best", "verithanam", "thalaiva", "level", "good", "love", "arputham", "marana"}
    neg_terms = {"worst", "waste", "mokka", "bore", "flaw", "bad", "disaster", "terrible", "hate", "daridram", "kevalam"}
    words_set = set(clean_text.split())
    has_pos_term = any(t in words_set for t in pos_terms)
    has_neg_term = any(t in words_set for t in neg_terms)
    has_sad_emoji = any(e in raw_text for e in ["๐Ÿ˜ข", "๐Ÿ˜ญ", "๐Ÿ˜ก", "๐Ÿ‘Ž", "๐Ÿคฎ", "๐Ÿ’”", "๐Ÿคฆ", "๐Ÿคฆโ€โ™‚๏ธ", "๐Ÿ˜ "])
    has_happy_emoji = any(e in raw_text for e in ["๐Ÿ˜Š", "๐Ÿ˜", "๐ŸŽ‰", "๐Ÿ”ฅ", "โค๏ธ", "๐Ÿ‘", "๐Ÿ‘", "๐Ÿ’ฏ", "๐Ÿ’ฅ", "๐Ÿคฉ"])
    is_emoji_dissonance = (has_pos_term and has_sad_emoji) or (has_neg_term and has_happy_emoji)

    # 3. Gating Decision Logic
    override_reason = None

    if (has_pivot and has_pos_term and has_neg_term) or (has_pivot and has_regret) or (has_regret and "acting" in clean_text):
        final_state = "Mixed_feelings"
        override_reason = "Contrastive pivot ('aana'/'but') connecting positive and negative clauses"
    elif is_emoji_dissonance and confidence < 0.85:
        final_state = "Mixed_feelings"
        override_reason = "Affective dissonance detected between text and emojis"
    elif confidence < model.confidence_threshold and (pivot_pred == "Mixed_feelings" or (emoji_pred == "Mixed_feelings" and not (has_neg_term and has_sad_emoji))):
        final_state = "Mixed_feelings"
        override_reason = "Contrastive pivot or discordance detected by specialist head"
    elif ("?" in raw_text or any(q in words_set for q in ["eppo", "eppadi", "enna", "ethu", "when", "why", "who", "what"])) and (base_pred == "Unknown_state" or prob_dict.get("Unknown_state", 0) > 0.15 or confidence < 0.60):
        final_state = "Unknown_state"
        override_reason = "Inquiry syntax ('?') and interrogative token detected"
    elif has_neg_term and not has_pos_term and (base_pred != "Negative"):
        # Strong negative keywords should not fall to Positive
        final_state = "Negative"
        override_reason = "Explicit negative evaluation keywords detected"
    else:
        final_state = base_pred

    latency_ms = (time.perf_counter() - t0) * 1000

    return {
        "raw_text": raw_text,
        "clean_text": clean_text,
        "final_state": final_state,
        "base_prediction": base_pred,
        "confidence": confidence,
        "override_reason": override_reason,
        "prob_dict": prob_dict,
        "latency_ms": latency_ms
    }

def print_prediction_result(res: dict):
    state_str = STATE_EMOJIS.get(res['final_state'], res['final_state'])
    print("\n" + "โ”€" * 70)
    print(f"  Input Comment : \"{res['raw_text']}\"")
    print(f"  Processed As  : \"{res['clean_text']}\"")
    print(f"  Predicted State: {state_str}")
    print("โ”€" * 70)
    print(f"  Base Model Prediction : {res['base_prediction']} (Confidence: {res['confidence']*100:.1f}%)")

    if res['override_reason']:
        print(f"  Specialist Intervention: [YES] Overridden to Mixed feelings")
        print(f"  Reason                : {res['override_reason']}")
    else:
        print(f"  Specialist Intervention: [NO] Base prediction accepted (Confidence >= 65% or no conflict)")

    print(f"  Inference Latency     : {res['latency_ms']:.3f} milliseconds")
    print("\n  Class Probability Breakdown:")
    for cls in TARGET_CLASSES:
        p = res['prob_dict'].get(cls, 0.0)
        bar = "โ–ˆ" * int(p * 30)
        print(f"    - {cls:<16}: {p*100:5.1f}%  |{bar:<30}|\n")
    print("โ”€" * 70 + "\n")

def main():
    model = get_or_train_model()

    # If the user passed a comment directly as a command-line argument:
    if len(sys.argv) > 1:
        user_comment = " ".join(sys.argv[1:])
        res = predict_comment(user_comment, model)
        print_prediction_result(res)
        return

    # Otherwise, run interactive prompt loop:
    print("=" * 70)
    print("     TANGLISH SENTIMENT PREDICTOR (ConG-MoE v2 Interactive CLI)")
    print("=" * 70)
    print("Type any Tanglish comment below and hit Enter.")
    print("Commands: 'exit' or 'q' to quit, 'samples' to test built-in examples.\n")

    sample_comments = [
        "Thalaivar entry vera level mass ๐Ÿ”ฅ padam blockbuster confirmed!",
        "Padam romba mokka da timing waste don't go theatre ๐Ÿคฎ worst movie",
        "Acting sema mass aana screenplay romba worst",
        "first half nalla irukku aana climax mokka ๐Ÿคฎ",
        "GOAT trailer vera level bro... eppo ott release date???"
    ]

    while True:
        try:
            user_input = input("Enter Tanglish Comment > ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nExiting. Vanakkam!")
            break

        if not user_input:
            continue
        if user_input.lower() in ["q", "quit", "exit"]:
            print("Exiting predictor. Vanakkam!")
            break
        if user_input.lower() == "samples":
            print("\n--- Running Built-In Sample Comments ---")
            for sample in sample_comments:
                res = predict_comment(sample, model)
                print_prediction_result(res)
            continue

        res = predict_comment(user_input, model)
        print_prediction_result(res)

if __name__ == "__main__":
    main()