diff --git a/Dockerfile b/Dockerfile index 5f8821b..765d09a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,25 +6,30 @@ ENV UV_COMPILE_BYTECODE=1 ENV UV_LINK_MODE=copy RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --frozen --no-install-project --no-dev + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project --no-dev ADD . /app WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --frozen --no-dev + uv sync --frozen --no-dev -FROM python:3.12-slim-bookworm AS runner +FROM python:3.12-slim AS runner -COPY --from=builder /app /app +ARG VERSION +ARG BUILD_TIME -WORKDIR /app +ENV PYTHON_ENV=production ENV PATH="/app/.venv/bin:$PATH" +COPY --from=builder /app /app + +WORKDIR /app + EXPOSE 8000 -CMD ["fastapi", "run", "app/main.py", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +CMD ["fastapi", "run", "server/main.py", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ff0c676..e1d985d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,16 +11,22 @@ dependencies = [ "tensorflow-cpu>=2.18.0", "weaviate-client>=3.26.7,<4.0.0", "firebase-admin>=6.6.0", + "numpy>=2.0.2", + "opencv-contrib-python-headless>=4.10.0.84", + "pydantic-settings>=2.7.0", + "scipy>=1.14.1", ] [dependency-groups] dev = [ + "changelog-gen>=0.13.5", + "pytest>=8.3.4", "ruff>=0.8.3", "taskipy>=1.14.1", ] [tool.taskipy.tasks] -dev = "fastapi dev app/main.py" +dev = "fastapi dev server/main.py" lint = "ruff check --fix" format = "ruff format" docker-build = "docker build --build-arg VERSION_TAG=dev -t unai-api-fastapi:dev ." diff --git a/app/__init__.py b/server/__init__.py similarity index 100% rename from app/__init__.py rename to server/__init__.py diff --git a/app/dependencies.py b/server/dependencies.py similarity index 100% rename from app/dependencies.py rename to server/dependencies.py diff --git a/app/main.py b/server/main.py similarity index 100% rename from app/main.py rename to server/main.py diff --git a/app/routers/__init__.py b/server/routers/__init__.py similarity index 100% rename from app/routers/__init__.py rename to server/routers/__init__.py diff --git a/app/routers/detect.py b/server/routers/detect.py similarity index 94% rename from app/routers/detect.py rename to server/routers/detect.py index 198bbf3..bcbd215 100644 --- a/app/routers/detect.py +++ b/server/routers/detect.py @@ -8,7 +8,7 @@ from PIL import Image import tensorflow as tf -from ..utils.helpers import Data, save_file, upload_file +from ..utils.helpers_obj import Data, save_file, upload_file from ..utils.models import Detector router = APIRouter( @@ -47,7 +47,7 @@ async def detect(background_tasks: BackgroundTasks, request: ImageRequest): background_tasks.add_task(upload_file, file_path, f"images/{id}.jpg") data = Data("temp-1") - data.images = {} if data.images == None else data.images + data.images = {} if data.images is None else data.images data.images[id] = image img = data.get_images() diff --git a/app/routers/health.py b/server/routers/health.py similarity index 80% rename from app/routers/health.py rename to server/routers/health.py index 7bd4823..35057cf 100644 --- a/app/routers/health.py +++ b/server/routers/health.py @@ -6,6 +6,7 @@ responses={404: {"description": "Not found"}}, ) + @router.get("/") async def get_health(): - return {"status": "OK", "version":"dev"} + return {"status": "OK", "version": "dev"} diff --git a/server/routers/scan.py b/server/routers/scan.py new file mode 100644 index 0000000..b55fe82 --- /dev/null +++ b/server/routers/scan.py @@ -0,0 +1,65 @@ +from fastapi import APIRouter, HTTPException +import base64 + +from utils.scan import ( + align_crop, + align_inputs, + detect_markers, + detect_qr, + extract_data, + highlight, +) + +router = APIRouter( + prefix="/scan", + tags=["scan"], + dependencies=[], + responses={404: {"description": "Not found"}}, +) + + +@router.post("/") +async def scan(images: list[str]): + meta_data = [] + cropped_images = [] + for image in images: + image = base64.b64decode(images[0].split(",")[1]) + + markers = detect_markers(image) + cropped_image = align_crop(image, markers) + cropped_images.append(cropped_image) + meta_data.append(detect_qr(cropped_image)) + + if not all(item["scale"] == meta_data[0]["scale"] for item in meta_data): + raise HTTPException(status_code=409, detail="Pages are not of a same scale") + + total_choice_indexes = set() + for data in meta_data: + start = data["choice"]["start"] + count = data["choice"]["count"] + + choice_indexes = list(range(start, start + count)) + total_choice_indexes.update(choice_indexes) + + if not set(range(1, meta_data[0]["choice"]["total"])).issubset( + total_choice_indexes + ): + raise HTTPException(status_code=400, detail="Insufficient number of pages") + + highlights = [] + choices = [] + for index, cropped_image in enumerate(cropped_images): + # print(meta_data) + option_count = meta_data[index]["option"] + start = meta_data[index]["choice"]["start"] + choice_count = meta_data[index]["choice"]["count"] + inputs = align_inputs(cropped_image, option_count, start, choice_count) + # print(inputs) + choices.extend(extract_data(cropped_image, inputs)) + highlights.append(highlight(cropped_image, option_count, inputs, choices)) + + # print(choices) + return { + "data": {"name": meta_data[0]["scale"], "choices": choices}, + "highlights": highlights, + } diff --git a/app/routers/search.py b/server/routers/search.py similarity index 89% rename from app/routers/search.py rename to server/routers/search.py index 1093f81..3b89f72 100644 --- a/app/routers/search.py +++ b/server/routers/search.py @@ -8,7 +8,7 @@ import weaviate as Weaviate import meilisearch as Meilisearch -from ..utils.helpers import Data +from ..utils.helpers_obj import Data from ..utils.models import OneShotClassifier router = APIRouter( @@ -20,16 +20,17 @@ classifier = OneShotClassifier() WEAVIATE_URL = os.getenv("WEAVIATE_URL") -WEAVIATE_URL = "127.0.0.1" if WEAVIATE_URL == None else WEAVIATE_URL +WEAVIATE_URL = "127.0.0.1" if WEAVIATE_URL is None else WEAVIATE_URL MEILISEARCH_URL = os.getenv("MEILISEARCH_URL") -MEILISEARCH_URL = "127.0.0.1" if MEILISEARCH_URL == None else MEILISEARCH_URL +MEILISEARCH_URL = "127.0.0.1" if MEILISEARCH_URL is None else MEILISEARCH_URL MEILISEARCH_API_KEY = os.getenv("MEILISEARCH_SECRET") -MEILISEARCH_API_KEY = "" if MEILISEARCH_API_KEY == None else MEILISEARCH_API_KEY +MEILISEARCH_API_KEY = "" if MEILISEARCH_API_KEY is None else MEILISEARCH_API_KEY weaviate = None meilisearch = None + class Box(BaseModel): x: float y: float @@ -62,8 +63,12 @@ def format(data): @router.post("/") async def search(request: ImageRequest): try: - weaviate = Weaviate.Client(url=WEAVIATE_URL) if weaviate == None else weaviate - meilisearch = Meilisearch.Client(MEILISEARCH_URL, MEILISEARCH_API_KEY) if meilisearch == None else meilisearch + weaviate = Weaviate.Client(url=WEAVIATE_URL) if weaviate is None else weaviate + meilisearch = ( + Meilisearch.Client(MEILISEARCH_URL, MEILISEARCH_API_KEY) + if meilisearch is None + else meilisearch + ) id = request.id file_path = f"assets/images/{id}.jpg" diff --git a/server/utils/__init__.py b/server/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/utils/helper_omr.py b/server/utils/helper_omr.py new file mode 100644 index 0000000..023cb90 --- /dev/null +++ b/server/utils/helper_omr.py @@ -0,0 +1,72 @@ +import numpy as np +import cv2 + + +def is_circle_inside(circle_center): + # from markers 3,5,11,9 + boundary = [ + [70.0, 390.5], + [2306.0, 390.5], + [2306.0, 3294.0], + [70.0, 3294.0], + ] + + x, y = circle_center + x_min, y_min = boundary[0] + x_max, y_max = boundary[2] + + if x_min <= x <= x_max and y_min <= y <= y_max: + return True + else: + return False + + +def choice_generator(option, index, total): + factor = 4 + index = index - 1 + unit = 15 + x = 55 + y = 100 + + while index < total: + if index % 40 == 0 and index != 0: + x += 110 + y = 100 + elif index % 5 == 0 and index != 0: + y += 15 + + y += unit + + choices = None + if option == 2: + choices = [ + {"value": 1, "chord": [(x) * factor, (y) * factor]}, + {"value": 0, "chord": [(x + unit) * factor, (y) * factor]}, + ] + elif option == 5: + choices = [ + {"value": 0, "chord": [(x) * factor, (y) * factor]}, + {"value": 1, "chord": [(x + 1 * unit) * factor, (y) * factor]}, + {"value": 2, "chord": [(x + 2 * unit) * factor, (y) * factor]}, + {"value": 3, "chord": [(x + 3 * unit) * factor, (y) * factor]}, + {"value": 4, "chord": [(x + 4 * unit) * factor, (y) * factor]}, + ] + + yield {"index": index + 1, "choices": choices} + + index += 1 + + +def calculate_bw_ratio(image): + # Threshold the image to get binary image with white pixels + _, binary = cv2.threshold(image, 250, 255, cv2.THRESH_BINARY) + + # Count the white pixels + num_white_pixels = np.count_nonzero(binary == 255) + + # Calculate the ratio of white pixels to total pixels + height, width = binary.shape + num_pixels = width * height + white_ratio = num_white_pixels / num_pixels + + return white_ratio diff --git a/app/utils/helpers.py b/server/utils/helpers_obj.py similarity index 96% rename from app/utils/helpers.py rename to server/utils/helpers_obj.py index 525faf3..4884983 100644 --- a/app/utils/helpers.py +++ b/server/utils/helpers_obj.py @@ -10,11 +10,11 @@ from PIL import Image, ImageOps, ExifTags PRESET = os.getenv("PRESET") -PRESET = "deploy" if PRESET == None else PRESET +PRESET = "deploy" if PRESET is None else PRESET STORAGE_BUCKET = os.getenv("STORAGE_BUCKET") -STORAGE_BUCKET = "" if STORAGE_BUCKET == None else STORAGE_BUCKET +STORAGE_BUCKET = "" if STORAGE_BUCKET is None else STORAGE_BUCKET FIREBASE_CONFIG = "" @@ -49,7 +49,7 @@ def upload_file(source_file_location: str, dest_file_location: str | None = None if PRESET != "deploy": return - if dest_file_location == None: + if dest_file_location is None: dest_file_location = source_file_location blob = bucket.blob(dest_file_location) @@ -63,7 +63,7 @@ def download_file(source_file_location: str, dest_file_location: str | None = No if file_exists_check(dest_file_location): return - if dest_file_location == None: + if dest_file_location is None: dest_file_location = source_file_location blob = bucket.blob(source_file_location) @@ -286,7 +286,7 @@ def __pipeline__(self, info, type, resize_dim, return_annotations): else (single_crop, annotation) ) - if return_annotations == True: + if return_annotations is True: yield { "id": info["id"], "photography": info["photography"], @@ -310,7 +310,7 @@ def __pipeline__(self, info, type, resize_dim, return_annotations): else (self.images[info["id"]], annotations) ) - if return_annotations == True: + if return_annotations is True: yield { "id": info["id"], "photography": info["photography"], @@ -346,12 +346,12 @@ def __img_pipeline__(self, input, type, resize_dim, return_annotations): else (single_crop, annotation) ) - if return_annotations == True: + if return_annotations is True: yield {"id": id, "image": single_crop, "bboxes": annotations} else: yield {"id": id, "image": single_crop} else: - if self.annotations == None: + if self.annotations is None: annotations = None image = ( resize(self.images[id], resize_dim) @@ -366,7 +366,7 @@ def __img_pipeline__(self, input, type, resize_dim, return_annotations): else (self.images[id], annotations) ) - if return_annotations == True: + if return_annotations is True: yield {"id": id, "image": image, "bboxes": annotations} else: yield {"id": id, "image": image} @@ -383,7 +383,7 @@ def get_image(self, select, type="full", resize_dim=None, return_annotations=Fal def get_images( self, select=None, type="full", resize_dim=None, return_annotations=False ): - if self.meta == None: + if self.meta is None: lst = map( lambda x: self.__img_pipeline__( x, type, resize_dim, return_annotations @@ -391,7 +391,7 @@ def get_images( self.images.items(), ) else: - if select == None: + if select is None: # print("Id", self.id) filtered_images = self.meta["images"] elif select == "face": diff --git a/app/utils/models.py b/server/utils/models.py similarity index 97% rename from app/utils/models.py rename to server/utils/models.py index 3025a7a..3088f71 100644 --- a/app/utils/models.py +++ b/server/utils/models.py @@ -3,20 +3,20 @@ import requests import tensorflow as tf -from ..utils.helpers import convert_box, resize +from ..utils.helpers_obj import convert_box, resize DET_DIM = (640, 640) CLASS_DIM = (256, 256) TF_SERVING_URL = os.getenv("TF_SERVING_URL") -TF_SERVING_URL = "" if TF_SERVING_URL == None else TF_SERVING_URL +TF_SERVING_URL = "" if TF_SERVING_URL is None else TF_SERVING_URL def fetch(path: str, method: str, body=None) -> dict: url = f"{TF_SERVING_URL}{path}" headers = {"Content-Type": "application/json"} - if body != None: + if body is not None: data = json.dumps(body) if method == "GET": @@ -160,7 +160,7 @@ def predict(self, inputs): # images = tf.cast(images, tf.float32)/255.0 images = tf.expand_dims(images, axis=0) - if self.dim == None: + if self.dim is None: dim = tf.map_fn((lambda x: x.shape[:2][::-1]), inputs, dtype=tf.int32) else: dim = tf.map_fn( diff --git a/server/utils/scan.py b/server/utils/scan.py new file mode 100644 index 0000000..a8e454f --- /dev/null +++ b/server/utils/scan.py @@ -0,0 +1,382 @@ +import json +import base64 +from fastapi import HTTPException + +import numpy as np +import cv2 +from cv2 import aruco +from PIL import Image, ImageDraw +from scipy.optimize import linear_sum_assignment + +from utils.helper_omr import calculate_bw_ratio, choice_generator, is_circle_inside + + +def detect_markers(image_buffer, findNecessary=True): + image_array = np.frombuffer(image_buffer, np.uint8) + image = cv2.imdecode(image_array, cv2.IMREAD_COLOR) + + image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + image = cv2.convertScaleAbs(image, alpha=1.5, beta=0) + # kernel = np.array([[-1, -1, -1], + # [-1, 9, -1], + # [-1, -1, -1]]) + # image = cv2.filter2D(image, -1, kernel) + + # cv2.imshow("detect_markers", cv2.resize(image, (0, 0), fx=0.55, fy=0.55)) + # cv2.waitKey(0) + + aruco_dict = aruco.getPredefinedDictionary(aruco.DICT_4X4_100) + parameters = aruco.DetectorParameters() + + corners, ids, rejected = aruco.detectMarkers( + image=image, dictionary=aruco_dict, parameters=parameters + ) + + if ids is None: + raise HTTPException(status_code=404, detail="Unable to Detect any marker") + + print("ids", ids.flatten()) + sufficient = sum(num in ids for num in [1, 2, 9, 11]) >= 4 + print("total found", list(num in ids for num in [1, 2, 9, 11])) + + if not sufficient and findNecessary: + raise HTTPException(status_code=404, detail="Unable to Detect Corner markers") + + markers = [ + { + "id": id[0].tolist(), + "positions": [ + float(np.mean(corner[0, :, 0])), + float(np.mean(corner[0, :, 1])), + ], + } + for id, corner in zip(ids, corners) + ] + markers.sort(key=lambda x: x["id"]) + + return markers + + +def align_crop(image_buffer, src_markers): + image_array = np.frombuffer(image_buffer, np.uint8) + image = cv2.imdecode(image_array, cv2.IMREAD_COLOR) + + # cv2.imshow("align_crop", cv2.resize(image, (0, 0), fx=0.55, fy=0.55)) + # cv2.waitKey(0) + + factor = 4 + width, height = 595 * factor, 842 * factor + + corners = [] + for target_key in [1, 2, 11, 9]: + src_marker = next( + ( + src_marker + for src_marker in src_markers + if src_marker["id"] == target_key + ), + None, + ) + if src_marker is not None: + corners.append(src_marker["positions"]) + + src_points = np.array(corners, dtype=np.float32) + dest_points = np.array( + [[70, 70], [width - 74, 70], [width - 74, height - 74], [70, height - 74]], + dtype=np.float32, + ) + + transform_matrix = cv2.getPerspectiveTransform(src_points, dest_points) + cropped_image = cv2.warpPerspective(image, transform_matrix, (width, height)) + + success, encoded_image = cv2.imencode(".jpg", cropped_image) + src_markers = detect_markers(encoded_image.tobytes()) + src_markers = [ + element for element in src_markers if element["id"] in [3, 5, 7, 9, 11] + ] + + dest_markers = [ + {"id": 1, "positions": [70.0, 70.0]}, + {"id": 2, "positions": [2306.0, 70.0]}, + {"id": 3, "positions": [70.0, 390.5]}, + {"id": 4, "positions": [1188.0, 390.5]}, + {"id": 5, "positions": [2306.0, 390.5]}, + {"id": 6, "positions": [70.25, 1842.25]}, + {"id": 7, "positions": [1188.25, 1842.25]}, + {"id": 8, "positions": [2306.25, 1842.25]}, + {"id": 9, "positions": [70.0, 3294.0]}, + {"id": 10, "positions": [1188.0, 3294.0]}, + {"id": 11, "positions": [2306.0, 3294.0]}, + ] + + src_points = [] + dest_points = [] + # matches = [] + + for index, src_marker in enumerate(src_markers): + dest_marker = next( + ( + dest_marker + for dest_marker in dest_markers + if dest_marker["id"] == src_marker["id"] + ), + None, + ) + if dest_marker is not None: + src_points.append(src_marker["positions"]) + dest_points.append(dest_marker["positions"]) + # matches.append((index, index)) + + src_points = np.array(src_points) + dest_points = np.array(dest_points) + + homography, _ = cv2.findHomography(src_points, dest_points, cv2.RANSAC, 5.0) + warped_image = cv2.warpPerspective(cropped_image, homography, (width, height)) + + image = cv2.cvtColor(warped_image, cv2.COLOR_BGR2GRAY) + image = cv2.convertScaleAbs(image, alpha=1.1, beta=0) + _, buffer = cv2.imencode(".jpg", warped_image) + + return buffer.tobytes() + + +def detect_qr(image_buffer): + image_array = np.frombuffer(image_buffer, np.uint8) + image = cv2.imdecode(image_array, cv2.IMREAD_COLOR) + + x = image.shape[1] - 105 - 380 + y = 55 + image = image[y : y + 380, x : x + 380] + image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + image = cv2.convertScaleAbs(image, alpha=1.5, beta=0) + + # Upscale the image + # image = sr.upsample(image) + # image = cv2.resize(image, (0, 0), fx=0.25, fy=0.25) + # kernel = np.array([[-1, -1, -1], + # [-1, 9, -1], + # [-1, -1, -1]]) + # image = cv2.filter2D(image, -1, kernel) + + # cv2.imshow("detect_qr", image) + # cv2.waitKey(0) + + detector = cv2.QRCodeDetector() + retval, info, points, _ = detector.detectAndDecodeMulti(image) + + if retval is False: + raise HTTPException(status_code=404, detail="Unable to detected QR Code") + + # print(info) + + try: + data = json.loads(info[0]) + return { + "scale": data["scale"], + "option": data["option"], + "choice": { + "start": data["start"], + "count": data["count"], + "total": data["total"], + }, + } + except: + raise HTTPException(status_code=400, detail="Invalid QR Code detected") + + +def align_inputs(image_buffer, options_count, choice_start, choice_count): + image_array = np.frombuffer(image_buffer, np.uint8) + image = cv2.imdecode(image_array, cv2.IMREAD_COLOR) + + image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + image = cv2.GaussianBlur(image, (5, 5), 0) + + # cv2.imshow("align_inputs", cv2.resize(image, (0,0), fx=0.2, fy=0.2)) + # cv2.waitKey(0) + + circles = cv2.HoughCircles( + image, + cv2.HOUGH_GRADIENT, + dp=1, + minDist=50, + param1=50, + param2=30, + minRadius=5, + maxRadius=50, + ) + + dest_circles = [] + if circles is not None: + circles = np.round(circles[0, :]).astype(int) + + for x, y, r in circles: + if is_circle_inside((x, y)): + dest_circles.append((x, y)) + + choices = list(choice_generator(options_count, choice_start, choice_count)) + + src_circles = np.array( + [choice["chord"] for data in choices for choice in data["choices"]] + ) + dest_circles = np.array(dest_circles) + + try: + distances = np.linalg.norm(src_circles[:, np.newaxis] - dest_circles, axis=-1) + except: + raise HTTPException(status_code=500, detail="Unable to calculate") + + row_indices, col_indices = linear_sum_assignment(distances) + + matched_pairs = [(i, j) for i, j in zip(row_indices, col_indices)] + + for i in range(len(choices)): + for j in range(len(choices[i]["choices"])): + choices[i]["choices"][j]["chord"] = None + + for pair in matched_pairs: + index = pair[0] + choices[index // options_count]["choices"][index % options_count]["chord"] = ( + dest_circles[pair[1]].tolist() + ) + + return choices + + +def extract_data(image_buffer, inputs): + image_array = np.frombuffer(image_buffer, np.uint8) + image = cv2.imdecode(image_array, cv2.IMREAD_COLOR) + image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + _, image = cv2.threshold(image, 64, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + + # cv2.imshow("align_inputs", cv2.resize(image, (0,0), fx=0.2, fy=0.2)) + # cv2.waitKey(0) + + factor = 4 + threshold = 0.12 + + results = [] + for input_data in inputs: + index = input_data["index"] + choices = input_data["choices"] + + bw_ratios = [] + for choice in choices: + chord = choice["chord"] + if chord is None: + continue + + x, y = chord + w, h = 12, 12 + left = int(x - (w / 2) * factor) + top = int(y - (h / 2) * factor) + width = int(w * factor) + height = int(h * factor) + + crop_image = image[top : top + height, left : left + width] + + # cv2.imwrite(f'./temp/inputs/{index}-{choice["name"]}.jpg', crop_image) + + bw_ratio = calculate_bw_ratio(crop_image) + bw_ratios.append(bw_ratio) + + choice_index = bw_ratios.index(min(bw_ratios)) if bw_ratios else None + second_choice_index = ( + bw_ratios.index( + min(bw_ratios[:choice_index] + bw_ratios[choice_index + 1 :]) + ) + if len(bw_ratios) > 1 + else None + ) + + delta_bw_ratio = ( + bw_ratios[choice_index] - bw_ratios[second_choice_index] + if choice_index is not None and second_choice_index is not None + else None + ) + + if delta_bw_ratio is not None and abs(delta_bw_ratio) >= threshold: + choice_index = choice_index if delta_bw_ratio < 0 else second_choice_index + else: + choice_index = None + + result = { + "index": index, + "value": choices[choice_index]["value"] + if choice_index is not None + else choice_index, + # 'deltaBWRatio': delta_bw_ratio + } + results.append(result) + + return results + + +def draw_circle(canvas, x, y, circle_type, value=None): + draw = ImageDraw.Draw(canvas) + + if circle_type == "alignment": + draw.ellipse( + (x - 27.5, y - 27.5, x + 27.5, y + 27.5), outline=(34, 197, 94), width=7 + ) + else: + color_map = [ + (225, 29, 72), + (192, 38, 211), + (147, 51, 234), + (79, 70, 229), + (96, 165, 250), + ] + color = color_map[value] if value is not None else color_map[0] + draw.ellipse( + (x - 12.5, y - 12.5, x + 12.5, y + 12.5), + fill=color, + outline=(0, 0, 0), + width=3, + ) + + +def highlight(image_buffer, option_count, inputs, responses): + image_array = np.frombuffer(image_buffer, np.uint8) + image = cv2.imdecode(image_array, cv2.IMREAD_COLOR) + image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + _, image = cv2.threshold(image, 64, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) + + is_alignment = True + is_response = True if responses is not None else False + + inputs = [[choice["chord"] for choice in input["choices"]] for input in inputs] + canvas = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) + + for q_index, input in enumerate(inputs): + for d_index, dot in enumerate(input): + if dot is None: + continue + + x, y = dot + + if is_alignment: + draw_circle(canvas, x, y, "alignment") + + if is_response: + choice = responses[q_index]["value"] if responses is not None else None + if choice is not None: + if option_count == 2 and choice == 1 - d_index: + draw_circle(canvas, x, y, "response", choice * 4) + elif option_count == 5 and choice == d_index: + draw_circle(canvas, x, y, "response", choice) + + canvas = cv2.cvtColor(np.array(canvas), cv2.COLOR_RGB2BGR) + + height, width = canvas.shape[:2] + new_width = int((720 / height) * width) + canvas = cv2.resize(canvas, (new_width, 720)) + + # cv2.imshow("highlighted", canvas) + # cv2.waitKey(0) + + _, buffer = cv2.imencode(".jpg", canvas) + image_base64 = base64.b64encode(buffer) + image_str = image_base64.decode("utf-8") + + return f"data:image/jpeg;base64,{image_str}" diff --git a/uv.lock b/uv.lock index 882bf5c..688d78a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,8 +1,12 @@ version = 1 requires-python = ">=3.12" resolution-markers = [ - "python_full_version < '3.13'", - "python_full_version >= '3.13'", + "python_full_version < '3.13' and platform_system == 'Darwin'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and platform_system == 'Linux'", + "(python_full_version < '3.13' and platform_machine != 'aarch64' and platform_system == 'Linux') or (python_full_version < '3.13' and platform_system != 'Darwin' and platform_system != 'Linux')", + "python_full_version >= '3.13' and platform_system == 'Darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_system == 'Linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_system == 'Linux') or (python_full_version >= '3.13' and platform_system != 'Darwin' and platform_system != 'Linux')", ] [[package]] @@ -140,6 +144,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, ] +[[package]] +name = "changelog-gen" +version = "0.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitpython" }, + { name = "jinja2" }, + { name = "pygments" }, + { name = "rtoml" }, + { name = "typer-slim" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/a4/a4389afbebb069db16faa2789913be3d0569ed6b973a9b48df790fcf8bcd/changelog_gen-0.13.5.tar.gz", hash = "sha256:ab326024008dd304e8e6b5c80ac0d7685b27c1edb65f1b4178f96581063c3804", size = 109892 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/2c/7c5b9ac70e7b6158874c1787bd6d1a52f91ff79e913cca0412fcc9b72795/changelog_gen-0.13.5-py3-none-any.whl", hash = "sha256:b972eb358237592efd7d2dd47bdc9d3251414bebe6662de5f444c059bddce6ab", size = 27495 }, +] + [[package]] name = "charset-normalizer" version = "3.4.0" @@ -331,6 +351,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/61/8001b38461d751cd1a0c3a6ae84346796a5758123f3ed97a1b121dfbf4f3/gast-0.6.0-py3-none-any.whl", hash = "sha256:52b182313f7330389f72b069ba00f174cfe2a06411099547288839c6cbafbd54", size = 21173 }, ] +[[package]] +name = "gitdb" +version = "4.0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/0d/bbb5b5ee188dec84647a4664f3e11b06ade2bde568dbd489d9d64adef8ed/gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b", size = 394469 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/5b/8f0c4a5bb9fd491c277c21eff7ccae71b47d43c4446c9d0c6cff2fe8c2c4/gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", size = 62721 }, +] + +[[package]] +name = "gitpython" +version = "3.1.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/a1/106fd9fa2dd989b6fb36e5893961f82992cf676381707253e0bf93eb1662/GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c", size = 214149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/bd/cc3a402a6439c15c3d4294333e13042b915bbeab54edc457c723931fed3f/GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff", size = 207337 }, +] + [[package]] name = "google-api-core" version = "2.24.0" @@ -632,6 +676,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + [[package]] name = "jinja2" version = "3.1.4" @@ -842,6 +895,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 }, ] +[[package]] +name = "opencv-contrib-python-headless" +version = "4.10.0.84" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/6f/b0192e6f3eceeaa9b2fc148717f19d077edd6be0166b10248db96b9324bc/opencv-contrib-python-headless-4.10.0.84.tar.gz", hash = "sha256:6351250db97e1f91f31afdec2436afb1c89594e3da02851e0f01e20ea16bbd9e", size = 150465571 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/19/f6a151219a493767da14245b45304d3254bd3887ff4ef081da541353d9ad/opencv_contrib_python_headless-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:be91c6c81e839613c6f3b15755bf71789839289d0e3440fab093e0708516ffcf", size = 63667534 }, + { url = "https://files.pythonhosted.org/packages/45/23/8559fbdaa944d9067c17451341dff464f1a76ed3cfbd0bb7d1a44b62d5a2/opencv_contrib_python_headless-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:252df47a7e1da280cef26ee0ecc1799841015ce3718214634bb15bc22d4cb308", size = 66278139 }, + { url = "https://files.pythonhosted.org/packages/93/c3/a399ad183bd94210e6a9002add4096ead4f0d0c36c0b1b65c3205a0baee5/opencv_contrib_python_headless-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb20ee077ac0955704d391c00639df6063cb67cb62606c07b97d8b635feff6", size = 34727901 }, + { url = "https://files.pythonhosted.org/packages/00/fc/b01f878cef02f619a4686683db31451d0e3e961646e65ec09ea802c8ceda/opencv_contrib_python_headless-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89c16eb5f888aee7bf664106e12c423705d29d1b094876b66aa4e33d4e8ec905", size = 55986159 }, + { url = "https://files.pythonhosted.org/packages/d2/af/65fa29ea39f410547c708b1007cd8846587715b95a2509361f699d044dca/opencv_contrib_python_headless-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:7581d7ffb7fff953436797dca2dfc5e70e100f721ea18ab84ebf11417ea21d0c", size = 34444721 }, + { url = "https://files.pythonhosted.org/packages/01/d4/dacf890940cb22279e1513b7ea41d97825a723153a5efce68bc52e7b3b6e/opencv_contrib_python_headless-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:660ded6b77b07f875f56065016677bbb6a3abca13903b9320164691a46474a7d", size = 45449488 }, +] + [[package]] name = "opt-einsum" version = "3.4.0" @@ -939,6 +1009,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828 }, ] +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + [[package]] name = "proto-plus" version = "1.25.0" @@ -1063,6 +1142,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, ] +[[package]] +name = "pydantic-settings" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/41/19b62b99e7530cfa1d6ccd16199afd9289a12929bef1a03aa4382b22e683/pydantic_settings-2.7.0.tar.gz", hash = "sha256:ac4bfd4a36831a48dbf8b2d9325425b549a0a6f18cea118436d728eb4f1c4d66", size = 79743 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/00/57b4540deb5c3a39ba689bb519a4e03124b24ab8589e618be4aac2c769bd/pydantic_settings-2.7.0-py3-none-any.whl", hash = "sha256:e00c05d5fa6cbbb227c84bd7487c5c1065084119b750df7c8c1a554aed236eb5", size = 29549 }, +] + [[package]] name = "pygments" version = "2.18.0" @@ -1095,6 +1187,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/ec/2eb3cd785efd67806c46c13a17339708ddc346cbb684eade7a6e6f79536a/pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", size = 106921 }, ] +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -1193,6 +1300,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, ] +[[package]] +name = "rtoml" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/93/59e1dc9829eafbfb349b1ff2dcfca647d7f7e7d87788de54ab0e402c7036/rtoml-0.12.0.tar.gz", hash = "sha256:662e56bd5953ee7ebcc5798507ae90daa329940a5d5157a48f3d477ebf99c55b", size = 43127 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/f8/ab3712301107d19ef256338838af335378cb87c43cc5144e159c9fb46222/rtoml-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ac75a75f15924fa582df465a3b1f4495710e3d4e1930837423ea396bcb1549b6", size = 322966 }, + { url = "https://files.pythonhosted.org/packages/ba/cc/499c45159e96247167c6e3ee293f2d4f16f7e7d9c1585025bbb902de57a3/rtoml-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd895de2745b4874498608948a9496e587b3154903ca8c6b4dec8f8b6c2a5252", size = 311730 }, + { url = "https://files.pythonhosted.org/packages/73/2a/a97927be7b586c9f50825295b3ff34d30c06ebaa41593a961645e0c84c4d/rtoml-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c1c82d2a79a943c33b851ec3745580ea93fbc40dcb970288439107b6e4a7062", size = 338995 }, + { url = "https://files.pythonhosted.org/packages/8f/22/fc829b0282c20dde98a66625ebc67a2d3bd9c3bb185e19b8dc09fac6b2ee/rtoml-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ada7cc9fc0b94d1f5095d71d8966d10ee2628d69c574e3ef8c9e6dd36a9d525", size = 359621 }, + { url = "https://files.pythonhosted.org/packages/4c/a4/96500a6d80c694813c0a795a90ec41d174344ce66acba8edb9507a3816d7/rtoml-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7e4c13ed587d5fc8012aaacca3b73d283191f5462f27b005cadbf9a30083428", size = 382684 }, + { url = "https://files.pythonhosted.org/packages/61/60/439bfff454a66c6cb197923400a9d07fd4664edf237983efcad8df1633a6/rtoml-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd24ed60f588aa7262528bfabf97ebf776ff1948ae78829c00389813cd482374", size = 482316 }, + { url = "https://files.pythonhosted.org/packages/d7/67/32b5f4ccb06876eec4bd339dc739e5e0ae30f3494f88012f9d293d265d9e/rtoml-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:827159e7313fa35b8495c3ec1c54526ccd2fbd9713084ad959c4455749b4a68d", size = 347280 }, + { url = "https://files.pythonhosted.org/packages/65/36/a0cab2a2a2e00c351d19706ea0afd4034529a9402b7577051baf4ec5cf34/rtoml-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fad4117620e22482468f28556362e778d44c2065dfac176bf42ac4997214ae4", size = 366405 }, + { url = "https://files.pythonhosted.org/packages/88/a8/155fa88275e54a3b336ab5c0dec2bad5c374d6a1c4bf085deffd16baf09a/rtoml-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5248359a67aa034e409f2b06fed02de964bf9dd7f401661076dd7ddf3a81659b", size = 518700 }, + { url = "https://files.pythonhosted.org/packages/04/80/5fe39d943ba2a40ef2dcf8af00fa0bf35d18b6d495abdacc5b67502a194b/rtoml-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:28a81c9335f2d7b9cdb6053940b35c590c675222d4935f7a4b8751071e5a5519", size = 518107 }, + { url = "https://files.pythonhosted.org/packages/fb/25/f5b371c08269db9a0c4df5e80244c7a2d21e41197f4d66ea80556fbaaa83/rtoml-0.12.0-cp312-cp312-win32.whl", hash = "sha256:b28c7882f60622645ff7dd180ddb85f4e018406b674ea86f65d99ac0f75747bc", size = 220464 }, + { url = "https://files.pythonhosted.org/packages/b8/d9/5e6df3255f3eb277a8b6b3c421aba85803d9aa73a9562c50878642b9b300/rtoml-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:d7e187c38a86202bde843a517d341c026f7b0eb098ad5396ed40f93170565bd7", size = 225520 }, + { url = "https://files.pythonhosted.org/packages/d5/b4/605d263956ef7287519df9c269de0409ea6589f4b1ddf6ce9e6d58a61e30/rtoml-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:477131a487140163cc9850a66d92a864fb507b37d81fb3366ad5203d30c85520", size = 217230 }, + { url = "https://files.pythonhosted.org/packages/88/f5/35c0dcfb152300980c05c8c810bd9927fa204db9722917a11d617718ce8c/rtoml-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12e99b493f0d59ad925b307b4c3b15c560ee44c672dce2ddce227e550560af5e", size = 322647 }, + { url = "https://files.pythonhosted.org/packages/3a/ed/d1d50706ff2ab0a934437609320fd8c4e0834e9bb5bba273ad76e86c9ca0/rtoml-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a058a1739a2519a41afe160280dcd791c202068e477ceb7ebf606830299c63af", size = 311468 }, + { url = "https://files.pythonhosted.org/packages/dc/0f/cb0c0b3db93775e3dfa7b83bfe8f9df7f75a2b61934c668cfaa377adcee2/rtoml-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f5ee3825c9c7aad732b184fed58cc2c368360ca8d553516663374937b9497be", size = 338539 }, + { url = "https://files.pythonhosted.org/packages/cd/49/3ce420d49d9beae463a08326dbe276dbb8f9c76730ffbc4e49349cc5ba32/rtoml-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3637da07651aa522fcaa81d7944167a9db886c687ec81c31aade0048caa51c97", size = 359135 }, + { url = "https://files.pythonhosted.org/packages/26/f5/257d2d2561597c3286e036053b6af4bd0f488d7adaf9e213ea1cf8c1e3a2/rtoml-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:559f77c916cf02e0261756a7924382e5b4a529a316106aba9b7ff4b3b39e227a", size = 382226 }, + { url = "https://files.pythonhosted.org/packages/f8/94/c415547d83b5831ef61302a41929d5013dc4d03c38fa77289f49c34e32e0/rtoml-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b9156c2d30a2917f172b9a98c251864d3063dc5bc9764147779245c8a690441", size = 482014 }, + { url = "https://files.pythonhosted.org/packages/e8/b8/87074f0c3f14b27dbe0eedd87cf41a5b00d4c12a06e16d76d6167f42a65d/rtoml-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bea9797f08311b0b605cae671abd884724d8d3d6524c184ccf8c70b220a9a68b", size = 346701 }, + { url = "https://files.pythonhosted.org/packages/27/57/2c25850a7f5597eaacc815194df3f78b99ff04201f48c2a573c0c1e05f97/rtoml-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b522f671f8964a79dda162c9985950422e27fe9420dd924257dee0184c8d047f", size = 365954 }, + { url = "https://files.pythonhosted.org/packages/fe/63/892c5c2087a159cd5bad8cab759b015fdd185d50ba97a91725548435b1f9/rtoml-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:321ee9dca365b5c1dab8c74617e7f8c941de3fdc10ac9f3c11c9ac261418ed80", size = 518241 }, + { url = "https://files.pythonhosted.org/packages/b6/96/89c80a946adbd2050999b7cee974120c28df16683b8a5bbf3a09ea6b2a1b/rtoml-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:57912b150aa48a8a90b599b57691a165092a9f5cf9a98bf431b1cd380e58414a", size = 517418 }, + { url = "https://files.pythonhosted.org/packages/3b/88/354f0f3388b38cb50a58e4abbeb364e08b7f6739f79c8a1a03f3326caaec/rtoml-0.12.0-cp313-cp313-win32.whl", hash = "sha256:7aebc94ed208ff46e6ce469ef30b98095932a3e74b99bde102a0f035d5034620", size = 220123 }, + { url = "https://files.pythonhosted.org/packages/aa/c8/8dc7e391ef6ee8967a8ac1a2c40e483d99b6c0e09c96ce0e5d4c01f88b9c/rtoml-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c88e48946adef48dce2dc54f1380f6ff0d580f06770f9ca9600ef330bc06c39", size = 225143 }, + { url = "https://files.pythonhosted.org/packages/a6/40/2e8640ffe564626424aba6f1ea19f41f278e46932e5431faf8265f6e1dcb/rtoml-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:730770673649220d4265d9986d3a9089d38434f36c1c629b98a58eb2bbee9cfb", size = 216902 }, +] + [[package]] name = "ruff" version = "0.8.3" @@ -1218,6 +1359,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/8f/e4fa95288b81233356d9a9dcaed057e5b0adc6399aa8fd0f6d784041c9c3/ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936", size = 9078754 }, ] +[[package]] +name = "scipy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/11/4d44a1f274e002784e4dbdb81e0ea96d2de2d1045b2132d5af62cc31fd28/scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417", size = 58620554 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/04/2bdacc8ac6387b15db6faa40295f8bd25eccf33f1f13e68a72dc3c60a99e/scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d", size = 39128781 }, + { url = "https://files.pythonhosted.org/packages/c8/53/35b4d41f5fd42f5781dbd0dd6c05d35ba8aa75c84ecddc7d44756cd8da2e/scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07", size = 29939542 }, + { url = "https://files.pythonhosted.org/packages/66/67/6ef192e0e4d77b20cc33a01e743b00bc9e68fb83b88e06e636d2619a8767/scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5", size = 23148375 }, + { url = "https://files.pythonhosted.org/packages/f6/32/3a6dedd51d68eb7b8e7dc7947d5d841bcb699f1bf4463639554986f4d782/scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc", size = 25578573 }, + { url = "https://files.pythonhosted.org/packages/f0/5a/efa92a58dc3a2898705f1dc9dbaf390ca7d4fba26d6ab8cfffb0c72f656f/scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310", size = 35319299 }, + { url = "https://files.pythonhosted.org/packages/8e/ee/8a26858ca517e9c64f84b4c7734b89bda8e63bec85c3d2f432d225bb1886/scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066", size = 40849331 }, + { url = "https://files.pythonhosted.org/packages/a5/cd/06f72bc9187840f1c99e1a8750aad4216fc7dfdd7df46e6280add14b4822/scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1", size = 42544049 }, + { url = "https://files.pythonhosted.org/packages/aa/7d/43ab67228ef98c6b5dd42ab386eae2d7877036970a0d7e3dd3eb47a0d530/scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f", size = 44521212 }, + { url = "https://files.pythonhosted.org/packages/50/ef/ac98346db016ff18a6ad7626a35808f37074d25796fd0234c2bb0ed1e054/scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79", size = 39091068 }, + { url = "https://files.pythonhosted.org/packages/b9/cc/70948fe9f393b911b4251e96b55bbdeaa8cca41f37c26fd1df0232933b9e/scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e", size = 29875417 }, + { url = "https://files.pythonhosted.org/packages/3b/2e/35f549b7d231c1c9f9639f9ef49b815d816bf54dd050da5da1c11517a218/scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73", size = 23084508 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/b028e3f3e59fae61fb8c0f450db732c43dd1d836223a589a8be9f6377203/scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e", size = 25503364 }, + { url = "https://files.pythonhosted.org/packages/a7/2f/6c142b352ac15967744d62b165537a965e95d557085db4beab2a11f7943b/scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d", size = 35292639 }, + { url = "https://files.pythonhosted.org/packages/56/46/2449e6e51e0d7c3575f289f6acb7f828938eaab8874dbccfeb0cd2b71a27/scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e", size = 40798288 }, + { url = "https://files.pythonhosted.org/packages/32/cd/9d86f7ed7f4497c9fd3e39f8918dd93d9f647ba80d7e34e4946c0c2d1a7c/scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06", size = 42524647 }, + { url = "https://files.pythonhosted.org/packages/f5/1b/6ee032251bf4cdb0cc50059374e86a9f076308c1512b61c4e003e241efb7/scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84", size = 44469524 }, +] + [[package]] name = "setuptools" version = "75.6.0" @@ -1245,6 +1413,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] +[[package]] +name = "smmap" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/04/b5bf6d21dc4041000ccba7eb17dd3055feb237e7ffc2c20d3fae3af62baa/smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", size = 22291 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a5/10f97f73544edcdef54409f1d839f6049a0d79df68adbc1ceb24d1aaca42/smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da", size = 24282 }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1272,7 +1449,7 @@ version = "1.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, - { name = "mslex", marker = "sys_platform == 'win32'" }, + { name = "mslex", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, { name = "psutil" }, { name = "tomli", marker = "python_full_version < '4.0'" }, ] @@ -1349,27 +1526,27 @@ name = "tensorflow-intel" version = "2.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "absl-py" }, - { name = "astunparse" }, - { name = "flatbuffers" }, - { name = "gast" }, - { name = "google-pasta" }, - { name = "grpcio" }, - { name = "h5py" }, - { name = "keras" }, - { name = "libclang" }, - { name = "ml-dtypes" }, - { name = "numpy" }, - { name = "opt-einsum" }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "requests" }, - { name = "setuptools" }, - { name = "six" }, - { name = "tensorboard" }, - { name = "termcolor" }, - { name = "typing-extensions" }, - { name = "wrapt" }, + { name = "absl-py", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "astunparse", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "flatbuffers", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "gast", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "google-pasta", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "grpcio", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "h5py", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "keras", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "libclang", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "ml-dtypes", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "numpy", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "opt-einsum", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "packaging", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "protobuf", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "requests", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "setuptools", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "six", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "tensorboard", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "termcolor", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "typing-extensions", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, + { name = "wrapt", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux') or (platform_system != 'Darwin' and platform_system != 'Linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ae/4e/44ce609139065035c56fe570fe7f0ee8d06180c99a424bac588472052c5d/tensorflow_intel-2.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:a5818043f565cf74179b67eb52fc060587ccecb9540141c39d84fbcb37ecff8c", size = 390262926 }, @@ -1428,6 +1605,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, ] +[[package]] +name = "typer-slim" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/7d/f8e0a2678a44573b2bb1e20abecb10f937a7101ce2b8e07f4eab4c721a3d/typer_slim-0.15.1.tar.gz", hash = "sha256:b8ce8fd2a3c7d52f0d0c1318776e7f2bf897fa203daf899f3863514aa926c725", size = 99874 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/7b/032ecd581e2170513bb6dc3cdb2581e20fdb94a272bae70fe93f2bca580b/typer_slim-0.15.1-py3-none-any.whl", hash = "sha256:20233cb89938ea3cca633afee10b906a1b0e7c5330f31ed8c55f4f0779efe6df", size = 44968 }, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -1445,13 +1635,19 @@ dependencies = [ { name = "fastapi", extra = ["standard"] }, { name = "firebase-admin" }, { name = "meilisearch" }, + { name = "numpy" }, + { name = "opencv-contrib-python-headless" }, { name = "pillow" }, + { name = "pydantic-settings" }, + { name = "scipy" }, { name = "tensorflow-cpu" }, { name = "weaviate-client" }, ] [package.dev-dependencies] dev = [ + { name = "changelog-gen" }, + { name = "pytest" }, { name = "ruff" }, { name = "taskipy" }, ] @@ -1461,13 +1657,19 @@ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.115.6" }, { name = "firebase-admin", specifier = ">=6.6.0" }, { name = "meilisearch", specifier = ">=0.33.0" }, + { name = "numpy", specifier = ">=2.0.2" }, + { name = "opencv-contrib-python-headless", specifier = ">=4.10.0.84" }, { name = "pillow", specifier = ">=11.0.0" }, + { name = "pydantic-settings", specifier = ">=2.7.0" }, + { name = "scipy", specifier = ">=1.14.1" }, { name = "tensorflow-cpu", specifier = ">=2.18.0" }, { name = "weaviate-client", specifier = ">=3.26.7,<4.0.0" }, ] [package.metadata.requires-dev] dev = [ + { name = "changelog-gen", specifier = ">=0.13.5" }, + { name = "pytest", specifier = ">=8.3.4" }, { name = "ruff", specifier = ">=0.8.3" }, { name = "taskipy", specifier = ">=1.14.1" }, ] @@ -1505,7 +1707,7 @@ wheels = [ [package.optional-dependencies] standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, { name = "httptools" }, { name = "python-dotenv" }, { name = "pyyaml" },