From ca9f66ee2448f9786efb866d2d5aa8a33c7780a4 Mon Sep 17 00:00:00 2001
From: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Date: Thu, 14 Dec 2023 20:26:43 -0600
Subject: [PATCH] feat: Remove OCR Support (#2838)
* remove ocr package
* remove tesseract
* remove OCR from app
* remove OCR from tests
* fix docs
---
.github/workflows/partial-backend.yml | 2 +-
docker/Dockerfile | 2 -
docs/docs/overrides/api.html | 2 +-
.../Domain/Recipe/RecipeActionMenu.vue | 14 -
.../RecipeOcrEditorPage.vue | 390 --------------
.../RecipeOcrEditorPageCanvas.vue | 488 ------------------
.../RecipeOcrEditorPageHelp.vue | 54 --
.../Recipe/RecipeOcrEditorPage/index.ts | 3 -
.../RecipePageParts/RecipePageHeader.vue | 15 +-
frontend/lib/api/client-user.ts | 5 -
frontend/lib/api/types/ocr.ts | 25 -
frontend/lib/api/types/recipe.ts | 1 -
frontend/lib/api/user/ocr.ts | 16 -
frontend/lib/api/user/recipes/recipe.ts | 10 -
.../pages/g/_groupSlug/r/_slug/ocr-editor.vue | 51 --
frontend/pages/g/_groupSlug/r/create.vue | 5 -
frontend/pages/g/_groupSlug/r/create/ocr.vue | 85 ---
frontend/types/ocr-types.ts | 73 ---
mealie/routes/__init__.py | 2 -
mealie/routes/ocr/__init__.py | 7 -
mealie/routes/ocr/pytesseract.py | 37 --
mealie/routes/recipe/recipe_crud_routes.py | 37 --
mealie/schema/ocr/__init__.py | 7 -
mealie/schema/ocr/ocr.py | 21 -
mealie/schema/recipe/recipe.py | 1 -
mealie/services/ocr/__init__.py | 0
mealie/services/ocr/pytesseract.py | 56 --
poetry.lock | 41 +-
pyproject.toml | 1 -
tests/data/images/test-ocr.png | Bin 11513 -> 0 bytes
tests/data/text/test-ocr.tsv | 73 ---
tests/data/text/test-ocr.txt | 9 -
.../services_tests/test_ocr_service.py | 58 ---
tests/utils/api_routes/__init__.py | 8 -
34 files changed, 29 insertions(+), 1570 deletions(-)
delete mode 100644 frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue
delete mode 100644 frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageCanvas.vue
delete mode 100644 frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageHelp.vue
delete mode 100644 frontend/components/Domain/Recipe/RecipeOcrEditorPage/index.ts
delete mode 100644 frontend/lib/api/types/ocr.ts
delete mode 100644 frontend/lib/api/user/ocr.ts
delete mode 100644 frontend/pages/g/_groupSlug/r/_slug/ocr-editor.vue
delete mode 100644 frontend/pages/g/_groupSlug/r/create/ocr.vue
delete mode 100644 frontend/types/ocr-types.ts
delete mode 100644 mealie/routes/ocr/__init__.py
delete mode 100644 mealie/routes/ocr/pytesseract.py
delete mode 100644 mealie/schema/ocr/__init__.py
delete mode 100644 mealie/schema/ocr/ocr.py
delete mode 100644 mealie/services/ocr/__init__.py
delete mode 100644 mealie/services/ocr/pytesseract.py
delete mode 100644 tests/data/images/test-ocr.png
delete mode 100644 tests/data/text/test-ocr.tsv
delete mode 100644 tests/data/text/test-ocr.txt
delete mode 100644 tests/unit_tests/services_tests/test_ocr_service.py
diff --git a/.github/workflows/partial-backend.yml b/.github/workflows/partial-backend.yml
index 80afa791eb6b..154d98626b51 100644
--- a/.github/workflows/partial-backend.yml
+++ b/.github/workflows/partial-backend.yml
@@ -67,7 +67,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
- sudo apt-get install libsasl2-dev libldap2-dev libssl-dev tesseract-ocr-all
+ sudo apt-get install libsasl2-dev libldap2-dev libssl-dev
poetry install
poetry add "psycopg2-binary==2.8.6"
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' || steps.cache-validate.outputs.cache-hit-success != 'true'
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 9594c0faffb2..ee422ff643eb 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -50,7 +50,6 @@ RUN apt-get update \
build-essential \
libpq-dev \
libwebp-dev \
- tesseract-ocr-all \
# LDAP Dependencies
libsasl2-dev libldap2-dev libssl-dev \
gnupg gnupg2 gnupg1 \
@@ -89,7 +88,6 @@ RUN apt-get update \
&& apt-get install --no-install-recommends -y \
gosu \
iproute2 \
- tesseract-ocr-all \
libldap-common \
&& rm -rf /var/lib/apt/lists/*
diff --git a/docs/docs/overrides/api.html b/docs/docs/overrides/api.html
index a54d6bf3c398..83038e2aead5 100644
--- a/docs/docs/overrides/api.html
+++ b/docs/docs/overrides/api.html
@@ -14,7 +14,7 @@
diff --git a/frontend/components/Domain/Recipe/RecipeActionMenu.vue b/frontend/components/Domain/Recipe/RecipeActionMenu.vue
index c5b6a72e76cb..ad24e0ed8b13 100644
--- a/frontend/components/Domain/Recipe/RecipeActionMenu.vue
+++ b/frontend/components/Domain/Recipe/RecipeActionMenu.vue
@@ -102,7 +102,6 @@ const SAVE_EVENT = "save";
const DELETE_EVENT = "delete";
const CLOSE_EVENT = "close";
const JSON_EVENT = "json";
-const OCR_EVENT = "ocr";
export default defineComponent({
components: { RecipeContextMenu, RecipeFavoriteBadge, RecipeTimerMenu, RecipeTimelineBadge },
@@ -139,10 +138,6 @@ export default defineComponent({
type: Boolean,
default: false,
},
- showOcrButton: {
- type: Boolean,
- default: false,
- },
},
setup(props, context) {
const deleteDialog = ref(false);
@@ -175,15 +170,6 @@ export default defineComponent({
},
];
- if (props.showOcrButton) {
- editorButtons.splice(2, 0, {
- text: i18n.t("ocr-editor.ocr-editor"),
- icon: $globals.icons.eye,
- event: OCR_EVENT,
- color: "accent",
- });
- }
-
function emitHandler(event: string) {
switch (event) {
case CLOSE_EVENT:
diff --git a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue
deleted file mode 100644
index 28b96d609696..000000000000
--- a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPage.vue
+++ /dev/null
@@ -1,390 +0,0 @@
-
-
-
-
-
-
-
- {{ loadingText }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t("general.recipe") }}
-
-
- {{ $t("recipe.ingredients") }}
-
-
- {{ $t("recipe.instructions") }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t("general.new") }}
-
-
-
-
-
-
-
-
-
-
- {{ $t("general.new") }}
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageCanvas.vue b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageCanvas.vue
deleted file mode 100644
index bcba5f4fce97..000000000000
--- a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageCanvas.vue
+++ /dev/null
@@ -1,488 +0,0 @@
-
-
-
-
- {{ section.sectionTitle }}
-
-
-
-
-
- {{ icon.icon }}
-
-
-
- {{ icon.tooltip }}
-
-
-
-
-
- {{ $t("general.save") }}
-
-
- {{ $t("general.close") }}
-
-
-
-
- {{ selectedText.trim() }}
-
-
-
-
-
diff --git a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageHelp.vue b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageHelp.vue
deleted file mode 100644
index 39ddf6e8338f..000000000000
--- a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageHelp.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
- {{ $globals.icons.help }}
-
- {{ $t("ocr-editor.help.help") }}
-
-
-
- {{ $t("ocr-editor.help.mouse-modes") }}
-
-
- {{ $globals.icons.selectMode }} {{ $t("ocr-editor.help.selection-mode") }}
-
- {{ $t("ocr-editor.help.selection-mode") }}
-
- - {{ $t("ocr-editor.help.selection-mode-steps.draw") }}
- - {{ $t("ocr-editor.help.selection-mode-steps.click") }}
- - {{ $t("ocr-editor.help.selection-mode-steps.result") }}
-
-
- {{ $globals.icons.panAndZoom }} {{ $t("ocr-editor.help.pan-and-zoom-mode") }}
-
- {{ $t("ocr-editor.help.pan-and-zoom-desc") }}
- {{ $t("ocr-editor.help.split-text-mode") }}
-
-
- {{ $globals.icons.preserveLines }}
- {{ $t("ocr-editor.help.split-modes.line-mode") }}
-
-
- {{ $t("ocr-editor.help.split-modes.line-mode-desc") }}
-
-
- {{ $globals.icons.preserveBlocks }}
- {{ $t("ocr-editor.help.split-modes.block-mode") }}
-
-
- {{ $t("ocr-editor.help.split-modes.block-mode-desc") }}
-
-
- {{ $globals.icons.flatten }} {{ $t("ocr-editor.help.split-modes.flat-mode") }}
-
- {{ $t("ocr-editor.help.split-modes.flat-mode-desc") }}
-
-
-
-
-
diff --git a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/index.ts b/frontend/components/Domain/Recipe/RecipeOcrEditorPage/index.ts
deleted file mode 100644
index ff8b655f3d15..000000000000
--- a/frontend/components/Domain/Recipe/RecipeOcrEditorPage/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import RecipeOcrEditorPage from "./RecipeOcrEditorPage.vue";
-
-export default RecipeOcrEditorPage;
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue
index c9e4934534e0..b17cab0ca17a 100644
--- a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue
@@ -50,7 +50,6 @@
:logged-in="isOwnGroup"
:open="isEditMode"
:recipe-id="recipe.id"
- :show-ocr-button="recipe.isOcrRecipe"
class="ml-auto mt-n8 pb-4"
@close="setMode(PageMode.VIEW)"
@json="toggleEditMode()"
@@ -58,13 +57,12 @@
@save="$emit('save')"
@delete="$emit('delete')"
@print="printRecipe"
- @ocr="goToOcrEditor"
/>
-
-
diff --git a/frontend/pages/g/_groupSlug/r/create.vue b/frontend/pages/g/_groupSlug/r/create.vue
index 72463ef0ff0f..dad973528bc1 100644
--- a/frontend/pages/g/_groupSlug/r/create.vue
+++ b/frontend/pages/g/_groupSlug/r/create.vue
@@ -52,11 +52,6 @@ export default defineComponent({
text: i18n.tc("recipe.import-with-zip"),
value: "zip",
},
- {
- icon: $globals.icons.fileImage,
- text: i18n.tc("recipe.create-recipe-from-an-image"),
- value: "ocr",
- },
{
icon: $globals.icons.link,
text: i18n.tc("recipe.bulk-url-import"),
diff --git a/frontend/pages/g/_groupSlug/r/create/ocr.vue b/frontend/pages/g/_groupSlug/r/create/ocr.vue
deleted file mode 100644
index 63acd2b61243..000000000000
--- a/frontend/pages/g/_groupSlug/r/create/ocr.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-
{{ $t('recipe.create-recipe-from-an-image') }}
-
- {{ $t('recipe.create-a-recipe-by-uploading-a-scan') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/types/ocr-types.ts b/frontend/types/ocr-types.ts
deleted file mode 100644
index 2b363cf239fe..000000000000
--- a/frontend/types/ocr-types.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { OcrTsvResponse } from "~/lib/api/types/ocr";
-import { Recipe } from "~/lib/api/types/recipe";
-
-export type CanvasRect = {
- startX: number;
- startY: number;
- w: number;
- h: number;
-};
-
-export type ImagePosition = {
- sx: number;
- sy: number;
- sWidth: number;
- sHeight: number;
- dx: number;
- dy: number;
- dWidth: number;
- dHeight: number;
- scale: number;
- panStartPoint: {
- x: number;
- y: number;
- };
-};
-
-export type Mouse = {
- current: {
- x: number;
- y: number;
- };
- down: boolean;
-};
-
-// https://stackoverflow.com/questions/58434389/export typescript-deep-keyof-of-a-nested-object/58436959#58436959
-type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]];
-
-type Join = K extends string | number
- ? P extends string | number
- ? `${K}${"" extends P ? "" : "."}${P}`
- : never
- : never;
-
-export type Leaves = [D] extends [never]
- ? never
- : T extends object
- ? { [K in keyof T]-?: Join> }[keyof T]
- : "";
-
-export type Paths = [D] extends [never]
- ? never
- : T extends object
- ? {
- [K in keyof T]-?: K extends string | number ? `${K}` | Join> : never;
- }[keyof T]
- : "";
-
-export type SelectedRecipeLeaves = Leaves;
-
-export type CanvasModes = "selection" | "panAndZoom";
-
-export type SelectedTextSplitModes = keyof OcrTsvResponse | "flatten";
-
-export type ToolbarIcons = {
- sectionTitle: string;
- eventHandler(mode: T): void;
- highlight: T;
- icons: {
- name: T;
- icon: string;
- tooltip: string;
- }[];
-}[];
diff --git a/mealie/routes/__init__.py b/mealie/routes/__init__.py
index e421c01ae5ab..849ec5b571f9 100644
--- a/mealie/routes/__init__.py
+++ b/mealie/routes/__init__.py
@@ -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)
diff --git a/mealie/routes/ocr/__init__.py b/mealie/routes/ocr/__init__.py
deleted file mode 100644
index e23bbc92ec8b..000000000000
--- a/mealie/routes/ocr/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-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
deleted file mode 100644
index d2ffdec62120..000000000000
--- a/mealie/routes/ocr/pytesseract.py
+++ /dev/null
@@ -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)
diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py
index bace402ebd2e..16b7bc687d0d 100644
--- a/mealie/routes/recipe/recipe_crud_routes.py
+++ b/mealie/routes/recipe/recipe_crud_routes.py
@@ -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
diff --git a/mealie/schema/ocr/__init__.py b/mealie/schema/ocr/__init__.py
deleted file mode 100644
index 1c28eee01c6d..000000000000
--- a/mealie/schema/ocr/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# This file is auto-generated by gen_schema_exports.py
-from .ocr import OcrAssetReq, OcrTsvResponse
-
-__all__ = [
- "OcrAssetReq",
- "OcrTsvResponse",
-]
diff --git a/mealie/schema/ocr/ocr.py b/mealie/schema/ocr/ocr.py
deleted file mode 100644
index fd5351110662..000000000000
--- a/mealie/schema/ocr/ocr.py
+++ /dev/null
@@ -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
diff --git a/mealie/schema/recipe/recipe.py b/mealie/schema/recipe/recipe.py
index ba49e00cd071..5b5cca65aed1 100644
--- a/mealie/schema/recipe/recipe.py
+++ b/mealie/schema/recipe/recipe.py
@@ -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 = []
diff --git a/mealie/services/ocr/__init__.py b/mealie/services/ocr/__init__.py
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/mealie/services/ocr/pytesseract.py b/mealie/services/ocr/pytesseract.py
deleted file mode 100644
index 83d84b676b09..000000000000
--- a/mealie/services/ocr/pytesseract.py
+++ /dev/null
@@ -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
diff --git a/poetry.lock b/poetry.lock
index 9bbd67e294de..3059793799bb 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -605,6 +605,7 @@ files = [
{file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"},
{file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"},
{file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"},
+ {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"},
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"},
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"},
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"},
@@ -613,6 +614,7 @@ files = [
{file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"},
{file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"},
{file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"},
+ {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"},
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"},
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"},
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"},
@@ -642,6 +644,7 @@ files = [
{file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"},
{file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"},
{file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"},
+ {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"},
{file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"},
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"},
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"},
@@ -650,6 +653,7 @@ files = [
{file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"},
{file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"},
{file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
+ {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"},
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"},
{file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"},
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"},
@@ -1862,21 +1866,6 @@ files = [
html5lib = "*"
rdflib = "*"
-[[package]]
-name = "pytesseract"
-version = "0.3.10"
-description = "Python-tesseract is a python wrapper for Google's Tesseract-OCR"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "pytesseract-0.3.10-py3-none-any.whl", hash = "sha256:8f22cc98f765bf13517ead0c70effedb46c153540d25783e04014f28b55a5fc6"},
- {file = "pytesseract-0.3.10.tar.gz", hash = "sha256:f1c3a8b0f07fd01a1085d451f5b8315be6eec1d5577a6796d46dc7a62bd4120f"},
-]
-
-[package.dependencies]
-packaging = ">=21.3"
-Pillow = ">=8.0.0"
-
[[package]]
name = "pytest"
version = "7.2.2"
@@ -2024,6 +2013,7 @@ files = [
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -2031,8 +2021,15 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+ {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -2049,6 +2046,7 @@ files = [
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -2056,6 +2054,7 @@ files = [
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -2972,6 +2971,16 @@ files = [
{file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"},
{file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"},
{file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"},
+ {file = "wrapt-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ecee4132c6cd2ce5308e21672015ddfed1ff975ad0ac8d27168ea82e71413f55"},
+ {file = "wrapt-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2020f391008ef874c6d9e208b24f28e31bcb85ccff4f335f15a3251d222b92d9"},
+ {file = "wrapt-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2feecf86e1f7a86517cab34ae6c2f081fd2d0dac860cb0c0ded96d799d20b335"},
+ {file = "wrapt-1.14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:240b1686f38ae665d1b15475966fe0472f78e71b1b4903c143a842659c8e4cb9"},
+ {file = "wrapt-1.14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9008dad07d71f68487c91e96579c8567c98ca4c3881b9b113bc7b33e9fd78b8"},
+ {file = "wrapt-1.14.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6447e9f3ba72f8e2b985a1da758767698efa72723d5b59accefd716e9e8272bf"},
+ {file = "wrapt-1.14.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:acae32e13a4153809db37405f5eba5bac5fbe2e2ba61ab227926a22901051c0a"},
+ {file = "wrapt-1.14.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49ef582b7a1152ae2766557f0550a9fcbf7bbd76f43fbdc94dd3bf07cc7168be"},
+ {file = "wrapt-1.14.1-cp311-cp311-win32.whl", hash = "sha256:358fe87cc899c6bb0ddc185bf3dbfa4ba646f05b1b0b9b5a27c2cb92c2cea204"},
+ {file = "wrapt-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:26046cd03936ae745a502abf44dac702a5e6880b2b01c29aea8ddf3353b68224"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"},
@@ -3025,4 +3034,4 @@ pgsql = ["psycopg2-binary"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
-content-hash = "5e0cc403c1ec022e6a75a4969dd97c55ce312daafd7d2024a3617eca25b2129f"
+content-hash = "984d725b667165ebbf82eeea32a38e4f4891192fc8c6be60864d25d5b36e7970"
diff --git a/pyproject.toml b/pyproject.toml
index 79cb8a450b50..b1fba8d7f90d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,7 +29,6 @@ passlib = "^1.7.4"
psycopg2-binary = { version = "^2.9.1", optional = true }
pydantic = "^1.10.4"
pyhumps = "^3.5.3"
-pytesseract = "^0.3.9"
python = "^3.10"
python-dateutil = "^2.8.2"
python-dotenv = "^1.0.0"
diff --git a/tests/data/images/test-ocr.png b/tests/data/images/test-ocr.png
deleted file mode 100644
index 1b699c9778d0b7b691845573347eac2e213664cb..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 11513
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