MyKindlebox/mykindlebox/server/routes/pedigreeScan.ts

87 lines
2.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Router } from 'express';
import { sql } from 'drizzle-orm';
import multer from 'multer';
import { db } from '../db/client';
import { rabbits } from '../db/schema';
import { isPedigreeScanEnabled, PedigreeScanError, scanPedigreeImage } from '../lib/pedigreeScan';
const router = Router();
const MEDIA_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp'] as const);
type MediaType = 'image/jpeg' | 'image/png' | 'image/webp';
// In memory: the image is forwarded to the API and discarded, never written to
// disk. The client downsizes to ~2400px JPEG first, so real uploads are well
// under the limit.
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 },
}).single('image');
// Lets the client decide whether to show the "Scan a pedigree" button.
router.get('/', (req, res) => {
res.json({ enabled: isPedigreeScanEnabled() });
});
// Confident herd match for a scanned sire/dam (FEATURE_OCR_Pedigree_Scan.md
// Q6): an exact ear number wins; otherwise exactly one exact-name match.
// Anything looser stays a manual entry the user can link by hand.
function findHerdMatch(name: string | null, earNumber: string | null) {
const columns = { id: rabbits.id, name: rabbits.name };
if (earNumber) {
const byEar = db
.select(columns)
.from(rabbits)
.where(sql`upper(${rabbits.earNumber}) = ${earNumber.toUpperCase()}`)
.limit(2)
.all();
if (byEar.length === 1) return byEar[0];
}
if (name) {
const byName = db
.select(columns)
.from(rabbits)
.where(sql`lower(${rabbits.name}) = ${name.toLowerCase()}`)
.limit(2)
.all();
if (byName.length === 1) return byName[0];
}
return null;
}
router.post('/', (req, res, next) => {
if (!isPedigreeScanEnabled()) {
res.status(503).json({ error: 'Pedigree scanning isn’t set up on this server.' });
return;
}
upload(req, res, async (uploadErr) => {
if (uploadErr) {
res.status(400).json({ error: uploadErr.message });
return;
}
const file = req.file;
if (!file || !MEDIA_TYPES.has(file.mimetype as MediaType)) {
res.status(400).json({ error: 'Upload a JPEG, PNG, or WebP photo of the pedigree.' });
return;
}
try {
const result = await scanPedigreeImage(file.buffer, file.mimetype as MediaType);
const matches = {
sire: findHerdMatch(result.ancestors.sire.name, result.ancestors.sire.ear_number),
dam: findHerdMatch(result.ancestors.dam.name, result.ancestors.dam.ear_number),
};
res.json({ ...result, matches });
} catch (err) {
if (err instanceof PedigreeScanError) {
res.status(err.status).json({ error: err.message });
return;
}
next(err);
}
});
});
export default router;