simonlesaumon commited on
Commit
ae29cb7
·
verified ·
1 Parent(s): 450fbc1

Upload artifact

Browse files
Files changed (1) hide show
  1. src/modal_app_rewrite_sota.py +238 -0
src/modal_app_rewrite_sota.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modal app: SOTA rewriting with BART-SFT-DPO model.
3
+
4
+ Uses the full trained pipeline (SFT + DPO adversarial) for AI text rewriting.
5
+ Much higher quality than the Qwen2.5-1.5B baseline.
6
+
7
+ Usage:
8
+ modal run -q src/modal_app_rewrite_sota.py --text "Your AI text" --verify
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import re
16
+ import sys
17
+ import time
18
+
19
+ import modal
20
+
21
+ image = (
22
+ modal.Image.debian_slim(python_version="3.12")
23
+ .env({"PIP_PROGRESS_BAR": "off", "PYTHONIOENCODING": "utf-8"})
24
+ .pip_install("torch>=2.4.0", "transformers>=4.45.0", "accelerate>=0.34.0", "huggingface_hub>=0.26.0")
25
+ )
26
+
27
+ app = modal.App("evasion-detection-sota", image=image)
28
+ hf_cache = modal.Volume.from_name("hf-cache", create_if_missing=True)
29
+
30
+ MODEL_REPO = "simonlesaumon/evasion-detection-models"
31
+ MODEL_NAME = "bart-sft-style-humanization" # SFT model (DPO overfit, use SFT directly)
32
+
33
+
34
+ @app.function(
35
+ gpu=os.getenv("MODAL_GPU", "T4"),
36
+ timeout=60 * 15,
37
+ scaledown_window=60 * 3,
38
+ volumes={"/root/.cache/huggingface": hf_cache},
39
+ )
40
+ def rewrite_sota(
41
+ text: str,
42
+ verify: bool = True,
43
+ max_input_length: int = 512,
44
+ max_output_length: int = 256,
45
+ temperature: float = 0.8,
46
+ top_p: float = 0.92,
47
+ repetition_penalty: float = 1.1,
48
+ ) -> dict:
49
+ """Rewrite AI text using the SOTA BART-SFT model with style embeddings."""
50
+ import torch
51
+ import torch.nn as nn
52
+ from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
53
+
54
+ print(f"[SOTA] Loading SFT model from {MODEL_REPO} subfolder={MODEL_NAME}...")
55
+ tokenizer = AutoTokenizer.from_pretrained("facebook/bart-large")
56
+
57
+ # Add style tokens that were used during SFT training
58
+ style_token_human = "<human>"
59
+ for tok in [style_token_human]:
60
+ if tok not in tokenizer.get_vocab():
61
+ tokenizer.add_tokens([tok])
62
+
63
+ # Load the BART base from SFT checkpoint
64
+ bart = AutoModelForSeq2SeqLM.from_pretrained(
65
+ MODEL_REPO, subfolder=MODEL_NAME, dtype=torch.float32,
66
+ )
67
+ if len(tokenizer) > bart.config.vocab_size:
68
+ bart.resize_token_embeddings(len(tokenizer))
69
+
70
+ # Load style modules
71
+ from huggingface_hub import hf_hub_download
72
+ style_path = hf_hub_download(
73
+ repo_id=MODEL_REPO, filename=f"{MODEL_NAME}/style_modules.pt",
74
+ )
75
+ style_ckpt = torch.load(style_path, map_location="cpu")
76
+
77
+ # Reconstruct style embeddings
78
+ hidden_size = bart.config.d_model
79
+ style_embeddings = nn.Embedding(2, hidden_size)
80
+ style_proj = nn.Sequential(
81
+ nn.Linear(hidden_size, 1024), nn.GELU(), nn.Linear(1024, hidden_size),
82
+ )
83
+ style_embeddings.load_state_dict(style_ckpt["style_embeddings"])
84
+ style_proj.load_state_dict(style_ckpt["style_proj"])
85
+
86
+ bart.eval()
87
+ style_embeddings.eval()
88
+ style_proj.eval()
89
+
90
+ if torch.cuda.is_available():
91
+ bart = bart.to("cuda")
92
+ style_embeddings = style_embeddings.to("cuda")
93
+ style_proj = style_proj.to("cuda")
94
+
95
+ print(f"[SOTA] Rewriting {len(text.split())} words with human style...")
96
+ start = time.time()
97
+
98
+ # Prepend <human> token to input text
99
+ input_text = f"{style_token_human} {text}"
100
+ inputs = tokenizer(
101
+ input_text, max_length=max_input_length, truncation=True,
102
+ return_tensors="pt",
103
+ )
104
+ if torch.cuda.is_available():
105
+ inputs = {k: v.to("cuda") for k, v in inputs.items()}
106
+
107
+ # Get encoder outputs
108
+ encoder_outputs = bart.model.encoder(
109
+ input_ids=inputs["input_ids"],
110
+ attention_mask=inputs["attention_mask"],
111
+ return_dict=True,
112
+ )
113
+
114
+ # Inject human style embedding
115
+ human_style_id = torch.tensor([1], device=encoder_outputs.last_hidden_state.device)
116
+ style_emb = style_embeddings(human_style_id)
117
+ style_emb = style_proj(style_emb)
118
+ encoder_outputs.last_hidden_state = (
119
+ encoder_outputs.last_hidden_state + style_emb.unsqueeze(1)
120
+ )
121
+
122
+ # Generate with style-injected encoder outputs
123
+ with torch.no_grad():
124
+ outputs = bart.generate(
125
+ encoder_outputs=encoder_outputs,
126
+ attention_mask=inputs["attention_mask"],
127
+ max_length=max_output_length,
128
+ temperature=temperature,
129
+ top_p=top_p,
130
+ repetition_penalty=repetition_penalty,
131
+ do_sample=True,
132
+ pad_token_id=tokenizer.eos_token_id,
133
+ )
134
+
135
+ rewritten = tokenizer.decode(outputs[0], skip_special_tokens=True)
136
+ elapsed = time.time() - start
137
+
138
+ result = {
139
+ "status": "completed",
140
+ "model": f"{MODEL_REPO}/{MODEL_NAME}",
141
+ "original": text,
142
+ "rewritten": rewritten,
143
+ "original_words": len(text.split()),
144
+ "rewritten_words": len(rewritten.split()),
145
+ "elapsed_seconds": round(elapsed, 2),
146
+ }
147
+
148
+ if verify:
149
+ result["verification"] = _verify_output(text, rewritten)
150
+
151
+ return result
152
+
153
+
154
+ def _verify_output(original: str, rewritten: str) -> dict:
155
+ """Verify rewrite quality."""
156
+ orig_w = len(original.split())
157
+ rew_w = len(rewritten.split())
158
+ ratio = rew_w / max(orig_w, 1)
159
+ issues, ok = [], []
160
+
161
+ if ratio < 0.4:
162
+ issues.append(f"Too short: {rew_w}w vs {orig_w}w")
163
+ elif ratio > 2.0:
164
+ issues.append(f"Too long: {rew_w}w vs {orig_w}w ({ratio:.2f}x)")
165
+ else:
166
+ ok.append(f"Length: {orig_w}w -> {rew_w}w ({ratio:.2f}x)")
167
+
168
+ artifacts = ["###", "Paraphrase:", "Here is", "Let me know"]
169
+ found = [a for a in artifacts if a.lower() in rewritten.lower()]
170
+ if found:
171
+ issues.append(f"Artifacts: {found}")
172
+ else:
173
+ ok.append("No artifacts detected")
174
+
175
+ orig_nums = set(re.findall(r'\b\d+\b', original))
176
+ rew_nums = set(re.findall(r'\b\d+\b', rewritten))
177
+ missing = orig_nums - rew_nums
178
+ if missing:
179
+ issues.append(f"Missing numbers: {missing}")
180
+ elif orig_nums:
181
+ ok.append(f"Numbers: {len(orig_nums)}/{len(orig_nums)} preserved")
182
+
183
+ return {
184
+ "passed": len(issues) == 0,
185
+ "ok": ok, "issues": issues,
186
+ "length_ratio": round(ratio, 2),
187
+ "original_words": orig_w, "rewritten_words": rew_w,
188
+ }
189
+
190
+
191
+ @app.local_entrypoint()
192
+ def main(
193
+ text: str = "",
194
+ text_file: str = "",
195
+ gpu: str = "T4",
196
+ verify: bool = True,
197
+ output: str = "output/rewrite_sota_result.json",
198
+ ):
199
+ """SOTA rewriting entrypoint."""
200
+ os.environ["MODAL_GPU"] = gpu
201
+
202
+ if text:
203
+ pass
204
+ elif text_file:
205
+ with open(text_file, "r", encoding="utf-8") as f:
206
+ text = f.read().strip()
207
+ else:
208
+ text = "Artificial intelligence has revolutionized natural language processing."
209
+
210
+ print("=" * 50)
211
+ print(f" SOTA Rewrite — BART-DPO")
212
+ print(f" Model: {MODEL_REPO}/{MODEL_NAME} | GPU: {gpu}")
213
+ print("=" * 50)
214
+ print(f"\n[Input] {len(text.split())} words:")
215
+ print(text[:200] + ("..." if len(text) > 200 else ""))
216
+
217
+ result = rewrite_sota.remote(text=text, verify=verify)
218
+
219
+ if result.get("status") == "completed":
220
+ rew = result["rewritten"]
221
+ print(f"\n--- Rewrite ---")
222
+ print(f" Words: {result['original_words']} -> {result['rewritten_words']}")
223
+ print(f" Time: {result['elapsed_seconds']}s")
224
+ print(f"\n {rew[:500]}")
225
+
226
+ if result.get("verification"):
227
+ v = result["verification"]
228
+ print(f"\n Verify: {'OK' if v['passed'] else 'ISSUES'}")
229
+ for check in v.get("ok", []):
230
+ print(f" + {check}")
231
+ for issue in v.get("issues", []):
232
+ print(f" - {issue}")
233
+
234
+ os.makedirs(os.path.dirname(output) or ".", exist_ok=True)
235
+ with open(output, "w", encoding="utf-8") as f:
236
+ json.dump(result, f, indent=2, ensure_ascii=False, default=str)
237
+
238
+ print(f"\n[Save] {output}")