From 130dc1d5012c453c10f039e3f4e159d5ab7e5fad Mon Sep 17 00:00:00 2001 From: Nicholas Ward Date: Mon, 17 Aug 2026 18:10:27 -0500 Subject: [PATCH] fix(ingest): resolve Docling stacked fraction OCR artifacts into clean measurements --- scripts/ingest-docling-book.mjs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/scripts/ingest-docling-book.mjs b/scripts/ingest-docling-book.mjs index 801f781..6de7440 100644 --- a/scripts/ingest-docling-book.mjs +++ b/scripts/ingest-docling-book.mjs @@ -222,13 +222,23 @@ function parseMetricAmount(str) { 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) { if (!text) { 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" }; 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/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" }; @@ -244,23 +254,23 @@ function parseUsFallback(text, name) { const ozMatch = s.match(/^([\d.]+)\s*oz$/); 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) { - let name = raw.trim().replace(/^[\s•\-\*]+/, ""); + let cleaned = cleanFractionText(raw).replace(/^[\s•\-\*]+/, ""); let notes = null; - const parenMatch = name.match(/^([^(]+)\s*\(([^)]+)\)$/); + const parenMatch = cleaned.match(/^([^(]+)\s*\(([^)]+)\)$/); if (parenMatch) { - name = parenMatch[1].trim(); + cleaned = parenMatch[1].trim(); notes = parenMatch[2].trim(); } - return { name, notes }; + return { name: cleaned, notes }; } function inferIngredientId(name) { @@ -397,9 +407,9 @@ export function parseDoclingBook(doclingJson, targetPages = null) { const row = grid.get(r); if (!row) continue; - const ingText = row.get(0) || ""; - const metricText = row.get(1) || ""; - const usText = row.get(2) || ""; + const ingText = cleanFractionText(row.get(0) || ""); + const metricText = cleanFractionText(row.get(1) || ""); + const usText = cleanFractionText(row.get(2) || ""); if (/total weight/i.test(ingText)) { const parsedTotal = parseMetricAmount(metricText) || parseMetricAmount(usText);