mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-04-14 00:45:35 -04:00
fix: unparsed ingredients poorly formatted when fed to NLP parser (#7086)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com> Co-authored-by: Michael Genson <genson.michael@gmail.com>
This commit is contained in:
committed by
GitHub
parent
449e3baa07
commit
4dd8d836e1
@@ -208,7 +208,7 @@ const props = defineProps<{
|
|||||||
ingredients: NoUndefinedField<RecipeIngredient[]>;
|
ingredients: NoUndefinedField<RecipeIngredient[]>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { parseIngredientText } = useIngredientTextParser();
|
const { ingredientToParserString } = useIngredientTextParser();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "update:modelValue", value: boolean): void;
|
(e: "update:modelValue", value: boolean): void;
|
||||||
@@ -373,7 +373,7 @@ async function parseIngredients() {
|
|||||||
try {
|
try {
|
||||||
const ingsAsString = props.ingredients
|
const ingsAsString = props.ingredients
|
||||||
.filter(ing => !ing.referencedRecipe)
|
.filter(ing => !ing.referencedRecipe)
|
||||||
.map(ing => parseIngredientText(ing, 1, false) ?? "");
|
.map(ing => ingredientToParserString(ing));
|
||||||
const { data, error } = await api.recipes.parseIngredients(parser.value, ingsAsString);
|
const { data, error } = await api.recipes.parseIngredients(parser.value, ingsAsString);
|
||||||
if (error || !data) {
|
if (error || !data) {
|
||||||
throw new Error("Failed to parse ingredients");
|
throw new Error("Failed to parse ingredients");
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useLocales } from "../use-locales";
|
|||||||
vi.mock("../use-locales");
|
vi.mock("../use-locales");
|
||||||
|
|
||||||
let parseIngredientText: (ingredient: RecipeIngredient, scale?: number, includeFormating?: boolean) => string;
|
let parseIngredientText: (ingredient: RecipeIngredient, scale?: number, includeFormating?: boolean) => string;
|
||||||
|
let ingredientToParserString: (ingredient: RecipeIngredient) => string;
|
||||||
|
|
||||||
describe("parseIngredientText", () => {
|
describe("parseIngredientText", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -13,7 +14,7 @@ describe("parseIngredientText", () => {
|
|||||||
locales: [{ value: "en-US", pluralFoodHandling: "always" }],
|
locales: [{ value: "en-US", pluralFoodHandling: "always" }],
|
||||||
locale: { value: "en-US", pluralFoodHandling: "always" },
|
locale: { value: "en-US", pluralFoodHandling: "always" },
|
||||||
} as any);
|
} as any);
|
||||||
({ parseIngredientText } = useIngredientTextParser());
|
({ parseIngredientText, ingredientToParserString } = useIngredientTextParser());
|
||||||
});
|
});
|
||||||
|
|
||||||
const createRecipeIngredient = (overrides: Partial<RecipeIngredient>): RecipeIngredient => ({
|
const createRecipeIngredient = (overrides: Partial<RecipeIngredient>): RecipeIngredient => ({
|
||||||
@@ -236,3 +237,116 @@ describe("parseIngredientText", () => {
|
|||||||
expect(parseIngredientText(ingredient, 1, false)).toEqual("< 1/10 cup salt");
|
expect(parseIngredientText(ingredient, 1, false)).toEqual("< 1/10 cup salt");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ingredientToParserString", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mocked(useLocales).mockReturnValue({
|
||||||
|
locales: [{ value: "en-US", pluralFoodHandling: "always" }],
|
||||||
|
locale: { value: "en-US", pluralFoodHandling: "always" },
|
||||||
|
} as any);
|
||||||
|
({ ingredientToParserString } = useIngredientTextParser());
|
||||||
|
});
|
||||||
|
|
||||||
|
const createRecipeIngredient = (overrides: Partial<RecipeIngredient>): RecipeIngredient => ({
|
||||||
|
quantity: 1,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unparsed ingredient with qty=1 and note containing fraction uses just the note", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 1,
|
||||||
|
unit: undefined,
|
||||||
|
food: undefined,
|
||||||
|
note: "1/2 cup apples",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ingredientToParserString(ingredient)).toEqual("1/2 cup apples");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ingredient with originalText uses originalText", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 1,
|
||||||
|
unit: { id: "1", name: "cup" },
|
||||||
|
food: { id: "1", name: "apples" },
|
||||||
|
note: "some note",
|
||||||
|
originalText: "half a cup of apples",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ingredientToParserString(ingredient)).toEqual("half a cup of apples");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parsed ingredient with unit and food uses full reconstruction", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 2,
|
||||||
|
unit: { id: "1", name: "cup" },
|
||||||
|
food: { id: "1", name: "flour" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ingredientToParserString(ingredient)).toEqual("2 cup flour");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ingredient with no data returns empty string", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 0,
|
||||||
|
unit: undefined,
|
||||||
|
food: undefined,
|
||||||
|
note: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ingredientToParserString(ingredient)).toEqual("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unparsed ingredient with note starting with an integer uses just the note", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 1,
|
||||||
|
unit: undefined,
|
||||||
|
food: undefined,
|
||||||
|
note: "2 tbsp olive oil",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ingredientToParserString(ingredient)).toEqual("2 tbsp olive oil");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unparsed ingredient with purely descriptive note uses just the note", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 1,
|
||||||
|
unit: undefined,
|
||||||
|
food: undefined,
|
||||||
|
note: "salt to taste",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ingredientToParserString(ingredient)).toEqual("salt to taste");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("originalText wins even when ingredient is unparsed (no unit, no food)", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 1,
|
||||||
|
unit: undefined,
|
||||||
|
food: undefined,
|
||||||
|
note: "2 tbsp olive oil",
|
||||||
|
originalText: "two tablespoons olive oil",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ingredientToParserString(ingredient)).toEqual("two tablespoons olive oil");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ingredient with only food (no unit) uses full reconstruction", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 2,
|
||||||
|
unit: undefined,
|
||||||
|
food: { id: "1", name: "apples" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ingredientToParserString(ingredient)).toEqual("2 apples");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ingredient with only unit (no food) uses full reconstruction", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 2,
|
||||||
|
unit: { id: "1", name: "cup" },
|
||||||
|
food: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ingredientToParserString(ingredient)).toEqual("2 cup");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -142,8 +142,24 @@ export function useIngredientTextParser() {
|
|||||||
return sanitizeIngredientHTML(text);
|
return sanitizeIngredientHTML(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function ingredientToParserString(ingredient: RecipeIngredient): string {
|
||||||
|
if (ingredient.originalText) {
|
||||||
|
return ingredient.originalText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the ingredient has no unit and no food, it's unparsed — the note
|
||||||
|
// contains the full ingredient text. Using parseIngredientText would
|
||||||
|
// incorrectly prepend the quantity (e.g. "1 1/2 cup apples").
|
||||||
|
if (!ingredient.unit && !ingredient.food) {
|
||||||
|
return ingredient.note || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseIngredientText(ingredient, 1, false) ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
useParsedIngredientText,
|
useParsedIngredientText,
|
||||||
parseIngredientText,
|
parseIngredientText,
|
||||||
|
ingredientToParserString,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user