File size: 15,789 Bytes
afe7037
c7240c3
 
3525747
afe7037
3525747
 
 
 
 
 
 
71e7a20
afe7037
 
3525747
afe7037
3525747
 
 
afe7037
3525747
71e7a20
afe7037
d0212f1
1d2999d
 
3525747
afe7037
3525747
 
 
 
 
 
 
7da23cf
 
3525747
 
ba6d692
7da23cf
ba6d692
 
 
7da23cf
 
 
ba6d692
 
7da23cf
 
 
ba6d692
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7da23cf
3525747
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ba6d692
3525747
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79883c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3525747
 
79883c6
3525747
71e7a20
3525747
 
 
 
 
 
 
79883c6
71e7a20
3525747
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71e7a20
3525747
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71e7a20
3525747
7e313b0
 
3525747
 
 
 
 
 
 
 
dfccb39
 
 
 
 
 
 
 
3525747
 
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
---
license: other
license_name: openmdw-1.1
license_link: LICENSE
base_model: nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16
library_name: transformers
language:
- en
tags:
- ai-text-detection
- idea-provenance
datasets:
- rishanthrajendhran/WildOutlines
---

# IdeaLens-NoParaphrase

IdeaLens-NoParaphrase is an ablation of IdeaLens: the same backbone, documents and labels, trained on the outlines
as extracted, without the paraphrasing step that removes the documents' wording. It reads a role-labelled outline and
returns P(human), the probability that the document's ideas are human.

