fix(ingest): resolve Docling stacked fraction OCR artifacts into clean measurements

This commit is contained in:
2026-08-17 18:10:27 -05:00
parent 63492445a9
commit 130dc1d501
+20 -10
View File
@@ -222,13 +222,23 @@ function parseMetricAmount(str) {
return null; return null;
} }
export function cleanFractionText(str) {
if (!str) return "";
return str
.replace(/[\r\n]+/g, " ")
.replace(/(\d+)\s*\/\s*\1\s*\/\s*(\d+)/g, (m, a, b) => `${a}/${b}`)
.replace(/(\d+)\s*\/\s*(\d+)/g, (m, a, b) => `${a}/${b}`)
.replace(/\s+/g, " ")
.trim();
}
function parseUsFallback(text, name) { function parseUsFallback(text, name) {
if (!text) { if (!text) {
if (/zest/i.test(name)) return { quantity: 1, unit_id: "each", notes: "Zest of 1" }; if (/zest/i.test(name)) return { quantity: 1, unit_id: "each", notes: "Zest of 1" };
if (/vanilla bean/i.test(name)) return { quantity: 1, unit_id: "each", notes: "1 bean" }; if (/vanilla bean/i.test(name)) return { quantity: 1, unit_id: "each", notes: "1 bean" };
return { quantity: 1, unit_id: "gram", notes: "To taste / as needed" }; return { quantity: 1, unit_id: "gram", notes: "To taste / as needed" };
} }
const s = text.replace(/[\r\n\s]+/g, " ").trim().toLowerCase(); const s = cleanFractionText(text).toLowerCase();
if (s.includes("1/8") || s.includes("⅛")) return { quantity: 0.6, unit_id: "gram", notes: "⅛ tsp" }; if (s.includes("1/8") || s.includes("⅛")) return { quantity: 0.6, unit_id: "gram", notes: "⅛ tsp" };
if (s.includes("1/4") || s.includes("¼")) return { quantity: 1.25, unit_id: "gram", notes: "¼ tsp" }; if (s.includes("1/4") || s.includes("¼")) return { quantity: 1.25, unit_id: "gram", notes: "¼ tsp" };
if (s.includes("1/2") || s.includes("½")) return { quantity: 2.5, unit_id: "gram", notes: "½ tsp" }; if (s.includes("1/2") || s.includes("½")) return { quantity: 2.5, unit_id: "gram", notes: "½ tsp" };
@@ -244,23 +254,23 @@ function parseUsFallback(text, name) {
const ozMatch = s.match(/^([\d.]+)\s*oz$/); const ozMatch = s.match(/^([\d.]+)\s*oz$/);
if (ozMatch && parseFloat(ozMatch[1]) > 0) { if (ozMatch && parseFloat(ozMatch[1]) > 0) {
return { quantity: Math.round(parseFloat(ozMatch[1]) * 28.3495 * 100) / 100, unit_id: "gram", notes: text }; return { quantity: Math.round(parseFloat(ozMatch[1]) * 28.3495 * 100) / 100, unit_id: "gram", notes: cleanFractionText(text) };
} }
return { quantity: 1, unit_id: "gram", notes: text }; return { quantity: 1, unit_id: "gram", notes: cleanFractionText(text) };
} }
function cleanIngredientName(raw) { function cleanIngredientName(raw) {
let name = raw.trim().replace(/^[\s•\-\*]+/, ""); let cleaned = cleanFractionText(raw).replace(/^[\s•\-\*]+/, "");
let notes = null; let notes = null;
const parenMatch = name.match(/^([^(]+)\s*\(([^)]+)\)$/); const parenMatch = cleaned.match(/^([^(]+)\s*\(([^)]+)\)$/);
if (parenMatch) { if (parenMatch) {
name = parenMatch[1].trim(); cleaned = parenMatch[1].trim();
notes = parenMatch[2].trim(); notes = parenMatch[2].trim();
} }
return { name, notes }; return { name: cleaned, notes };
} }
function inferIngredientId(name) { function inferIngredientId(name) {
@@ -397,9 +407,9 @@ export function parseDoclingBook(doclingJson, targetPages = null) {
const row = grid.get(r); const row = grid.get(r);
if (!row) continue; if (!row) continue;
const ingText = row.get(0) || ""; const ingText = cleanFractionText(row.get(0) || "");
const metricText = row.get(1) || ""; const metricText = cleanFractionText(row.get(1) || "");
const usText = row.get(2) || ""; const usText = cleanFractionText(row.get(2) || "");
if (/total weight/i.test(ingText)) { if (/total weight/i.test(ingText)) {
const parsedTotal = parseMetricAmount(metricText) || parseMetricAmount(usText); const parsedTotal = parseMetricAmount(metricText) || parseMetricAmount(usText);