SPB Git

Repository Public

README.md 18.7 KB

HTR logo

HTR — Human Text Reconstruction

Map LLM-rewritten scientific prose back towards the register of pre-2018 human writing.
Public research platform of UQO Working Paper WP-AI-0002 · htr.spboucher.ai

status: research preview models: 4, default Qwen2.5-1.5B-HTR Pangram 4.1: HTR-Tiny and Flan-T5-Large 0 % flagged, 54/54 Human license: Apache-2.0 node: m4mm:8540 latency p50 by model tracking: none server: Python standard library API: JSON, 30 requests per minute

[!IMPORTANT] Outputs are synthetic reconstructions. They are a model's estimate of pre-2018-style prose given an LLM rewrite. They are not the original text and not evidence of human authorship or of AI use. Do not use HTR for AI-text detection, authorship attribution, grading or academic-integrity decisions. The deployed models are Medium-stage research models; their gains over the untouched input are small by construction (reconstruction from a paraphrase is non-identifiable).


# Contents

# What it is

HTR models are trained on pairs of (LLM rewrite → human original). The originals are rights-verified scientific passages published before 2018. For a rewritten paragraph, the platform:

  1. protects citations, equations and URLs behind opaque placeholders,
  2. encodes the protected text (HTR models: 32 k SentencePiece vocabulary, ≤ 640 tokens; pretrained backbones: their own tokenizer, context as a text prefix),
  3. decodes a reconstruction (greedy or beam 3 for HTR models, greedy for the backbones; seeded and deterministic),
  4. restores the placeholders and checks numbers, citations, negations, hedges and length against the input. Every deviation is returned as a warning.

The web UI (/) is a workbench with these features:

  • a model selector (compact card per model: family, parameters, speed, test Δ chrF++, fidelity, Pangram share; the default highlighted; a caveat for the backbones); the choice is kept in localStorage, share links and history
  • input and output panes, with optional context (discipline, era or year, section) and the decoding mode (beam 3 for HTR models only)
  • a "loading model…" state when the chosen model is not yet in memory (first call can take a minute)
  • Reconstruction / Changes / Checks tabs: the plain text, a word diff, and the preservation checks; the status line names the model used
  • copy, .txt and .json downloads; share links (text in the URL hash, never on the server); a history of the last 10 runs in localStorage
  • light and dark themes; responsive down to 360 px; print styles

# Models

All metrics are differences in chrF++ versus the identity baseline (B0, return the input unchanged) on the Medium Haiku pairs. They are exploratory, except the confirmatory test value of HTR-Tiny (95 % interval in brackets; ± as reported). Numbers / placeholders are preservation rates on the test split; Pangram is the share of 54 reconstructions the Pangram classifier labels Human.

