From c712e2c1bd69242ddbf983eecafd70301a373535 Mon Sep 17 00:00:00 2001 From: Miroito Date: Tue, 17 May 2022 11:41:43 +0200 Subject: [PATCH 01/62] Add pytesseract --- poetry.lock | 42 ++++++++++++++++++++++++++++++------------ pyproject.toml | 1 + 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/poetry.lock b/poetry.lock index 51a04fe7dfc..39fad0e7787 100644 --- a/poetry.lock +++ b/poetry.lock @@ -821,7 +821,7 @@ requests = ["requests"] name = "packaging" version = "21.3" description = "Core utilities for Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -1083,6 +1083,17 @@ category = "dev" optional = false python-versions = ">=3.7" +name = "pytesseract" +version = "0.3.9" +description = "Python-tesseract is a python wrapper for Google's Tesseract-OCR" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +packaging = ">=21.3" +Pillow = ">=8.0.0" + [[package]] name = "pytest" version = "6.2.5" @@ -1647,17 +1658,16 @@ attrs = [ {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] bcrypt = [ - {file = "bcrypt-3.2.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40"}, - {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa"}, - {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa"}, - {file = "bcrypt-3.2.2-cp36-abi3-win32.whl", hash = "sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e"}, - {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, - {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b589229207630484aefe5899122fb938a5b017b0f4349f769b8c13e78d99a8fd"}, + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a0584a92329210fcd75eb8a3250c5a941633f8bfaf2a18f81009b097732839b7"}, + {file = "bcrypt-3.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:56e5da069a76470679f312a7d3d23deb3ac4519991a0361abc11da837087b61d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, ] beautifulsoup4 = [ {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, @@ -1800,6 +1810,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, + {file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"}, {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, @@ -1812,6 +1823,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, + {file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"}, {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, @@ -1820,6 +1832,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, + {file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"}, {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, @@ -1828,6 +1841,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, + {file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"}, {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, @@ -1836,6 +1850,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, + {file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"}, {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, @@ -2308,6 +2323,9 @@ pyrsistent = [ {file = "pyrsistent-0.18.1-cp39-cp39-win32.whl", hash = "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc"}, {file = "pyrsistent-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07"}, {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, +pytesseract = [ + {file = "pytesseract-0.3.9-py2.py3-none-any.whl", hash = "sha256:fecda37d1e4eaf744c657cd03a5daab4eb97c61506ac5550274322c8ae32eca2"}, + {file = "pytesseract-0.3.9.tar.gz", hash = "sha256:7e2bafc7f48d1bb71443ce4633a56f5e21925a98f220a36c336297edcd1956d0"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, diff --git a/pyproject.toml b/pyproject.toml index 9120221ef80..84decf7fc7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ python-ldap = "^3.3.1" pydantic = "^1.9.1" tzdata = "^2021.5" pyhumps = "^3.5.3" +pytesseract = "^0.3.9" [tool.poetry.dev-dependencies] pylint = "^2.6.0" From cd164b450ef6095d848360890bb0f141d194c902 Mon Sep 17 00:00:00 2001 From: Miroito Date: Tue, 17 May 2022 14:08:26 +0200 Subject: [PATCH 02/62] Add simple ocr endpoint replace extension argument --- frontend/api/class-interfaces/recipes/recipe.ts | 12 ++++++++++++ frontend/pages/recipe/create.vue | 5 +++++ mealie/routes/__init__.py | 2 ++ mealie/routes/ocr/__init__.py | 7 +++++++ mealie/routes/ocr/pytesseract.py | 13 +++++++++++++ mealie/routes/recipe/recipe_crud_routes.py | 16 ++++++++++++++++ mealie/services/ocr/__init.py__ | 0 mealie/services/ocr/pytesseract.py | 16 ++++++++++++++++ 8 files changed, 71 insertions(+) create mode 100644 mealie/routes/ocr/__init__.py create mode 100644 mealie/routes/ocr/pytesseract.py create mode 100644 mealie/services/ocr/__init.py__ create mode 100644 mealie/services/ocr/pytesseract.py diff --git a/frontend/api/class-interfaces/recipes/recipe.ts b/frontend/api/class-interfaces/recipes/recipe.ts index 2f305b7ff6c..669b29c2e80 100644 --- a/frontend/api/class-interfaces/recipes/recipe.ts +++ b/frontend/api/class-interfaces/recipes/recipe.ts @@ -34,6 +34,7 @@ const routes = { recipesCategory: `${prefix}/recipes/category`, recipesParseIngredient: `${prefix}/parser/ingredient`, recipesParseIngredients: `${prefix}/parser/ingredients`, + recipesCreateFromOcr: `${prefix}/recipes/create-ocr`, recipesRecipeSlug: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}`, recipesRecipeSlugExport: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/exports`, @@ -116,4 +117,15 @@ export class RecipeAPI extends BaseCRUDAPI { getZipRedirectUrl(recipeSlug: string, token: string) { return `${routes.recipesRecipeSlugExportZip(recipeSlug)}?token=${token}`; } + + async getShared(item_id: string) { + return await this.requests.get(routes.recipeShareToken(item_id)); + } + + async createFromOcr(file: File) { + const formData = new FormData(); + formData.append("file", file); + formData.append("extension", file.name.split(".").pop() ?? ""); + return await this.requests.post(routes.recipesCreateFromOcr, formData); + } } diff --git a/frontend/pages/recipe/create.vue b/frontend/pages/recipe/create.vue index f6432552692..becad45f2eb 100644 --- a/frontend/pages/recipe/create.vue +++ b/frontend/pages/recipe/create.vue @@ -42,6 +42,11 @@ export default defineComponent({ text: "Import with URL", value: "url", }, + { + icon: $globals.icons.edit, + text: "Create recipe from image", + value: "ocr", + }, { icon: $globals.icons.edit, text: "Create Recipe", diff --git a/mealie/routes/__init__.py b/mealie/routes/__init__.py index 849ec5b571f..e421c01ae5a 100644 --- a/mealie/routes/__init__.py +++ b/mealie/routes/__init__.py @@ -7,6 +7,7 @@ comments, explore, groups, + ocr, organizers, parser, recipe, @@ -31,3 +32,4 @@ router.include_router(admin.router) router.include_router(validators.router) router.include_router(explore.router) +router.include_router(ocr.router) diff --git a/mealie/routes/ocr/__init__.py b/mealie/routes/ocr/__init__.py new file mode 100644 index 00000000000..e23bbc92ec8 --- /dev/null +++ b/mealie/routes/ocr/__init__.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +from . import pytesseract + +router = APIRouter(prefix="/ocr") + +router.include_router(pytesseract.router) diff --git a/mealie/routes/ocr/pytesseract.py b/mealie/routes/ocr/pytesseract.py new file mode 100644 index 00000000000..5142c4cb25f --- /dev/null +++ b/mealie/routes/ocr/pytesseract.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter, File + +from mealie.routes._base import BaseUserController, controller +from mealie.services.ocr.pytesseract import OCR + +router = APIRouter() + + +@controller(router) +class OCRController(BaseUserController): + @router.post("/", response_model=str) + def image_to_string(self, file: bytes = File(...)): + return OCR.image_to_string(file) diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py index 2459e613d26..61c5f938f6e 100644 --- a/mealie/routes/recipe/recipe_crud_routes.py +++ b/mealie/routes/recipe/recipe_crud_routes.py @@ -44,6 +44,7 @@ EventTypes, ) from mealie.services.recipe.recipe_data_service import InvalidDomainError, NotAnImageError, RecipeDataService +from mealie.services.ocr.pytesseract import OCR from mealie.services.recipe.recipe_service import RecipeService from mealie.services.recipe.template_service import TemplateService from mealie.services.scraper.recipe_bulk_scraper import RecipeBulkScraperService @@ -435,3 +436,18 @@ def upload_recipe_asset( 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(...)): + """Takes an image and creates a recipe based on the image""" + slug = self.service.create_one(CreateRecipe(name="New OCR Recipe")).slug + RecipeController.upload_recipe_asset(self, slug, "Original recipe image", "", extension, file) + description = OCR.image_to_string(file.file) + recipe = self.mixins.get_one(slug) + recipe.description = description + recipe.settings.show_assets = True + self.mixins.update_one(recipe, slug) + + return slug diff --git a/mealie/services/ocr/__init.py__ b/mealie/services/ocr/__init.py__ new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mealie/services/ocr/pytesseract.py b/mealie/services/ocr/pytesseract.py new file mode 100644 index 00000000000..b1f6047a0e2 --- /dev/null +++ b/mealie/services/ocr/pytesseract.py @@ -0,0 +1,16 @@ +import pytesseract +from PIL import Image + +from mealie.services._base_service import BaseService + + +class OCR(BaseService): + """ + Class for ocr engines. + """ + + def image_to_string(image_data): + """ + Returns a plain text translation of an image + """ + return pytesseract.image_to_string(Image.open(image_data)) From 42b0fbe4cbb46d2b7b621b25e2eb87efa341470e Mon Sep 17 00:00:00 2001 From: Miroito Date: Thu, 14 Jul 2022 11:30:10 +0200 Subject: [PATCH 03/62] feat/ocr-editor gui --- frontend/api/class-interfaces/ocr.ts | 13 + frontend/api/index.ts | 5 + .../Domain/Recipe/RecipeDialogBulkAdd.vue | 13 +- frontend/pages/recipe/_slug/ocr-editor.vue | 713 ++++++++++++++++++ frontend/pages/recipe/create.vue | 2 +- frontend/pages/recipe/create/ocr.vue | 57 ++ frontend/types/api-types/ocr.ts | 21 + frontend/types/components.d.ts | 4 - frontend/utils/icons/icons.ts | 6 + mealie/routes/ocr/pytesseract.py | 22 + mealie/routes/recipe/recipe_crud_routes.py | 3 - mealie/schema/ocr/__init__.py | 0 mealie/schema/ocr/ocr.py | 16 + mealie/services/ocr/pytesseract.py | 8 + 14 files changed, 872 insertions(+), 11 deletions(-) create mode 100644 frontend/api/class-interfaces/ocr.ts create mode 100644 frontend/pages/recipe/_slug/ocr-editor.vue create mode 100644 frontend/pages/recipe/create/ocr.vue create mode 100644 frontend/types/api-types/ocr.ts create mode 100644 mealie/schema/ocr/__init__.py create mode 100644 mealie/schema/ocr/ocr.py diff --git a/frontend/api/class-interfaces/ocr.ts b/frontend/api/class-interfaces/ocr.ts new file mode 100644 index 00000000000..488da87a20e --- /dev/null +++ b/frontend/api/class-interfaces/ocr.ts @@ -0,0 +1,13 @@ +import { BaseAPI } from "~/api/_base"; + +const prefix = "/api"; + +export class OcrAPI extends BaseAPI { + + async tsv(file: File) { + const formData = new FormData(); + formData.append("file", file); + return await this.requests.post(`${prefix}/ocr/tsv`, formData); + } + +} diff --git a/frontend/api/index.ts b/frontend/api/index.ts index d434b5b7403..3acb52f1a5b 100644 --- a/frontend/api/index.ts +++ b/frontend/api/index.ts @@ -24,6 +24,7 @@ import { MultiPurposeLabelsApi } from "./class-interfaces/group-multiple-purpose import { GroupEventNotifierApi } from "./class-interfaces/group-event-notifier"; import { MealPlanRulesApi } from "./class-interfaces/group-mealplan-rules"; import { GroupDataSeederApi } from "./class-interfaces/group-seeder"; +import {OcrAPI} from "./class-interfaces/ocr"; import { ApiRequestInstance } from "~/types/api"; class Api { @@ -52,6 +53,7 @@ class Api { public groupEventNotifier: GroupEventNotifierApi; public upload: UploadFile; public seeders: GroupDataSeederApi; + public ocr: OcrAPI; constructor(requests: ApiRequestInstance) { // Recipes @@ -90,6 +92,9 @@ class Api { this.bulk = new BulkActionsAPI(requests); this.groupEventNotifier = new GroupEventNotifierApi(requests); + // ocr + this.ocr = new OcrAPI(requests); + Object.freeze(this); } } diff --git a/frontend/components/Domain/Recipe/RecipeDialogBulkAdd.vue b/frontend/components/Domain/Recipe/RecipeDialogBulkAdd.vue index 620cb2d3105..445d3a690ee 100644 --- a/frontend/components/Domain/Recipe/RecipeDialogBulkAdd.vue +++ b/frontend/components/Domain/Recipe/RecipeDialogBulkAdd.vue @@ -2,7 +2,7 @@
@@ -58,10 +58,17 @@ + + diff --git a/frontend/pages/recipe/create.vue b/frontend/pages/recipe/create.vue index becad45f2eb..ba10b4715bf 100644 --- a/frontend/pages/recipe/create.vue +++ b/frontend/pages/recipe/create.vue @@ -44,7 +44,7 @@ export default defineComponent({ }, { icon: $globals.icons.edit, - text: "Create recipe from image", + text: "Create recipe from an image", value: "ocr", }, { diff --git a/frontend/pages/recipe/create/ocr.vue b/frontend/pages/recipe/create/ocr.vue new file mode 100644 index 00000000000..cc81b03dfaf --- /dev/null +++ b/frontend/pages/recipe/create/ocr.vue @@ -0,0 +1,57 @@ + + diff --git a/frontend/types/api-types/ocr.ts b/frontend/types/api-types/ocr.ts new file mode 100644 index 00000000000..c8ffde1e4c2 --- /dev/null +++ b/frontend/types/api-types/ocr.ts @@ -0,0 +1,21 @@ +/* tslint:disable */ +/* eslint-disable */ +/** +/* This file was automatically generated from pydantic models by running pydantic2ts. +/* Do not modify it by hand - just update the pydantic models and then re-run the script +*/ + +export interface OcrTsvResponse { + level: number; + pageNum: number; + blockNum: number; + parNum: number; + lineNum: number; + wordNum: number; + left: number; + top: number; + width: number; + height: number; + conf: number; + text: string; +} diff --git a/frontend/types/components.d.ts b/frontend/types/components.d.ts index 0d9e4c134e7..1c7d22d8875 100644 --- a/frontend/types/components.d.ts +++ b/frontend/types/components.d.ts @@ -30,10 +30,6 @@ import AdvancedOnly from "@/components/global/AdvancedOnly.vue"; import BasePageTitle from "@/components/global/BasePageTitle.vue"; import ButtonLink from "@/components/global/ButtonLink.vue"; -import TheSnackbar from "@/components/layout/TheSnackbar.vue"; -import AppHeader from "@/components/layout/AppHeader.vue"; -import AppSidebar from "@/components/layout/AppSidebar.vue"; -import AppFooter from "@/components/layout/AppFooter.vue"; declare module "vue" { export interface GlobalComponents { diff --git a/frontend/utils/icons/icons.ts b/frontend/utils/icons/icons.ts index 41a45e02bb9..9686ebc348b 100644 --- a/frontend/utils/icons/icons.ts +++ b/frontend/utils/icons/icons.ts @@ -118,6 +118,8 @@ import { mdiHelpCircleOutline, mdiDocker, mdiUndo, + mdiSelectionDrag, + mdiCursorMove } from "@mdi/js"; export const icons = { @@ -253,4 +255,8 @@ export const icons = { slotMachine: mdiSlotMachine, chevronDown: mdiChevronDown, chevronRight: mdiChevronRight, + + // Ocr toolbar + selectMode: mdiSelectionDrag, + panAndZoom: mdiCursorMove, }; diff --git a/mealie/routes/ocr/pytesseract.py b/mealie/routes/ocr/pytesseract.py index 5142c4cb25f..2433a542edd 100644 --- a/mealie/routes/ocr/pytesseract.py +++ b/mealie/routes/ocr/pytesseract.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, File from mealie.routes._base import BaseUserController, controller +from mealie.schema.ocr.ocr import OcrTsvResponse from mealie.services.ocr.pytesseract import OCR router = APIRouter() @@ -11,3 +12,24 @@ class OCRController(BaseUserController): @router.post("/", response_model=str) def image_to_string(self, file: bytes = File(...)): return OCR.image_to_string(file) + + @router.post("/tsv", response_model=list[OcrTsvResponse]) + def image_to_tsv(self, file: bytes = File(...)): + tsv = OCR.image_to_tsv(file) + lines = tsv.split("\n") + titles = [t.strip() for t in lines[0].split("\t")] + response = [] + # len-1 because the last line is empty + for i in range(1, len(lines) - 1): + d = {} + for key, value in zip(titles, lines[i].split("\t")): + if key == "text": + d[key] = value.strip() + elif key == "conf": + d[key] = float(value.strip()) + else: + d[key] = int(value.strip()) + + response.append(d) + + return response diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py index 61c5f938f6e..c61939d7f74 100644 --- a/mealie/routes/recipe/recipe_crud_routes.py +++ b/mealie/routes/recipe/recipe_crud_routes.py @@ -44,7 +44,6 @@ EventTypes, ) from mealie.services.recipe.recipe_data_service import InvalidDomainError, NotAnImageError, RecipeDataService -from mealie.services.ocr.pytesseract import OCR from mealie.services.recipe.recipe_service import RecipeService from mealie.services.recipe.template_service import TemplateService from mealie.services.scraper.recipe_bulk_scraper import RecipeBulkScraperService @@ -444,9 +443,7 @@ def create_recipe_ocr(self, extension: str = Form(...), file: UploadFile = File( """Takes an image and creates a recipe based on the image""" slug = self.service.create_one(CreateRecipe(name="New OCR Recipe")).slug RecipeController.upload_recipe_asset(self, slug, "Original recipe image", "", extension, file) - description = OCR.image_to_string(file.file) recipe = self.mixins.get_one(slug) - recipe.description = description recipe.settings.show_assets = True self.mixins.update_one(recipe, slug) diff --git a/mealie/schema/ocr/__init__.py b/mealie/schema/ocr/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mealie/schema/ocr/ocr.py b/mealie/schema/ocr/ocr.py new file mode 100644 index 00000000000..aee3adcb91f --- /dev/null +++ b/mealie/schema/ocr/ocr.py @@ -0,0 +1,16 @@ +from mealie.schema._mealie import MealieModel + + +class OcrTsvResponse(MealieModel): + level: int + page_num: int + block_num: int + par_num: int + line_num: int + word_num: int + left: int + top: int + width: int + height: int + conf: float + text: str diff --git a/mealie/services/ocr/pytesseract.py b/mealie/services/ocr/pytesseract.py index b1f6047a0e2..78ba69232b5 100644 --- a/mealie/services/ocr/pytesseract.py +++ b/mealie/services/ocr/pytesseract.py @@ -1,3 +1,5 @@ +from io import BytesIO + import pytesseract from PIL import Image @@ -14,3 +16,9 @@ def image_to_string(image_data): Returns a plain text translation of an image """ return pytesseract.image_to_string(Image.open(image_data)) + + def image_to_tsv(image_data): + """ + Returns tsv formatted output + """ + return pytesseract.image_to_data(Image.open(BytesIO(image_data))) From 13c5fe56caf9d69fa86ec868a4cb4c987604711f Mon Sep 17 00:00:00 2001 From: Miroito Date: Thu, 14 Jul 2022 12:19:44 +0200 Subject: [PATCH 04/62] fix frontend linting issues --- frontend/pages/recipe/_slug/ocr-editor.vue | 5 +---- frontend/pages/recipe/create/ocr.vue | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/pages/recipe/_slug/ocr-editor.vue b/frontend/pages/recipe/_slug/ocr-editor.vue index 358c3cf6eb3..7af5b400260 100644 --- a/frontend/pages/recipe/_slug/ocr-editor.vue +++ b/frontend/pages/recipe/_slug/ocr-editor.vue @@ -182,13 +182,12 @@ import { onMounted, reactive, toRefs, - useContext, useRouter, + nextTick, } from "@nuxtjs/composition-api"; import { until } from "@vueuse/core"; import { invoke } from "@vueuse/shared"; import draggable from "vuedraggable"; -import { nextTick } from "vue"; import { useUserApi, useStaticRoutes } from "~/composables/api"; import { useRecipe } from "~/composables/recipes"; import { OcrTsvResponse } from "~/types/api-types/ocr"; @@ -269,13 +268,11 @@ export default defineComponent({ RecipeActionMenu, }, setup() { - const { $globals } = useContext(); const route = useRoute(); const router = useRouter(); const slug = route.value.params.slug; const api = useUserApi(); - const loadingTsv = ref(true); const tsv = ref([]); const { recipe, loading, fetchRecipe } = useRecipe(slug); diff --git a/frontend/pages/recipe/create/ocr.vue b/frontend/pages/recipe/create/ocr.vue index cc81b03dfaf..0d9527deb12 100644 --- a/frontend/pages/recipe/create/ocr.vue +++ b/frontend/pages/recipe/create/ocr.vue @@ -42,7 +42,7 @@ export default defineComponent({ async function createByOcr(file: File) { console.log("file: ", file); const { response } = await api.recipes.createFromOcr(file); - // @ts-ignore + // @ts-ignore returns a string and not a full Recipe handleResponse(response); } From 362cafd39d2bab93b399707fbcfa762264d13798 Mon Sep 17 00:00:00 2001 From: Miroito Date: Sat, 16 Jul 2022 12:22:41 +0200 Subject: [PATCH 05/62] Add service unit tests --- .pre-commit-config.yaml | 1 + tests/data/images/test-ocr.png | Bin 0 -> 11513 bytes tests/data/text/test-ocr.tsv | 73 ++++++++++++++++++ tests/data/text/test-ocr.txt | 9 +++ .../services_tests/test_ocr_service.py | 15 ++++ 5 files changed, 98 insertions(+) create mode 100644 tests/data/images/test-ocr.png create mode 100644 tests/data/text/test-ocr.tsv create mode 100644 tests/data/text/test-ocr.txt create mode 100644 tests/unit_tests/services_tests/test_ocr_service.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc3cf44705b..4b6eabec8c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,6 +9,7 @@ repos: - id: check-toml - id: end-of-file-fixer - id: trailing-whitespace + exclude-types: ["txt","tsv"] - repo: https://github.com/sondrelg/pep585-upgrade rev: "v1.0.1" # Use the sha / tag you want to point at hooks: diff --git a/tests/data/images/test-ocr.png b/tests/data/images/test-ocr.png new file mode 100644 index 0000000000000000000000000000000000000000..1b699c9778d0b7b691845573347eac2e213664cb GIT binary patch literal 11513 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 Date: Sat, 16 Jul 2022 14:07:00 +0200 Subject: [PATCH 06/62] Add split text modes & single ingredient/instruction editing --- .../Domain/Recipe/RecipeIngredientEditor.vue | 11 +- .../Domain/Recipe/RecipeInstructions.vue | 1 + frontend/pages/recipe/_slug/ocr-editor.vue | 131 +++++++++++++++--- frontend/utils/icons/icons.ts | 8 +- 4 files changed, 130 insertions(+), 21 deletions(-) diff --git a/frontend/components/Domain/Recipe/RecipeIngredientEditor.vue b/frontend/components/Domain/Recipe/RecipeIngredientEditor.vue index 78f3e14141b..faea6c1002b 100644 --- a/frontend/components/Domain/Recipe/RecipeIngredientEditor.vue +++ b/frontend/components/Domain/Recipe/RecipeIngredientEditor.vue @@ -8,6 +8,7 @@ class="mx-1 mt-3 mb-4" :placeholder="$t('recipe.section-title')" style="max-width: 500px" + @click="$emit('clickIngredientField', 'title')" > @@ -81,7 +82,15 @@
- + {{ $globals.icons.arrowUpDown }} diff --git a/frontend/components/Domain/Recipe/RecipeInstructions.vue b/frontend/components/Domain/Recipe/RecipeInstructions.vue index 6e5fb619c2b..d569bc7ac56 100644 --- a/frontend/components/Domain/Recipe/RecipeInstructions.vue +++ b/frontend/components/Domain/Recipe/RecipeInstructions.vue @@ -176,6 +176,7 @@ blur: imageUploadMode, }" @drop.stop.prevent="handleImageDrop(index, $event)" + @click="$emit('clickInstructionField', `${index}.text`)" > Selection mode - + + + diff --git a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/index.ts b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/index.ts new file mode 100644 index 00000000000..ff8b655f3d1 --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/index.ts @@ -0,0 +1,3 @@ +import RecipeOcrEditorPage from "./RecipeOcrEditorPage.vue"; + +export default RecipeOcrEditorPage; diff --git a/frontend/pages/recipe/_slug/ocr-editor.vue b/frontend/pages/recipe/_slug/ocr-editor.vue index 408358332af..97edfd8c6cb 100644 --- a/frontend/pages/recipe/_slug/ocr-editor.vue +++ b/frontend/pages/recipe/_slug/ocr-editor.vue @@ -1,839 +1,25 @@ diff --git a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageHelp.vue b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageHelp.vue new file mode 100644 index 00000000000..9205be67eec --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageHelp.vue @@ -0,0 +1,53 @@ + + From ac9da82a663a930f8e252ae68bb414b1843b5da5 Mon Sep 17 00:00:00 2001 From: Miroito Date: Sat, 24 Sep 2022 17:11:13 +0200 Subject: [PATCH 60/62] Safeguard recipe properties access --- .../Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue index 4bbbdc4f7c6..4a324f31d8a 100644 --- a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue +++ b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue @@ -326,7 +326,9 @@ export default defineComponent({ } const canvasSetText = function () { - setPropertyValueByPath(props.recipe, state.selectedRecipeField, state.canvasSelectedText); + if (state.selectedRecipeField !== "") { + setPropertyValueByPath(props.recipe, state.selectedRecipeField, state.canvasSelectedText); + } }; function updateSelectedText(value: string) { From ad59df2a645bf00dfdf2900cd4d3d5fd24b0e0a5 Mon Sep 17 00:00:00 2001 From: Miroito Date: Sat, 24 Sep 2022 17:41:16 +0200 Subject: [PATCH 61/62] Add loading frontend animation due to longer request time --- .../Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue index 4a324f31d8a..762bb79f679 100644 --- a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue +++ b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue @@ -7,6 +7,12 @@ > +
+ + + {{ loadingText }} + +