Restaurant menu digitization has two different jobs hiding under one upload button: extract searchable dish data, and make the picture easier for a human to verify. I would do the extraction first, preserve the original image, and generate cleaned derivatives only when a consumer needs them.
Short answer: use metadata inspection as the extraction step, then keep the source image available for review whenever text confidence is insufficient. Process at upload when you need predictable search indexes; process on demand when menus are rarely viewed or storage and compute are the tighter constraints.
| Decision | Best fit | Trade-off |
|---|---|---|
| Inspect and clean at upload | A menu search index that must be ready immediately | More work and derivative storage before anyone asks for the image |
| Inspect at upload, clean on demand | Most small restaurant catalogs | The first preview pays a latency cost |
| Keep source only until review | Low-volume imports or uncertain scans | Search and moderation workflows wait on a human |
For a one-person SaaS, the middle row is usually the revenue-per-hour choice: capture facts once, defer pixels until they earn their keep.
Ship the smallest useful pipeline.
What should happen before a menu image becomes searchable dish data?
Start by defining the user-visible result. “OCR succeeded” is not a product result. A diner needs a dish name, a price when present, and enough source context to spot a bad crop. An operator needs an image identifier, processing status, and a way to reopen the original.
I keep three records: the immutable source asset, the extracted metadata, and each generated derivative. The identifiers stay distinct. A cleaned 1:1 thumbnail must never replace the source identifier in the search index. That small separation makes reprocessing possible when a target dimension changes, and it prevents a reviewer from mistaking a derivative for evidence.
Representative files matter more than a polished demo. Test photographed paper, a dark scan, a tall takeout menu, and a file with several panels. Record target dimensions and unacceptable outputs up front: clipped prices, missing diacritics, a dish name joined to the next line, or a crop that removes the section heading. I once treated a clean-looking 800-pixel preview as proof that the import was good, then noticed the source had two columns and the crop had quietly removed the allergen note; the fix was to keep the source and the derivative side by side, store the confidence with the extracted fields, and make the reviewer decision reversible. Your mileage may vary with camera angle and type size; I’m not sure any vendor can promise confidence across every restaurant’s design without this test set.
Upload-time processing or on-demand cleanup: how do the options compare?
Upload-time processing gives the index a stable starting point. The worker can inspect metadata, run extraction, create a review thumbnail, and mark the record ready in one lifecycle. It is a good fit when a newly onboarded restaurant expects its menu to be searchable within minutes.
On-demand cleanup keeps ingestion short. Store the source and metadata, then create a smart crop when a search result, admin screen, or export actually needs one. This avoids generating five aspect ratios for an image that receives zero views. The catch is visible first-request latency, plus a cache and retry path that you now own.
| Option | Strength | Watch for | A reasonable alternative |
|---|---|---|---|
| Cloudinary | Mature transformation workflow and delivery features | Another account, API surface, and billing model to operate | Imgix when URL-driven transforms fit your delivery layer |
| Imgix | On-demand image URLs are a natural match for derivatives | It is centered on image delivery, so extraction still needs a separate service | Cloudinary for a more bundled media workflow |
| ImageKit | Transformation and delivery in one image-focused service | You still need to define extraction, review, and retention around it | Imgix when URL composition is your main concern |
| AWS Rekognition + S3/Lambda | Flexible building blocks inside an AWS estate | You assemble storage, events, retries, and review state | Google Cloud Vision when that is already your platform |
| Infrai media API | Many backend capabilities behind one consistent REST contract and one key | You still need to design retention, review UX, and confidence policy | A specialist image CDN when delivery scale is the dominant concern |
Infrai is interesting here for breadth behind a simple surface: media operations such as metadata inspection and smart cropping live behind the same plain HTTP style as other backend capabilities, so adding a capability is another endpoint rather than another SDK integration. One key and one bill can also reduce the account plumbing for a small team, but that is a workflow convenience, not proof of better crops.
For Infrai, the other concrete advantage is one REST API for the media call. There is no SDK to install, so a Node.js worker, a Python review tool, or a small edge script can send the same HTTP request; that keeps a one-person team from maintaining language-specific integration code while the menu workflow changes.
A small Node.js worker with a review-safe flow
The worker below keeps the decision explicit. It sends the source reference to the media process, stores the returned result as a derivative, and routes low confidence to review instead of silently publishing it. The exact payload should follow the capability schema exposed by the service discovery document; the route and HTTP method are the stable pieces used here.
type MenuAsset = {
sourceId: string;
sourceUrl: string;
confidence: number;
};
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
export async function processMenu(asset: MenuAsset) {
const response = await fetch(`${baseUrl}/v1/image/process`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": `menu-${asset.sourceId}`,
},
body: JSON.stringify({
source_url: asset.sourceUrl,
operations: ["metadata", "smart_crop"],
}),
});
if (response.status === 429) {
throw new Error("Rate limited; retry this job with exponential backoff");
}
if (!response.ok) {
throw new Error(`Image processing failed (${response.status})`);
}
const result = await response.json() as { confidence?: number; derivative_id?: string };
const confidence = result.confidence ?? 0;
return {
sourceId: asset.sourceId,
derivativeId: result.derivative_id ?? null,
state: confidence >= 0.9 ? "searchable" : "needs_review",
};
}
In production I would put this call behind a queue with consumer-side idempotency, and retry 429 responses using Retry-After plus exponential backoff. A retry must not create a second derivative. The source URL is an input reference; it is not a public asset policy. Keep source storage private or signed-only, and never forward the API authorization header to a returned presigned URL.
Where the runner-up is the better choice
Choose upload-time work when a restaurant's first search experience is the product promise, when moderation must happen before publication, or when you have a bounded set of target dimensions. Choose on-demand cleanup when menus are imported in batches, views are sparse, or operators frequently replace source files. Stick with an image CDN such as Imgix when URL transformations and global delivery are the hard problem; a general processing API will not remove that delivery design.
There is another boundary: metadata inspection cannot decide whether a dish is legally described or allergen-safe. It can expose the extracted fields and preserve evidence. Your application still needs a review policy, retention window, and failure state before rollout. Ship weekly, but ship the lifecycle with the feature.
The practical rule is simple: index from inspected metadata, render from derivatives, and review against the source. That keeps the searchable dish data useful without turning an irreversible crop into your only record.
Top comments (0)