Instructions to use ProCreations/Zap with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ProCreations/Zap with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="ProCreations/Zap")# Load model directly from transformers import AutoTokenizer, AutoModelForTokenClassification tokenizer = AutoTokenizer.from_pretrained("ProCreations/Zap") model = AutoModelForTokenClassification.from_pretrained("ProCreations/Zap", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Zap ⚡ — a tiny web-form classifier for autofill
Zap looks at a web form and tells a password manager what every input wants and what the form is for. It runs locally, has 20.7M parameters, and ships bf16 weights (41 MB), plus ONNX files (fp32 83 MB, int8 21 MB).
- Field types (26):
username,email,current-password,new-password,one-time-code,tel, names, address parts, payment-card parts,bday,search,captcha,other - Form purposes (11):
login,signup,password-change,password-recovery,otp,payment,address,newsletter,contact,search,other
So your autofill can answer questions like "is this a login form?", "which box gets the saved password, and which one gets a newly generated password?", "is this the email field, or is it a honeypot?"
Held-out websites: 6,838 real forms and 19,811 fields from Common Crawl pages on hosts never seen in training:
| Zap | keyword heuristic¹ | |
|---|---|---|
| field accuracy (26 classes) | 97.2% | 82.7% |
| field macro-F1 | 0.945 | 0.723 |
| form-purpose accuracy (11 classes) | 95.7% | 71.6% |
| password-field detection F1 | 0.996 | 0.968 |
| current vs new password (on password fields) | 99.2% | 85.5% |
login-identifier (username/email) F1 |
0.994 | 0.945 |
autocomplete removed from the input: recovers the developer's own token² |
97.8% | 78.7% |
¹ A baseline of the kind password managers ship: it trusts a valid autocomplete, then the input type, then multilingual keyword regexes over name/id/placeholder/label/nearby text.
² On 1,622 test fields where the site's autocomplete token agrees with the reference label, the token is deleted from the model input. The score is how often Zap still predicts it. This check doesn't depend on the LLM teacher.
Quick start (Python)
# pip install transformers torch lxml selectolax huggingface_hub
import sys
from huggingface_hub import snapshot_download
sys.path.insert(0, snapshot_download("ProCreations/Zap"))
from zap_infer import Zap
zap = Zap.from_pretrained("ProCreations/Zap") # CPU, fp32 compute from the bf16 weights
for form in zap.classify_html(html, url="https://northwind.example/login"):
print(form["form"], round(form["form_score"], 3))
for f in form["fields"]:
print(" ", f["type"], f["name"], "->", f["label"], round(f["score"], 3))
Output for a page that has a sign-in form and a separate "New here? Create account" block:
login 0.966
input text login -> username 0.96
input password pw -> current-password 0.962
signup 0.961
input text email -> email 0.957
input password pass 1 -> new-password 0.956
input password pass 2 -> new-password 0.955
Neither block sets a useful autocomplete attribute: the login box has autocomplete="off", and the signup inputs are typeless <input>s with placeholders. Zap tells them apart from context.
In a browser extension (JavaScript)
Zap doesn't read raw HTML. It reads a compact text rendering of each form: page title, URL words, form attributes, heading, buttons and links, and for every field its type, name, id, autocomplete, placeholder, label, nearby text and so on. zap-extract.js builds that rendering from the live DOM, so it sees exactly what the model was trained on. zap-infer.js handles windowing and decoding with transformers.js.
import * as transformers from "@huggingface/transformers";
// load zap-extract.js and zap-infer.js first (content scripts), or require() them in Node
const zap = await ZapModel.load(transformers, "ProCreations/Zap"); // onnx/model.onnx; pass { dtype: "q8" } for the int8 file
const results = await zap.classify(Zap.zapExtract(document, location.href));
for (const r of results) {
console.log(r.form, r.formScore);
for (const f of r.fields) console.log(f.element, f.label, f.score); // f.element is the <input>/<select>/<textarea>
}
Parity checks: these run on 400 random real pages.
- Extraction: the JS extractor (DOM via jsdom) and the Python extractor produce identical model input for all 821 forms the JS side finds. The Python side finds 4 extra forms on 2 malformed spam pages. The Python side re-parses pages with an HTML5-spec parser so its tree matches a browser's.
- End to end: JS (transformers.js + ONNX) and Python (PyTorch) predict identical labels for 818/818 forms and 1,413/1,413 fields.
Speed: measured on an Apple M4 Max CPU with onnxruntime via transformers.js, model only.
- 5.3 ms per form with fp32 ONNX, and 3.1 ms with int8 ONNX.
- The int8 file agrees with fp32 on 99.91% of predictions, and bf16 PyTorch on 99.99%.
How the output is decoded: each field is read at its [FLD] token, restricted to the field:* labels. The form purpose is read at [CLS], restricted to the form:* labels. A form longer than 512 tokens is split into windows that each repeat the form context, and the [CLS] logits are averaged across windows.
Labels
| field label | meaning |
|---|---|
username |
account identifier that is not strictly an email: username, user ID, "email or username", "email or phone" |
email |
an email address (login, signup, newsletter, checkout, …) |
current-password |
existing password / sign-in PIN (login, "current password") |
new-password |
password being created or set, including "confirm password" |
one-time-code |
2FA / OTP / SMS / email verification code, including each box of split code inputs |
tel |
phone number or part of one |
name, given-name, family-name, organization |
person / company names |
street-address, address-line2, address-level2 (city), address-level1 (state/region), postal-code, country |
address parts |
cc-name, cc-number, cc-exp, cc-exp-month, cc-exp-year, cc-csc |
payment card |
bday |
date of birth (or part of it) |
search, captcha |
search box, CAPTCHA / anti-spam answer |
other |
everything else: messages, coupons, quantities, honeypots… |
Label names follow the HTML autocomplete tokens where one exists. username vs email is decided by what the field accepts: an email-only login box is email. On a login form, fill the saved login identifier into either one.
Evaluation
Splits are made by website host: 4% test, 2% val. Every deduplicated form signature belongs to exactly one split. Headline numbers use only real web forms. Synthetic forms (see below) are scored separately.
Per class, real held-out test (bf16 weights; F1, with n in parentheses):
| field | F1 (n) | field | F1 (n) |
|---|---|---|---|
| 0.990 (3193) | postal-code | 0.970 (322) | |
| current-password | 0.991 (1838) | captcha | 0.953 (301) |
| new-password | 0.987 (712) | address-level2 | 0.897 (292) |
| username | 0.976 (1284) | organization | 0.947 (256) |
| tel | 0.988 (1337) | street-address | 0.958 (208) |
| name | 0.962 (1461) | address-level1 | 0.891 (175) |
| given-name | 0.943 (603) | country | 0.913 (158) |
| family-name | 0.982 (506) | one-time-code | 0.929 (135) |
| search | 0.963 (948) | bday | 0.932 (53) |
| other | 0.968 (5955) | address-line2 | 0.847 (48) |
| form | F1 (n) | form | F1 (n) |
|---|---|---|---|
| login | 0.992 (1836) | password-change | 0.943 (50) |
| contact | 0.963 (1640) | otp | 0.897 (40) |
| search | 0.954 (1043) | password-recovery | 0.933 (38) |
| newsletter | 0.965 (667) | address | 0.783 (35) |
| signup | 0.962 (417) | payment | 0.667 (8) |
| other | 0.893 (1064) |
Real payment-card forms are very rare in a static crawl: the real test has 8 payment forms and 6 card fields. On the synthetic held-out test (84 forms), the card fields cc-number, cc-name, cc-exp, cc-exp-month, cc-exp-year and cc-csc score F1 = 1.00, payment forms 0.98 and otp forms 1.00. Synthetic forms are easier than real ones, so treat these as a sanity check, not a real-world number.
Most common confusions: given-name↔name (a lone "Name" field sitting next to "Last name"), search↔other (site filters), username↔email ("Login" boxes that probably take an email), and address-level1/2↔other.
Label quality: a second, independent reviewer model re-labeled a stratified sample of 80 teacher-labeled test forms (217 fields) straight from the HTML. It found 1 clear field error out of 210 checkable fields (0.5%) and 1 clear form-purpose error in 80 (1.3%), plus 6 ambiguous cases of each kind. The one systematic teacher weakness it found is honeypot fields that carry realistic labels.
The full reports are in eval/, including per-class precision/recall and confusions for bf16 and fp32 weights and the heuristic baseline.
How it was made
Data. 62 random WARC files from Common Crawl CC-MAIN-2026-39 (September 2026) were streamed in full, one request per file. That's 998,946 pages with at least one fillable field, 2.30M forms, and 948,742 unique forms after deduplicating by form signature. Pages are re-serialized through an HTML5-spec parser (lexbor) before extraction so the tree matches what a browser builds. Fields outside any <form> are grouped the way a single-page app lays them out (nearest ancestor with a button).
Labels. A private, high-batch Qwen3.8-Flash-Next (NVFP4, SGLang, thinking off, greedy decoding) ran inside the training job. It labeled every field and the form purpose for 170,163 unique forms in three rounds:
- A priority-weighted pass: password forms first, then multi-field forms, then single-field ones.
- A targeted pass over not-yet-labeled real forms whose fields look like address, OTP or payment fields.
- The synthetic top-up described below.
The teacher saw the cleaned form HTML with numbered fields, the page title and the URL, plus a label spec with rules for the hard cases: current vs new password, email vs username, honeypots, and autocomplete values set only to block autofill.
- Calibration: on 222 forms where site developers set standard
autocompletetokens, the teacher (withautocompletehidden) agreed with the developer token on 84% of fields. Manual inspection showed almost every disagreement was the site being wrong, e.g. Gravity Forms honeypots taggedautocomplete="new-password", or "First Name" fields taggedname. - Synthetic top-up: static crawls rarely contain card-payment, OTP and password-change pages. The teacher therefore also wrote 2,578 synthetic forms in 23 languages and 12 markup styles, from React div-soup with no
<form>to split card fields, with deliberately wrong or missingautocomplete. They go through the same extractor and the same labeling pass, and make up about 1% of training forms.
Model. BertForTokenClassification: 8 layers, hidden size 384, 6 heads, FFN 1536, 16k WordPiece vocabulary trained on serialized forms, 512 positions. A single token-classification head predicts form:* at [CLS] and field:* at every [FLD] marker, so the whole form is classified in one pass and every field sees its neighbours. That is how Zap tells new-password from current-password, and a signup email from a login username.
Training. Done on one RTX PRO 6000 (Blackwell) with bf16 autocast:
- MLM pretraining on 892k unlabeled train-split forms (108M tokens, 3 epochs, about 8 minutes).
- Fine-tuning on 217,782 labeled rows, with rare-class forms oversampled ×3 and label smoothing 0.05. Hint dropout randomly removes
autocomplete(25%), labels, names and placeholders, and flips 8% of password inputs totype=textto mimic "show password" toggles. - The best of 6 epochs was picked on validation.
The published weights are bf16. The whole project took about 6 hours of workstation time: harvesting, three rounds of teacher labeling, synthesis, two training runs, evaluation and export.
Limitations
- Zap reads the DOM text and attributes, not pixels or CSS. In a browser, drop invisible inputs (honeypots) before classifying, e.g. with
offsetParent === nullorgetComputedStyle. The training crawl only saw static HTML, where some CSS-hidden honeypots remained, so Zap usually labels thoseother, but not always. - The training pages come from a static crawl, so heavily scripted flows (identifier-first sign-in, checkout iframes) are covered mainly by synthetic examples. Real-world accuracy on payment forms is not well measured (8 test forms).
- The labels come from an LLM teacher. It is audited, but it isn't human annotation, so expect a small amount of label noise, especially on ambiguous
name/given-nameandsearch/othercases. - Fields inside cross-origin iframes (e.g. hosted card fields) are invisible to any content script, including
zap-extract.js.
Files
| file | what |
|---|---|
model.safetensors |
bf16 weights (BertForTokenClassification, 20.7M params) |
config.json, tokenizer.json, tokenizer_config.json |
config with the 37-label map; 16k WordPiece tokenizer |
onnx/model.onnx, onnx/model_quantized.onnx |
fp32 and int8 ONNX for onnxruntime / transformers.js |
zap_extract.py / zap-extract.js |
form extractor (Python canonical / DOM port) |
zap_infer.py / zap-infer.js |
windowing + inference helpers |
eval/ |
evaluation reports, export parity, dataset statistics |
training/ |
the code used to harvest, label, synthesize, train and export |
License
MIT
- Downloads last month
- -