Upload / OCR
Pick who reads the image. Everything else is optional; leave a field empty to use the server default.
PC Claude uses Jay's Claude subscription. Each image starts a fresh Claude Code session on his PC: about 10 s, and Claude Code reports a notional USD 0.03 per image (0.21 on one test). It is OFF unless enabled in Settings, only works while the PC is on, and the image goes to Anthropic. If it is unavailable, RapidOCR answers instead.
Test
Runs synthetic images through the real pipeline with RapidOCR and (optionally) PC Claude, then compares them. Needs an admin key.
Settings
Server-wide defaults. A value you set on the Upload page for one request always wins. Needs an admin key.
Before you turn PC Claude ON: every image starts a fresh claude -p session on Jay's PC (about 7,400 tokens of fixed overhead, about 10 s). In our tests Claude Code reported a notional cost of about USD 0.03 per image (USD 0.21 on the very first call, not reproduced). It runs on Jay's Claude subscription, so how much allowance it really uses is not verified, heavy use can hit plan limits, and it only works while the PC is on. The image goes to Anthropic. In our synthetic tests it was clearly more accurate only on handwriting-style and heavily damaged images.
API Docs
Base URL: · bearer-key auth.
POST /v1/ocr
Multipart form. Header Authorization: Bearer <key>. Max image 10 MB. Every field except file is optional.
| Field | Meaning |
|---|---|
file | image (png, jpg, webp, …) |
mode | no-ai (RapidOCR, default) · pc-direct (PC Claude reads the image itself) · pc-verify (RapidOCR first, PC Claude checks it). PC Claude is off by default (server setting pc_enabled); it uses Jay's Claude subscription, about 10 s and a notional USD 0.03 per image, and the image goes to Anthropic. When PC Claude is off, offline or fails, RapidOCR answers and fallback says why. The older ai-direct / ai-verify (OpenRouter) modes still exist on the server but are not offered here. |
engine | rapidocr (the only engine). tesseract was removed; a request that still sends it is served by RapidOCR and the response carries engine_note |
lang, psm | ignored (they only applied to Tesseract) |
hints | JSON string {"expect":"digits|amount|date|alnum|text","pattern":"AAA9999A","decimals":2}. pattern: A letter, 9 digit, other characters literal |
wordlist | words separated by newlines or commas. Unmatched words are snapped to the closest entry (see wordlist_cutoff); PC Claude is told to prefer these spellings |
wordlist_cutoff | 0.5–1, similarity needed to snap a word (default 0.8) |
context | free text describing the document, given to PC Claude |
preprocess | JSON string: {"grayscale":true,"autocontrast":true,"sharpen":true,"upscale":2,"threshold":128,"pad":15}. pad adds a white border; RapidOCR uses 40 by default (best result in our synthetic option sweep). Upscaling made results worse in our tests, so it is never applied by default. |
Response (200)
{
"text": "RM 12,345.67",
"confidence": 0.97, // 0..1 (RapidOCR), or null when PC Claude answered
"engine": "rapidocr", // or "pc-claude:<model>" when PC Claude answered
"mode": "no-ai",
"spans": [],
"fallback": null, // "pc-disabled" | "pc-offline" | "pc-failed:…" (RapidOCR answered instead)
"via": null, // "pc" when PC Claude answered
"pc_note": null, // why PC Claude was not used
"engine_note": null, // set when a removed engine name was mapped to RapidOCR
"cost_usd": 0.0,
"wordlist_changes": [], // [{"from","to","score"}] words changed by the word list
"ms": 412
}
Errors: 400 bad image/params · 401 missing or invalid key · 403 admin key needed · 413 too large.
POST /v1/claude/ask – general LLM through PC Claude
For other apps that want an LLM answer without an API key. Needs a key with the claude role (ask Jay). JSON body: prompt (required, ≤ 60,000 chars), system, image_b64 (PNG, optional), model, timeout (5–180 s). Answer: {"text","model","seconds","via":"pc-claude","cost_usd":0}.
When PC Claude cannot answer you get 503 with detail.code = pc-disabled (switched off in Settings) or pc-offline (Jay's PC is off), or 502 pc-failed. Callers must have their own fallback; these requests run one at a time on Jay's PC and use his Claude subscription.
Recipes – read a known layout with zero AI
Upload anything (xlsx, xls, csv, docx, PDF with a text layer, or an image) and get your fields back. A layout seen before is read by code (no LLM tokens); a new layout is read by your own LLM once, and the service turns that answer into a reusable recipe. You choose the field keys; the service never interprets them.
Profile = your own name for one kind of document (a-z 0-9 . _ -, e.g. oms-bills). Recipes are scoped to your key + profile + layout: another key can never read, hit or change them (a foreign id answers 404). Any client key may use these endpoints.
POST /v1/extract
Multipart: file (≤ 10 MB), profile, optional fields (JSON array or comma list of the keys you want), optional learn = auto (default) | off.
curl -s https://recognize.wenjyue.com/v1/extract -H "Authorization: Bearer $KEY" \
-F file=@bill.xlsx -F profile=oms-bills -F 'fields=["invoice_no","total"]'
// recipe hit: no AI was used
{ "recipe": {"state":"hit","id":7,"fp":"…","near":false}, "ai_used": false, "complete": true, "missing": [],
"values": {"invoice_no": {"value":"PRT-30177","source":"recipe","confidence":0.95,"span":"PRT-30177","check":false}},
"items": [{"desc":"Ceramic Planter","qty":300}], "items_check": false }
// recipe miss: use your own LLM on "raw", then teach the service (below)
{ "recipe": {"state":"miss","fp":"…","reason":"no_recipe"}, "ai_used": false, "values": {}, "missing": ["invoice_no","total"],
"raw": "A1=Invoice No: B1=…", "learn_job": null, "learner": "not_permitted" }
check:true = low confidence (weak date, low OCR score): show it to a person. complete:false = some wanted keys were not found: treat as a miss for those keys. Scanned PDFs (no text layer) answer 422 needs_ocr: send the page as an image.
With a claude-role key and learn=auto, a miss also queues a learner job (deduped per key + profile + layout) that asks PC Claude for the values; poll GET /v1/recipes/jobs/{id}. learner says learner_off (PC Claude is OFF by default: the job stays pending), pc_offline, ready, not_permitted (your role may not use PC Claude), off_by_request or queue_full.
POST /v1/recipes/learn – the cheapest path: teach it what you already extracted
Multipart: file, profile, answer (JSON), optional items (JSON array), doc_type, origin = ai (default) | manual (human-checked: never replaced by an AI re-learn). answer is {"fields":{key: value | {"value":v,"cell":"B7"}}, "items":[{key: value}]} or just the flat {key: value} map. cell is only a hint (B7 on sheets, p1:l12 on PDF/image lines).
curl -s https://recognize.wenjyue.com/v1/recipes/learn -H "Authorization: Bearer $KEY" \
-F file=@bill.xlsx -F profile=oms-bills \
-F 'answer={"fields":{"invoice_no":"PRT-30177","total":1250.5}}' \
-F 'items=[{"desc":"Ceramic Planter","qty":300}]'
// { "outcome":"active", "score":1.0, "located":11, "unresolved":[], "diffs":[], "recipe_id":7, "fp":"…", "ai_used":false }
The service finds where each value sits, keeps a locator only after replaying it reproduced your value, and activates the recipe only if ≥ 90% of your fields replay (outcome = active; otherwise draft, which is never used for reading). Invented values stay a draft. locked = an active manual recipe already exists.
POST /v1/recipes/answer – have the service read ONE whole document for you (stateless)
For apps that keep their own recipes (they only need the values of one new layout). Send the whole file and the field keys you want; PC Claude reads it and the service answers with the values it found. Any valid API key may call it, including an ordinary client key: you do not need Jay's claude or admin key, the service pays the PC Claude cost on your behalf. Nothing is stored: no recipe is made, no value is kept. Your app derives and keeps its own recipe from the answer.
Multipart: file (xlsx, xls, csv, docx, text-layer PDF; an image or scan is read with RapidOCR first), profile, fields (JSON list [{"key","desc"}] of header fields), optional item_fields (same shape, the line-item columns), optional doc_hint (short text, e.g. "commercial invoice").
curl -s https://recognize.wenjyue.com/v1/recipes/answer -H "Authorization: Bearer $KEY" \
-F file=@invoice.xlsx -F profile=my-invoices \
-F 'fields=[{"key":"invoice_no","desc":"invoice number"},{"key":"total","desc":""}]' \
-F 'item_fields=[{"key":"desc","desc":""},{"key":"qty","desc":""}]'
// 202 { "job_id": "3f9c1a7e20bd", "deduped": false, "learner": "ok" }
curl -s https://recognize.wenjyue.com/v1/recipes/answer/3f9c1a7e20bd -H "Authorization: Bearer $KEY"
// { "job_id":"3f9c1a7e20bd", "state":"done", "learner":"ok", "seconds": 14.2, "error":"",
// "answer": { "doc_type":"commercial invoice",
// "fields": { "invoice_no": {"value":"PRT-30177","label":"Invoice No","text":"PRT-30177"} },
// "items": [ { "desc": {"value":"Ceramic Planter","text":"Ceramic Planter"}, "qty": {"value":"300","text":"300 pcs"} } ] } }
state = pending | running | done | failed; learner = ok | pc_off (PC Claude is switched off in Settings: the job waits) | pc_offline (Jay's PC is off). Poll every few seconds; if pc_off/pc_offline lasts more than a few minutes, use your own fallback. The answer is coordinate-free on purpose (callers number cells differently): locate the value in your own reading of the same file using value + label (the printed label next to it, blank if the service could not confirm it) + text (the exact cell or line that contains it). Only the key that submitted a job can read it; anyone else gets 404.
Checked by the server: every value must literally occur in the text the service extracted from your file (case, width, whitespace and number format such as 1,200.50 = 1200.50 are normalised); a value that does not is dropped. The job failed (error: no_valid_fields) if no field survives. Text inside the document is treated as data, never as instructions.
Quota: each key may start 100 jobs per UTC day (server setting answer_daily_cap); beyond that 429 {"error":"daily_cap"}. Sending the same file again (same SHA-256, same key) while its job is still held returns the same job_id with "deduped": true and costs nothing. At most 20 jobs are open at once (429 {"error":"queue_full"}). Other errors: 400 bad params · 413 over 10 MB · 415 unsupported type · 422 needs_ocr (scanned PDF: send the pages as images) / empty.
Privacy: the uploaded file is read in memory only (no temp file). The extracted text goes to Claude on Jay's PC (Anthropic) and nowhere else. The text, the answer and the job exist in memory only until you collect the answer (the first GET that shows done/failed removes it, so collect once and keep your copy) or 10 minutes, whichever comes first. Nothing about the document (values, text, filename) is written to disk, a database or a log; the log line carries only job id, profile, counts, seconds and outcome. Do not upload documents you are not allowed to send to Claude.
Fallback pattern for callers
1. POST /v1/extract -> hit and complete? use values. Done, zero tokens. 2. miss -> run YOUR OWN LLM on the file (or on "raw") 3. POST /v1/recipes/learn -> send the same file + the values your LLM produced 4. next file with that layout -> step 1 hits.
Manage
GET /v1/recipes?profile=&state=– your recipes withhits,misses,state;GET /v1/recipes/{id}– one recipe with its locators (no document values).PATCH /v1/recipes/{id}body{"state":"active"|"off"}– switch a recipe on or off (offretires it; rows are never deleted). A row that is only a sighting of an unknown layout has no recipe:409.GET /v1/recipes/stats?profile=–coverage,ai_calls_avoided,top_unmatchedlayouts (the ones worth teaching next).tokens_savedis an ESTIMATE (3,000 tokens per avoided LLM call, not measured).
Privacy: the service stores only the layout fingerprint, label texts, recipe locators, counters and the file's SHA-256, never the uploaded file or its values. Learner jobs hold the sample's cell text in memory until the recipe is made. PC Claude learning sends the compact cell dump of the sample (cell text with coordinates) to Claude on Jay's PC (Anthropic), not to any other third party; /v1/recipes/learn sends nothing off this server. Errors: 400 bad params · 413 over 10 MB · 415 unsupported type · 422 needs_ocr / no_fingerprint (too few label cells to recognise the layout again).
Other endpoints
GET|PUT /v1/settings– server defaults (admin key).POST /v1/test/start,GET /v1/test/{id}– the test matrix (admin key).GET /health– no auth: engines, PC Claude state (pc.online,pc_enabled).
Example
Privacy and limits
no-ai(RapidOCR) keeps the image on this server. Images are not stored; they are discarded after the response.pc-*modes and/v1/claude/asksend the image or prompt (and your word list and context) off this server to Anthropic, through Claude on Jay's PC. They need the PC to be on;GET /healthshowspc.online.- Whisper is not available: it transcribes audio, not images.
- Accuracy depends on the image. The Test page measures it; the figures in the tooltips come from stored synthetic benchmarks, not real photos.
Status
loading…