batmac's picture
Add browser-only AI text detector running ONNX locally
2b48d3b verified
Raw History Blame Contribute Delete
3.39 kB
import { bar, probColor } from './scoring.js';
// Locale-independent thousands grouping, matching the Python app's f"{n:,}".
export const formatNumber = (n) => n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
const escapeHtml = (s) =>
s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
export function highlightHtml(text, spans, scores) {
const parts = ['<div class="aid-highlight">'];
let cursor = 0;
spans.forEach((span, i) => {
parts.push(escapeHtml(text.slice(cursor, span.start)).replace(/\n/g, '<br>'));
const score = scores[i];
parts.push(
`<span title="P(AI) = ${score.toFixed(3)}" data-p="${score.toFixed(3)}" ` +
`style="background:${probColor(score)}; border-radius:3px; padding:1px 2px;">` +
`${escapeHtml(span.sentence)}</span>`,
);
cursor = span.end;
});
parts.push(escapeHtml(text.slice(cursor)).replace(/\n/g, '<br>'));
parts.push('</div>');
return parts.join('');
}
export function reportMarkdown(
result,
{ threshold, aggregation, modelRepo, device, dtype, chars },
) {
const label = aggregation === 'max' ? 'Max' : 'Mean';
const note =
result.windows === 1
? 'one window, so mean and max are identical'
: aggregation === 'max'
? 'highest-scoring window'
: `average across ${result.windows} windows`;
const scores = result.sentences.map((s) => s.score);
const atOrAbove = scores.filter((s) => s >= threshold).length;
return [
'# AI Text Detection Report',
'',
`**Model:** \`${modelRepo}\``,
`**Backend:** ${dtype} ONNX on ${device}`,
`**Characters:** ${formatNumber(chars)} | **Sentences:** ${scores.length} | **Windows scored:** ${result.windows}`,
`**Aggregation:** ${label} (${note}) | **Threshold:** ${threshold.toFixed(2)}`,
'',
'## Verdict',
'',
`### ${result.verdict}`,
'',
`\`${bar(result.score)}\` **score = ${result.score.toFixed(3)}** (${result.aboveThreshold ? 'above' : 'below'} ${threshold.toFixed(2)})`,
'',
'| Metric | Value |',
'| --- | --- |',
`| Score (${label} of windows) | ${result.score.toFixed(3)} |`,
`| Mean of windows | ${result.scoreMean.toFixed(3)} |`,
`| Max of windows | ${result.scoreMax.toFixed(3)} |`,
`| Sentences at or above threshold | ${atOrAbove}/${scores.length} |`,
`| Max sentence score | ${Math.max(...scores).toFixed(3)} |`,
`| Min sentence score | ${Math.min(...scores).toFixed(3)} |`,
`| Decision threshold | ${threshold.toFixed(2)} |`,
'',
'## Sentence-level breakdown',
'',
...result.sentences.map((s) => {
const snippet = s.sentence.length <= 160 ? s.sentence : `${s.sentence.slice(0, 157)}...`;
return `- \`${s.score.toFixed(3)}\` \`${bar(s.score, 12)}\` ${snippet}`;
}),
'',
'## Most AI-like passages',
'',
...[...result.sentences]
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map((s) => `> **${s.score.toFixed(3)}** - ${s.sentence}`),
'',
'---',
'',
'_Scores come from an ONNX model running in your browser, and nothing is uploaded. They are probabilities, not proof: the model card warns against using this detector as the sole basis for high-stakes decisions. The threshold is a display choice, only 0.50 comes from the model card, and no false-positive rate is claimed for any other value._',
].join('\n');
}