VictorM-Coder commited on
Commit
41a5821
·
verified ·
1 Parent(s): 96c50c6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +107 -143
app.py CHANGED
@@ -1,184 +1,148 @@
1
  import torch
2
  from transformers import AutoTokenizer, AutoModelForCausalLM
3
- import re
4
  import numpy as np
5
  import pandas as pd
 
6
  import gradio as gr
7
 
8
- # ----------------------------------------------------
9
- # LOAD CAUSAL LM (distilGPT2 = FAST + LIGHT)
10
- # ----------------------------------------------------
11
  MODEL_NAME = "distilgpt2"
12
 
13
  tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
14
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
15
-
16
  model = AutoModelForCausalLM.from_pretrained(MODEL_NAME).to(device).eval()
17
 
18
 
19
- # ----------------------------------------------------
20
  # SENTENCE SPLITTER
21
- # ----------------------------------------------------
22
  def sentence_split(text):
23
  text = text.replace("\n", ". ")
24
- sentences = re.split(r'(?<=[.!?])\s+', text)
25
- return [s.strip() for s in sentences if s.strip()]
26
 
27
 
28
- # ----------------------------------------------------
29
- # PERPLEXITY FUNCTION
30
- # ----------------------------------------------------
31
  def perplexity(sentence):
32
- inputs = tokenizer(sentence, return_tensors="pt").to(device)
33
  with torch.no_grad():
34
- out = model(**inputs, labels=inputs["input_ids"])
35
  return float(torch.exp(out.loss))
36
 
37
 
38
- # ----------------------------------------------------
39
- # SIMPLE TEXT PERTURBATION (DetectGPT trick)
40
- # ----------------------------------------------------
41
- def perturb(text):
42
- words = text.split()
43
- if len(words) < 4:
44
- return text
45
- idx = np.random.randint(0, len(words))
46
- words[idx] += " "
47
- return " ".join(words)
48
-
49
-
50
- # ----------------------------------------------------
51
- # BASE PERPLEXITY + DETECTGPT SCORE
52
- # ----------------------------------------------------
53
- def detectgpt_base_and_score(sentence, perturbations=3):
54
- """
55
- Returns:
56
- base_perplexity, detectgpt_score
57
- """
58
- try:
59
- base = perplexity(sentence)
60
- except Exception:
61
- return None, 0.0
62
-
63
- pert_scores = []
64
- for _ in range(perturbations):
65
- p = perturb(sentence)
66
- try:
67
- pert_scores.append(perplexity(p))
68
- except Exception:
69
- continue
70
-
71
- if not pert_scores:
72
- return base, 0.0
73
-
74
- score = float(np.mean(pert_scores) - base)
75
- return base, score
76
-
77
-
78
- # ----------------------------------------------------
79
- # MAIN CLASSIFIER
80
- # ----------------------------------------------------
81
  def classify_text(text):
82
- if not text.strip():
83
- return "⚠️ Please enter some text.", None, None
84
 
85
  sentences = sentence_split(text)
86
- if not sentences:
87
- return "⚠️ No valid sentences found.", None, None
88
-
89
- perps = []
90
- scores = []
91
- tmp_results = []
92
-
93
- # 1. Compute base perplexity & DetectGPT score per sentence
94
- for s in sentences:
95
- base_perp, score = detectgpt_base_and_score(s)
96
- if base_perp is None:
97
- base_perp = float("nan")
98
-
99
- perps.append(base_perp)
100
- scores.append(score)
101
- tmp_results.append({"sentence": s, "perp": base_perp, "score": score})
102
-
103
- # Handle NaNs if any
104
- perps_clean = [p for p in perps if not np.isnan(p)]
105
- if perps_clean:
106
- median_perp = float(np.median(perps_clean))
107
- else:
108
- median_perp = np.nan
109
-
110
- # 2. Classify using calibrated rule
111
- results = []
 
 
 
 
 
 
 
 
112
  highlighted = []
