mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-23 10:45:20 -05:00
feat: Import + Translate recipe images with OpenAI (#3974)
Co-authored-by: Johan Lindell <johan@lindell.me> Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
This commit is contained in:
@@ -19,6 +19,7 @@ class AppInfo(MealieModel):
|
||||
oidc_redirect: bool
|
||||
oidc_provider_name: str
|
||||
enable_openai: bool
|
||||
enable_openai_image_services: bool
|
||||
|
||||
|
||||
class AppTheme(MealieModel):
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class OpenAIBase(BaseModel):
|
||||
"""
|
||||
@@ -8,3 +12,16 @@ class OpenAIBase(BaseModel):
|
||||
"""
|
||||
|
||||
__doc__ = "" # we don't want to include the docstring in the JSON schema
|
||||
|
||||
@classmethod
|
||||
def parse_openai_response(cls, response: str | None):
|
||||
"""
|
||||
This method should be implemented in the child class. It should
|
||||
parse the JSON response from OpenAI and return a dictionary.
|
||||
"""
|
||||
|
||||
try:
|
||||
return cls.model_validate_json(response or "")
|
||||
except Exception:
|
||||
logger.debug(f"Failed to parse OpenAI response as {cls}. Response: {response}")
|
||||
raise
|
||||
|
||||
205
mealie/schema/openai/recipe.py
Normal file
205
mealie/schema/openai/recipe.py
Normal file
@@ -0,0 +1,205 @@
|
||||
from textwrap import dedent
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from ._base import OpenAIBase
|
||||
|
||||
|
||||
class OpenAIRecipeIngredient(OpenAIBase):
|
||||
title: str | None = Field(
|
||||
None,
|
||||
description=dedent(
|
||||
"""
|
||||
The title of the section of the recipe that the ingredient is found in. Recipes may not specify
|
||||
ingredient sections, in which case this should be left blank.
|
||||
Only the first item in the section should have this set,
|
||||
whereas subsuquent items should have their titles left blank (unless they start a new section).
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
text: str = Field(
|
||||
...,
|
||||
description=dedent(
|
||||
"""
|
||||
The text of the ingredient. This should represent the entire ingredient, such as "1 cup of flour" or
|
||||
"2 cups of onions, chopped". If the ingredient is completely blank, skip it and do not add the ingredient,
|
||||
since this field is required.
|
||||
|
||||
If the ingredient has no text, but has a title, include the title on the
|
||||
next ingredient instead.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class OpenAIRecipeInstruction(OpenAIBase):
|
||||
title: str | None = Field(
|
||||
None,
|
||||
description=dedent(
|
||||
"""
|
||||
The title of the section of the recipe that the instruction is found in. Recipes may not specify
|
||||
instruction sections, in which case this should be left blank.
|
||||
Only the first instruction in the section should have this set,
|
||||
whereas subsuquent instructions should have their titles left blank (unless they start a new section).
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
text: str = Field(
|
||||
...,
|
||||
description=dedent(
|
||||
"""
|
||||
The text of the instruction. This represents one step in the recipe, such as "Preheat the oven to 350",
|
||||
or "Sauté the onions for 20 minutes". Sometimes steps can be longer, such as "Bring a large pot of lightly
|
||||
salted water to a boil. Add ditalini pasta and cook for 8 minutes or until al dente; drain.".
|
||||
|
||||
Sometimes, but not always, recipes will include their number in front of the text, such as
|
||||
"1.", "2.", or "Step 1", "Step 2", or "First", "Second". In the case where they are directly numbered
|
||||
("1.", "2.", "Step one", "Step 1", "Step two", "Step 2", etc.), you should not include the number in
|
||||
the text. However, if they use words ("First", "Second", etc.), then those should be included.
|
||||
|
||||
If the instruction is completely blank, skip it and do not add the instruction, since this field is
|
||||
required. If the ingredient has no text, but has a title, include the title on the next
|
||||
instruction instead.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class OpenAIRecipeNotes(OpenAIBase):
|
||||
title: str | None = Field(
|
||||
None,
|
||||
description=dedent(
|
||||
"""
|
||||
The title of the note. Notes may not specify a title, and just have a body of text. In this case,
|
||||
title should be left blank, and all content should go in the note text. If the note title is just
|
||||
"note" or "info", you should ignore it and leave the title blank.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
text: str = Field(
|
||||
...,
|
||||
description=dedent(
|
||||
"""
|
||||
The text of the note. This should represent the entire note, such as "This recipe is great for
|
||||
a summer picnic" or "This recipe is a family favorite". They may also include additional prep
|
||||
instructions such as "to make this recipe gluten free, use gluten free flour", or "you may prepare
|
||||
the dough the night before and refrigerate it until ready to bake".
|
||||
|
||||
If the note is completely blank, skip it and do not add the note, since this field is required.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class OpenAIRecipe(OpenAIBase):
|
||||
name: str = Field(
|
||||
...,
|
||||
description=dedent(
|
||||
"""
|
||||
The name or title of the recipe. If you're unable to determine the name of the recipe, you should
|
||||
make your best guess based upon the ingredients and instructions provided.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
description: str | None = Field(
|
||||
...,
|
||||
description=dedent(
|
||||
"""
|
||||
A long description of the recipe. This should be a string that describes the recipe in a few words
|
||||
or sentences. If the recipe doesn't have a description, you should return None.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
recipe_yield: str | None = Field(
|
||||
None,
|
||||
description=dedent(
|
||||
"""
|
||||
The yield of the recipe. For instance, if the recipe makes 12 cookies, the yield is "12 cookies".
|
||||
If the recipe makes 2 servings, the yield is "2 servings". Typically yield consists of a number followed
|
||||
by the word "serving" or "servings", but it can be any string that describes the yield. If the yield
|
||||
isn't specified, you should return None.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
total_time: str | None = Field(
|
||||
None,
|
||||
description=dedent(
|
||||
"""
|
||||
The total time it takes to make the recipe. This should be a string that describes a duration of time,
|
||||
such as "1 hour and 30 minutes", "90 minutes", or "1.5 hours". If the recipe has multiple times, choose
|
||||
the longest time. If the recipe doesn't specify a total time or duration, or it specifies a prep time or
|
||||
perform time but not a total time, you should return None. Do not duplicate times between total time, prep
|
||||
time and perform time.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
prep_time: str | None = Field(
|
||||
None,
|
||||
description=dedent(
|
||||
"""
|
||||
The time it takes to prepare the recipe. This should be a string that describes a duration of time,
|
||||
such as "30 minutes", "1 hour", or "1.5 hours". If the recipe has a total time, the prep time should be
|
||||
less than the total time. If the recipe doesn't specify a prep time, you should return None. If the recipe
|
||||
supplies only one time, it should be the total time. Do not duplicate times between total time, prep
|
||||
time and coperformok time.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
perform_time: str | None = Field(
|
||||
None,
|
||||
description=dedent(
|
||||
"""
|
||||
The time it takes to cook the recipe. This should be a string that describes a duration of time,
|
||||
such as "30 minutes", "1 hour", or "1.5 hours". If the recipe has a total time, the perform time should be
|
||||
less than the total time. If the recipe doesn't specify a perform time, you should return None. If the
|
||||
recipe specifies a cook time, active time, or other time besides total or prep, you should use that
|
||||
time as the perform time. If the recipe supplies only one time, it should be the total time, and not the
|
||||
perform time. Do not duplicate times between total time, prep time and perform time.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
ingredients: list[OpenAIRecipeIngredient] = Field(
|
||||
[],
|
||||
description=dedent(
|
||||
"""
|
||||
A list of ingredients used in the recipe. Ingredients should be inserted in the order they appear in the
|
||||
recipe. If the recipe has no ingredients, you should return an empty list.
|
||||
|
||||
Often times, but not always, ingredients are separated by line breaks. Use these as a guide to
|
||||
separate ingredients.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
instructions: list[OpenAIRecipeInstruction] = Field(
|
||||
[],
|
||||
description=dedent(
|
||||
"""
|
||||
A list of ingredients used in the recipe. Ingredients should be inserted in the order they appear in the
|
||||
recipe. If the recipe has no ingredients, you should return an empty list.
|
||||
|
||||
Often times, but not always, instructions are separated by line breaks and/or separated by paragraphs.
|
||||
Use these as a guide to separate instructions. They also may be separated by numbers or words, such as
|
||||
"1.", "2.", "Step 1", "Step 2", "First", "Second", etc.
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
notes: list[OpenAIRecipeNotes] = Field(
|
||||
[],
|
||||
description=dedent(
|
||||
"""
|
||||
A list of notes found in the recipe. Notes should be inserted in the order they appear in the recipe.
|
||||
They may appear anywhere on the recipe, though they are typically found under the instructions.
|
||||
"""
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user