Engineering

Engineering work

Data pipelines, document AI and a production website that I designed, built and ran myself. Each case study includes what broke and what I changed because of it.

Case studies

Systems where the hard part was everything around the happy path

DOCUMENT AI · OCRSole engineer · Deacons Publishers (Prep50)

Printed page citations from image-only scans

Recovering the printed page number for 83,807 textbook sections after OCR had discarded page positions and the PDFs turned out to have no text layer.

Private repo
99.3%
sections with a page citation
93 of 102
books citing printed page numbers
24.9 h
unattended run, 0 failed books
~$32
total cost, on AWS credit

Generated study notes needed to cite a real page, like “Comprehensive Biology, p. 142”. The data for that did not exist: the OCR worker had thrown away page indexes, and the scans were pure image containers with nothing to extract. The approach was to OCR a strip at the bottom of each page, use its last line of text to find where the page ends in the book’s Markdown, and read the printed page number from the same strip. Books whose numbers could not be read fall back to an honest scan position instead of a guessed page.

WHAT BROKE, AND WHAT I DID ABOUT IT

The first crop contained no text at all

I set the strip to 12% of page height without measuring, and 0 of 56 test pages anchored. An ink profile showed the bottom 15% of these pages is blank margin; the only mark in frame was a pen scribble on the scanner bed. Measuring first and moving the strip to 28% took anchoring to 80%.

Repeated headings outvoted the right answer

A practical physics book has 89 sections called “Method”. Two stray text matches far down the book were beating a correct cluster of 70 matches. Scoring clusters by support before position cut dropped anchors from 51 to 2, and a longest increasing subsequence stopped one bad anchor from blocking every page after it.

Zero errors was hiding a 15x slowdown

Throughput sat at 0.20 pages/s with no errors, because my retries were quietly turning throttling into waiting. Measuring showed Bedrock limits are per account per region, so a second instance would have made both slower. A cross-region inference profile reached 1.33 pages/s and cut mean latency from 22.3s to 4.7s.

The trust test used the wrong measure

I gated printed numbers on the share of readings agreeing on one offset. It failed on science books, where a page’s last token is often a quantity: a readable chemistry book scored 0.466. Adding a dominance test (24:1 for that book, 1.5:1 for a genuinely unreadable one) fixed it. OCR and matching were separate stages, so the fix took half an hour instead of another 25-hour run.

STACK

  • Python
  • AWS Bedrock
  • EC2
  • S3
  • pypdf
  • PostgreSQL 17
  • Concurrency & backoff
APPLIED AI · VECTOR SEARCHSole engineer · Deacons Publishers (Prep50)

Prep50 Coverage

Semantic duplicate detection and exam forecasting over a ~40,000-question WAEC archive.

~40K
questions indexed
~2 min
verdict per paper
54-82%
of a paper with precedent
4
verdict tiers

Exam boards recycle questions more often than published policy suggests, but nobody could measure it: “is this a repeat?” was a human judgement made one question at a time. Drop in a new paper and this returns a per-question verdict in about two minutes: word-for-word template repeat, same concept reworded, or genuinely new.

WHAT BROKE, AND WHAT I DID ABOUT IT

Calibration killed the obvious design

The plan was cosine similarity above a threshold. Measuring first showed the 5th percentile of true repeats (0.674) sat below the 95th percentile of unrelated questions (0.847), so no cutoff separates them. Cosine was demoted from a decision to a retrieval mechanism, and the hard verdict now needs a deterministic fingerprint match.

A question is not similar to itself

Re-embedding a question as a query and comparing it to its own stored vector gave a median cosine of 0.862 and never reached 1.0, a task-type asymmetry between RETRIEVAL_DOCUMENT and RETRIEVAL_QUERY. That single measurement invalidates the “just use cosine > 0.9” intuition, and moved the shipped thresholds from 0.92/0.80 to 0.80/0.75.

An LLM reranker that can never block a batch

Embeddings are lexically biased, so the top 20 candidates go to gpt-4o-mini with their answer options attached, because the options carry the meaning cosine cannot see. Any failure falls back to cosine ordering and keeps going, every score ships with a one-sentence reason for the reviewer, and it costs about a cent per batch.

PII already committed to git history

Auditing before open-sourcing turned up CSV exports of live user tables, with emails, phone numbers and addresses, committed months earlier. Fixed with a git filter-repo rewrite, verified by walking every object in the rewritten history rather than trusting the file listing. A private repo is not a security control.

STACK

  • Python
  • FastAPI
  • PostgreSQL 17
  • pgvector
  • Vertex AI
  • OpenAI
  • Next.js 16
  • React 19
  • TypeScript
  • Docker
  • Nginx
DATA ENGINEERING · DOCUMENT AISole engineer · Deacons Publishers (Prep50) · 2025 to present

From exam paper to vector index

A six-stage pipeline turning two decades of WAEC and JAMB papers, from printed booklets to screenshots to inconsistent Word documents, into a classified, queryable corpus.

Private repo
55,106
questions in production
11,000+
extracted and verified
3,730
learning objectives
12
subjects covered

An exam-prep product is only as good as its question bank, and this one existed as paper: scanned booklets, tutor videos recorded off a projector, and Word documents typed by a dozen people across fifteen years. The interesting engineering is not the AI. It is everything built around the AI so that when it is wrong, and it is regularly wrong, nothing downstream silently breaks.

WHAT BROKE, AND WHAT I DID ABOUT IT

A one-character typo hid 793 questions

The uploader wrote to a prep50/ prefix; every downstream filter tested for pre50/. Nothing errored, no test failed, every run reported success, and 793 verified questions were quietly skipped for months. A filter clause that matches zero rows is now treated as a failure, not a valid result.