113
- ai_count = 0
114
- total = len(tmp_results)
115
-
116
- SCORE_THRESHOLD = 0.05 # Require meaningful positive signal
117
-
118
- for item in tmp_results:
119
- s = item["sentence"]
120
- perp = item["perp"]
121
- score = item["score"]
122
-
123
- # Default label is Human
124
- label = "Human"
125
-
126
- # Conditions for AI-like:
127
- # - score significantly positive
128
- # - perplexity lower than median (more predictable)
129
- if not np.isnan(perp) and not np.isnan(median_perp):
130
- if (score > SCORE_THRESHOLD) and (perp < median_perp):
131
- label = "AI"
132
-
133
- if label == "AI":
134
- ai_count += 1
135
- highlighted.append(
136
- f"<p style='color:red;font-weight:bold'>{s}</p>"
137
- )
138
  else:
139
- highlighted.append(
140
- f"<p style='color:green;font-weight:bold'>{s}</p>"
141
- )
142
-
143
- results.append([
144
- s,
145
- label,
146
- f"{perp:.2f}" if not np.isnan(perp) else "NaN",
147
- f"{score:.4f}"
148
- ])
149
-
150
- # 3. Document-level AI percentage = fraction of AI sentences
151
- if total > 0:
152
- doc_ai_percent = (ai_count / total) * 100.0
153
- else:
154
- doc_ai_percent = 0.0
155
-
156
- df = pd.DataFrame(
157
- results,
158
- columns=["Sentence", "Label", "Perplexity", "DetectGPT Score"]
159
- )
160
  html = "\n".join(highlighted)
161
 
162
- return f"⚖️ Document AI Likelihood (approx): {doc_ai_percent:.1f}%", html, df
 
 
 
163
 
164
 
165
- # ----------------------------------------------------
 
166
  # GRADIO UI
167
- # ----------------------------------------------------
168
  with gr.Blocks() as demo:
169
- gr.Markdown("## 🧠 Writenix DetectGPT (Calibrated, distilgpt2)")
170
 
171
- text_input = gr.Textbox(
172
- label="Enter text",
173
- lines=14,
174
- placeholder="Paste your essay here…"
175
- )
176
 
177
- classify_btn = gr.Button("🚀 Detect AI")
178
 
179
- ai_score = gr.Label(label="Overall AI Likelihood")
180
  highlighted = gr.HTML()
181
- table = gr.Dataframe(headers=["Sentence", "Label", "Perplexity", "DetectGPT Score"], wrap=True)
182
 
183
  classify_btn.click(classify_text, text_input, [ai_score, highlighted, table])
184
 
 
1
  import torch
2
  from transformers import AutoTokenizer, AutoModelForCausalLM
 
3
  import numpy as np
4
  import pandas as pd
5
+ import re
6
  import gradio as gr
7
 
8
+ # ----------------------------------------------
9
+ # LOAD FAST MODEL (DistilGPT2)
10
+ # ----------------------------------------------
11
  MODEL_NAME = "distilgpt2"
12
 
13
  tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
14
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
15
  model = AutoModelForCausalLM.from_pretrained(MODEL_NAME).to(device).eval()
16
 
17
 
18
+ # ----------------------------------------------
19
  # SENTENCE SPLITTER
20
+ # ----------------------------------------------
21
  def sentence_split(text):
22
  text = text.replace("\n", ". ")
23
+ s = re.split(r'(?<=[.!?])\s+', text)
24
+ return [x.strip() for x in s if x.strip()]
25
 
26
 
27
+ # ----------------------------------------------
28
+ # PERPLEXITY
29
+ # ----------------------------------------------
30
  def perplexity(sentence):
31
+ enc = tokenizer(sentence, return_tensors="pt").to(device)
32
  with torch.no_grad():
33
+ out = model(**enc, labels=enc["input_ids"])
34
  return float(torch.exp(out.loss))
35
 
36
 