id Name Family Params Val Δ Test Δ Held-out domain Δ Held-out period Δ Numbers Placeholders Pangram Human · mean AI score · flagged p50 latency*
htr-tiny-0.3.0 HTR-Tiny 0.3.0 (paper's M*) htr 33.5 M — +0.44 [+0.30, +0.58] (confirmatory) −0.10 −0.53 0.991 0.992 54/54 · 0.000 · 0 % 0.3 s
htr-base-0.1.0 HTR-Base 0.1.0 htr 335 M +0.83 +0.69 ± 0.05 +0.29 −0.12 0.992 0.993 53/54 · 0.003 · 1.9 % 0.7 (1.1 beam 3) s
flan-t5-large-htr Flan-T5-Large-HTR (google/flan-t5-large) hf-seq2seq 783 M +2.04 ± 0.15 +1.79 ± 0.07 +1.27 ± 0.18 +1.09 ± 0.14 0.986 0.992 54/54 · 0.000 · 0 % 1.5 s
qwen2.5-1.5b-htr (default) Qwen2.5-1.5B-HTR (Qwen/Qwen2.5-1.5B) hf-causal 1.54 B +2.19 ± 0.12 +1.97 ± 0.06 +1.78 ± 0.13 +1.30 ± 0.14 0.997 0.998 48/54 (6 Mixed) · 0.032 · 11.1 % 2.1 s

* greedy decoding, ~600-character paragraph, Apple GPU (MPS) on m4mm, measured 2026-11-10 (3 000 characters: Flan-T5 ≈6 s, Qwen ≈8 s); live values at /api/stats.

Detector sensitivity (Pangram 4.1, n = 54 test passages, 2026-11-10). Human originals: 54/54 labelled Human, mean AI-likelihood 0.000; the LLM rewrites given as input: 29 Human / 25 Mixed (46 % flagged), mean 0.188. HTR-Tiny 0.3.0 and Flan-T5-Large-HTR reach 0 % flagged — 54/54 labelled Human, mean AI score 0.000; HTR-Base 53/54 (mean 0.003), Qwen2.5-1.5B-HTR 48/54 (mean 0.032). Each card stores this under metrics.pangram (n, human_share, flagged_share, mean_ai_likelihood, detector, date), and the platform shows it on the selector tiles, the comparison table and every model card. This measures one commercial detector's behaviour on a 54-passage sample. A low AI-likelihood is not evidence of human authorship, and HTR must not be used to evade AI-text detection or for integrity decisions.

Pretrained backbones (Flan-T5, Qwen). Their pretraining corpus may contain pre-2018 scientific text; the memorisation controls of the paper cover only the HTR training pairs (see its threats-to-validity section). Beam search is not offered for them (beam is mapped to greedy and the response says so). Flan-T5 reads at most 512 tokens (about 2 000 characters) and returns a truncation warning beyond that.

Each model lives in models/<id>/ with a tracked model_card.json (schema in models/README.md); the weights are not in git.

# Architecture

text
htr-platform/
├── server.py            # stdlib ThreadingHTTPServer: UI, JSON API, model registry, model cards, gzip, CSP, rate limits
├── htr/                 # Python package (copy of WP-AI-0002 src/htr): inference pipeline, tokenizer, models, HF backbones
│   └── inference/backbones.py  # HFSeq2SeqReconstructor, HFCausalReconstructor, load_reconstructor(dir), ModelRegistry
├── models/              # one directory per model — weights NOT in git, model_card.json (+ README.md) tracked
│   ├── README.md               # model list, card schema, how to add one
│   ├── htr-tiny-0.3.0/  htr-base-0.1.0/        # model.safetensors, config.json, tokenizer.model, release_meta.json, README.md
│   ├── flan-t5-large-htr/  qwen2.5-1.5b-htr/   # Hugging Face save_pretrained directories
├── scripts/add_model.sh # laptop → node: rsync weights, restart, check /api/models, commit the card
├── static/              # index.html, app.css, app.js (single CSS + single JS, no framework),
│   │                    # card.html, 404.html, favicon.svg, og.svg, site.webmanifest
├── docs/badges/         # static shields-style SVG badges used by this README
└── requirements.txt     # torch, transformers, accelerate, sentencepiece, safetensors, … (inference only)
  • Model registry. ModelRegistry(models/, device=HTR_DEVICE, max_resident=2, pinned=("htr-tiny-0.3.0",)) discovers the sub-directories at start-up. The model flagged default in its card is loaded immediately (if it fails, HTR-Tiny takes over for the hero demo); the others load on their first request (load times are logged). At most two non-pinned models stay resident (LRU eviction, torch.mps.empty_cache() after eviction); HTR-Tiny is pinned.
  • Inference runs on the Apple GPU (--device mps, env HTR_DEVICE) with one lock per model, so a slow Qwen request does not block a Tiny request. Backbone generation is capped at HTR_MAX_GEN_S seconds (default 55) through generation_config.max_time, so a 3 000-character input finishes in about a minute at most.
  • No third-party web dependencies. The fonts are system fonts, and the icons, logo and OpenGraph image are inline or self-hosted SVG. The CSP is default-src 'self' and the page makes no external request. Assets are cache-busted with ?v=<sha256>, served immutable and gzip-compressed.
  • Theming. The design tokens use CSS light-dark(), and the toggle stores your choice in localStorage (htr.theme); the model choice is htr.model.

# API reference

Base URL: https://htr.spboucher.ai. No key and no account are needed. All responses are JSON unless noted.

Method Path Description
POST /api/reconstruct Reconstruct one passage with the chosen model
GET /api/models Model cards with loaded, load_s, default, beam_supported, rate_limit_per_min, request counts
GET /api/info Default model metadata and metrics, model ids, loaded ids, limits, conditioning flag
GET /api/stats Uptime, per-model requests / errors / latency p50, p95, max, limits
GET /api/examples Example inputs
GET /health Liveness: {"ok": true, "model": …, "default": …, "loaded": […], "requests": …, "version": …}
GET /model-card, /model-card/<id> Structured model card of the default model / of <id>: header band, metric tiles, Pangram block, bar comparison vs identity and the other models, training/decoding/limits/call-it sections, then the model's README.md if present (?raw returns the Markdown)
GET /og.svg, /site.webmanifest, /robots.txt Metadata assets

# POST /api/reconstruct

Request body:

Field Type Notes
text string, required 1–8 000 characters
model string model id from /api/models (default: the registry default, qwen2.5-1.5b-htr); unknown id → 400
discipline string e.g. economics, biomedicine, finance, physics, …
pub_year int clamped to 1500–2017
section_role string abstract, introduction, methods, results, discussion, …
beam 1 | 3 greedy (default) or beam 3 — HTR models only; mapped to greedy for the backbones (beam_requested and decoding_note say so)

The HTR models of architecture m1 ignore context (/api/info returns "conditioning": false when one of them is the default); the backbones receive it as a text prefix. The context fields are echoed in metadata_used.

bash
curl -s https://htr.spboucher.ai/api/reconstruct \
  -H 'Content-Type: application/json' \
  -d '{"text": "Across all specifications, the estimated elasticity remains positive and statistically significant, ranging from 0.18 to 0.24 [12].", "model": "htr-tiny-0.3.0", "discipline": "economics", "pub_year": 2015, "beam": 3}'
python
import json, urllib.request
req = urllib.request.Request("https://htr.spboucher.ai/api/reconstruct",
    data=json.dumps({"text": "…LLM-rewritten passage…", "model": "qwen2.5-1.5b-htr"}).encode(),
    headers={"Content-Type": "application/json"})
r = json.load(urllib.request.urlopen(req))
print(r["model"], r["latency_s"], "s"); print(r["reconstruction"]); print(r["warnings"])

Response (200):

jsonc
{
  "input_text": "…",
  "reconstruction": "…",                 // synthetic text
  "warnings": ["negation count changed by -1"],
  "preservation": {
    "placeholders_exact": 1.0, "placeholder_recall": 1.0,
    "number_recall": 1.0, "number_precision": 1.0,
    "negation_delta": 0.0, "hedge_delta": 0.0, "sentence_count_delta": 0.0
  },
  "metadata_used": {"discipline": "economics", "pub_year": 2015, "arch": "hf-causal"},
  "diff_html": "… <del>…</del> <ins>…</ins> …",   // word diff, HTML-escaped
  "diff_stats": {"words_in": 64, "words_out": 60, "words_added": 21, "words_removed": 22, "word_similarity": 0.68},
  "model": "Qwen2.5-1.5B-HTR", "model_id": "qwen2.5-1.5b-htr", "family": "hf-causal", "device": "mps",
  "beam": 1, "beam_requested": 3, "decoding": "greedy",
  "decoding_note": "beam search is only offered for HTR models; this backbone used greedy decoding",
  "load_s": 9.8,                          // only present when the call had to load the model first
  "latency_s": 8.4,
  "notice": "Synthetic reconstruction produced by HTR. …"
}

Errors return {"error": "…"} with one of these codes: 400 (invalid JSON, empty text or unknown model), 413 (more than 8 000 characters or a body over 64 kB), 429 (rate limit, with Retry-After: 60), 500 (inference failure, with model_id). Every POST response carries X-RateLimit-Limit and X-RateLimit-Remaining for the limit that applies to the chosen model.

model, model_id, family, device, beam_requested, decoding, decoding_note and load_s were added in platform v0.3.0 and are additive: v0.2 clients that omit model get the default model. v0.4.0 adds metrics.pangram to each card and pangram_reference to /api/models.

# Deployment (MacLustr)

Item Value
Node m4mm (Mac mini M4 Pro, 12 cores, 48 GB, LAN), directory ~/apps/htr (this git clone)
Process PM2 htr-web: .venv/bin/python server.py --port 8540 --host 0.0.0.0 --device mps (env HTR_MODELS_DIR, HTR_DEVICE=mps, OMP_NUM_THREADS=4, max_memory_restart 20G)
Public URL https://htr.spboucher.ai: MacLustr Tunnel (Caddy on BHS64 → WireGuard → m4mm:8540)
Manifest M1M32:~/dispatch/apps/htr.json (placement prefers m4mm and avoids the WP-AI-0002 training nodes)
Git origin spbgit htr-platform.git (gateway M1M32)

The MacLustr rule is remote-first: edit on the node, restart, verify, then commit and push from the node.

bash
ssh m4mm
export PATH=/opt/homebrew/bin:$PATH; cd ~/apps/htr
# … edit server.py / static/* …
python3 -m py_compile server.py
pm2 restart htr-web --update-env
curl -s https://htr.spboucher.ai/health
git add -A && git commit -m "…" && git push origin main

mld status htr shows the current placement. mld move htr --to <node> migrates the app and repoints the tunnel route (the weights under models/ move with the directory).

# Shipping a new model

No code change is needed: the registry discovers models/<id>/ at start-up and the UI builds itself from /api/models.

  1. Export the model directory on the laptop (HTR release: model.safetensors, config.json, tokenizer.model, release_meta.json, README.md; backbone: the Hugging Face save_pretrained directory).
  2. Write model_card.json in that directory (fields and example in models/README.md: id, name, family ∈ htr | hf-seq2seq | hf-causal, n_params, base_model, licence, default, speed_hint, max_len / max_new_tokens, metrics, notes (backbones: keep the pretraining-corpus caveat), trained_on, exported_at).
  3. From the laptop:
    bash
    scripts/add_model.sh <laptop-model-dir> <id>        # e.g. scripts/add_model.sh artifacts/runs/medium_llm_qwen25_3b/best qwen2.5-3b-htr
    The script validates the card, rsyncs the directory to m4mm:~/apps/htr/models/<id>/, restarts htr-web, waits until /api/models lists the id, and commits + pushes the tracked card files from the node.
  4. Check https://htr.spboucher.ai/model-card/<id> and run one POST /api/reconstruct with "model": "<id>". Planned ids: flan-t5-xl-htr, qwen2.5-3b-htr, htr-large.

To change the default model, set "default": true in exactly one card (and false in the previous one), then pm2 restart htr-web --update-env.

# Development

bash
cd ~/apps/htr                       # or any clone that has models/ and .venv
.venv/bin/python server.py --port 8541 --host 127.0.0.1 --device mps
open http://127.0.0.1:8541/         # from the laptop: ssh -L 8541:127.0.0.1:8541 m4mm
  • --device cpu runs everything on CPU; --models /path/to/dir serves another models directory; HTR_MAX_GEN_S caps backbone generation time.
  • Dependencies: uv pip install --python .venv/bin/python -r requirements.txt (torch ≥ 2.4, transformers ≥ 4.45, accelerate).
  • The frontend is plain HTML, CSS and JS: no build step and no bundler. Keep app.css plus app.js under 70 kB.
  • The CSP forbids inline scripts. The server passes page data (examples, demo, info, models) to the frontend in <script type="application/json" id="boot">.

# Limits and rate limits

Limit Value
Characters per request 8 000 (HTR models read about 640 tokens, Flan-T5 512; longer inputs are truncated, with a warning for Flan-T5)
Request body 64 kB
Rate 30 requests per minute per client IP (X-Forwarded-For); 10 per minute for the pretrained backbones
Concurrency 1 inference at a time per model (other requests for the same model wait)
Generation time backbones capped at 55 s per request (HTR_MAX_GEN_S)
Latency see the model table above; live per-model p50/p95 at /api/stats
Storage none: the server keeps no texts, cookies or analytics. The log records only client IP, model id, length, latency and warning count

# Honest-use notice

Reconstruction from a paraphrase is non-identifiable: many originals share one rewrite. A model's output is one plausible estimate. The platform is a research artefact for studying how LLM rewriting changes scientific prose, and a suggestion tool for authors who check every change. Verify every number, citation and negation against your source. Never present the output as the original text or as proof of who wrote it. The pretrained backbones add one caveat: their pretraining corpus may contain pre-2018 scientific text, and the paper's memorisation controls cover only the HTR training pairs.

  • Paper: Human Text Reconstruction: Learning the Inverse of LLM-Induced Transformations in Scientific Writing, UQO Working Paper WP-AI-0002 (Simon-Pierre Boucher, Université du Québec en Outaouais, 2026)
  • Live platform: https://htr.spboucher.ai · model cards: https://htr.spboucher.ai/model-card · https://htr.spboucher.ai/api/models
  • Repository: spbgit htr-platform.git (gitsrv:srv/git/htr-platform.git)
  • Licence: code and HTR weights under Apache-2.0; the fine-tuned backbones keep the Apache-2.0 licence of google/flan-t5-large and Qwen/Qwen2.5-1.5B. Use is restricted to non-commercial research where CC-BY-NC sources were included in training (see models/<id>/LICENSE-MODEL.txt and the cards).
Files main
Menu

Theme