RPOMS AI
RPOMS AI: router cosmetic inspection where the AI observes and rules decide
Architecture, Vision Pipeline, Rule Engine, Evaluation, Self-hosted Inference & Deployment
Operational Workflow & State Machine
End-to-End Pipeline- 01Photograph one face of the router
- 02Quality gate
- 03Vision model observes
- 04ERTH rules decide
- 05Qualification gate
- 06Acceptable, Not acceptable or Unclear
Overview
RPOMS AI checks the cosmetic condition of a refurbished router from a photo. Someone photographs one face of the casing, and the system answers ACCEPTABLE, NOT ACCEPTABLE or UNCLEAR, with a short explanation, against the programme's written acceptance criteria. An admin area, not linked from the public page, covers inspections and reviews, the reference set, model configuration and lifecycle, settings and an audit log.
It belongs to the RPOMS ecosystem but shares nothing with RPOMS: its own repository, its own database and its own deployment. I built it alone, from the design of the decision pipeline to the evaluation and the inference setup.
Problem
The programme's cosmetic acceptance criteria exist only as thirteen annotated example photos in a spreadsheet, each with a note and a yes or no. The notes use words like long, multiple, minimal and small, with no numbers behind them.
Handing that to a vision model and asking whether a router passes would produce confident verdicts with nothing traceable behind them. The goal was a decision that can be explained, reproduced and audited, and that says Unclear rather than guessing.
How a decision is made
One pipeline function is shared by the public check and the evaluation runs. First a quality gate, with no model involved, rejects a photo that is too small, blurred, dark, over-exposed, full of glare, or where no router or no plain surface is visible. Then the vision model reports observations only: the kind of mark, how many, how large and how visible. Its output contract has no verdict field.
A deterministic rule engine applies ERTH's categories to those observations, and every decision records the rule version it was made under. Finally a qualification gate compares the verdict with what the active model has proved on the reference set: a model may accept only if it made no false accepts and at least two correct accepts there. Anything it has not earned becomes UNCLEAR, which the public sees as a request to check manually.
The thirteen references are pinned by hash and protected by database triggers that refuse updates and deletes. Because their original images carry drawn annotations, annotation-free copies are derived for evaluation, also hash-pinned.
Self-hosted inference
The application runs on Vercel; the model does not. Inference runs on local hardware through Ollama, behind a small gateway that checks a token, reached through a Cloudflare tunnel. The model is Qwen2.5-VL 7B, open-weight and Apache-2.0 licensed, so the inspection itself costs nothing.
The cost that replaced money was time. On the CPU a new photo took 119 seconds. Enabling the integrated GPU through Vulkan brought the median to 41 seconds (38 to 82), and capping the image the model sees at 896 pixels, the scale it was evaluated at, kept a full-size phone photo inside the 60-second function limit.
Evaluation
On the thirteen references with annotations removed, the model's raw observations run through the rules give 3 correct, 6 false accepts, 1 false reject and 3 unclear. That is not good enough to accept a unit, and the system says so rather than hiding it.
Under the qualification gate, the latest run (22 September 2026) gives 2 correct rejections, 0 wrong verdicts and 11 Unclear. Removing the gate would have turned six damaged routers into passes to gain one correct acceptance. Four degraded control photos (glare, blur, a very dark frame and a blank frame) were all stopped by the quality gate before reaching the model.
Thirteen references are a floor, not statistical evidence, so no accuracy figure is claimed. The reference photos are client material and are not shown here.
Security and data
The public check needs no account. It accepts JPEG or PNG only, checked by magic bytes, up to 4 MB, from the same origin, with burst and hourly rate limits keyed by an HMAC of the address. The browser downscales and re-encodes each photo before upload, which strips EXIF and location data.
Photos are stored in PostgreSQL and served only through an admin-guarded route. Admins sign in with a scrypt-hashed password or Google on an allow-list; sessions are HMAC-signed, HttpOnly cookies that expire after twelve hours, the role is re-read on every request, and an unauthorised request gets a 404. Row Level Security is enabled on every table.
Testing
247 automated tests with Vitest across 16 files, using PGlite, PostgreSQL compiled to WebAssembly, so the schema, triggers and queries are tested without a database server. Two further tests call a live model and skip themselves when no model endpoint is configured.
What's live, and what isn't
“It exists in the repository” and “it runs in production” are different claims. This is the difference, stated rather than left to be assumed.
- ImplementedPublic checker and admin areaDeployed on Vercel with PostgreSQL on Supabase.
- ImplementedQuality gate, rule engine, qualification gateIn the deployed code and covered by the test suite.
- Built, not liveSelf-hosted inference through the gatewayVerified end to end on local hardware on 20 and 22 September 2026. Available only while the inference machine and tunnel are running.
- Not connectedActive model in productionNo model is activated in the production database yet, so a production check currently answers that the AI check is unavailable.
- Not connectedUse on the refurbishment lineNot used on real line photos yet; no real inspection photo with a verified verdict has been evaluated.
- Not connectedTraining and a permanent inference hostDataset versioning and model lifecycle are scaffolded; training is deliberately not wired, and the move to a dedicated server has not started.
Technical stack
Application
Data
Vision & decision
Inference hosting
Security & quality
Limitations
- The model is weak at this task: on the references it never reported a dent or tape residue.
- Inference is available only while the local machine and tunnel are up, and handles one analysis at a time.
- Thirteen low-resolution references are a floor, not statistical evidence; no accuracy figure is claimed.
- The model sees photos at 896 pixels on the long edge, which can hide small marks.
- The repository is private. Access can be granted on request.
What's next
- 01Activate a model in production and move inference to a dedicated, always-on server.
- 02Collect real workshop photos with verdicts from an ERTH-trained reviewer, and re-evaluate on them.
- 03Train or fine-tune only once there are enough verified examples per verdict.
Source and further reading
RPOMS AI is proprietary work for a client programme. The ERTH reference photos it is evaluated on are client material and do not appear on this page.
Finished the technical breakdown?
Return to the concise HR overview or discover other projects in the portfolio.
Writing from this work
Production Broke and Nothing Had Changed: A Paused Free-Tier Database
Admin sign-in returned 503 and nothing was being saved, with no deploy and no config change. One credential-free request that tells a database outage from wrong credentials.
What Moving Self-Hosted Inference to a Server Will and Won't Fix
A migration plan, not a result: why moving RPOMS AI's model from a laptop to a dedicated server would improve availability but not accuracy, and the gates it must pass first.
Vercel or a Self-Hosted Ubuntu Box? How I Chose Hosting App by App
Eight deployed apps, three hosting shapes: Vercel for pages and serverless APIs, a self-hosted Ubuntu box for a stateful API, and a hybrid for a model Vercel can't run.
Two Ways I Use Supabase Row Level Security: Deny-All Behind a Server, or Per-Owner Policies
Across five projects, RLS does one of two jobs: close the Data API while a trusted server does the work, or let Postgres enforce who owns each row. When to pick which.
Testing Without Paid APIs or Hardware, and the Places Fakes Lied
Fake LLM providers, a simulated label printer and an in-process Postgres keep my test suites free and offline. What each proved, and three places a fake fell short.
Storing Uploaded Photos in Postgres bytea, Served Only to Admins
Why RPOMS AI keeps inspection photos in a PostgreSQL bytea column instead of a bucket, how they are served only through an admin-guarded route, and when this stops being a good idea.
Shadow-Testing a Replacement Model With Next.js after(), Without the User Waiting
How RPOMS AI is built to run a candidate model on real requests after the response is sent, record whether it agreed, and never show it. The lifecycle around it, and its limits.
Baseline Security Headers in Next.js, and Why the CSP Waited
Adding five response headers to a Next.js site that served none, why the Content-Security-Policy was deliberately deferred, and a nonce-free CSP on another app.
Running Qwen2.5-VL 7B on an Intel Iris Xe With Ollama and Vulkan: 119 s to 41 s
Setting up a 7B vision model on a Windows laptop with no discrete GPU: enabling the Iris Xe under Ollama, where the time actually goes, and what the GPU profile showed.
Rate Limiting a Public Endpoint on Serverless, Keyed by an HMAC of the Address
Two layers for an anonymous upload endpoint on Vercel: an honest in-memory burst limiter, and a durable hourly count in Postgres keyed by an HMAC so the address is never stored.
A Qualification Gate: Computing What a Model Is Allowed to Decide From Its Own Results
Instead of trusting a model or not, RPOMS AI computes per verdict and per criterion what the model has earned the right to say, from a reference run. The policy, the code and the trade-off.
Two Windows Bugs in a Node Launcher: "C:\Program" and a PATH That Hadn't Updated
A one-command launcher for Ollama, a gateway and cloudflared broke on Windows twice: shell: true split the node path at its space, and a fresh install was not on the open terminal's PATH.
Implemented, Available, Not Connected: Writing Honest Status Into Project Docs
"It exists in the repository" and "it runs in production" are different claims. The three-state status model I use across my projects, with real rows from each.
Reference Data Nobody Can Edit: Postgres Triggers, Hash Pinning and Append-Only History
Protecting the examples an AI system is measured against: a trigger that refuses UPDATE and DELETE, SHA-256 hashes checked by tests, append-only history, and what these do not stop.
Hardening a Public Photo Upload: Magic Bytes, Header Dimensions and Stripping EXIF Twice
An anonymous endpoint that decodes images is easy to abuse. How RPOMS AI re-encodes photos in the browser, trusts none of it on the server, and never stores the uploaded bytes.
"Unclear" Is Not One Answer: Five Decision States for an AI Inspection
An AI check that cannot decide can fail for four very different reasons. How RPOMS AI records which one, maps it to what the public sees, and makes a missing mapping a type error.
Exposing a Local Ollama to a Vercel App: A 90-Line Token Gateway and a Cloudflare Quick Tunnel
Ollama has no authentication, so its port must never face the internet. The small Node gateway RPOMS AI puts in front of it, the tunnel, and how each hop is diagnosed.
Evaluating a Vision Model When You Only Have Thirteen Examples
Thirteen labelled photos are not a test set, but they can still be an honest evaluation. Separating what is known from what is assumed, removing leaked annotations, and controls built on purpose.
Enforcing Architecture Boundaries in a Next.js App With One Test File
A Vitest file that reads the source tree and fails when a client component imports server code, an admin route skips its auth guard, or a minifier hazard creeps back in.
Encoding Vague Acceptance Criteria Without Inventing Thresholds
Criteria that said long, multiple and small with no numbers: how a pure rule engine applies vague words as categories, orders checks for safety and answers UNCLEAR.
Why Every Real Photo Timed Out: Capping Vision Input at 896 px to Fit a 60-Second Function
Full-size phone photos never got an answer from RPOMS AI's vision model. Encoding cost grew faster than pixel count; capping at the evaluated scale took 114 s down to 38 s.
The 21-Second Benchmark That Was Wrong: Ollama Reuses an Encoded Image
A vision-model latency figure that was half the real cost, because the benchmark sent the same image twice. How it was caught, corrected, and what a fair benchmark needs.
A Photo Quality Gate That Runs Before the Vision Model: Blur, Exposure and Glare From Raw Pixels
How RPOMS AI rejects blurred, dark, over-exposed and glare-filled photos with plain pixel arithmetic before any model is called, and how the thresholds were calibrated.
A Vision Model That Only Observes: Building RPOMS AI Without Letting the Model Decide
A router surface inspector where the vision model reports what it sees and a deterministic rule engine decides. Why the split, what a 7B open model actually scored, the blank-image control that changed the prompt, and a safety gate that turns most answers into "check manually".