File size: 5,436 Bytes
d08010b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import warnings
warnings.filterwarnings("ignore")

import joblib
import numpy as np
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification



# ============================================================
# Load TF-IDF models (trained by us on 728K samples)
# ============================================================
vectorizer = joblib.load("text_vectorizer.pkl")
lr_model   = joblib.load("text_model_lr.pkl")
sgd_model  = joblib.load("text_model_sgd.pkl")

# ============================================================
# Load GLYPH β€” DeBERTa-v3 pretrained detector
# 98.85% accuracy, covers GPT-2 through GPT-4 (14 AI families)
# Used as benchmark comparison model
# ============================================================
GLYPH_MODEL = "ogmatrixllm/glyph-v1.1"
device      = torch.device("cuda" if torch.cuda.is_available() else "cpu")

print("Loading GLYPH model...")
try:
    # IMPORTANT: use_fast=False required β€” fast tokenizer has
    # a confirmed regression in transformers>=4.47 for DeBERTa-v3
    glyph_tokenizer = AutoTokenizer.from_pretrained(GLYPH_MODEL, use_fast=False)
    glyph_model     = AutoModelForSequenceClassification.from_pretrained(GLYPH_MODEL)
    glyph_model.to(device)
    glyph_model.eval()
    GLYPH_AVAILABLE = True
    print("βœ“ GLYPH loaded")
    print(glyph_model.config.id2label)
    print("LABEL_0 = HUMAN , LABEL_1= AI")
except Exception as e:
    print(f"βœ— GLYPH not available: {e}")
    GLYPH_AVAILABLE = False


# ============================================================
# Individual predictors
# ============================================================

def predict_lr(vec) -> dict:
    proba   = lr_model.predict_proba(vec)[0]
    ai_prob = float(proba[1])           # index 1 = AI
    return {
        "label":      "AI-generated" if ai_prob >= 0.5 else "Human-written",
        "confidence": round(ai_prob, 4),
        "ai_prob":    round(ai_prob, 4)
    }


def predict_sgd(vec) -> dict:
    if hasattr(sgd_model, "predict_proba"):
        proba   = sgd_model.predict_proba(vec)[0]
        ai_prob = float(proba[1])
    else:
        df      = float(sgd_model.decision_function(vec)[0])
        ai_prob = 1 / (1 + np.exp(-df))
    return {
        "label":      "AI-generated" if ai_prob >= 0.5 else "Human-written",
        "confidence": round(ai_prob, 4),
        "ai_prob":    round(ai_prob, 4)
    }


def predict_glyph(text: str) -> dict | None:
    """
    GLYPH β€” DeBERTa-v3-base fine-tuned detector.
    98.85% accuracy across 14 AI model families (GPT-2 to GPT-4).
    LABEL_0 = human, LABEL_1 = AI-generated.
    """
    if not GLYPH_AVAILABLE:
        return None
    try:
        inputs = glyph_tokenizer(
            text,
            return_tensors="pt",
            truncation=True,
            max_length=512          # trained at 512, longer texts truncated
        ).to(device)

        with torch.no_grad():
            logits = glyph_model(**inputs).logits
            probs  = torch.softmax(logits, dim=-1)

        p_human, p_ai = probs[0].tolist()
        ai_prob       = round(float(p_ai), 4)
        label         = "AI-generated" if p_ai > 0.5 else "Human-written"

        return {
            "label":      label,
            "confidence": round(max(p_human, p_ai), 4),
            "ai_prob":    ai_prob
        }
    except Exception as e:
        print(f"GLYPH error: {e}")
        return None


# ============================================================
# Main predict function
# ============================================================

def predict_text(text: str) -> dict:
    vec = vectorizer.transform([text])

    # Run all models
    lr_result    = predict_lr(vec)
    sgd_result   = predict_sgd(vec)
    glyph_result = predict_glyph(text)

    # Weighted average
    # GLYPH gets higher weight β€” 98.85% accuracy vs ~91% for TF-IDF models
    scores  = [lr_result["ai_prob"], sgd_result["ai_prob"]]
    weights = [1.0, 1.0]

    if glyph_result:
        scores.append(glyph_result["ai_prob"])
        weights.append(2.0)     # GLYPH gets 2x weight β€” significantly better model

    avg_ai_score = round(
        sum(s * w for s, w in zip(scores, weights)) / sum(weights), 4
    )

    final_label = "AI-generated" if avg_ai_score >= 0.5 else "Human-written"
    confidence  = avg_ai_score if avg_ai_score >= 0.5 else 1 - avg_ai_score

    # Warning logic
    all_labels = [lr_result["label"], sgd_result["label"]]
    if glyph_result:
        all_labels.append(glyph_result["label"])

    warning = None
    if 0.4 <= avg_ai_score <= 0.6:
        warning = "Borderline prediction β€” low confidence. Consider this result as indicative only."
    elif len(set(all_labels)) > 1:
        warning = "Models disagree on this text. GLYPH result is weighted higher as it covers more AI model families."

    # Short text warning (GLYPH docs: <50 words has reduced F1)
    if len(text.split()) < 50:
        warning = (warning or "") + " Short text detected β€” accuracy may be reduced. Provide more text for reliable detection."
        warning = warning.strip()

    return {
        "lr":    lr_result,
        "sgd":   sgd_result,
        "glyph": glyph_result,    # None if unavailable
        "final": {
            "label":      final_label,
            "confidence": round(float(confidence), 4),
            "ai_score":   avg_ai_score
        },
        "warning": warning
    }