Project Context
VAON built an OCR microservice extracting text and identifiers (serial, IMEI, part number, model number) from photographs of product labels. The hard requirement was not accuracy on clean scans, but working on real camera photos using CPU only. Verified: 0.997 confidence in English, 0.999 in Japanese, both fully correct, on a 132 MB model set with no GPU.
Challenges
Input images are real photographs, not clean scans
Labels are captured on camera under working conditions: tilted, blurred, unevenly lit, cluttered backgrounds. An engine that scores well on clean print can fail outright here. The evaluation set therefore had to be photographs, not rendered text.
The default engine reads Vietnamese diacritics wrong: it drops them entirely
Given Sản phẩm Việt Nam, the default engine returns San pham Viet Nam. The diacritics are gone, not degraded. English, Chinese and Japanese show no such problem. This is a single-language failure needing its own handling, not a global parameter change.
The service has to run CPU-only to control infrastructure cost
No GPU dependency was a constraint from the start. That rules out most heavyweight OCR models and forces selection on model size as well as accuracy. Every option was measured under identical CPU conditions.
Identifiers sit scattered across the label, so reading the text is not enough
A single label carries several numeric groups. Recognising characters does not solve the problem: the service must know which group is the serial and which the part number, and bind each label to the value beside it. This is a structural problem, not a character-recognition one.
VAON's Solution
Split the service into two fully independent OCR endpoints rather than one merged pipeline. Clients select the engine by language need instead of being forced through a single processing chain.
Chose PP-OCRv6_medium as the default engine because it covers Chinese, English, Japanese and Latin scripts within one 132 MB model, verified by real testing, with no additional training required.
Added VietOCR as a secondary engine for Vietnamese diacritics, toggled by VIETOCR_ENABLED and off by default. Why this rather than replacing the default engine: VietOCR weighs roughly 623 MB, close to five times the primary engine, so that cost should only be paid where Vietnamese is genuinely needed.
Built one shared identifier-extraction layer for both branches, rule-based: regex label matching, spatial nearest-neighbour association, and a Luhn checksum for IMEI. Rules were chosen over a dedicated model so every result stays transparent and auditable step by step.
Gave each engine its own concurrency control (asyncio.Semaphore with threading.Lock), so the engines cannot contend for each other's resources under load.
Tightened input handling at the middleware layer: an upload size limit, a 25-megapixel ceiling against decompression bombs, pillow==12.3.0 pinned for CVE-2023-4863, and SHA256 verification of downloaded weights.
Value Delivered
100% correct in English and Japanese, at 0.997-0.999 confidence
The string S/N: ABC123456 scored 0.997 confidence. The Japanese string 日本語のテスト製品番号123 scored 0.999. Both were read fully correctly by the default engine, with no per-language configuration.
Vietnamese diacritics: from entirely wrong to fully correct
The same string the default engine returned as Sn phm Vit Nam is read correctly by VietOCR at 0.913 confidence. The problem was isolated to one language and solved with an on-demand engine, rather than trading off accuracy system-wide.
132 MB, no GPU required
The default model set is 59 MB detection plus 73 MB recognition, running entirely on CPU. In the recorded demo, one identifier read completed at 1,800 ms. Infrastructure therefore sits on commodity machines, not GPU instances.
Identifier extraction that can be audited
Because extraction is rule-based rather than model-based, every identifier traces back to the rule that matched it: which label, which position, whether it passed the Luhn checksum. When a result is wrong, the cause is identifiable immediately instead of requiring retraining.
54 automated tests, and a published list of limitations
The service carries 42 unit tests and 12 integration tests against the real model, split into two runs. During development a new route was found missing its upload size limit; it was patched before release. An 11 MB upload now returns 413 as designed.