Compare commits

..

No commits in common. "master" and "2.7.0" have entirely different histories.

22 changed files with 2194 additions and 132736 deletions

3
.gitignore vendored
View File

@ -48,5 +48,4 @@ __pycache__/
!data/* !data/*
!drivers !drivers
# unignore files # ignore files
!bom.*

View File

@ -1,35 +1,32 @@
# CI for services, check gitlab repo for python package CI
include: include:
- project: "Gitlab/gitlab" - project: "Gitlab/gitlab"
ref: main ref: 0.3.0
file: "/ci-templates/research/versioning-build-test-release.gitlab-ci.yml" file: "/ci-templates/research/dvc-versioning-build-release.gitlab-ci.yml"
- project: "Gitlab/gitlab" # file: "/ci-templates/research/versioning-build+azure_model-test-release.gitlab-ci.yml"
ref: main
file: "/ci-templates/research/docs.gitlab-ci.yml"
# set project variables here
variables: variables:
NEXUS_PROJECT_DIR: red # subfolder in Nexus docker-gin where your container will be stored NEXUS_PROJECT_DIR: red
IMAGENAME: $CI_PROJECT_NAME # if the project URL is gitlab.example.com/group-name/project-1, CI_PROJECT_NAME is project-1 IMAGENAME: "${CI_PROJECT_NAME}"
pages: #################################
only: # temp. disable integration tests, b/c they don't cover the CV analysis case yet
- master # KEEP THIS, necessary because `master` branch and not `main` branch trigger integration tests:
###################
# INTEGRATION TESTS
trigger-integration-tests:
extends: .integration-tests
# ADD THE MODEL BUILD WHICH SHOULD TRIGGER THE INTEGRATION TESTS
# needs:
# - job: docker-build::model_name
# artifacts: true
rules: rules:
- when: never - when: never
######### release build:
# RELEASE stage: release
release:
extends: .release
needs: needs:
- !reference [.needs-versioning, needs] # leave this line as is - job: set custom version
artifacts: true
optional: true
- job: calculate patch version
artifacts: true
optional: true
- job: calculate minor version
artifacts: true
optional: true
- job: build docker nexus
artifacts: true
#################################

View File

@ -13,7 +13,9 @@ logger.add(sys.stdout, level="INFO")
def bashcmd(cmds: list) -> str: def bashcmd(cmds: list) -> str:
try: try:
logger.debug(f"running: {' '.join(cmds)}") logger.debug(f"running: {' '.join(cmds)}")
return subprocess.run(cmds, check=True, capture_output=True, text=True).stdout.strip("\n") return subprocess.run(
cmds, check=True, capture_output=True, text=True
).stdout.strip("\n")
except: except:
logger.warning(f"Error executing the following bash command: {' '.join(cmds)}.") logger.warning(f"Error executing the following bash command: {' '.join(cmds)}.")
raise raise

View File

@ -5,7 +5,7 @@ default_language_version:
python: python3.10 python: python3.10
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 rev: v4.5.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@ -34,7 +34,7 @@ repos:
- --profile black - --profile black
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 24.10.0 rev: 24.3.0
hooks: hooks:
- id: black - id: black
# exclude: ^(docs/|notebooks/|data/|src/secrets/) # exclude: ^(docs/|notebooks/|data/|src/secrets/)
@ -42,7 +42,7 @@ repos:
- --line-length=120 - --line-length=120
- repo: https://github.com/compilerla/conventional-pre-commit - repo: https://github.com/compilerla/conventional-pre-commit
rev: v4.0.0 rev: v3.2.0
hooks: hooks:
- id: conventional-pre-commit - id: conventional-pre-commit
pass_filenames: false pass_filenames: false

View File

@ -1,19 +1,11 @@
############### FROM python:3.10
# BUILDER IMAGE
FROM python:3.10-slim as builder
ARG GITLAB_USER
ARG GITLAB_ACCESS_TOKEN
ARG USERNAME
ARG TOKEN
ARG PYPI_REGISTRY_RESEARCH=https://gitlab.knecon.com/api/v4/groups/19/-/packages/pypi ARG PYPI_REGISTRY_RESEARCH=https://gitlab.knecon.com/api/v4/groups/19/-/packages/pypi
ARG POETRY_SOURCE_REF_RESEARCH=gitlab-research ARG POETRY_SOURCE_REF_RESEARCH=gitlab-research
ARG PYPI_REGISTRY_RED=https://gitlab.knecon.com/api/v4/groups/12/-/packages/pypi ARG PYPI_REGISTRY_RED=https://gitlab.knecon.com/api/v4/groups/12/-/packages/pypi
ARG POETRY_SOURCE_REF_RED=gitlab-red ARG POETRY_SOURCE_REF_RED=gitlab-red
ARG PYPI_REGISTRY_FFORESIGHT=https://gitlab.knecon.com/api/v4/groups/269/-/packages/pypi
ARG POETRY_SOURCE_REF_FFORESIGHT=gitlab-fforesight
ARG VERSION=dev ARG VERSION=dev
LABEL maintainer="Research <research@knecon.com>" LABEL maintainer="Research <research@knecon.com>"
@ -21,58 +13,29 @@ LABEL version="${VERSION}"
WORKDIR /app WORKDIR /app
###########
# ENV SETUP
ENV PYTHONDONTWRITEBYTECODE=true
ENV PYTHONUNBUFFERED=true ENV PYTHONUNBUFFERED=true
ENV POETRY_HOME=/opt/poetry ENV POETRY_HOME=/opt/poetry
ENV PATH="$POETRY_HOME/bin:$PATH" ENV PATH="$POETRY_HOME/bin:$PATH"
RUN apt-get update && \
apt-get install -y curl git bash build-essential libffi-dev libssl-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN curl -sSL https://install.python-poetry.org | python3 - RUN curl -sSL https://install.python-poetry.org | python3 -
RUN poetry --version
COPY ./src ./src
COPY ./config ./config
COPY pyproject.toml poetry.lock ./ COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.create true && \
poetry config virtualenvs.in-project true && \ RUN ls -hal
RUN echo "${USERNAME} ${TOKEN} ${PYPI_REGISTRY_RED} ${POETRY_SOURCE_REF_RED} "
RUN poetry config virtualenvs.create false && \
poetry config installer.max-workers 10 && \ poetry config installer.max-workers 10 && \
poetry config repositories.${POETRY_SOURCE_REF_RESEARCH} ${PYPI_REGISTRY_RESEARCH} && \ poetry config repositories.${POETRY_SOURCE_REF_RESEARCH} ${PYPI_REGISTRY_RESEARCH} && \
poetry config http-basic.${POETRY_SOURCE_REF_RESEARCH} ${GITLAB_USER} ${GITLAB_ACCESS_TOKEN} && \ poetry config http-basic.${POETRY_SOURCE_REF_RESEARCH} ${USERNAME} ${TOKEN} && \
poetry config repositories.${POETRY_SOURCE_REF_RED} ${PYPI_REGISTRY_RED} && \ poetry config repositories.${POETRY_SOURCE_REF_RED} ${PYPI_REGISTRY_RED} && \
poetry config http-basic.${POETRY_SOURCE_REF_RED} ${GITLAB_USER} ${GITLAB_ACCESS_TOKEN} && \ poetry config http-basic.${POETRY_SOURCE_REF_RED} ${USERNAME} ${TOKEN} && \
poetry config repositories.${POETRY_SOURCE_REF_FFORESIGHT} ${PYPI_REGISTRY_FFORESIGHT} && \ poetry install --without=test -vv --no-interaction
poetry config http-basic.${POETRY_SOURCE_REF_FFORESIGHT} ${GITLAB_USER} ${GITLAB_ACCESS_TOKEN} && \
poetry install --without=dev,docs,test -vv --no-interaction --no-root
##################
# COPY SOURCE CODE
COPY ./config ./config
COPY ./src ./src
###############
# WORKING IMAGE
FROM python:3.10-slim
# COPY BILL OF MATERIALS (BOM)
COPY bom.json /bom.json
# COPY SOURCE CODE FROM BUILDER IMAGE
COPY --from=builder /app /app
WORKDIR /app
ENV PATH="/app/.venv/bin:$PATH"
############
# NETWORKING
EXPOSE 5000 EXPOSE 5000
EXPOSE 8080 EXPOSE 8080
################
# LAUNCH COMMAND
CMD [ "python", "src/serve.py"] CMD [ "python", "src/serve.py"]

View File

@ -4,7 +4,7 @@
docker docker-build-run docker-build docker-run \ docker docker-build-run docker-build docker-run \
docker-rm docker-rm-container docker-rm-image \ docker-rm docker-rm-container docker-rm-image \
pre-commit get-licenses prep-commit \ pre-commit get-licenses prep-commit \
docs sphinx_html sphinx_apidoc bom docs sphinx_html sphinx_apidoc
.DEFAULT_GOAL := run .DEFAULT_GOAL := run
export DOCKER=docker export DOCKER=docker
@ -89,6 +89,3 @@ sphinx_html:
sphinx_apidoc: sphinx_apidoc:
cp ./README.md ./docs/source/README.md && cp -r ./data ./docs/source/data/ && poetry run sphinx-apidoc ./src -o ./docs/source/modules --no-toc --module-first --follow-links --separate --force cp ./README.md ./docs/source/README.md && cp -r ./data ./docs/source/data/ && poetry run sphinx-apidoc ./src -o ./docs/source/modules --no-toc --module-first --follow-links --separate --force
bom:
cyclonedx-py poetry -o bom.json

30096
bom.json

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,11 @@
[asyncio]
max_concurrent_tasks = 10
[dynamic_tenant_queues]
enabled = true
[metrics.prometheus] [metrics.prometheus]
enabled = true enabled = true
prefix = "redactmanager_cv_analysis_service" prefix = "redactmanager_cv_analysis_service"
[tracing]
enabled = true
# possible values "opentelemetry" | "azure_monitor" (Excpects APPLICATIONINSIGHTS_CONNECTION_STRING environment variable.)
type = "azure_monitor"
[tracing.opentelemetry] [tracing.opentelemetry]
enabled = true
endpoint = "http://otel-collector-opentelemetry-collector.otel-collector:4318/v1/traces" endpoint = "http://otel-collector-opentelemetry-collector.otel-collector:4318/v1/traces"
service_name = "redactmanager_cv_analysis_service" service_name = "redactmanager_cv_analyisis_service"
exporter = "otlp" exporter = "otlp"
[webserver] [webserver]
@ -36,15 +25,6 @@ input_queue = "request_queue"
output_queue = "response_queue" output_queue = "response_queue"
dead_letter_queue = "dead_letter_queue" dead_letter_queue = "dead_letter_queue"
tenant_event_queue_suffix = "_tenant_event_queue"
tenant_event_dlq_suffix = "_tenant_events_dlq"
tenant_exchange_name = "tenants-exchange"
queue_expiration_time = 300000 # 5 minutes in milliseconds
service_request_queue_prefix = "cv_analysis_request_queue"
service_request_exchange_name = "cv_analysis_request_exchange"
service_response_exchange_name = "cv_analysis_response_exchange"
service_dlq_name = "cv_analysis_dlq"
[storage] [storage]
backend = "s3" backend = "s3"
@ -62,6 +42,3 @@ connection_string = ""
[storage.tenant_server] [storage.tenant_server]
public_key = "" public_key = ""
endpoint = "http://tenant-user-management:8081/internal-api/tenants" endpoint = "http://tenant-user-management:8081/internal-api/tenants"
[kubernetes]
pod_name = "test_pod"

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

8
flake.lock generated
View File

@ -20,17 +20,17 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1715155958, "lastModified": 1711703276,
"narHash": "sha256-I/V8oiPfK0KIQUc+3sAQLJJYa7L3edd9gdnKP2XvT7E=", "narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "240b1d794bbfca3522dec880a3aa300932bbfd98", "rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "240b1d794bbfca3522dec880a3aa300932bbfd98",
"type": "github" "type": "github"
} }
}, },

View File

@ -2,7 +2,7 @@
description = "An flake to use a Python poetry project in an FHS environment when poetry2nix is uncooperative"; description = "An flake to use a Python poetry project in an FHS environment when poetry2nix is uncooperative";
inputs = { inputs = {
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs/240b1d794bbfca3522dec880a3aa300932bbfd98"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
}; };
outputs = { outputs = {
self, self,

5709
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "cv-analysis-service" name = "cv-analysis-service"
version = "2.30.0" version = "2.6.1"
description = "" description = ""
authors = ["Isaac Riley <isaac.riley@knecon.com>"] authors = ["Isaac Riley <isaac.riley@knecon.com>"]
readme = "README.md" readme = "README.md"
@ -25,8 +25,8 @@ coverage = "^5.5"
dependency-check = "^0.6.0" dependency-check = "^0.6.0"
lorem-text = "^2.1" lorem-text = "^2.1"
PyMuPDF = "^1.19.6" PyMuPDF = "^1.19.6"
pyinfra = { version = "3.4.2", source = "gitlab-research" } pyinfra = { version = "^2.2.0", source = "gitlab-research" }
kn-utils = { version = ">=0.4.0", source = "gitlab-research" } kn-utils = { version = "0.2.7", source = "gitlab-research" }
pdf2img = { version = "0.7.0", source = "gitlab-red" } pdf2img = { version = "0.7.0", source = "gitlab-red" }
dvc-azure = "^2.21.2" dvc-azure = "^2.21.2"
pymupdf = "^1.24.1" pymupdf = "^1.24.1"
@ -43,7 +43,6 @@ mypy = "^1.10.0"
pylint = "^3.1.0" pylint = "^3.1.0"
pre-commit = "^3.7.0" pre-commit = "^3.7.0"
semver = "^3.0.2" semver = "^3.0.2"
cyclonedx-bom = "^4.4.3"
[tool.poetry.group.docs.dependencies] [tool.poetry.group.docs.dependencies]
sphinx = "^7.2.6" sphinx = "^7.2.6"

View File

@ -1,41 +0,0 @@
#!/bin/bash
python_version=$1
gitlab_user=$2
gitlab_personal_access_token=$3
# cookiecutter https://gitlab.knecon.com/knecon/research/template-python-project.git --checkout master
# latest_dir=$(ls -td -- */ | head -n 1) # should be the dir cookiecutter just created
# cd $latest_dir
pyenv install $python_version
pyenv local $python_version
pyenv shell $python_version
# install poetry globally (PREFERRED), only need to install it once
# curl -sSL https://install.python-poetry.org | python3 -
# remember to update poetry once in a while
poetry self update
# install poetry in current python environment, can lead to multiple instances of poetry being installed on one system (DISPREFERRED)
# pip install --upgrade pip
# pip install poetry
poetry config virtualenvs.in-project true
poetry config installer.max-workers 10
poetry config repositories.gitlab-research https://gitlab.knecon.com/api/v4/groups/19/-/packages/pypi
poetry config http-basic.gitlab-research ${gitlab_user} ${gitlab_personal_access_token}
poetry config repositories.gitlab-red https://gitlab.knecon.com/api/v4/groups/12/-/packages/pypi
poetry config http-basic.gitlab-red ${gitlab_user} ${gitlab_personal_access_token}
poetry config repositories.gitlab-fforesight https://gitlab.knecon.com/api/v4/groups/269/-/packages/pypi
poetry config http-basic.gitlab-fforesight ${gitlab_user} ${gitlab_personal_access_token}
poetry env use $(pyenv which python)
poetry install --with=dev
poetry update
source .venv/bin/activate
pre-commit install
pre-commit autoupdate