It is `nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16` fine-tuned with LoRA (rank 64) on the outlines of 1M English
web documents ([WildOutlines](https://huggingface.co/datasets/rishanthrajendhran/WildOutlines), `outline` field).

**Try it in your browser:** the [IdeaLens & ProseLens demo](http://ideadetector.ai/) scores your own text with both detectors,
no installation or keys needed.

## Results

This model is not reported in the paper. It is released for comparison with IdeaLens.

## Usage

Scoring a document takes two steps:

1. **Extract an outline.** An LLM writes the outline from the document, its format's role vocabulary and six worked
   examples (the paper uses Gemini 3.7 Flash). The [idealens](https://github.com/RishanthRajendhran/IdeaLens) package ships the prompts, role vocabularies and
   worked examples, and runs this step. Score the outline as extracted; the paraphrasing step is only for training data.
2. **Score the outline** with this model, as below.

### Quick start: everything with the idealens package

[idealens](https://github.com/RishanthRajendhran/IdeaLens) ([PyPI](https://pypi.org/project/idealens/)) classifies each document's format, extracts its outline with the prompt, role
vocabulary and worked examples IdeaLens-NoParaphrase was trained with, scores the outline on vLLM and applies the thresholds in this
repo:

```bash
pip install "idealens[vllm]"
export GEMINI_API_KEY=...   # or --provider vertex | openai | anthropic | openrouter | compatible
idealens run docs.jsonl -o scores.jsonl --model IdeaLens-NoParaphrase --dry-run   # price the LLM calls first
idealens run docs.jsonl -o scores.jsonl --model IdeaLens-NoParaphrase
```

Input is JSONL with a `text` field per document. The same steps in Python:

```python
import idealens as il

texts = [open("document.txt").read()]
formats = il.classify(texts)                  # one of the eight formats per document
outlines = il.extract(texts, formats)         # role-labelled outlines
with il.Detector("IdeaLens-NoParaphrase") as det:          # vLLM, with this repo's thresholds
    records = det.score_outlines(outlines, format=formats)

r = records[0]
print(r["p_human"], r["verdict"]["ai"], r["verdict"]["cut"])   # P(human); flagged at the 1% global cut?
print(outlines[0].render())                   # the outline that was scored
```

`classify` and `extract` call Gemini 3.7 Flash, the extractor the thresholds were fitted with; pass
`provider=idealens.providers.make("openai", "gpt-6-sol")` (or Vertex, Anthropic, OpenRouter, a local server) to use
another. Each record also carries verdicts at every calibrated false-positive rate under the global, per-format and
per-topic schemes. The [package README](https://github.com/RishanthRajendhran/IdeaLens#ways-to-use-idealens) covers batch jobs, scoring outlines you already have and calibrating
on your own data.

### Mix and match: extract with the package, score with your own code

```python
import idealens as il

text = open("document.txt").read()
outline = il.extract([text], il.classify([text]))[0].render()   # one "[Role] content" line per item
print(p_human(outline))   # p_human (transformers) or p_human_batch (vLLM), defined below
```

The rest of this section runs the model directly.

### Load the merged model (66 GB download)

```python
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

tok = AutoTokenizer.from_pretrained("rishanthrajendhran/IdeaLens-NoParaphrase")
model = AutoModelForCausalLM.from_pretrained("rishanthrajendhran/IdeaLens-NoParaphrase", dtype=torch.bfloat16, device_map="auto").eval()
```

The weights take 59 GiB of GPU memory, and each input adds more; see *Hardware requirements*.

### Or apply the adapter to the base model (3 GB download)

`adapter/` holds the LoRA adapter as trained, in the layout of the Tinker training service. If you already have the
base model, `load_adapter.py` merges the adapter into it in memory. The resulting weights are bit-identical to the
merged model's:

```python
import importlib.util
from huggingface_hub import hf_hub_download

path = hf_hub_download("rishanthrajendhran/IdeaLens-NoParaphrase", "load_adapter.py")
spec = importlib.util.spec_from_file_location("load_adapter", path)
la = importlib.util.module_from_spec(spec); spec.loader.exec_module(la)
model, tok = la.load_model()   # base model + adapter/, then la.p_human(model, tok, outline)
```

Do not load `adapter/` with `peft.PeftModel`. In transformers, Nemotron fuses the Mamba gate and x projections into
one `in_proj` and stores each layer's 128 routed experts as a single 3D tensor, so PEFT has nowhere to attach most of
the adapter and skips it without a warning; the model then scores close to the base model. `tinker-cookbook`'s
`weights.build_hf_model` can also merge the adapter into full weights.

### Score an outline

Write the outline one item per line, as `[Role] content`, or take it from `il.extract` as above. IdeaLens-NoParaphrase compares the next-token probabilities of `human` and `ai`:

```python
SYSTEM = "Given a role-labelled outline of a document, answer with one word: human if the source document was human-written, ai if it was AI-generated."
SUFFIX = "<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n"
HUMAN, AI = 50755, 2464   # token ids of "human" and "ai"

@torch.no_grad()
def p_human(outline):
    ids = tok.encode(f"<|im_start|>system\n{SYSTEM}<|im_end|>\n<|im_start|>user\n{outline}{SUFFIX}",
                     add_special_tokens=False)
    logits = model(torch.tensor([ids], device=model.device)).logits[0, -1].float()
    return torch.softmax(logits[[HUMAN, AI]], -1)[0].item()

outline = ("[Central Development] A town's water supply fails after a drought, and residents organise to share wells.\n"
           "[Background Context] The reservoir has been shrinking for three summers.\n"
           "[Open Question] Whether the council will fund a new pipeline remains undecided.")
print(p_human(outline))
```

Build the prompt string exactly as above rather than through the chat template.

### Score with vLLM

For many inputs, vLLM is about 15 times faster than the code above and fits much longer inputs on one 80 GB GPU.
With vLLM 0.21 (install `xgrammar==0.2.1`; later releases require transformers < 5), reusing `SYSTEM`, `SUFFIX`,
`HUMAN` and `AI` from above:

```python
import math, os
os.environ.setdefault("VLLM_USE_FLASHINFER_SAMPLER", "0")   # FlashInfer kernels compile CUDA code and need nvcc
os.environ.setdefault("VLLM_USE_FLASHINFER_MOE_FP16", "0")
os.environ.setdefault("VLLM_USE_DEEP_GEMM", "0")            # H100 warmup crashes when DeepGEMM is not installed
from transformers import AutoTokenizer
from vllm import LLM, SamplingParams

tok = AutoTokenizer.from_pretrained("rishanthrajendhran/IdeaLens-NoParaphrase")
llm = LLM(model="rishanthrajendhran/IdeaLens-NoParaphrase", dtype="bfloat16", max_num_seqs=256, enable_prefix_caching=False,
          max_logprobs=20, enable_flashinfer_autotune=False, seed=0)
sp = SamplingParams(max_tokens=1, temperature=0.0, logprobs=20)

def p_human_batch(outlines):
    prompts = [{"prompt_token_ids": tok.encode(f"<|im_start|>system\n{SYSTEM}<|im_end|>\n<|im_start|>user\n{x}{SUFFIX}",
                                               add_special_tokens=False)} for x in outlines]
    out = []
    for r in llm.generate(prompts, sp, use_tqdm=False):
        lp = r.outputs[0].logprobs[0]           # the top 20 next-token log-probabilities
        out.append(1 / (1 + math.exp(lp[AI].logprob - lp[HUMAN].logprob)))
    return out

# when done: without this, vLLM 0.21 keeps a script running after its last line
llm.llm_engine.engine_core.shutdown()
```

`max_num_seqs=256` keeps every running sequence's Mamba state in memory; vLLM's H100 default (1,024) does not fit
beside the weights. If `human` or `ai` is missing from the top 20 (rare), score the prompt followed by each label
token with `SamplingParams(max_tokens=1, prompt_logprobs=0)` and read the last prompt log-probability of each.
Scores agree with the training-time scores to about 0.001 in P(human) on average; A100 and H100 GPUs differ by as much.

### Thresholds

IdeaLens-NoParaphrase flags a document as having AI ideas when P(human) is below a cut. Each cut is set so
that a given share of human documents is flagged (the false-positive rate, FPR), measured on the 80,000 human
documents in WildOutlines's `calibration` split (10,000 per format). The paper's operating point is the global
cut at 1% FPR. These cuts were fitted on scores from the merged weights in this repo, computed locally in bf16.

| FPR | 0.1% | 0.5% | 1% | 2% | 5% |
|---|---:|---:|---:|---:|---:|
| Global cut | 0.00538 | 0.03359 | 0.09947 | 0.50391 | 0.94738 |

Per-format cuts give each format its own operating point. They need the document's format, which the paper
assigns with WebOrganizer's annotation prompt run on Gemini 3.7 Flash; the calibration documents use the formats
recorded in WildOutlines. Each is the
format's own quantile, shrunk toward the global cut with weight n / (n + 2500); at 0.1% FPR 10,000 documents
per format are too few, so there is no per-format cut. A document outside these eight formats has no
per-format cut; do not fall back to the global cut for it.

| Format | 0.5% | 1% | 2% | 5% |
|---|---:|---:|---:|---:|
| Nonfiction Writing | 0.01682 | 0.04034 | 0.14608 | 0.74359 |
| Knowledge Article | 0.02455 | 0.05113 | 0.16982 | 0.79109 |
| Personal Blog | 0.10743 | 0.47575 | 0.79001 | 0.97165 |
| News Article | 0.02812 | 0.07552 | 0.33610 | 0.86126 |
| Academic Writing | 0.17216 | 0.61199 | 0.85898 | 0.98434 |
| User Reviews | 0.15260 | 0.35787 | 0.76215 | 0.97357 |
| Personal About Page | 0.05767 | 0.23257 | 0.65484 | 0.94379 |
| Creative Writing | 0.36622 | 0.60222 | 0.80796 | 0.96994 |

`thresholds.json` holds every cut at full precision, plus per-topic cuts for the WebOrganizer topics with enough calibration documents.

These rates hold for English web documents like the training data. For another domain, fit the cut on
human documents from that domain.

IdeaLens-NoParaphrase scores 91% of human calibration documents above 0.99, so its cuts at higher FPRs sit close
to 1 and small shifts in score move the realised FPR a long way.

## Hardware requirements

Measured with transformers 5.15 in bf16 on NVIDIA H100 80GB GPUs (our other runs used A100 80GB), with transformers'
PyTorch implementation of the Mamba layers (no fused Mamba kernels installed). We have not tried CPU-only inference.

| | Merged model | Adapter route (`load_adapter.py`) |
|---|---|---|
| Download | 65.8 GB | 65.8 GB base model + 3.1 GB adapter |
| Peak CPU RAM while loading | 60 GiB | 60 GiB |
| GPU memory once loaded | 58.8 GiB | 58.8 GiB (66 GiB during the ~10 s it takes to apply the adapter) |

GPU memory then grows with the length of the input, by about 4.2 MiB per token at typical lengths, scoring one
input at a time:

| Input tokens | 500 | 1,000 | 2,000 | 4,000 | 8,000 |
|---|---:|---:|---:|---:|---:|
| Peak GPU memory, one 80 GB GPU | 61.0 GiB | 63.1 GiB | 67.3 GiB | 75.7 GiB | does not fit |
| Peak memory per GPU, two 80 GB GPUs (`device_map="auto"`) | | | | 47.4 GiB | 63.7 GiB |
| Seconds per input, H100 | 0.18 | 0.34 | 0.66 | 1.32 | 2.70 |

Inputs of 6,000 tokens do not fit on one 80 GB GPU and 12,000 do not fit on two; lowering the Mamba chunk size from
128 to 64 did not change either limit.

IdeaLens-NoParaphrase reads outlines, which are short. The outlines in WildOutlines's calibration split average about 640
tokens with the prompt, and the longest is under 3,800, so one 80 GB GPU (A100 80GB or H100 80GB) is enough. Outline
extraction runs through an LLM API and needs no local GPU.

## Intended use and limitations

- IdeaLens-NoParaphrase estimates the provenance of a document's ideas. It should not be the sole basis for decisions about a person's work.
- It was trained on English web documents of at least 500 words in eight long-form formats (Nonfiction Writing, Knowledge Article, Personal Blog, News Article, Academic Writing, User Reviews, Personal About Page, Creative Writing).
- Its training labels come from the Pangram prose detector, applied to whole documents. They record who wrote the prose, and because these outlines keep some of the documents' wording, the model can learn from that wording as well as from the ideas.
- Errors in outline extraction carry into the score.

## Related models

| Model | Backbone | Reads |
|---|---|---|
| [IdeaLens](https://huggingface.co/rishanthrajendhran/IdeaLens) | Nemotron-3.5-Lightning-30B-A3B, LoRA | outline |
| [ProseLens](https://huggingface.co/rishanthrajendhran/ProseLens) | Nemotron-3.5-Lightning-30B-A3B, LoRA | document text |
| [IdeaLens-NoParaphrase](https://huggingface.co/rishanthrajendhran/IdeaLens-NoParaphrase) (this model) | Nemotron-3.5-Lightning-30B-A3B, LoRA | outline, trained without paraphrasing |
| [IdeaLens-Qwen3.5-9B](https://huggingface.co/rishanthrajendhran/IdeaLens-Qwen3.5-9B) | Qwen3.5-9B, classification head | outline |
| [IdeaLens-ModernBERT-L](https://huggingface.co/rishanthrajendhran/IdeaLens-ModernBERT-L) | ModernBERT-large | outline |
| [ProseLens-ModernBERT-L](https://huggingface.co/rishanthrajendhran/ProseLens-ModernBERT-L) | ModernBERT-large | document text |
| [IdeaLens-LogisticClassifier](https://huggingface.co/rishanthrajendhran/IdeaLens-LogisticClassifier) | logistic regression over text-embedding-3-large | outline |
| [IdeaLens-ModernBERT-L-NoParaphrase](https://huggingface.co/rishanthrajendhran/IdeaLens-ModernBERT-L-NoParaphrase) | ModernBERT-large | outline, trained without paraphrasing |
| [IdeaLens-ModernBERT-L-RolesOnly](https://huggingface.co/rishanthrajendhran/IdeaLens-ModernBERT-L-RolesOnly) | ModernBERT-large | role labels only |
| [IdeaLens-Qwen3.5-9B-PerItem](https://huggingface.co/rishanthrajendhran/IdeaLens-Qwen3.5-9B-PerItem) | Qwen3.5-9B, classification head | single outline items, pooled |
| [IdeaLens-ModernBERT-L-PerItem](https://huggingface.co/rishanthrajendhran/IdeaLens-ModernBERT-L-PerItem) | ModernBERT-large | single outline items, pooled |
| [IdeaLens-LogisticClassifier-PerItem](https://huggingface.co/rishanthrajendhran/IdeaLens-LogisticClassifier-PerItem) | logistic regression over text-embedding-3-large | single outline items, pooled |

Training data: [WildOutlines](https://huggingface.co/datasets/rishanthrajendhran/WildOutlines).

All IdeaLens models and datasets are in the [IdeaLens collection](https://huggingface.co/collections/rishanthrajendhran/idealens-6abee785ce6196fc0be9200f).

## License

OpenMDW-1.1, the license of the base model (see `LICENSE`).

## Citation

```bibtex
@article{idealens2026,
  title         = {IdeaLens: Detecting AI Ideas in Long-form Writing},
  author        = {Rajendhran, Rishanth and Choi, Minjoon and Russell, Jenna and Namuduri, Ramya and B{\"o}l{\"o}ni-Turgut, Deniz and Karpinska, Marzena and Wieting, John and Iyyer, Mohit},
  journal       = {arXiv preprint arXiv:2610.06778},
  year          = {2026},
  eprint        = {2610.06778},
  archivePrefix = {arXiv},
  primaryClass  = {cs.CL},
  url           = {https://arxiv.org/abs/2610.06778}
}
```