37
+ # ----------------------------------------------
38
+ # TOKEN-LEVEL ENTROPY
39
+ # ----------------------------------------------
40
+ def token_entropy(sentence):
41
+ enc = tokenizer(sentence, return_tensors="pt").to(device)
42
+ input_ids = enc["input_ids"][0]
43
+
44
+ with torch.no_grad():
45
+ outputs = model(enc["input_ids"], labels=enc["input_ids"])
46
+ logits = outputs.logits[0]
47
+
48
+ entropies = []
49
+ for i in range(1, len(input_ids)):
50
+ probs = torch.softmax(logits[i-1], dim=-1)
51
+ entropy = -torch.sum(probs * torch.log(probs + 1e-10))
52
+ entropies.append(float(entropy))
53
+
54
+ return np.mean(entropies), np.std(entropies)
55
+
56
+
57
+ # ----------------------------------------------
58
+ # TURNITIN-STYLE SCORING PIPELINE
59
+ # ----------------------------------------------
60
+ def analyze_sentence(sentence):
61
+ perp = perplexity(sentence)
62
+ mean_ent, std_ent = token_entropy(sentence)
63
+ length = len(sentence.split())
64
+ punct = sum([sentence.count(p) for p in ".,;:!?"])
65
+
66
+ return {
67
+ "sentence": sentence,
68
+ "perplexity": perp,
69
+ "entropy_mean": mean_ent,
70
+ "entropy_std": std_ent,
71
+ "length": length,
72
+ "punctuation": punct
73
+ }
74
+
75
+
76
+ # ----------------------------------------------
77
+ # MAIN TURNITIN STYLE DETECTOR
78
+ # ----------------------------------------------
 
79
  def classify_text(text):
 
 
80
 
81
  sentences = sentence_split(text)
82
+ stats = [analyze_sentence(s) for s in sentences]
83
+
84
+ df = pd.DataFrame(stats)
85
+
86
+ # ---------- TURNITIN STYLE METRICS ----------
87
+ perplexity_mean = df["perplexity"].mean()
88
+ perplexity_std = df["perplexity"].std()
89
+
90
+ entropy_mean = df["entropy_mean"].mean()
91
+ entropy_std = df["entropy_std"].mean()
92
+
93
+ length_std = df["length"].std()
94
+ punct_std = df["punctuation"].std()
95
+
96
+ # ---------- NORMALIZED SCORES ----------
97
+ # Low variance = AI-like
98
+ burstiness_score = np.exp(-perplexity_std)
99
+
100
+ entropy_smoothness = np.exp(-entropy_std)
101
+
102
+ length_uniformity = np.exp(-length_std / (df["length"].mean() + 1e-5))
103
+ punct_uniformity = np.exp(-punct_std / (df["punctuation"].mean() + 1e-5))
104
+
105
+ # ---------- ENSEMBLE SCORE (Turnitin-like) ----------
106
+ ai_score = (
107
+ 0.35 * burstiness_score +
108
+ 0.25 * entropy_smoothness +
109
+ 0.20 * length_uniformity +
110
+ 0.20 * punct_uniformity
111
+ )
112
+
113
+ ai_percent = float(ai_score * 100)
114
+
115
+ # ---------- PER-SENTENCE LABELS ----------
116
  highlighted = []
117
+ for i, row in df.iterrows():
118
+ is_ai = row["perplexity"] < perplexity_mean * 0.75 and row["entropy_std"] < entropy_std * 0.8
119
+ if is_ai:
120
+ highlighted.append(f"<p style='color:red;font-weight:bold'>{row['sentence']}</p>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  else:
122
+ highlighted.append(f"<p style='color:green;font-weight:bold'>{row['sentence']}</p>")
123
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  html = "\n".join(highlighted)
125
 
126
+ # Display readable columns
127
+ df_display = df[["sentence", "perplexity", "entropy_mean", "entropy_std", "length", "punctuation"]]
128
+
129
+ return f"⚖️ Estimated AI Probability (Turnitin-style): {ai_percent:.1f}%", html, df_display
130
 
131
 
132
+
133
+ # ----------------------------------------------
134
  # GRADIO UI
135
+ # ----------------------------------------------
136
  with gr.Blocks() as demo:
137
+ gr.Markdown("## 🧠 Writenix Turnitin-Style AI Detector")
138
 
139
+ text_input = gr.Textbox(label="Enter text", lines=10, placeholder="Paste your essay...")
 
 
 
 
140
 
141
+ classify_btn = gr.Button("🚀 Analyze")
142
 
143
+ ai_score = gr.Label(label="Turnitin-Style AI Likelihood")
144
  highlighted = gr.HTML()
145
+ table = gr.Dataframe(headers=["Sentence", "Perplexity", "Entropy Mean", "Entropy Std", "Length", "Punctuation"], wrap=True)
146
 
147
  classify_btn.click(classify_text, text_input, [ai_score, highlighted, table])
148