View File

@ -26,5 +26,4 @@ best_result = list(pipe(data={"pdf": pdf_bytes, "vlp_output": vlp_output}))
# print(best_result) # print(best_result)
if __name__ == "__main__":
annotate_pdf(pdf_bytes, best_result, output_path=args.output) annotate_pdf(pdf_bytes, best_result, output_path=args.output)

View File

@ -135,11 +135,9 @@ FILTERS: dict[str, dict[int, np.ndarray | None]] = {
def filter_fp_col_lines(line_list: list[int], filt_sums: Array) -> list[int]: def filter_fp_col_lines(line_list: list[int], filt_sums: Array) -> list[int]:
if not list(line_list): if not line_list:
return [] return []
centers = list(np.where((filt_sums[1:-1] < filt_sums[:-2]) * (filt_sums[1:-1] < filt_sums[2:]))[0] + 1) centers = list(np.where((filt_sums[1:-1] < filt_sums[:-2]) * (filt_sums[1:-1] < filt_sums[2:]))[0] + 1)
if not centers:
return []
if line_list[0] > centers[0]: if line_list[0] > centers[0]:
centers = centers[1:] + [len(filt_sums) - 1] centers = centers[1:] + [len(filt_sums) - 1]

View File

@ -4,9 +4,7 @@ from pathlib import Path
from typing import Union from typing import Union
import fitz # type: ignore import fitz # type: ignore
from kn_utils.logging import logger from kn_utils.logging import logger # type: ignore
from cv_analysis.utils.image_extraction import mirror_horizontal # type: ignore
def annotate_pdf( def annotate_pdf(
@ -29,9 +27,8 @@ def annotate_page(page: fitz.Page, prediction):
bbox = itemgetter("x1", "y1", "x2", "y2")(box["box"]) bbox = itemgetter("x1", "y1", "x2", "y2")(box["box"])
label, probability, uuid = itemgetter("label", "probability", "uuid")(box) label, probability, uuid = itemgetter("label", "probability", "uuid")(box)
bbox = mirror_on_x_axis(bbox, page.bound().height)
x0, y0, x1, y1 = bbox x0, y0, x1, y1 = bbox
print(page.bound)
page.draw_rect(fitz.Rect(x0, y0, x1, y1), color=(0, 0, 1), width=2) page.draw_rect(fitz.Rect(x0, y0, x1, y1), color=(0, 0, 1), width=2)
label_x, label_y = x0, y0 - 5 label_x, label_y = x0, y0 - 5
page.insert_text( page.insert_text(
@ -43,18 +40,18 @@ def annotate_page(page: fitz.Page, prediction):
for line in prediction.get("tableLines", []): for line in prediction.get("tableLines", []):
start = itemgetter("x1", "y1")(line) start = itemgetter("x1", "y1")(line)
end = itemgetter("x2", "y2")(line) end = itemgetter("x2", "y2")(line)
bbox = (*start, *end)
height = page.bound()[3]
bbox = mirror_horizontal(bbox, page_height=height)
start = tuple(bbox[:2])
end = tuple(bbox[2:])
page.draw_line(start, end, color=(1, 0, 0.5), width=1) page.draw_line(start, end, color=(1, 0, 0.5), width=1)
return page return page
def mirror_on_x_axis(bbox, page_height):
x0, y0, x1, y1 = bbox
y0_new = page_height - y1
y1_new = page_height - y0
return x0, y0_new, x1, y1_new
@singledispatch @singledispatch
def provide_byte_stream(pdf: Union[bytes, Path, str]) -> None: def provide_byte_stream(pdf: Union[bytes, Path, str]) -> None:
pass pass

View File

@ -3,10 +3,10 @@ import os
import cv2 import cv2
from matplotlib import pyplot as plt from matplotlib import pyplot as plt
# if os.environ.get("USER") == "isaac": if os.environ["USER"] == "isaac":
# import matplotlib import matplotlib
# matplotlib.use("module://matplotlib-backend-wezterm") matplotlib.use("module://matplotlib-backend-wezterm")
def show_image_cv2(image, maxdim=700): def show_image_cv2(image, maxdim=700):

View File

@ -2,7 +2,7 @@ import json
from dataclasses import dataclass from dataclasses import dataclass
from functools import partial from functools import partial
from operator import itemgetter from operator import itemgetter
from typing import Literal, SupportsIndex, Tuple from typing import SupportsIndex, Tuple
import fitz # type: ignore import fitz # type: ignore
import numpy as np import numpy as np
@ -39,7 +39,7 @@ def transform_image_coordinates_to_pdf_coordinates(
return rect.x0, rect.y0, rect.x1, rect.y1 return rect.x0, rect.y0, rect.x1, rect.y1
def rescale_to_pdf(bbox: BBoxType, page_info: PageInfo) -> BBoxType: def rescale_to_pdf(bbox: BBoxType, page_info: PageInfo) -> tuple[float, float, float, float]:
round3 = lambda x: tuple(map(lambda y: round(y, 3), x)) round3 = lambda x: tuple(map(lambda y: round(y, 3), x))
pdf_h, pdf_w = page_info.height, page_info.width pdf_h, pdf_w = page_info.height, page_info.width
@ -54,41 +54,6 @@ def rescale_to_pdf(bbox: BBoxType, page_info: PageInfo) -> BBoxType:
return round3((bbox[0] * ratio_w, bbox[1] * ratio_h, bbox[2] * ratio_w, bbox[3] * ratio_h)) return round3((bbox[0] * ratio_w, bbox[1] * ratio_h, bbox[2] * ratio_w, bbox[3] * ratio_h))
def mirror_horizontal(bbox: BBoxType, page_height: int | float) -> BBoxType:
x0, y0, x1, y1 = bbox
y0_new = page_height - y0
y1_new = page_height - y1
return x0, y0_new, x1, y1_new
def mirror_vertical(bbox: BBoxType, page_width: int | float) -> BBoxType:
x0, y0, x1, y1 = bbox
x0_new = page_width - x1
x1_new = page_width - x0
return x0_new, y0, x1_new, y1
def derotate_image(bbox: BBoxType, page_info: PageInfo) -> BBoxType:
logger.debug(f"{page_info.rotation=}")
match page_info.rotation:
case 0:
bbox = mirror_horizontal(bbox, page_info.height)
case 90:
pass
case 180:
bbox = mirror_vertical(bbox, page_info.height)
case 270:
bbox = mirror_vertical(mirror_horizontal(bbox, page_info.height), page_info.height)
case _:
logger.warning(f"Unknown rotation: {page_info.rotation}")
pass
return bbox
def transform_table_lines_by_page_info(bboxes: dict, offsets: tuple, page_info: PageInfo) -> dict: def transform_table_lines_by_page_info(bboxes: dict, offsets: tuple, page_info: PageInfo) -> dict:
transform = partial(rescale_to_pdf, page_info=page_info) transform = partial(rescale_to_pdf, page_info=page_info)
@ -102,11 +67,9 @@ def transform_table_lines_by_page_info(bboxes: dict, offsets: tuple, page_info:
return (x1 + offset_x, y1 + offset_y, x2 + offset_x, y2 + offset_y) return (x1 + offset_x, y1 + offset_y, x2 + offset_x, y2 + offset_y)
derotate = partial(derotate_image, page_info=page_info)
unpack = itemgetter("x1", "y1", "x2", "y2") unpack = itemgetter("x1", "y1", "x2", "y2")
pack = lambda x: {"x1": x[0], "y1": x[1], "x2": x[2], "y2": x[3]} pack = lambda x: {"x1": x[0], "y1": x[1], "x2": x[2], "y2": x[3]}
convert = compose(pack, derotate, apply_offsets, transform, unpack) convert = compose(pack, apply_offsets, transform, unpack)
table_lines = bboxes.get("tableLines", []) table_lines = bboxes.get("tableLines", [])
bboxes["tableLines"] = list(map(convert, table_lines)) bboxes["tableLines"] = list(map(convert, table_lines))