From f6510c05bae0559e7091fb008a47d3238bb79a58 Mon Sep 17 00:00:00 2001 From: Nicholas Ward Date: Mon, 17 Aug 2026 18:15:50 -0500 Subject: [PATCH] feat(ingest): extract stated yield descriptions and servings count across recipes --- scripts/ingest-docling-book.mjs | 37 ++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/scripts/ingest-docling-book.mjs b/scripts/ingest-docling-book.mjs index 6de7440..f0ce23e 100644 --- a/scripts/ingest-docling-book.mjs +++ b/scripts/ingest-docling-book.mjs @@ -506,16 +506,36 @@ export function parseDoclingBook(doclingJson, targetPages = null) { textSources.push(...nextPage.texts); } +const WORD_TO_NUMBER = { + "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10, "dozen": 12, "half": 0.5 +}; + +function parseYieldServings(yieldText) { + if (!yieldText) return null; + const s = yieldText.replace(/^yields?:\s*/i, "").trim().toLowerCase(); + const digitMatch = s.match(/^(\d+)/); + if (digitMatch) return parseInt(digitMatch[1], 10); + const wordMatch = s.match(/^(one|two|three|four|five|six|seven|eight|nine|ten|dozen|half)/); + if (wordMatch && WORD_TO_NUMBER[wordMatch[1]]) return WORD_TO_NUMBER[wordMatch[1]]; + return null; +} + const steps = []; let currentSectionPrefix = ""; let inProcedure = false; let inChefNotes = false; + let yieldDescription = null; const chefNotesList = []; const narrativeTexts = []; for (const textNode of textSources) { const text = textNode.text.trim(); + if (/^yields?:\s*/i.test(text)) { + yieldDescription = text; + continue; + } + // Match procedure section headers like "Dough Packet Procedure:", "Assembly Procedure:", "Procedure:" if (/procedure:?$/i.test(text)) { inProcedure = true; @@ -552,27 +572,38 @@ export function parseDoclingBook(doclingJson, targetPages = null) { const sumWeight = allItems.reduce((s, i) => s + (i.unit_id === "gram" ? i.quantity : 0), 0); const yieldQuantity = totalYieldGrams || (sumWeight > 0 ? sumWeight : 1000); + const yieldServings = parseYieldServings(yieldDescription); + + const allNotes = [...chefNotesList]; + if (yieldDescription) { + allNotes.unshift(yieldDescription); + } + + const summary = narrativeTexts.length > 0 + ? (yieldDescription ? `${yieldDescription}. ${narrativeTexts.join(" ")}` : narrativeTexts.join(" ")) + : yieldDescription || null; recipes.push({ id: slugId, title, page_no: pageNo, chapter: chapterInfo.name, - summary: narrativeTexts.length > 0 ? narrativeTexts.join(" ") : null, + summary, categories: [chapterInfo.category], tags: ["pastry_chefs_little_black_book", chapterInfo.category, "classic"], yield_quantity: Math.round(yieldQuantity * 100) / 100, yield_unit_id: "gram", - yield_servings: null, + yield_servings: yieldServings, yield_basis: "theoretical", yield: { quantity: Math.round(yieldQuantity * 100) / 100, unit_id: "gram", + servings: yieldServings, basis: "theoretical", }, components: formattedComponents, steps: steps.length > 0 ? steps : [{ id: "step_1", order: 1, instruction: "Prepare formulation according to standard pastry method.", equipment_ids: [] }], - notes: chefNotesList, + notes: allNotes, shelf_life: parseShelfLife(chefNotesList), }); }