Two databases cannot share a transaction

Migration writes to production Postgres, then records the ID back in MySQL. Instead of pretending that gap does not exist, the Postgres insert and its objective link share one transaction, and a failure on the second write journals the new ID to a recovery file with enough context to reconcile by hand. Loud, bounded and repairable beats silent.

Knowing when not to call the model

Mathematics documents already carried human-authored subtopics, so sending them to an LLM would have been slower, costlier and less accurate than the people who tagged them. The classifier takes a direct path when an objective already exists: 425 of 445 questions on the last run matched production taxonomy exactly, at zero API cost.

Every destructive operation is a plan and an apply

Nothing mutates data in one step. The taxonomy cleanup planner is strictly read-only. It normalizes, groups duplicates, picks a canonical row by actual usage and writes a reviewable plan, flagging the risky cases. Only then does the applier run. It retired 202 objective IDs, 3 of which needed human judgement.

STACK

  • Python
  • GPT-4o vision
  • Gemini 2.5 Pro
  • EasyOCR
  • OpenCV
  • PyMuPDF
  • PostgreSQL 17
  • pgvector
  • MySQL 8
  • FastAPI
  • Docker
ML INFRASTRUCTURE · AWSSole engineer · Deacons Publishers (Prep50)

Textbook digitization pipeline

An event-driven GPU pipeline turning 102 scanned curriculum textbooks into clean, structured Markdown, unattended, recovering from the failure modes of open-source ML inference at scale.

Private repo
102
textbooks digitized
31,887
pages processed
A10G
GPU worker, 24GB VRAM
0
recurrences after the fix

Scanned exam-prep textbooks, dense with tables, multi-column layouts and diagrams, that off-the-shelf OCR mangled badly enough to be unusable. A PDF leaves a field laptop, lands in S3, fires an SQS job, and a GPU worker returns structured Markdown with images preserved. No polling, no manual triggering, least-privilege IAM scoped per component.

WHAT BROKE, AND WHAT I DID ABOUT IT

Poison pages sent the model into runaway generation

Dense answer-key pages hung the VLM indefinitely. A live py-spy stack trace pinned it inside HuggingFace's _calc_banned_ngram_tokens, whose cost scales with what has already been generated. The fix kills the whole process group on a page-count-scaled timeout, because MinerU spawns its own child server and a naive kill would orphan it still holding GPU memory.

A leak that only appeared after days of uptime

A wave of unexplained crashes with nothing in dmesg or the kernel journal. The real signal was buried in the model server's own subprocess output: CUDA out of memory from VRAM fragmentation accumulating across hundreds of sequential invocations. One allocator setting fixed it, verified across a full backlog re-run.

Rejecting the clever fix on purpose

Recursively bisecting a failing chunk to isolate the exact bad page worked, and cost 45 to 75 minutes per book, because every bisection level pays its own full timeout. Against a 90-book backlog that is GPU time stolen from 89 other books. Fail-fast to a dead-letter queue won, and the rejected approach is documented so it does not get silently re-proposed.

Graceful degradation instead of total loss

A chunk timeout used to cost the entire remaining book. Now it drops to one page at a time under a smaller timeout and skips only the pages that are genuinely unprocessable, marking each one as a red callout in the viewer so QA goes straight to it. Books that would have been a total loss now land complete with a handful of flagged pages. One hit this three times and still produced 372 files.

STACK

  • Python
  • Bash
  • AWS EC2
  • S3
  • SQS + DLQ
  • IAM
  • CloudWatch
  • systemd
  • MinerU
  • PyTorch / CUDA
  • Flask
  • py-spy
WEBCORPORATE WEBSITE

B-Lord Group

blordgroup.ng

I built and deployed the official B-Lord Group website as sole developer. The group works in fintech and real estate, and most of its visitors arrive on phones over slow mobile connections, so the site was built mobile first and kept light.

Structure

Mapped the group's divisions into a page hierarchy a first-time visitor can follow in one pass.

Front end

Responsive Next.js pages designed for small Android phones first, then scaled up to desktop, with page weight kept low for slow mobile networks.

Deployment

Deployed on an Ubuntu server with Nginx in front of the Next.js app.

Security check before launch

Reviewed the finished site for exposed endpoints and unauthenticated data paths, the same class of flaw I look for in other people's systems.

Visit blordgroup.ng →
CLIENT
B-Lord Group
DOMAIN
blordgroup.ng
ROLE
Sole developer
STACK
Next.js · Nginx · Ubuntu
TYPE
Corporate website
The blordgroup.ng home page on a desktop browser: hero headline, product button and three headline statistics.
DESKTOP · 1440 PX
Toolkit

Tools I use in production

DATA & AI

  • Python
  • PostgreSQL
  • pgvector
  • MySQL
  • Embeddings & vector search
  • LLM pipelines (OpenAI, Gemini, Bedrock)
  • OCR (MinerU, EasyOCR, OpenCV)

CLOUD & INFRASTRUCTURE

  • AWS EC2
  • S3
  • SQS
  • IAM
  • CloudWatch
  • Docker
  • Nginx
  • systemd
  • Linux

WEB

  • JavaScript
  • TypeScript
  • React
  • Next.js
  • FastAPI
  • Node.js
  • PHP
  • Tailwind CSS

SECURITY

  • Burp Suite
  • Nmap
  • Access control testing
  • OWASP Top 10
  • Threat modelling
Get in touch

Hiring, or working on a hard data problem?

I am open to engineering roles and contract work in document AI, data pipelines and web. Vulnerability reports go to the security address.