Skip to main content
valen

Check scanned forms against the record

An equipment desk scans every handwritten loan form and types the details into its system. Before a loan is closed, each scan is checked against the typed record: does the paper say the same name, serial number and return date, and is it signed?

CleanWrong return dateUnsigned
Loan form for Lin ChenLoan form for Omar HaddadLoan form for Sara Novak

How it works​

Code turns each field of the record into a statement a person could check at a glance, written the way it would appear on paper ("The return date handwritten on the form is 12 March 2026"), and adds one question about the signature box. All four predicates go in one request with the scan.

Field checksStatus
any below 0.3reject, listing the failed fields
any 0.3 – 0.7review
all 0.7 or aboveaccept

Run it​

Download the bundle once. The recorded answers in caches/document_check.json let the script run without an API key; --replay off calls the free hosted API instead.

Bash
curl -fLO https://valen-team.github.io/cookbook/valen-cookbook.zip
python -m zipfile -e valen-cookbook.zip .
cd valen-cookbook && python -m pip install -r requirements.txt
python document_check.py
python document_check.py --replay off --base-url https://api.valen-team.workers.dev/v1

Code​

document_check.py
"""Check a scanned loan form against the record typed into the system, field by field. Apache-2.0."""
from datetime import date
import json

from common import HERE, client, emit, image_bytes, image_input, parser, request

FIELDS = {"name": "borrower name", "serial": "serial number", "return_date": "return date"}


def written(field, value):
# The record is stored in machine form; ask about it the way a person would read it on paper.
if field != "return_date":
return value
day = date.fromisoformat(value)
return f"{day.day} {day:%B %Y}"


def run(predict, record):
questions = [{"name": field, "type": "predicate",
"instructions": f"The {label} handwritten on the form is {written(field, record[field])}."}
for field, label in FIELDS.items()]
questions.append({"name": "signed", "type": "predicate",
"instructions": "The signature box contains a handwritten signature."})
answers = request(predict, image_input(image_bytes(HERE / "media" / record["scan"])), questions)
checks = {name: answers[name]["probability"] for name in [*FIELDS, "signed"]}
failed = [name for name, p in checks.items() if p < 0.3]
unclear = [name for name, p in checks.items() if 0.3 <= p < 0.7]
status = "reject" if failed else "review" if unclear else "accept"
return {"scan": record["scan"], "status": status, "failed": failed, "unclear": unclear, "checks": checks}


def main():
args = parser(__doc__).parse_args()
records = json.loads((HERE / "document_check_data.json").read_text())
with client(args) as predict:
emit([run(predict, record) for record in records], args)


if __name__ == "__main__":
main()
document_check_data.json
[
{"scan": "loan-a.jpg", "name": "Lin Chen", "serial": "CAM-20417", "return_date": "2026-03-12"},
{"scan": "loan-b.jpg", "name": "Omar Haddad", "serial": "CAM-20562", "return_date": "2026-03-12"},
{"scan": "loan-c.jpg", "name": "Sara Novak", "serial": "LAP-11873", "return_date": "2026-03-19"}
]

Output​

Recorded output
[
{
"scan": "loan-a.jpg",
"status": "accept",
"failed": [],
"unclear": [],
"checks": {
"name": 0.9992125595012277,
"serial": 0.9983302126024323,
"return_date": 0.9976673179646833,
"signed": 0.9990492985319737
}
},
{
"scan": "loan-b.jpg",
"status": "reject",
"failed": [
"return_date"
],
"unclear": [],
"checks": {
"name": 0.9986719510816103,
"serial": 0.9984982673413811,
"return_date": 0.007865409520616382,
"signed": 0.997229301770541
}
},
{
"scan": "loan-c.jpg",
"status": "reject",
"failed": [
"signed"
],
"unclear": [],
"checks": {
"name": 0.9989126426472942,
"serial": 0.9990923741991619,
"return_date": 0.997633862429052,
"signed": 0.006129618756955932
}
}
]

Make it yours​

  • Ask about the record in the form's own wording: dates as they are written, names as they are signed.
  • For long documents, crop each field region in code and send the crops as separate requests.
  • The forms here are generated for this recipe with a handwriting font.