From c3af04d375c25277e06c40362de4b3f79009f283 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:29:19 +0100 Subject: [PATCH 1/9] :sparkles: add support for crop utility --- mindee/input/__init__.py | 2 - mindee/v2/__init__.py | 4 ++ mindee/v2/product/crop/__init__.py | 13 ++++ mindee/v2/product/crop/crop_box.py | 18 ++++++ mindee/v2/product/crop/crop_inference.py | 19 ++++++ mindee/v2/product/crop/crop_parameters.py | 9 +++ mindee/v2/product/crop/crop_response.py | 17 +++++ mindee/v2/product/crop/crop_result.py | 20 ++++++ tests/data | 2 +- tests/v2/utilities/__init__.py | 0 tests/v2/utilities/crop/__init__.py | 0 .../utilities/crop/test_crop_integration.py | 31 +++++++++ tests/v2/utilities/crop/test_crop_response.py | 63 +++++++++++++++++++ tests/v2/utilities/split/__init__.py | 0 .../split}/test_split_integration.py | 5 +- .../split}/test_split_response.py | 0 16 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 mindee/v2/product/crop/__init__.py create mode 100644 mindee/v2/product/crop/crop_box.py create mode 100644 mindee/v2/product/crop/crop_inference.py create mode 100644 mindee/v2/product/crop/crop_parameters.py create mode 100644 mindee/v2/product/crop/crop_response.py create mode 100644 mindee/v2/product/crop/crop_result.py create mode 100644 tests/v2/utilities/__init__.py create mode 100644 tests/v2/utilities/crop/__init__.py create mode 100644 tests/v2/utilities/crop/test_crop_integration.py create mode 100644 tests/v2/utilities/crop/test_crop_response.py create mode 100644 tests/v2/utilities/split/__init__.py rename tests/v2/{parsing => utilities/split}/test_split_integration.py (87%) rename tests/v2/{parsing => utilities/split}/test_split_response.py (100%) diff --git a/mindee/input/__init__.py b/mindee/input/__init__.py index 31973802..1afea578 100644 --- a/mindee/input/__init__.py +++ b/mindee/input/__init__.py @@ -1,7 +1,6 @@ from mindee.input.local_response import LocalResponse from mindee.input.base_parameters import BaseParameters from mindee.input.inference_parameters import InferenceParameters -from mindee.v2.product.split.split_parameters import SplitParameters from mindee.input.page_options import PageOptions from mindee.input.polling_options import PollingOptions from mindee.input.sources.base_64_input import Base64Input @@ -26,6 +25,5 @@ "PathInput", "PollingOptions", "UrlInputSource", - "SplitParameters", "WorkflowOptions", ] diff --git a/mindee/v2/__init__.py b/mindee/v2/__init__.py index 136bbc42..368138f6 100644 --- a/mindee/v2/__init__.py +++ b/mindee/v2/__init__.py @@ -1,7 +1,11 @@ +from mindee.v2.product.crop.crop_parameters import CropParameters +from mindee.v2.product.crop.crop_response import CropResponse from mindee.v2.product.split.split_parameters import SplitParameters from mindee.v2.product.split.split_response import SplitResponse __all__ = [ + "CropParameters", + "CropResponse", "SplitResponse", "SplitParameters", ] diff --git a/mindee/v2/product/crop/__init__.py b/mindee/v2/product/crop/__init__.py new file mode 100644 index 00000000..f8d50d5e --- /dev/null +++ b/mindee/v2/product/crop/__init__.py @@ -0,0 +1,13 @@ +from mindee.v2.product.crop.crop_box import CropBox +from mindee.v2.product.crop.crop_inference import CropInference +from mindee.v2.product.crop.crop_parameters import CropParameters +from mindee.v2.product.crop.crop_response import CropResponse +from mindee.v2.product.crop.crop_result import CropResult + +__all__ = [ + "CropBox", + "CropInference", + "CropParameters", + "CropResponse", + "CropResult", +] diff --git a/mindee/v2/product/crop/crop_box.py b/mindee/v2/product/crop/crop_box.py new file mode 100644 index 00000000..bbd83a58 --- /dev/null +++ b/mindee/v2/product/crop/crop_box.py @@ -0,0 +1,18 @@ +from mindee.parsing.common.string_dict import StringDict +from mindee.parsing.v2.field.field_location import FieldLocation + + +class CropBox: + """Crop inference result.""" + + location: FieldLocation + """Page box of the crop inference.""" + object_type: str + """Document type of the crop inference.""" + + def __init__(self, server_response: StringDict): + self.location = FieldLocation(server_response["location"]) + self.object_type = server_response["object_type"] + + def __str__(self) -> str: + return f"* :Location: {self.location}\n :Object Type: {self.object_type}" diff --git a/mindee/v2/product/crop/crop_inference.py b/mindee/v2/product/crop/crop_inference.py new file mode 100644 index 00000000..5e3dd19f --- /dev/null +++ b/mindee/v2/product/crop/crop_inference.py @@ -0,0 +1,19 @@ +from mindee.parsing.common.string_dict import StringDict +from mindee.v2.parsing.inference.base_inference import BaseInference +from mindee.v2.product.crop.crop_result import CropResult + + +class CropInference(BaseInference): + """Crop inference result.""" + + result: CropResult + """Result of a crop inference.""" + _slug: str = "crop" + """Slug of the endpoint.""" + + def __init__(self, raw_response: StringDict) -> None: + super().__init__(raw_response) + self.result = CropResult(raw_response["result"]) + + def __str__(self) -> str: + return f"Inference\n#########\n{self.model}\n{self.file}\n{self.result}\n" diff --git a/mindee/v2/product/crop/crop_parameters.py b/mindee/v2/product/crop/crop_parameters.py new file mode 100644 index 00000000..ad2919aa --- /dev/null +++ b/mindee/v2/product/crop/crop_parameters.py @@ -0,0 +1,9 @@ +from mindee.input.base_parameters import BaseParameters + + +class CropParameters(BaseParameters): + """ + Parameters accepted by the crop utility v2 endpoint. + """ + + _slug: str = "utilities/crop" diff --git a/mindee/v2/product/crop/crop_response.py b/mindee/v2/product/crop/crop_response.py new file mode 100644 index 00000000..828baa9e --- /dev/null +++ b/mindee/v2/product/crop/crop_response.py @@ -0,0 +1,17 @@ +from mindee.parsing.common.string_dict import StringDict +from mindee.v2.parsing.inference import BaseResponse +from mindee.v2.product.crop.crop_inference import CropInference + + +class CropResponse(BaseResponse): + """Represent a crop inference response from Mindee V2 API.""" + + inference: CropInference + """Inference object for crop inference.""" + + _slug: str = "utilities/crop" + """Slug of the inference.""" + + def __init__(self, raw_response: StringDict) -> None: + super().__init__(raw_response) + self.inference = CropInference(raw_response["inference"]) diff --git a/mindee/v2/product/crop/crop_result.py b/mindee/v2/product/crop/crop_result.py new file mode 100644 index 00000000..3e3878be --- /dev/null +++ b/mindee/v2/product/crop/crop_result.py @@ -0,0 +1,20 @@ +from typing import List + +from mindee.parsing.common.string_dict import StringDict +from mindee.v2.product.crop.crop_box import CropBox + + +class CropResult: + """Crop result info.""" + + crops: List[CropBox] + + def __init__(self, raw_response: StringDict) -> None: + self.crops = [CropBox(crop) for crop in raw_response["crops"]] + + def __str__(self) -> str: + crops = "\n" + if len(self.crops) > 0: + crops += "\n\n".join([str(crop) for crop in self.crops]) + out_str = f"Crops\n======{crops}" + return out_str diff --git a/tests/data b/tests/data index e6495fb5..67ccc1d9 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit e6495fb50c992f9c4624ae14a0404c4c194e4519 +Subproject commit 67ccc1d9cf8b6263860f79eafbaa2e8b8dd7ac3f diff --git a/tests/v2/utilities/__init__.py b/tests/v2/utilities/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/v2/utilities/crop/__init__.py b/tests/v2/utilities/crop/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/v2/utilities/crop/test_crop_integration.py b/tests/v2/utilities/crop/test_crop_integration.py new file mode 100644 index 00000000..18eaaac5 --- /dev/null +++ b/tests/v2/utilities/crop/test_crop_integration.py @@ -0,0 +1,31 @@ +import os + +import pytest + +from mindee import ClientV2, PathInput +from mindee.v2 import CropParameters, CropResponse +from tests.utils import V2_UTILITIES_DATA_DIR + + +@pytest.fixture(scope="session") +def crop_model_id() -> str: + """Identifier of the Financial Document model, supplied through an env var.""" + return os.getenv("MINDEE_V2_SE_TESTS_CROP_MODEL_ID") + + +@pytest.fixture(scope="session") +def v2_client() -> ClientV2: + return ClientV2() + + +@pytest.mark.integration +@pytest.mark.v2 +def test_crop_blank(v2_client: ClientV2, crop_model_id: str): + input_source = PathInput(V2_UTILITIES_DATA_DIR / "crop" / "default_sample.jpg") + response = v2_client.enqueue_and_get_result( + CropResponse, input_source, CropParameters(crop_model_id) + ) + assert response.inference is not None + assert response.inference.file.name == "default_sample.jpg" + assert response.inference.result.crops + assert len(response.inference.result.crops) == 2 diff --git a/tests/v2/utilities/crop/test_crop_response.py b/tests/v2/utilities/crop/test_crop_response.py new file mode 100644 index 00000000..0f74e2e2 --- /dev/null +++ b/tests/v2/utilities/crop/test_crop_response.py @@ -0,0 +1,63 @@ +import pytest + +from mindee import LocalResponse +from mindee.v2.product.crop.crop_box import CropBox +from mindee.v2.product.crop import CropInference +from mindee.v2.product.crop.crop_response import CropResponse +from mindee.v2.product.crop.crop_result import CropResult +from tests.utils import V2_UTILITIES_DATA_DIR + + +@pytest.mark.v2 +def test_crop_single(): + input_inference = LocalResponse(V2_UTILITIES_DATA_DIR / "crop" / "crop_single.json") + crop_response = input_inference.deserialize_response(CropResponse) + assert isinstance(crop_response.inference, CropInference) + assert crop_response.inference.result.crops + assert len(crop_response.inference.result.crops[0].location.polygon) == 4 + assert crop_response.inference.result.crops[0].location.polygon[0][0] == 0.15 + assert crop_response.inference.result.crops[0].location.polygon[0][1] == 0.254 + assert crop_response.inference.result.crops[0].location.polygon[1][0] == 0.85 + assert crop_response.inference.result.crops[0].location.polygon[1][1] == 0.254 + assert crop_response.inference.result.crops[0].location.polygon[2][0] == 0.85 + assert crop_response.inference.result.crops[0].location.polygon[2][1] == 0.947 + assert crop_response.inference.result.crops[0].location.polygon[3][0] == 0.15 + assert crop_response.inference.result.crops[0].location.polygon[3][1] == 0.947 + assert crop_response.inference.result.crops[0].location.page == 0 + assert crop_response.inference.result.crops[0].object_type == "invoice" + + +@pytest.mark.v2 +def test_crop_multiple(): + input_inference = LocalResponse( + V2_UTILITIES_DATA_DIR / "crop" / "crop_multiple.json" + ) + crop_response = input_inference.deserialize_response(CropResponse) + assert isinstance(crop_response.inference, CropInference) + assert isinstance(crop_response.inference.result, CropResult) + assert isinstance(crop_response.inference.result.crops[0], CropBox) + assert len(crop_response.inference.result.crops) == 2 + + assert len(crop_response.inference.result.crops[0].location.polygon) == 4 + assert crop_response.inference.result.crops[0].location.polygon[0][0] == 0.214 + assert crop_response.inference.result.crops[0].location.polygon[0][1] == 0.079 + assert crop_response.inference.result.crops[0].location.polygon[1][0] == 0.476 + assert crop_response.inference.result.crops[0].location.polygon[1][1] == 0.079 + assert crop_response.inference.result.crops[0].location.polygon[2][0] == 0.476 + assert crop_response.inference.result.crops[0].location.polygon[2][1] == 0.979 + assert crop_response.inference.result.crops[0].location.polygon[3][0] == 0.214 + assert crop_response.inference.result.crops[0].location.polygon[3][1] == 0.979 + assert crop_response.inference.result.crops[0].location.page == 0 + assert crop_response.inference.result.crops[0].object_type == "invoice" + + assert len(crop_response.inference.result.crops[1].location.polygon) == 4 + assert crop_response.inference.result.crops[1].location.polygon[0][0] == 0.547 + assert crop_response.inference.result.crops[1].location.polygon[0][1] == 0.15 + assert crop_response.inference.result.crops[1].location.polygon[1][0] == 0.862 + assert crop_response.inference.result.crops[1].location.polygon[1][1] == 0.15 + assert crop_response.inference.result.crops[1].location.polygon[2][0] == 0.862 + assert crop_response.inference.result.crops[1].location.polygon[2][1] == 0.97 + assert crop_response.inference.result.crops[1].location.polygon[3][0] == 0.547 + assert crop_response.inference.result.crops[1].location.polygon[3][1] == 0.97 + assert crop_response.inference.result.crops[1].location.page == 0 + assert crop_response.inference.result.crops[1].object_type == "invoice" diff --git a/tests/v2/utilities/split/__init__.py b/tests/v2/utilities/split/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/v2/parsing/test_split_integration.py b/tests/v2/utilities/split/test_split_integration.py similarity index 87% rename from tests/v2/parsing/test_split_integration.py rename to tests/v2/utilities/split/test_split_integration.py index bd577505..04854e77 100644 --- a/tests/v2/parsing/test_split_integration.py +++ b/tests/v2/utilities/split/test_split_integration.py @@ -3,8 +3,7 @@ import pytest from mindee import ClientV2, PathInput -from mindee.input import SplitParameters -from mindee.v2 import SplitResponse +from mindee.v2 import SplitParameters, SplitResponse from tests.utils import V2_UTILITIES_DATA_DIR @@ -25,7 +24,7 @@ def test_split_blank(v2_client: ClientV2, split_model_id: str): input_source = PathInput(V2_UTILITIES_DATA_DIR / "split" / "default_sample.pdf") response = v2_client.enqueue_and_get_result( SplitResponse, input_source, SplitParameters(split_model_id) - ) # Note: do not use blank_1.pdf for this. + ) assert response.inference is not None assert response.inference.file.name == "default_sample.pdf" assert response.inference.result.splits diff --git a/tests/v2/parsing/test_split_response.py b/tests/v2/utilities/split/test_split_response.py similarity index 100% rename from tests/v2/parsing/test_split_response.py rename to tests/v2/utilities/split/test_split_response.py From 4a2199e867f945239da17889e583112a509be6e8 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:05:23 +0100 Subject: [PATCH 2/9] :sparkles: add support for OCR V2 (+ fix tests) --- .github/workflows/_test-code-samples.yml | 2 +- .github/workflows/_test-integrations.yml | 3 + .../extras/code_samples/v2_classification.txt | 27 ++++ docs/extras/code_samples/v2_crop.txt | 27 ++++ docs/extras/code_samples/v2_ocr.txt | 27 ++++ mindee/v2/__init__.py | 6 +- mindee/v2/product/__init__.py | 8 ++ mindee/v2/product/crop/crop_box.py | 4 +- mindee/v2/product/ocr/__init__.py | 15 ++ mindee/v2/product/ocr/ocr_inference.py | 19 +++ mindee/v2/product/ocr/ocr_page.py | 25 ++++ mindee/v2/product/ocr/ocr_parameters.py | 9 ++ mindee/v2/product/ocr/ocr_response.py | 17 +++ mindee/v2/product/ocr/ocr_result.py | 21 +++ mindee/v2/product/ocr/ocr_word.py | 17 +++ tests/test_code_samples.sh | 29 +++- .../utilities/crop/test_crop_integration.py | 2 +- tests/v2/utilities/ocr/__init__.py | 0 .../v2/utilities/ocr/test_ocr_integration.py | 34 +++++ tests/v2/utilities/ocr/test_ocr_response.py | 132 ++++++++++++++++++ .../utilities/split/test_split_integration.py | 2 +- 21 files changed, 419 insertions(+), 7 deletions(-) create mode 100644 docs/extras/code_samples/v2_classification.txt create mode 100644 docs/extras/code_samples/v2_crop.txt create mode 100644 docs/extras/code_samples/v2_ocr.txt create mode 100644 mindee/v2/product/ocr/__init__.py create mode 100644 mindee/v2/product/ocr/ocr_inference.py create mode 100644 mindee/v2/product/ocr/ocr_page.py create mode 100644 mindee/v2/product/ocr/ocr_parameters.py create mode 100644 mindee/v2/product/ocr/ocr_response.py create mode 100644 mindee/v2/product/ocr/ocr_result.py create mode 100644 mindee/v2/product/ocr/ocr_word.py create mode 100644 tests/v2/utilities/ocr/__init__.py create mode 100644 tests/v2/utilities/ocr/test_ocr_integration.py create mode 100644 tests/v2/utilities/ocr/test_ocr_response.py diff --git a/.github/workflows/_test-code-samples.yml b/.github/workflows/_test-code-samples.yml index 171e0fed..6e77ab8a 100644 --- a/.github/workflows/_test-code-samples.yml +++ b/.github/workflows/_test-code-samples.yml @@ -40,7 +40,7 @@ jobs: - name: Tests code samples run: | - ./tests/test_code_samples.sh ${{ secrets.MINDEE_ACCOUNT_SE_TESTS }} ${{ secrets.MINDEE_ENDPOINT_SE_TESTS }} ${{ secrets.MINDEE_API_KEY_SE_TESTS }} ${{ secrets.MINDEE_V2_SE_TESTS_API_KEY }} ${{ secrets.MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID }} + ./tests/test_code_samples.sh ${{ secrets.MINDEE_ACCOUNT_SE_TESTS }} ${{ secrets.MINDEE_ENDPOINT_SE_TESTS }} ${{ secrets.MINDEE_API_KEY_SE_TESTS }} ${{ secrets.MINDEE_V2_SE_TESTS_API_KEY }} ${{ secrets.MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_CROP_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_CROP_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID }} - name: Notify Slack Action on Failure uses: ravsamhq/notify-slack-action@2.3.0 diff --git a/.github/workflows/_test-integrations.yml b/.github/workflows/_test-integrations.yml index 70b7b0e9..eac22534 100644 --- a/.github/workflows/_test-integrations.yml +++ b/.github/workflows/_test-integrations.yml @@ -49,7 +49,10 @@ jobs: MINDEE_V2_API_KEY: ${{ secrets.MINDEE_V2_SE_TESTS_API_KEY }} MINDEE_V2_FINDOC_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID }} MINDEE_V2_SE_TESTS_BLANK_PDF_URL: ${{ secrets.MINDEE_V2_SE_TESTS_BLANK_PDF_URL }} + MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID }} + MINDEE_V2_SE_TESTS_CROP_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_CROP_MODEL_ID }} MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID }} + MINDEE_V2_SE_TESTS_OCR_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_OCR_MODEL_ID }} run: | pytest --cov mindee -m integration diff --git a/docs/extras/code_samples/v2_classification.txt b/docs/extras/code_samples/v2_classification.txt new file mode 100644 index 00000000..7334648c --- /dev/null +++ b/docs/extras/code_samples/v2_classification.txt @@ -0,0 +1,27 @@ +from mindee import ClientV2, ClassificationParameters, ClassificationResponse, PathInput + +input_path = "/path/to/the/file.ext" +api_key = "MY_API_KEY" +model_id = "MY_CLASSIFICATION_MODEL_ID" + +# Init a new client +mindee_client = ClientV2(api_key) + +# Set inference parameters +params = ClassificationParameters( + # ID of the model, required. + model_id=model_id, +) + +# Load a file from disk +input_source = PathInput(input_path) + +# Send for processing +response = mindee_client.enqueue_and_get_result( + ClassificationResponse, + input_source, + params, +) + +# Print a brief summary of the parsed data +print(response.inference) diff --git a/docs/extras/code_samples/v2_crop.txt b/docs/extras/code_samples/v2_crop.txt new file mode 100644 index 00000000..2ab55873 --- /dev/null +++ b/docs/extras/code_samples/v2_crop.txt @@ -0,0 +1,27 @@ +from mindee import ClientV2, CropParameters, CropResponse, PathInput + +input_path = "/path/to/the/file.ext" +api_key = "MY_API_KEY" +model_id = "MY_CROP_MODEL_ID" + +# Init a new client +mindee_client = ClientV2(api_key) + +# Set inference parameters +params = CropParameters( + # ID of the model, required. + model_id=model_id, +) + +# Load a file from disk +input_source = PathInput(input_path) + +# Send for processing +response = mindee_client.enqueue_and_get_result( + CropResponse, + input_source, + params, +) + +# Print a brief summary of the parsed data +print(response.inference) diff --git a/docs/extras/code_samples/v2_ocr.txt b/docs/extras/code_samples/v2_ocr.txt new file mode 100644 index 00000000..de3ae170 --- /dev/null +++ b/docs/extras/code_samples/v2_ocr.txt @@ -0,0 +1,27 @@ +from mindee import ClientV2, OCRParameters, OCRResponse, PathInput + +input_path = "/path/to/the/file.ext" +api_key = "MY_API_KEY" +model_id = "MY_OCR_MODEL_ID" + +# Init a new client +mindee_client = ClientV2(api_key) + +# Set inference parameters +params = OCRParameters( + # ID of the model, required. + model_id=model_id, +) + +# Load a file from disk +input_source = PathInput(input_path) + +# Send for processing +response = mindee_client.enqueue_and_get_result( + OCRResponse, + input_source, + params, +) + +# Print a brief summary of the parsed data +print(response.inference) diff --git a/mindee/v2/__init__.py b/mindee/v2/__init__.py index 368138f6..1986a45b 100644 --- a/mindee/v2/__init__.py +++ b/mindee/v2/__init__.py @@ -1,11 +1,15 @@ from mindee.v2.product.crop.crop_parameters import CropParameters from mindee.v2.product.crop.crop_response import CropResponse +from mindee.v2.product.ocr.ocr_parameters import OCRParameters +from mindee.v2.product.ocr.ocr_response import OCRResponse from mindee.v2.product.split.split_parameters import SplitParameters from mindee.v2.product.split.split_response import SplitResponse __all__ = [ - "CropParameters", "CropResponse", + "CropParameters", + "OCRResponse", + "OCRParameters", "SplitResponse", "SplitParameters", ] diff --git a/mindee/v2/product/__init__.py b/mindee/v2/product/__init__.py index 136bbc42..1986a45b 100644 --- a/mindee/v2/product/__init__.py +++ b/mindee/v2/product/__init__.py @@ -1,7 +1,15 @@ +from mindee.v2.product.crop.crop_parameters import CropParameters +from mindee.v2.product.crop.crop_response import CropResponse +from mindee.v2.product.ocr.ocr_parameters import OCRParameters +from mindee.v2.product.ocr.ocr_response import OCRResponse from mindee.v2.product.split.split_parameters import SplitParameters from mindee.v2.product.split.split_response import SplitResponse __all__ = [ + "CropResponse", + "CropParameters", + "OCRResponse", + "OCRParameters", "SplitResponse", "SplitParameters", ] diff --git a/mindee/v2/product/crop/crop_box.py b/mindee/v2/product/crop/crop_box.py index bbd83a58..62a32840 100644 --- a/mindee/v2/product/crop/crop_box.py +++ b/mindee/v2/product/crop/crop_box.py @@ -6,9 +6,9 @@ class CropBox: """Crop inference result.""" location: FieldLocation - """Page box of the crop inference.""" + """Location which includes cropping coordinates for the detected object, within the source document.""" object_type: str - """Document type of the crop inference.""" + """Type or classification of the detected object.""" def __init__(self, server_response: StringDict): self.location = FieldLocation(server_response["location"]) diff --git a/mindee/v2/product/ocr/__init__.py b/mindee/v2/product/ocr/__init__.py new file mode 100644 index 00000000..b67e467a --- /dev/null +++ b/mindee/v2/product/ocr/__init__.py @@ -0,0 +1,15 @@ +from mindee.v2.product.ocr.ocr_inference import OCRInference +from mindee.v2.product.ocr.ocr_page import OCRPage +from mindee.v2.product.ocr.ocr_parameters import OCRParameters +from mindee.v2.product.ocr.ocr_response import OCRResponse +from mindee.v2.product.ocr.ocr_result import OCRResult +from mindee.v2.product.ocr.ocr_word import OCRWord + +__all__ = [ + "OCRInference", + "OCRPage", + "OCRParameters", + "OCRResponse", + "OCRResult", + "OCRWord", +] diff --git a/mindee/v2/product/ocr/ocr_inference.py b/mindee/v2/product/ocr/ocr_inference.py new file mode 100644 index 00000000..ffe7c888 --- /dev/null +++ b/mindee/v2/product/ocr/ocr_inference.py @@ -0,0 +1,19 @@ +from mindee.parsing.common.string_dict import StringDict +from mindee.v2.parsing.inference.base_inference import BaseInference +from mindee.v2.product.ocr.ocr_result import OCRResult + + +class OCRInference(BaseInference): + """OCR inference result.""" + + result: OCRResult + """Result of a ocr inference.""" + _slug: str = "ocr" + """Slug of the endpoint.""" + + def __init__(self, raw_response: StringDict) -> None: + super().__init__(raw_response) + self.result = OCRResult(raw_response["result"]) + + def __str__(self) -> str: + return f"Inference\n#########\n{self.model}\n{self.file}\n{self.result}\n" diff --git a/mindee/v2/product/ocr/ocr_page.py b/mindee/v2/product/ocr/ocr_page.py new file mode 100644 index 00000000..6c93bbd4 --- /dev/null +++ b/mindee/v2/product/ocr/ocr_page.py @@ -0,0 +1,25 @@ +from typing import List + +from mindee.parsing.common.string_dict import StringDict +from mindee.v2.product.ocr.ocr_word import OCRWord + + +class OCRPage: + """OCR result for a single page.""" + + words: List[OCRWord] + """List of words extracted from the document page.""" + content: str + """Full text content extracted from the document page.""" + + def __init__(self, raw_response: StringDict) -> None: + self.words = [OCRWord(word) for word in raw_response["words"]] + self.content = raw_response["content"] + + def __str__(self) -> str: + ocr_words = "\n" + if len(self.words) > 0: + ocr_words += "\n\n".join([str(word) for word in self.words]) + out_str = f"OCR Words\n======{ocr_words}" + out_str += f"\n\n:Content: {self.content}" + return out_str diff --git a/mindee/v2/product/ocr/ocr_parameters.py b/mindee/v2/product/ocr/ocr_parameters.py new file mode 100644 index 00000000..7ed8c263 --- /dev/null +++ b/mindee/v2/product/ocr/ocr_parameters.py @@ -0,0 +1,9 @@ +from mindee.input.base_parameters import BaseParameters + + +class OCRParameters(BaseParameters): + """ + Parameters accepted by the ocr utility v2 endpoint. + """ + + _slug: str = "utilities/ocr" diff --git a/mindee/v2/product/ocr/ocr_response.py b/mindee/v2/product/ocr/ocr_response.py new file mode 100644 index 00000000..318d507a --- /dev/null +++ b/mindee/v2/product/ocr/ocr_response.py @@ -0,0 +1,17 @@ +from mindee.parsing.common.string_dict import StringDict +from mindee.v2.parsing.inference import BaseResponse +from mindee.v2.product.ocr.ocr_inference import OCRInference + + +class OCRResponse(BaseResponse): + """Represent an OCR inference response from Mindee V2 API.""" + + inference: OCRInference + """Inference object for ocr inference.""" + + _slug: str = "utilities/ocr" + """Slug of the inference.""" + + def __init__(self, raw_response: StringDict) -> None: + super().__init__(raw_response) + self.inference = OCRInference(raw_response["inference"]) diff --git a/mindee/v2/product/ocr/ocr_result.py b/mindee/v2/product/ocr/ocr_result.py new file mode 100644 index 00000000..28c837c7 --- /dev/null +++ b/mindee/v2/product/ocr/ocr_result.py @@ -0,0 +1,21 @@ +from typing import List + +from mindee.parsing.common.string_dict import StringDict +from mindee.v2.product.ocr.ocr_page import OCRPage + + +class OCRResult: + """OCR result info.""" + + pages: List[OCRPage] + """List of OCR results for each page in the document.""" + + def __init__(self, raw_response: StringDict) -> None: + self.pages = [OCRPage(ocr) for ocr in raw_response["pages"]] + + def __str__(self) -> str: + pages = "\n" + if len(self.pages) > 0: + pages += "\n\n".join([str(ocr) for ocr in self.pages]) + out_str = f"Pages\n======{pages}" + return out_str diff --git a/mindee/v2/product/ocr/ocr_word.py b/mindee/v2/product/ocr/ocr_word.py new file mode 100644 index 00000000..64678bfd --- /dev/null +++ b/mindee/v2/product/ocr/ocr_word.py @@ -0,0 +1,17 @@ +from mindee.geometry.polygon import Polygon + + +class OCRWord: + """OCR result for a single word extracted from the document page.""" + + content: str + """Text content of the word.""" + polygon: Polygon + """Position information as a list of points in clockwise order.""" + + def __init__(self, raw_response: dict): + self.content = raw_response["content"] + self.polygon = Polygon(raw_response["polygon"]) + + def __str__(self) -> str: + return self.content diff --git a/tests/test_code_samples.sh b/tests/test_code_samples.sh index 8fd1ee57..a473510c 100755 --- a/tests/test_code_samples.sh +++ b/tests/test_code_samples.sh @@ -7,7 +7,10 @@ ENDPOINT=$2 API_KEY=$3 API_KEY_V2=$4 MODEL_ID=$5 -SPLIT_MODEL_ID=$6 +CROP_MODEL_ID=$6 +SPLIT_MODEL_ID=$7 +OCR_MODEL_ID=$8 +CLASSIFICATION_MODEL_ID=$9 for f in $(find ./docs/extras/code_samples -maxdepth 1 -name "*.txt" -not -name "workflow_*.txt" | sort -h) do @@ -37,6 +40,22 @@ do sed -i "s/my-api-key/$API_KEY/" $OUTPUT_FILE fi + if echo "${f}" | grep -q "v2_classification.txt" + then + sed -i "s/MY_API_KEY/$API_KEY_V2/" $OUTPUT_FILE + sed -i "s/MY_CLASSIFICATION_MODEL_ID/$CLASSIFICATION_MODEL_ID/" $OUTPUT_FILE + else + sed -i "s/my-api-key/$API_KEY/" $OUTPUT_FILE + fi + + if echo "${f}" | grep -q "v2_crop.txt" + then + sed -i "s/MY_API_KEY/$API_KEY_V2/" $OUTPUT_FILE + sed -i "s/MY_CROP_MODEL_ID/$CROP_MODEL_ID/" $OUTPUT_FILE + else + sed -i "s/my-api-key/$API_KEY/" $OUTPUT_FILE + fi + if echo "${f}" | grep -q "v2_split.txt" then sed -i "s/MY_API_KEY/$API_KEY_V2/" $OUTPUT_FILE @@ -45,6 +64,14 @@ do sed -i "s/my-api-key/$API_KEY/" $OUTPUT_FILE fi + if echo "${f}" | grep -q "v2_ocr.txt" + then + sed -i "s/MY_API_KEY/$API_KEY_V2/" $OUTPUT_FILE + sed -i "s/MY_OCR_MODEL_ID/$OCR_MODEL_ID/" $OUTPUT_FILE + else + sed -i "s/my-api-key/$API_KEY/" $OUTPUT_FILE + fi + if echo "$f" | grep -q "custom_v1.txt" then sed -i "s/my-account/$ACCOUNT/g" $OUTPUT_FILE diff --git a/tests/v2/utilities/crop/test_crop_integration.py b/tests/v2/utilities/crop/test_crop_integration.py index 18eaaac5..b96a2230 100644 --- a/tests/v2/utilities/crop/test_crop_integration.py +++ b/tests/v2/utilities/crop/test_crop_integration.py @@ -20,7 +20,7 @@ def v2_client() -> ClientV2: @pytest.mark.integration @pytest.mark.v2 -def test_crop_blank(v2_client: ClientV2, crop_model_id: str): +def test_crop_default_sample(v2_client: ClientV2, crop_model_id: str): input_source = PathInput(V2_UTILITIES_DATA_DIR / "crop" / "default_sample.jpg") response = v2_client.enqueue_and_get_result( CropResponse, input_source, CropParameters(crop_model_id) diff --git a/tests/v2/utilities/ocr/__init__.py b/tests/v2/utilities/ocr/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/v2/utilities/ocr/test_ocr_integration.py b/tests/v2/utilities/ocr/test_ocr_integration.py new file mode 100644 index 00000000..6d37a34b --- /dev/null +++ b/tests/v2/utilities/ocr/test_ocr_integration.py @@ -0,0 +1,34 @@ +import os + +import pytest + +from mindee import ClientV2, PathInput +from mindee.v2 import OCRParameters, OCRResponse +from mindee.v2.product.ocr import OCRInference, OCRResult +from tests.utils import V2_UTILITIES_DATA_DIR + + +@pytest.fixture(scope="session") +def ocr_model_id() -> str: + """Identifier of the Financial Document model, supplied through an env var.""" + return os.getenv("MINDEE_V2_SE_TESTS_OCR_MODEL_ID") + + +@pytest.fixture(scope="session") +def v2_client() -> ClientV2: + return ClientV2() + + +@pytest.mark.integration +@pytest.mark.v2 +def test_ocr_default_sample(v2_client: ClientV2, ocr_model_id: str): + input_source = PathInput(V2_UTILITIES_DATA_DIR / "ocr" / "default_sample.jpg") + response = v2_client.enqueue_and_get_result( + OCRResponse, input_source, OCRParameters(ocr_model_id) + ) + assert response.inference is not None + assert response.inference.file.name == "default_sample.jpg" + assert isinstance(response.inference, OCRInference) + assert isinstance(response.inference.result, OCRResult) + assert len(response.inference.result.pages) == 1 + assert len(response.inference.result.pages[0].words) > 0 diff --git a/tests/v2/utilities/ocr/test_ocr_response.py b/tests/v2/utilities/ocr/test_ocr_response.py new file mode 100644 index 00000000..41cb0d61 --- /dev/null +++ b/tests/v2/utilities/ocr/test_ocr_response.py @@ -0,0 +1,132 @@ +import pytest + +from mindee import LocalResponse +from mindee.v2.product.ocr.ocr_page import OCRPage +from mindee.v2.product.ocr import OCRInference +from mindee.v2.product.ocr.ocr_response import OCRResponse +from mindee.v2.product.ocr.ocr_result import OCRResult +from tests.utils import V2_UTILITIES_DATA_DIR + + +@pytest.mark.v2 +def test_ocr_single(): + input_inference = LocalResponse(V2_UTILITIES_DATA_DIR / "ocr" / "ocr_single.json") + ocr_response = input_inference.deserialize_response(OCRResponse) + assert isinstance(ocr_response.inference, OCRInference) + assert ocr_response.inference.result.pages + assert len(ocr_response.inference.result.pages) == 1 + assert ocr_response.inference.result.pages[0].words[0].content == "Shipper:" + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[0][0] + == 0.09742441209406495 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[0][1] + == 0.07007125890736342 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[1][0] + == 0.15621500559910415 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[1][1] + == 0.07046714172604909 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[2][0] + == 0.15621500559910415 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[2][1] + == 0.08155186064924783 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[3][0] + == 0.09742441209406495 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[3][1] + == 0.08155186064924783 + ) + assert len(ocr_response.inference.result.pages[0].words) == 305 + assert ocr_response.inference.result.pages[0].content == ( + "Shipper: GLOBAL FREIGHT SOLUTIONS INC. 123 OCEAN DRIVE SHANGHAI, CHINA TEL: " + "86-21-12345678 FAX: 86-21-87654321\nConsignee: PACIFIC TRADING CO. 789 TRADE " + "STREET SINGAPORE 567890 SINGAPORE TEL: 65-65432100 FAX: 65-65432101\nNotify " + "Party (Complete name and address): SAME AS CONSIGNEE\nBILL OF LADING\nJob No " + ".: XYZ123456\nGLOBAL SHIPPING CO\nPlace of receipt:\nSHANGHAI, CHINA\nOcean " + "vessel:\nGLOBAL VOYAGER V-202\nPort of loading:\nSHANGHAI, CHINA\nPort of " + "discharge:\nLOS ANGELES, USA\nPlace of delivery:\nLOS ANGELES, USA\nMarks and " + "numbers:\nP+F\n(IN DIA.)\nP/N: 12345\nDRAWING NO. A1B2C3\nNumber and kinds of " + "packages: 1CTN ELECTRONIC COMPONENTS 50 PCS\nDescription of goods:\nGross " + "weight:\n500 KGS\nMeasurement:\n1.5 M3\nP/O: 987654 LOT NO. " + "112233\nFFAU1234567/40'HQ/CFS-CFS ICTN/500KGS/1.5M3 SEAL NO:ABC1234567\nMADE " + 'IN CHINA\nSAY TOTAL:\n2 PLTS ONLY\n"FREIGHT COLLECT" CFS-CFS\n** SURRENDERED ' + "**\nFreight and Charge\nOCEAN FREIGHT\nRevenue tons\nRate\nPrepaid\nCollect\n" + "AS ARRANGED\nThe goods and instructions are accepted and dealt with subject " + "to the Standard Conditions printed overleaf. Taken in charge in apparent good " + "order and condition, unless otherwise noted herein, at the place of receipt " + "for transport and delivery as mentioned above. One of these Combined " + "Transport Bills of Lading must be surrendered duly endorsed in exchange for " + "the goods. In Witness whereof the original Combined Transport Bills of Lading " + "all of this tenor and date have been signed in the number stated below, one " + "of which being accomplished the other(s) to be void.\nUSD: 31.57 SHIPPED ON " + "BOARD: 30. SEP. 2022\nFreight Amount OCEAN FREIGHT\nFreight payable at\n" + "DESTINATION\nNumber of original\nZERO (0)\nCargo insurance\nnot covered\n" + "Covered according to attached Policy\nPlace and date of issue\nTAIPEI, " + "TAIWAN: 30. SEP. 2022\nFor delivery of goods please apply to: INTERNATIONAL " + "LOGISTICS LTD 456 SHIPPING LANE LOS ANGELES, CA 90001 USA TEL:1-213-9876543 " + "FAX:1-213-9876544 ATTN: MR. JOHN DOE\nSignature: GLOBAL SHIPPING CO., " + "LTD.\nBY\nAS CARRIER" + ) + + +@pytest.mark.v2 +def test_ocr_multiple(): + input_inference = LocalResponse(V2_UTILITIES_DATA_DIR / "ocr" / "ocr_multiple.json") + ocr_response = input_inference.deserialize_response(OCRResponse) + assert isinstance(ocr_response.inference, OCRInference) + assert isinstance(ocr_response.inference.result, OCRResult) + assert isinstance(ocr_response.inference.result.pages[0], OCRPage) + assert len(ocr_response.inference.result.pages) == 3 + + assert len(ocr_response.inference.result.pages[0].words) == 295 + assert ocr_response.inference.result.pages[0].words[0].content == "FICTIOCORP" + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[0][0] + == 0.06649402824332337 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[0][1] + == 0.03957449719523875 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[1][0] + == 0.23219061218068954 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[1][1] + == 0.03960015049938432 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[2][0] + == 0.23219061218068954 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[2][1] + == 0.06770762074155151 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[3][0] + == 0.06649402824332337 + ) + assert ( + ocr_response.inference.result.pages[0].words[0].polygon[3][1] + == 0.06770762074155151 + ) + + assert len(ocr_response.inference.result.pages[1].words) == 450 + assert ocr_response.inference.result.pages[1].words[0].content == "KEOLIO" + + assert len(ocr_response.inference.result.pages[2].words) == 355 + assert ocr_response.inference.result.pages[2].words[0].content == "KEOLIO" diff --git a/tests/v2/utilities/split/test_split_integration.py b/tests/v2/utilities/split/test_split_integration.py index 04854e77..c37561a1 100644 --- a/tests/v2/utilities/split/test_split_integration.py +++ b/tests/v2/utilities/split/test_split_integration.py @@ -20,7 +20,7 @@ def v2_client() -> ClientV2: @pytest.mark.integration @pytest.mark.v2 -def test_split_blank(v2_client: ClientV2, split_model_id: str): +def test_split_default_sample(v2_client: ClientV2, split_model_id: str): input_source = PathInput(V2_UTILITIES_DATA_DIR / "split" / "default_sample.pdf") response = v2_client.enqueue_and_get_result( SplitResponse, input_source, SplitParameters(split_model_id) From 3d4a2e425ba92e24364c4ce19fc86b9d2a43428b Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:58:00 +0100 Subject: [PATCH 3/9] :sparkles: add support for Classification --- mindee/v2/__init__.py | 8 ++++ mindee/v2/product/__init__.py | 8 ++++ mindee/v2/product/classification/__init__.py | 21 +++++++++++ .../classification_classifier.py | 14 +++++++ .../classification_inference.py | 19 ++++++++++ .../classification_parameters.py | 9 +++++ .../classification/classification_response.py | 19 ++++++++++ .../classification/classification_result.py | 16 ++++++++ mindee/v2/product/split/split_range.py | 7 +++- tests/v2/{utilities => product}/__init__.py | 0 .../classification}/__init__.py | 0 .../test_classification_integration.py | 37 +++++++++++++++++++ .../test_classification_response.py | 32 ++++++++++++++++ .../ocr => product/crop}/__init__.py | 0 .../crop/test_crop_integration.py | 0 .../crop/test_crop_response.py | 0 .../split => product/ocr}/__init__.py | 0 .../ocr/test_ocr_integration.py | 0 .../ocr/test_ocr_response.py | 0 tests/v2/product/split/__init__.py | 0 .../split/test_split_integration.py | 0 .../split/test_split_response.py | 0 22 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 mindee/v2/product/classification/__init__.py create mode 100644 mindee/v2/product/classification/classification_classifier.py create mode 100644 mindee/v2/product/classification/classification_inference.py create mode 100644 mindee/v2/product/classification/classification_parameters.py create mode 100644 mindee/v2/product/classification/classification_response.py create mode 100644 mindee/v2/product/classification/classification_result.py rename tests/v2/{utilities => product}/__init__.py (100%) rename tests/v2/{utilities/crop => product/classification}/__init__.py (100%) create mode 100644 tests/v2/product/classification/test_classification_integration.py create mode 100644 tests/v2/product/classification/test_classification_response.py rename tests/v2/{utilities/ocr => product/crop}/__init__.py (100%) rename tests/v2/{utilities => product}/crop/test_crop_integration.py (100%) rename tests/v2/{utilities => product}/crop/test_crop_response.py (100%) rename tests/v2/{utilities/split => product/ocr}/__init__.py (100%) rename tests/v2/{utilities => product}/ocr/test_ocr_integration.py (100%) rename tests/v2/{utilities => product}/ocr/test_ocr_response.py (100%) create mode 100644 tests/v2/product/split/__init__.py rename tests/v2/{utilities => product}/split/test_split_integration.py (100%) rename tests/v2/{utilities => product}/split/test_split_response.py (100%) diff --git a/mindee/v2/__init__.py b/mindee/v2/__init__.py index 1986a45b..c97f7080 100644 --- a/mindee/v2/__init__.py +++ b/mindee/v2/__init__.py @@ -1,3 +1,9 @@ +from mindee.v2.product.classification.classification_parameters import ( + ClassificationParameters, +) +from mindee.v2.product.classification.classification_response import ( + ClassificationResponse, +) from mindee.v2.product.crop.crop_parameters import CropParameters from mindee.v2.product.crop.crop_response import CropResponse from mindee.v2.product.ocr.ocr_parameters import OCRParameters @@ -6,6 +12,8 @@ from mindee.v2.product.split.split_response import SplitResponse __all__ = [ + "ClassificationResponse", + "ClassificationParameters", "CropResponse", "CropParameters", "OCRResponse", diff --git a/mindee/v2/product/__init__.py b/mindee/v2/product/__init__.py index 1986a45b..6d5b3bc3 100644 --- a/mindee/v2/product/__init__.py +++ b/mindee/v2/product/__init__.py @@ -1,3 +1,9 @@ +from mindee.v2.product.classification.classification_parameters import ( + ClassificationParameters, +) +from mindee.v2.product.classification.classification_response import ( + ClassificationResponse, +) from mindee.v2.product.crop.crop_parameters import CropParameters from mindee.v2.product.crop.crop_response import CropResponse from mindee.v2.product.ocr.ocr_parameters import OCRParameters @@ -6,6 +12,8 @@ from mindee.v2.product.split.split_response import SplitResponse __all__ = [ + "ClassificationParameters", + "ClassificationResponse", "CropResponse", "CropParameters", "OCRResponse", diff --git a/mindee/v2/product/classification/__init__.py b/mindee/v2/product/classification/__init__.py new file mode 100644 index 00000000..61fddbf5 --- /dev/null +++ b/mindee/v2/product/classification/__init__.py @@ -0,0 +1,21 @@ +from mindee.v2.product.classification.classification_classifier import ( + ClassificationClassifier, +) +from mindee.v2.product.classification.classification_inference import ( + ClassificationInference, +) +from mindee.v2.product.classification.classification_parameters import ( + ClassificationParameters, +) +from mindee.v2.product.classification.classification_response import ( + ClassificationResponse, +) +from mindee.v2.product.classification.classification_result import ClassificationResult + +__all__ = [ + "ClassificationClassifier", + "ClassificationInference", + "ClassificationParameters", + "ClassificationResponse", + "ClassificationResult", +] diff --git a/mindee/v2/product/classification/classification_classifier.py b/mindee/v2/product/classification/classification_classifier.py new file mode 100644 index 00000000..9f49674f --- /dev/null +++ b/mindee/v2/product/classification/classification_classifier.py @@ -0,0 +1,14 @@ +from mindee.parsing.common.string_dict import StringDict + + +class ClassificationClassifier: + """Document level classification.""" + + document_type: str + """The document type, as identified on given classification values.""" + + def __init__(self, server_response: StringDict): + self.document_type = server_response["document_type"] + + def __str__(self) -> str: + return f":Document Type: {self.document_type}" diff --git a/mindee/v2/product/classification/classification_inference.py b/mindee/v2/product/classification/classification_inference.py new file mode 100644 index 00000000..ae83660b --- /dev/null +++ b/mindee/v2/product/classification/classification_inference.py @@ -0,0 +1,19 @@ +from mindee.parsing.common.string_dict import StringDict +from mindee.v2.parsing.inference.base_inference import BaseInference +from mindee.v2.product.classification.classification_result import ClassificationResult + + +class ClassificationInference(BaseInference): + """The inference result for a classification utility request.""" + + result: ClassificationResult + """Result of a classification inference.""" + _slug: str = "classification" + """Slug of the endpoint.""" + + def __init__(self, raw_response: StringDict) -> None: + super().__init__(raw_response) + self.result = ClassificationResult(raw_response["result"]) + + def __str__(self) -> str: + return f"Inference\n#########\n{self.model}\n{self.file}\n{self.result}\n" diff --git a/mindee/v2/product/classification/classification_parameters.py b/mindee/v2/product/classification/classification_parameters.py new file mode 100644 index 00000000..590d1f2e --- /dev/null +++ b/mindee/v2/product/classification/classification_parameters.py @@ -0,0 +1,9 @@ +from mindee.input.base_parameters import BaseParameters + + +class ClassificationParameters(BaseParameters): + """ + Parameters accepted by the classification utility v2 endpoint. + """ + + _slug: str = "utilities/classification" diff --git a/mindee/v2/product/classification/classification_response.py b/mindee/v2/product/classification/classification_response.py new file mode 100644 index 00000000..01abb3a0 --- /dev/null +++ b/mindee/v2/product/classification/classification_response.py @@ -0,0 +1,19 @@ +from mindee.parsing.common.string_dict import StringDict +from mindee.v2.parsing.inference import BaseResponse +from mindee.v2.product.classification.classification_inference import ( + ClassificationInference, +) + + +class ClassificationResponse(BaseResponse): + """Represent a classification inference response from Mindee V2 API.""" + + inference: ClassificationInference + """Inference object for classification inference.""" + + _slug: str = "utilities/classification" + """Slug of the inference.""" + + def __init__(self, raw_response: StringDict) -> None: + super().__init__(raw_response) + self.inference = ClassificationInference(raw_response["inference"]) diff --git a/mindee/v2/product/classification/classification_result.py b/mindee/v2/product/classification/classification_result.py new file mode 100644 index 00000000..2d314ffb --- /dev/null +++ b/mindee/v2/product/classification/classification_result.py @@ -0,0 +1,16 @@ +from mindee.parsing.common.string_dict import StringDict +from mindee.v2.product.classification.classification_classifier import ( + ClassificationClassifier, +) + + +class ClassificationResult: + """Classification result info.""" + + classification: ClassificationClassifier + + def __init__(self, raw_response: StringDict) -> None: + self.classification = ClassificationClassifier(raw_response["classification"]) + + def __str__(self) -> str: + return f"Classification\n======{self.classification}" diff --git a/mindee/v2/product/split/split_range.py b/mindee/v2/product/split/split_range.py index 21a85405..e0e70110 100644 --- a/mindee/v2/product/split/split_range.py +++ b/mindee/v2/product/split/split_range.py @@ -7,9 +7,12 @@ class SplitRange: """Split inference result.""" page_range: List[int] - """Page range of the split inference.""" + """ + 0-based page indexes, where the first integer indicates the start page and the + second integer indicates the end page. + """ document_type: str - """Document type of the split inference.""" + """The document type, as identified on given classification values.""" def __init__(self, server_response: StringDict): self.page_range = server_response["page_range"] diff --git a/tests/v2/utilities/__init__.py b/tests/v2/product/__init__.py similarity index 100% rename from tests/v2/utilities/__init__.py rename to tests/v2/product/__init__.py diff --git a/tests/v2/utilities/crop/__init__.py b/tests/v2/product/classification/__init__.py similarity index 100% rename from tests/v2/utilities/crop/__init__.py rename to tests/v2/product/classification/__init__.py diff --git a/tests/v2/product/classification/test_classification_integration.py b/tests/v2/product/classification/test_classification_integration.py new file mode 100644 index 00000000..75229de4 --- /dev/null +++ b/tests/v2/product/classification/test_classification_integration.py @@ -0,0 +1,37 @@ +import os + +import pytest + +from mindee import ClientV2, PathInput +from mindee.v2 import ClassificationParameters, ClassificationResponse +from tests.utils import V2_UTILITIES_DATA_DIR + + +@pytest.fixture(scope="session") +def classification_model_id() -> str: + """Identifier of the Financial Document model, supplied through an env var.""" + return os.getenv("MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID") + + +@pytest.fixture(scope="session") +def v2_client() -> ClientV2: + return ClientV2() + + +@pytest.mark.integration +@pytest.mark.v2 +def test_classification_default_sample( + v2_client: ClientV2, classification_model_id: str +): + input_source = PathInput( + V2_UTILITIES_DATA_DIR / "classification" / "default_invoice.jpg" + ) + response = v2_client.enqueue_and_get_result( + ClassificationResponse, + input_source, + ClassificationParameters(classification_model_id), + ) + assert response.inference is not None + assert response.inference.file.name == "default_invoice.jpg" + assert response.inference.result.classification + assert response.inference.result.classification.document_type == "invoice" diff --git a/tests/v2/product/classification/test_classification_response.py b/tests/v2/product/classification/test_classification_response.py new file mode 100644 index 00000000..6cfb5426 --- /dev/null +++ b/tests/v2/product/classification/test_classification_response.py @@ -0,0 +1,32 @@ +import pytest + +from mindee import LocalResponse +from mindee.v2.product.classification.classification_classifier import ( + ClassificationClassifier, +) +from mindee.v2.product.classification import ClassificationInference +from mindee.v2.product.classification.classification_response import ( + ClassificationResponse, +) +from mindee.v2.product.classification.classification_result import ClassificationResult +from tests.utils import V2_UTILITIES_DATA_DIR + + +@pytest.mark.v2 +def test_classification_single(): + input_inference = LocalResponse( + V2_UTILITIES_DATA_DIR / "classification" / "classification_single.json" + ) + classification_response = input_inference.deserialize_response( + ClassificationResponse + ) + assert isinstance(classification_response.inference, ClassificationInference) + assert isinstance(classification_response.inference.result, ClassificationResult) + assert isinstance( + classification_response.inference.result.classification, + ClassificationClassifier, + ) + assert ( + classification_response.inference.result.classification.document_type + == "invoice" + ) diff --git a/tests/v2/utilities/ocr/__init__.py b/tests/v2/product/crop/__init__.py similarity index 100% rename from tests/v2/utilities/ocr/__init__.py rename to tests/v2/product/crop/__init__.py diff --git a/tests/v2/utilities/crop/test_crop_integration.py b/tests/v2/product/crop/test_crop_integration.py similarity index 100% rename from tests/v2/utilities/crop/test_crop_integration.py rename to tests/v2/product/crop/test_crop_integration.py diff --git a/tests/v2/utilities/crop/test_crop_response.py b/tests/v2/product/crop/test_crop_response.py similarity index 100% rename from tests/v2/utilities/crop/test_crop_response.py rename to tests/v2/product/crop/test_crop_response.py diff --git a/tests/v2/utilities/split/__init__.py b/tests/v2/product/ocr/__init__.py similarity index 100% rename from tests/v2/utilities/split/__init__.py rename to tests/v2/product/ocr/__init__.py diff --git a/tests/v2/utilities/ocr/test_ocr_integration.py b/tests/v2/product/ocr/test_ocr_integration.py similarity index 100% rename from tests/v2/utilities/ocr/test_ocr_integration.py rename to tests/v2/product/ocr/test_ocr_integration.py diff --git a/tests/v2/utilities/ocr/test_ocr_response.py b/tests/v2/product/ocr/test_ocr_response.py similarity index 100% rename from tests/v2/utilities/ocr/test_ocr_response.py rename to tests/v2/product/ocr/test_ocr_response.py diff --git a/tests/v2/product/split/__init__.py b/tests/v2/product/split/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/v2/utilities/split/test_split_integration.py b/tests/v2/product/split/test_split_integration.py similarity index 100% rename from tests/v2/utilities/split/test_split_integration.py rename to tests/v2/product/split/test_split_integration.py diff --git a/tests/v2/utilities/split/test_split_response.py b/tests/v2/product/split/test_split_response.py similarity index 100% rename from tests/v2/utilities/split/test_split_response.py rename to tests/v2/product/split/test_split_response.py From a596ee1b685b2849ac2cd0b768430069c8e22adf Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:06:58 +0100 Subject: [PATCH 4/9] fix imports --- mindee/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/mindee/__init__.py b/mindee/__init__.py index 33a2087c..0f8bf1ae 100644 --- a/mindee/__init__.py +++ b/mindee/__init__.py @@ -22,6 +22,16 @@ from mindee.parsing.common.predict_response import PredictResponse from mindee.parsing.common.workflow_response import WorkflowResponse from mindee.parsing.v2 import InferenceResponse, JobResponse +from mindee.v2.product.classification.classification_parameters import ( + ClassificationParameters, +) +from mindee.v2.product.classification.classification_response import ( + ClassificationResponse, +) +from mindee.v2.product.crop.crop_parameters import CropParameters +from mindee.v2.product.crop.crop_response import CropResponse +from mindee.v2.product.ocr.ocr_parameters import OCRParameters +from mindee.v2.product.ocr.ocr_response import OCRResponse from mindee.v2.product.split.split_parameters import SplitParameters from mindee.v2.product.split.split_response import SplitResponse @@ -30,8 +40,12 @@ "AsyncPredictResponse", "Base64Input", "BytesInput", + "ClassificationResponse", + "ClassificationParameters", "Client", "ClientV2", + "CropParameters", + "CropResponse", "DataSchema", "DataSchemaField", "DataSchemaReplace", @@ -42,6 +56,8 @@ "Job", "JobResponse", "LocalResponse", + "OCRParameters", + "OCRResponse", "PageOptions", "PathInput", "PollingOptions", From ef12c6cdb658c3d843cb55ca7fbe4cc63a8d3532 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:16:09 +0100 Subject: [PATCH 5/9] fix typos again --- .github/workflows/_test-code-samples.yml | 2 +- tests/test_code_samples.sh | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/_test-code-samples.yml b/.github/workflows/_test-code-samples.yml index 6e77ab8a..9263af5b 100644 --- a/.github/workflows/_test-code-samples.yml +++ b/.github/workflows/_test-code-samples.yml @@ -40,7 +40,7 @@ jobs: - name: Tests code samples run: | - ./tests/test_code_samples.sh ${{ secrets.MINDEE_ACCOUNT_SE_TESTS }} ${{ secrets.MINDEE_ENDPOINT_SE_TESTS }} ${{ secrets.MINDEE_API_KEY_SE_TESTS }} ${{ secrets.MINDEE_V2_SE_TESTS_API_KEY }} ${{ secrets.MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_CROP_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_CROP_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID }} + ./tests/test_code_samples.sh ${{ secrets.MINDEE_ACCOUNT_SE_TESTS }} ${{ secrets.MINDEE_ENDPOINT_SE_TESTS }} ${{ secrets.MINDEE_API_KEY_SE_TESTS }} ${{ secrets.MINDEE_V2_SE_TESTS_API_KEY }} ${{ secrets.MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_CROP_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_OCR_MODEL_ID }} ${{ secrets.MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID }} - name: Notify Slack Action on Failure uses: ravsamhq/notify-slack-action@2.3.0 diff --git a/tests/test_code_samples.sh b/tests/test_code_samples.sh index a473510c..58557a88 100755 --- a/tests/test_code_samples.sh +++ b/tests/test_code_samples.sh @@ -7,10 +7,10 @@ ENDPOINT=$2 API_KEY=$3 API_KEY_V2=$4 MODEL_ID=$5 -CROP_MODEL_ID=$6 -SPLIT_MODEL_ID=$7 -OCR_MODEL_ID=$8 -CLASSIFICATION_MODEL_ID=$9 +CLASSIFICATION_MODEL_ID=$6 +CROP_MODEL_ID=$7 +SPLIT_MODEL_ID=$8 +OCR_MODEL_ID=$9 for f in $(find ./docs/extras/code_samples -maxdepth 1 -name "*.txt" -not -name "workflow_*.txt" | sort -h) do From fdc99d904999aba527da17885d7a51e8e5bea4f3 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:21:31 +0100 Subject: [PATCH 6/9] fix mixup --- tests/test_code_samples.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_code_samples.sh b/tests/test_code_samples.sh index 58557a88..e651fec3 100755 --- a/tests/test_code_samples.sh +++ b/tests/test_code_samples.sh @@ -9,8 +9,8 @@ API_KEY_V2=$4 MODEL_ID=$5 CLASSIFICATION_MODEL_ID=$6 CROP_MODEL_ID=$7 -SPLIT_MODEL_ID=$8 -OCR_MODEL_ID=$9 +OCR_MODEL_ID=$8 +SPLIT_MODEL_ID=$9 for f in $(find ./docs/extras/code_samples -maxdepth 1 -name "*.txt" -not -name "workflow_*.txt" | sort -h) do From 4ffea7a139e6c3ed2e11626914e571fa37a235bb Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 4 Feb 2026 21:59:06 +0100 Subject: [PATCH 7/9] fixes --- docs/extras/code_samples/v2_classification.txt | 14 +++++++++++--- docs/extras/code_samples/v2_crop.txt | 14 +++++++++++--- docs/extras/code_samples/v2_ocr.txt | 14 +++++++++++--- tests/v2/product/ocr/test_ocr_integration.py | 2 +- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/docs/extras/code_samples/v2_classification.txt b/docs/extras/code_samples/v2_classification.txt index 7334648c..1160b71c 100644 --- a/docs/extras/code_samples/v2_classification.txt +++ b/docs/extras/code_samples/v2_classification.txt @@ -1,4 +1,9 @@ -from mindee import ClientV2, ClassificationParameters, ClassificationResponse, PathInput +from mindee import ( + ClientV2, + PathInput, + ClassificationParameters, + ClassificationResponse, +) input_path = "/path/to/the/file.ext" api_key = "MY_API_KEY" @@ -7,7 +12,7 @@ model_id = "MY_CLASSIFICATION_MODEL_ID" # Init a new client mindee_client = ClientV2(api_key) -# Set inference parameters +# Set parameters params = ClassificationParameters( # ID of the model, required. model_id=model_id, @@ -16,7 +21,7 @@ params = ClassificationParameters( # Load a file from disk input_source = PathInput(input_path) -# Send for processing +# Send for processing using polling response = mindee_client.enqueue_and_get_result( ClassificationResponse, input_source, @@ -25,3 +30,6 @@ response = mindee_client.enqueue_and_get_result( # Print a brief summary of the parsed data print(response.inference) + +# Access the classification result +classifications: list = response.inference.result.classifications diff --git a/docs/extras/code_samples/v2_crop.txt b/docs/extras/code_samples/v2_crop.txt index 2ab55873..115cba20 100644 --- a/docs/extras/code_samples/v2_crop.txt +++ b/docs/extras/code_samples/v2_crop.txt @@ -1,4 +1,9 @@ -from mindee import ClientV2, CropParameters, CropResponse, PathInput +from mindee import ( + ClientV2, + PathInput, + CropParameters, + CropResponse, +) input_path = "/path/to/the/file.ext" api_key = "MY_API_KEY" @@ -7,7 +12,7 @@ model_id = "MY_CROP_MODEL_ID" # Init a new client mindee_client = ClientV2(api_key) -# Set inference parameters +# Set parameters params = CropParameters( # ID of the model, required. model_id=model_id, @@ -16,7 +21,7 @@ params = CropParameters( # Load a file from disk input_source = PathInput(input_path) -# Send for processing +# Send for processing using polling response = mindee_client.enqueue_and_get_result( CropResponse, input_source, @@ -25,3 +30,6 @@ response = mindee_client.enqueue_and_get_result( # Print a brief summary of the parsed data print(response.inference) + +# Access the crop result +crops: list = response.inference.result.crops diff --git a/docs/extras/code_samples/v2_ocr.txt b/docs/extras/code_samples/v2_ocr.txt index de3ae170..41b6e3b1 100644 --- a/docs/extras/code_samples/v2_ocr.txt +++ b/docs/extras/code_samples/v2_ocr.txt @@ -1,4 +1,9 @@ -from mindee import ClientV2, OCRParameters, OCRResponse, PathInput +from mindee import ( + ClientV2, + PathInput, + OCRParameters, + OCRResponse, +) input_path = "/path/to/the/file.ext" api_key = "MY_API_KEY" @@ -7,7 +12,7 @@ model_id = "MY_OCR_MODEL_ID" # Init a new client mindee_client = ClientV2(api_key) -# Set inference parameters +# Set parameters params = OCRParameters( # ID of the model, required. model_id=model_id, @@ -16,7 +21,7 @@ params = OCRParameters( # Load a file from disk input_source = PathInput(input_path) -# Send for processing +# Send for processing using polling response = mindee_client.enqueue_and_get_result( OCRResponse, input_source, @@ -25,3 +30,6 @@ response = mindee_client.enqueue_and_get_result( # Print a brief summary of the parsed data print(response.inference) + +# Access the ocr result +ocrs: list = response.inference.result.ocrs diff --git a/tests/v2/product/ocr/test_ocr_integration.py b/tests/v2/product/ocr/test_ocr_integration.py index 6d37a34b..30f7eb86 100644 --- a/tests/v2/product/ocr/test_ocr_integration.py +++ b/tests/v2/product/ocr/test_ocr_integration.py @@ -31,4 +31,4 @@ def test_ocr_default_sample(v2_client: ClientV2, ocr_model_id: str): assert isinstance(response.inference, OCRInference) assert isinstance(response.inference.result, OCRResult) assert len(response.inference.result.pages) == 1 - assert len(response.inference.result.pages[0].words) > 0 + assert len(response.inference.result.pages[0].words) > 5 From b8be3117adf77a5decc242f45649421cabd6f183 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:07:05 +0100 Subject: [PATCH 8/9] fix typos --- docs/extras/code_samples/v2_classification.txt | 2 +- docs/extras/code_samples/v2_ocr.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/extras/code_samples/v2_classification.txt b/docs/extras/code_samples/v2_classification.txt index 1160b71c..1f362e41 100644 --- a/docs/extras/code_samples/v2_classification.txt +++ b/docs/extras/code_samples/v2_classification.txt @@ -32,4 +32,4 @@ response = mindee_client.enqueue_and_get_result( print(response.inference) # Access the classification result -classifications: list = response.inference.result.classifications +classification: list = response.inference.result.classification diff --git a/docs/extras/code_samples/v2_ocr.txt b/docs/extras/code_samples/v2_ocr.txt index 41b6e3b1..c39a000b 100644 --- a/docs/extras/code_samples/v2_ocr.txt +++ b/docs/extras/code_samples/v2_ocr.txt @@ -32,4 +32,4 @@ response = mindee_client.enqueue_and_get_result( print(response.inference) # Access the ocr result -ocrs: list = response.inference.result.ocrs +pages: list = response.inference.result.pages From f68faf35a100a21751560a408901261febeac5da Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:18:49 +0100 Subject: [PATCH 9/9] fix typo --- docs/extras/code_samples/v2_classification.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extras/code_samples/v2_classification.txt b/docs/extras/code_samples/v2_classification.txt index 1f362e41..b5bbd237 100644 --- a/docs/extras/code_samples/v2_classification.txt +++ b/docs/extras/code_samples/v2_classification.txt @@ -32,4 +32,4 @@ response = mindee_client.enqueue_and_get_result( print(response.inference) # Access the classification result -classification: list = response.inference.result.classification +classification: str = response.inference.result.classification.document_type