Merge branch 'mealie-next' into fix/translation-issues-when-scraping

This commit is contained in:
Michael Genson
2024-01-02 19:17:26 -06:00
committed by GitHub
88 changed files with 1047 additions and 2503 deletions

View File

@@ -11,7 +11,7 @@
"user": {
"user-updated": "Bruker oppdatert",
"password-updated": "Passord oppdatert",
"invalid-current-password": "Feil nåværende passord",
"invalid-current-password": "Nåværende passord er feil",
"ldap-update-password-unavailable": "Kan ikke oppdatere passordet, brukeren kontrolleres av LDAP"
},
"group": {
@@ -29,7 +29,7 @@
"generic-updated": "{name} ble oppdatert",
"generic-created-with-url": "{name} har blitt opprettet, {url}",
"generic-updated-with-url": "{name} har blitt oppdatert, {url}",
"generic-duplicated": "Det er blitt laget kopi av {name}",
"generic-duplicated": "{name} har blitt duplisert",
"generic-deleted": "{name} har blitt slettet"
}
}

View File

@@ -23,7 +23,7 @@
"broccolini": "broccolini",
"broccoli-rabe": "broccoli rabe",
"brussels-sprouts": "λαχανάκια Βρυξελλών",
"cabbage": "cabbage",
"cabbage": "λάχανο",
"cauliflower": "κουνουπίδι",
"chinese-leaves": "κινέζικα φύλλα",
"collard-greens": "collard greens",
@@ -52,34 +52,34 @@
"chicory": "chicory",
"chilli-peppers": "chilli peppers",
"chives": "chives",
"chocolate": "chocolate",
"cilantro": "cilantro",
"cinnamon": "cinnamon",
"chocolate": "σοκολάτα",
"cilantro": "κόλιανδρος",
"cinnamon": "κανέλα",
"clarified-butter": "clarified butter",
"coconut": "coconut",
"coconut-milk": "coconut milk",
"coffee": "coffee",
"coconut": "καρύδα",
"coconut-milk": "γάλα καρύδας",
"coffee": "καφές",
"confectioners-sugar": "confectioners' sugar",
"coriander": "coriander",
"corn": "corn",
"coriander": "κόλιανδρος",
"corn": "καλαμπόκι",
"corn-syrup": "corn syrup",
"cottonseed-oil": "cottonseed oil",
"courgette": "courgette",
"cream-of-tartar": "cream of tartar",
"cucumber": "cucumber",
"cumin": "cumin",
"cucumber": "αγγούρι",
"cumin": "κύμινο",
"daikon": "daikon",
"dairy-products-and-dairy-substitutes": "dairy products and dairy substitutes",
"eggs": "eggs",
"eggs": "αυγά",
"ghee": "ghee",
"milk": "milk",
"milk": "γάλα",
"dandelion": "dandelion",
"demerara-sugar": "demerara sugar",
"dough": "dough",
"dough": "ζυμάρι",
"edible-cactus": "edible cactus",
"eggplant": "eggplant",
"eggplant": "μελιτζάνα",
"endive": "endive",
"fats": "fats",
"fats": "λιπαρά",
"speck": "speck",
"fava-beans": "fava beans",
"fiddlehead": "fiddlehead",

View File

@@ -36,7 +36,7 @@
"butternut-pumpkin": "flaskegresskar",
"butternut-squash": "butternut squash",
"cactus-edible": "kaktus, spiselig",
"calabrese": "calabrese Fersk Pølse",
"calabrese": "calabrese",
"cannabis": "cannabis",
"capsicum": "chilipepper",
"caraway": "karve",
@@ -45,7 +45,7 @@
"cayenne-pepper": "kayenne pepper",
"celeriac": "sellerirot",
"celery": "selleri",
"cereal-grains": "frokostblanding korn",
"cereal-grains": "frokostblandingkorn",
"rice": "ris",
"chard": "bladbete",
"cheese": "ost",

View File

