1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-05 21:45:25 +02:00

feat: Migrate from CRF++ to Ingredient Parser (a Python package) (#5061)

This commit is contained in:
Michael Genson 2025-02-28 08:17:28 -06:00 committed by GitHub
parent ec1a9d78ac
commit b12aea8272
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 367 additions and 592 deletions

View file

@ -2,10 +2,29 @@ import pytest
from fastapi.testclient import TestClient
from mealie.schema.recipe.recipe_ingredient import RegisteredParser
from tests.unit_tests.test_ingredient_parser import TestIngredient, crf_exists, test_ingredients
from tests.unit_tests.test_ingredient_parser import TestIngredient
from tests.utils import api_routes
from tests.utils.fixture_schemas import TestUser
nlp_test_ingredients = [
TestIngredient("½ cup all-purpose flour", 0.5, "cup", "all-purpose flour", ""),
TestIngredient("1½ teaspoons ground black pepper", 1.5, "teaspoon", "ground black pepper", ""),
TestIngredient("⅔ cup unsweetened flaked coconut", 0.667, "cup", "unsweetened flaked coconut", ""),
TestIngredient("⅓ cup panko bread crumbs", 0.333, "cup", "panko bread crumbs", ""),
TestIngredient("1/8 cup all-purpose flour", 0.125, "cup", "all-purpose flour", ""),
TestIngredient("1/32 cup all-purpose flour", 0.031, "cup", "all-purpose flour", ""),
TestIngredient("1 1/2 cups chopped onion ", 1.5, "cup", "onion", "chopped"),
TestIngredient(
"2 pounds russet potatoes, peeled, and cut into 3/4-inch cubes ",
2,
"pound",
"russet potatoes",
"peeled, and cut into 3/4 inch cubes",
),
TestIngredient("2 tablespoons (30ml) vegetable oil ", 2, "tablespoon", "vegetable oil", ""),
TestIngredient("2 teaspoons salt (to taste) ", 2, "teaspoon", "salt", "to taste"),
]
def assert_ingredient(api_response: dict, test_ingredient: TestIngredient):
assert api_response["ingredient"]["quantity"] == pytest.approx(test_ingredient.quantity)
@ -14,8 +33,7 @@ def assert_ingredient(api_response: dict, test_ingredient: TestIngredient):
assert api_response["ingredient"]["note"] == test_ingredient.comments
@pytest.mark.skipif(not crf_exists(), reason="CRF++ not installed")
@pytest.mark.parametrize("test_ingredient", test_ingredients)
@pytest.mark.parametrize("test_ingredient", nlp_test_ingredients)
def test_recipe_ingredient_parser_nlp(api_client: TestClient, test_ingredient: TestIngredient, unique_user: TestUser):
payload = {"parser": RegisteredParser.nlp, "ingredient": test_ingredient.input}
response = api_client.post(api_routes.parser_ingredient, json=payload, headers=unique_user.token)
@ -23,13 +41,12 @@ def test_recipe_ingredient_parser_nlp(api_client: TestClient, test_ingredient: T
assert_ingredient(response.json(), test_ingredient)
@pytest.mark.skipif(not crf_exists(), reason="CRF++ not installed")
def test_recipe_ingredients_parser_nlp(api_client: TestClient, unique_user: TestUser):
payload = {"parser": RegisteredParser.nlp, "ingredients": [x.input for x in test_ingredients]}
payload = {"parser": RegisteredParser.nlp, "ingredients": [x.input for x in nlp_test_ingredients]}
response = api_client.post(api_routes.parser_ingredients, json=payload, headers=unique_user.token)
assert response.status_code == 200
for api_ingredient, test_ingredient in zip(response.json(), test_ingredients, strict=False):
for api_ingredient, test_ingredient in zip(response.json(), nlp_test_ingredients, strict=False):
assert_ingredient(api_ingredient, test_ingredient)

View file

@ -1,8 +1,6 @@
import asyncio
import json
import shutil
from dataclasses import dataclass
from fractions import Fraction
import pytest
from pydantic import UUID4
@ -27,10 +25,6 @@ from mealie.schema.recipe.recipe_ingredient import (
from mealie.schema.user.user import GroupBase
from mealie.services.openai import OpenAIService
from mealie.services.parser_services import RegisteredParser, get_parser
from mealie.services.parser_services.crfpp.processor import (
CRFIngredient,
convert_list_to_crf_model,
)
from tests.utils.factories import random_int, random_string
@ -43,10 +37,6 @@ class TestIngredient:
comments: str
def crf_exists() -> bool:
return shutil.which("crf_test") is not None
def build_parsed_ing(food: str | None, unit: str | None) -> ParsedIngredient:
ing = RecipeIngredient(unit=None, food=None)
if food:
@ -134,32 +124,6 @@ def parsed_ingredient_data(
return foods, units
# TODO - add more robust test cases
test_ingredients = [
TestIngredient("½ cup all-purpose flour", 0.5, "cup", "all-purpose flour", ""),
TestIngredient("1½ teaspoons ground black pepper", 1.5, "teaspoon", "black pepper", "ground"),
TestIngredient("⅔ cup unsweetened flaked coconut", 0.667, "cup", "coconut", "unsweetened flaked"),
TestIngredient("⅓ cup panko bread crumbs", 0.333, "cup", "panko bread crumbs", ""),
# Small Fraction Tests - PR #1369
# Reported error is was for 1/8 - new lowest expected threshold is 1/32
TestIngredient("1/8 cup all-purpose flour", 0.125, "cup", "all-purpose flour", ""),
TestIngredient("1/32 cup all-purpose flour", 0.031, "cup", "all-purpose flour", ""),
]
@pytest.mark.skipif(not crf_exists(), reason="CRF++ not installed")
def test_nlp_parser() -> None:
models: list[CRFIngredient] = convert_list_to_crf_model([x.input for x in test_ingredients])
# Iterate over models and test_ingredients to gather
for model, test_ingredient in zip(models, test_ingredients, strict=False):
assert round(float(sum(Fraction(s) for s in model.qty.split())), 3) == pytest.approx(test_ingredient.quantity)
assert model.comment == test_ingredient.comments
assert model.name == test_ingredient.food
assert model.unit == test_ingredient.unit
@pytest.mark.parametrize(
"input, quantity, unit, food, comment",
[