@@ -3,22 +3,22 @@
"name": "Produce"
},
{
"name": "Grains"
"name": "Σιτηρά"
},
{
"name": "Fruits"
"name": "Φρούτα"
},
{
"name": "Vegetables"
"name": "Λαχανικά"
},
{
"name": "Meat"
"name": "Κρέας"
},
{
"name": "Seafood"
"name": "Θαλασσινά"
},
{
"name": "Beverages"
"name": "Ποτά"
},
{
"name": "Baked Goods"
@@ -33,10 +33,10 @@
"name": "Confectionary"
},
{
"name": "Dairy Products"
"name": "Γαλακτοκομικά"
},
{
"name": "Frozen Foods"
"name": "Κατεψυγμένα Φαγητά"
},
{
"name": "Health Foods"
@@ -48,18 +48,18 @@
"name": "Meat Products"
},
{
"name": "Snacks"
"name": "Σνακ"
},
{
"name": "Spices"
"name": "Μπαχαρικά"
},
{
"name": "Sweets"
"name": "Γλυκά"
},
{
"name": "Alcohol"
"name": "Αλκοόλ"
},
{
"name": "Other"
"name": "Άλλα"
}
]

View File

@@ -1,6 +1,6 @@
[
{
"name": "Produce"
"name": "农产品"
},
{
"name": "谷物"
@@ -30,7 +30,7 @@
"name": "调味品"
},
{
"name": "Confectionary"
"name": "糖果类"
},
{
"name": "乳制品"
@@ -54,7 +54,7 @@
"name": "调味品"
},
{
"name": "甜味剂"
"name": "甜"
},
{
"name": "酒类"

View File

@@ -2,17 +2,17 @@
"teaspoon": {
"name": "teaspoon",
"description": "",
"abbreviation": "tsp"
"abbreviation": "κ.γ."
},
"tablespoon": {
"name": "tablespoon",
"description": "",
"abbreviation": "tbsp"
"abbreviation": "κ.σ."
},
"cup": {
"name": "cup",
"name": "φλιτζάνι",
"description": "",
"abbreviation": "cup"
"abbreviation": "φλ."
},
"fluid-ounce": {
"name": "fluid ounce",
@@ -42,7 +42,7 @@
"liter": {
"name": "λίτρο",
"description": "",
"abbreviation": "λ"
"abbreviation": "l"
},
"pound": {
"name": "pound",
@@ -57,17 +57,17 @@
"gram": {
"name": "γραμ",
"description": "",
"abbreviation": "γ"
"abbreviation": "γρ."
},
"kilogram": {
"name": "κιλό",
"description": "",
"abbreviation": "κιλ"
"abbreviation": "kg"
},
"milligram": {
"name": "χιλιοστόγραμμο",
"description": "",
"abbreviation": "μιλιγκράμ"
"abbreviation": "mg"
},
"splash": {
"name": "splash",

View File

@@ -15,17 +15,17 @@
"abbreviation": "kopp"
},
"fluid-ounce": {
"name": "us væske unse",
"name": "væskeunse",
"description": "",
"abbreviation": "fl oz"
},
"pint": {
"name": "halvliter",
"name": "pint",
"description": "",
"abbreviation": "pt"
},
"quart": {
"name": "quart",
"name": "kvart",
"description": "",
"abbreviation": "qt"
},
@@ -60,27 +60,27 @@
"abbreviation": "g"
},
"kilogram": {
"name": "kilo",
"name": "kilogram",
"description": "",
"abbreviation": "kg"
},
"milligram": {
"name": "mg",
"name": "milligram",
"description": "",
"abbreviation": "mg"
},
"splash": {
"name": "splash",
"name": "skvett",
"description": "",
"abbreviation": ""
},
"dash": {
"name": "dash",
"name": "klype",
"description": "",
"abbreviation": ""
},
"serving": {
"name": "servering",
"name": "porsjon",
"description": "",
"abbreviation": ""
},

View File

@@ -7,7 +7,6 @@ from . import (
comments,
explore,
groups,
ocr,
organizers,
parser,
recipe,
@@ -32,4 +31,3 @@ router.include_router(unit_and_foods.router)
router.include_router(admin.router)
router.include_router(validators.router)
router.include_router(explore.router)
router.include_router(ocr.router)

View File

@@ -1,7 +0,0 @@
from fastapi import APIRouter
from . import pytesseract
router = APIRouter(prefix="/ocr")
router.include_router(pytesseract.router)

View File

@@ -1,37 +0,0 @@
from fastapi import APIRouter, File
from mealie.routes._base import BaseUserController, controller
from mealie.schema.ocr.ocr import OcrAssetReq, OcrTsvResponse
from mealie.services.ocr.pytesseract import OcrService
from mealie.services.recipe.recipe_data_service import RecipeDataService
from mealie.services.recipe.recipe_service import RecipeService
router = APIRouter()
@controller(router)
class OCRController(BaseUserController):
def __init__(self):
self.ocr_service = OcrService()
@router.post("/", response_model=str)
def image_to_string(self, file: bytes = File(...)):
return self.ocr_service.image_to_string(file)
@router.post("/file-to-tsv", response_model=list[OcrTsvResponse])
def file_to_tsv(self, file: bytes = File(...)):
tsv = self.ocr_service.image_to_tsv(file)
return self.ocr_service.format_tsv_output(tsv)
@router.post("/asset-to-tsv", response_model=list[OcrTsvResponse])
def asset_to_tsv(self, req: OcrAssetReq):
recipe_service = RecipeService(self.repos, self.user, self.group)
recipe = recipe_service._get_recipe(req.recipe_slug)
if recipe.id is None:
return []
data_service = RecipeDataService(recipe.id, recipe.group_id)
asset_path = data_service.dir_assets.joinpath(req.asset_name)
file = open(asset_path, "rb")
tsv = self.ocr_service.image_to_tsv(file.read())
return self.ocr_service.format_tsv_output(tsv)

View File

@@ -1,7 +1,7 @@
from functools import cached_property
from pathlib import Path
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from mealie.core.dependencies.dependencies import temporary_zip_path
from mealie.core.security import create_file_token
@@ -50,6 +50,10 @@ class RecipeBulkActionsController(BaseUserController):
@router.get("/export/download")
def get_exported_data_token(self, path: Path):
"""Returns a token to download a file"""
path = Path(path).resolve()
if not path.is_relative_to(self.folders.DATA_DIR):
raise HTTPException(400, "path must be relative to data directory")
return {"fileToken": create_file_token(path)}

View File

@@ -27,10 +27,7 @@ from mealie.schema.make_dependable import make_dependable
from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeLastMade, RecipeSummary
from mealie.schema.recipe.recipe_asset import RecipeAsset
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient
from mealie.schema.recipe.recipe_scraper import ScrapeRecipeTest
from mealie.schema.recipe.recipe_settings import RecipeSettings
from mealie.schema.recipe.recipe_step import RecipeStep
from mealie.schema.recipe.request_helpers import RecipeDuplicate, RecipeZipTokenResponse, UpdateImageResponse
from mealie.schema.response import PaginationBase, PaginationQuery
from mealie.schema.response.pagination import RecipeSearchQuery
@@ -489,37 +486,3 @@ class RecipeController(BaseRecipeController):
self.mixins.update_one(recipe, slug)
return asset_in
# ==================================================================================================================
# OCR
@router.post("/create-ocr", status_code=201, response_model=str)
def create_recipe_ocr(
self, extension: str = Form(...), file: UploadFile = File(...), makefilerecipeimage: bool = Form(...)
):
"""Takes an image and creates a recipe based on the image"""
slug = self.service.create_one(
Recipe(
name="New OCR Recipe",
recipe_ingredient=[RecipeIngredient(note="", title=None, unit=None, food=None, original_text=None)],
recipe_instructions=[RecipeStep(text="")],
is_ocr_recipe=True,
settings=RecipeSettings(show_assets=True),
id=None,
image=None,
recipe_yield=None,
rating=None,
orgURL=None,
date_added=None,
date_updated=None,
created_at=None,
update_at=None,
nutrition=None,
)
).slug
RecipeController.upload_recipe_asset(self, slug, "Original recipe image", "", extension, file)
if makefilerecipeimage:
# Get the pointer to the beginning of the file to read it once more
file.file.seek(0)
self.update_recipe_image(slug, file.file.read(), extension)
return slug

View File

@@ -3,6 +3,7 @@ from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, status
from starlette.responses import FileResponse
from mealie.core.config import get_app_dirs
from mealie.core.dependencies import validate_file_token
router = APIRouter(prefix="/api/utils", tags=["Utils"], include_in_schema=True)
@@ -12,6 +13,14 @@ router = APIRouter(prefix="/api/utils", tags=["Utils"], include_in_schema=True)
async def download_file(file_path: Path = Depends(validate_file_token)):
"""Uses a file token obtained by an active user to retrieve a file from the operating
system."""
file_path = Path(file_path).resolve()
dirs = get_app_dirs()
if not file_path.is_relative_to(dirs.DATA_DIR):
raise HTTPException(status.HTTP_400_BAD_REQUEST)
if not file_path.is_file():
raise HTTPException(status.HTTP_400_BAD_REQUEST)

View File

@@ -1,7 +0,0 @@
# This file is auto-generated by gen_schema_exports.py
from .ocr import OcrAssetReq, OcrTsvResponse
__all__ = [
"OcrAssetReq",
"OcrTsvResponse",
]

View File

@@ -1,21 +0,0 @@
from mealie.schema._mealie import MealieModel
class OcrTsvResponse(MealieModel):
level: int = 0
page_num: int = 0
block_num: int = 0
par_num: int = 0
line_num: int = 0
word_num: int = 0
left: int = 0
top: int = 0
width: int = 0
height: int = 0
conf: float = 0.0
text: str = ""
class OcrAssetReq(MealieModel):
recipe_slug: str
asset_name: str

View File

@@ -128,7 +128,6 @@ class Recipe(RecipeSummary):
assets: list[RecipeAsset] | None = []
notes: list[RecipeNote] | None = []
extras: dict | None = {}
is_ocr_recipe: bool | None = False
comments: list[RecipeCommentOut] | None = []

View File

@@ -1,56 +0,0 @@
from io import BytesIO
import pytesseract
from PIL import Image
from mealie.schema.ocr.ocr import OcrTsvResponse
from mealie.services._base_service import BaseService
class OcrService(BaseService):
"""
Class for ocr engines.
"""
def image_to_string(self, image_data):
"""
Returns a plain text translation of an image
"""
return pytesseract.image_to_string(Image.open(image_data))
def image_to_tsv(self, image_data, lang=None):
"""
Returns the pytesseract default tsv output
"""
if lang is not None:
return pytesseract.image_to_data(Image.open(BytesIO(image_data)), lang=lang)
return pytesseract.image_to_data(Image.open(BytesIO(image_data)))
def format_tsv_output(self, tsv: str) -> list[OcrTsvResponse]:
"""
Returns a OcrTsvResponse from a default pytesseract tsv output
"""
lines = tsv.split("\n")
titles = [t.strip() for t in lines[0].split("\t")]
response: list[OcrTsvResponse] = []
for i in range(1, len(lines)):
if lines[i] == "":
continue
line = OcrTsvResponse()
for key, value in zip(titles, lines[i].split("\t"), strict=False):
if key == "text":
setattr(line, key, value.strip())
elif key == "conf":
setattr(line, key, float(value.strip()))
elif key in OcrTsvResponse.__fields__:
setattr(line, key, int(value.strip()))
else:
continue
if isinstance(line, OcrTsvResponse):
response.append(line)
return response