diff --git a/Azaion.AI/README.md b/Azaion.AI/README.md index 635eee7..5ba19f2 100644 --- a/Azaion.AI/README.md +++ b/Azaion.AI/README.md @@ -21,44 +21,41 @@ Windows - [Install CUDA](https://developer.nvidia.com/cuda-12-1-0-download-archive) Linux -* ``` - sudo apt install nvidia-driver-535 - - wget https://developer.download.nvidia.com/compute/cudnn/9.2.0/local_installers/cudnn-local-repo-ubuntu2204-9.2.0_1.0-1_amd64.deb - sudo dpkg -i cudnn-local-repo-ubuntu2204-9.2.0_1.0-1_amd64.deb - - sudo cp /var/cudnn-local-repo-ubuntu2204-9.2.0/cudnn-*-keyring.gpg /usr/share/keyrings/ - sudo apt-get update - sudo apt-get -y install cudnn nvidia-cuda-toolkit -y - nvcc --version - ``` +``` + sudo apt install nvidia-driver-535 + + wget https://developer.download.nvidia.com/compute/cudnn/9.2.0/local_installers/cudnn-local-repo-ubuntu2204-9.2.0_1.0-1_amd64.deb + sudo dpkg -i cudnn-local-repo-ubuntu2204-9.2.0_1.0-1_amd64.deb + + sudo cp /var/cudnn-local-repo-ubuntu2204-9.2.0/cudnn-*-keyring.gpg /usr/share/keyrings/ + sudo apt-get update + sudo apt-get -y install cudnn nvidia-cuda-toolkit -y + nvcc --version +``` +

Install dependencies

-``` Make sure that your virtual env is installed with links to the global python packages and headers, like this: -python -m venv --system-site-packages venv -This is crucial for the Build because build needs Python.h header and other files. - -python -m pip install --upgrade pip -pip install --upgrade huggingface_hub -pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 - -pip install git+https://github.com/airockchip/ultralytics_yolov8.git -- or -pip install ultralytics - -pip install cython -pip uninstall -y opencv-python -pip install opencv-python -pip install msgpack +``` + python -m venv --system-site-packages venv +``` +This is crucial for the build because build needs Python.h header and other files. ``` -* fbgemm.dll error (Windows specific) - ``` - copypaste libomp140.x86_64.dll to C:\Windows\System32 - ``` + python -m pip install --upgrade pip + pip install --upgrade huggingface_hub + pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 + pip install ultralytics + + pip uninstall -y opencv-python + pip install opencv-python cython msgpack cryptography rstream +``` +In case of fbgemm.dll error (Windows specific): + + - copypaste libomp140.x86_64.dll to C:\Windows\System32 +

Build

``` -python setup.py build_ext --inplace -``` \ No newline at end of file + python setup.py build_ext --inplace +``` diff --git a/Azaion.AI/annotation.pxd b/Azaion.AI/annotation.pxd new file mode 100644 index 0000000..de8c628 --- /dev/null +++ b/Azaion.AI/annotation.pxd @@ -0,0 +1,8 @@ +cdef class Detection: + cdef double x, y, w, h + cdef int cls + +cdef class Annotation: + cdef bytes image + cdef float time + cdef list[Detection] detections \ No newline at end of file diff --git a/Azaion.AI/annotation.pyx b/Azaion.AI/annotation.pyx new file mode 100644 index 0000000..21c6cb2 --- /dev/null +++ b/Azaion.AI/annotation.pyx @@ -0,0 +1,13 @@ +cdef class Detection: + def __init__(self, double x, double y, double w, double h, int cls): + self.x = x + self.y = y + self.w = w + self.h = h + self.cls = cls + +cdef class Annotation: + def __init__(self, bytes image_bytes, float time, list[Detection] detections): + self.image = image_bytes + self.time = time + self.detections = detections \ No newline at end of file diff --git a/Azaion.AI/api_client.pxd b/Azaion.AI/api_client.pxd new file mode 100644 index 0000000..af52823 --- /dev/null +++ b/Azaion.AI/api_client.pxd @@ -0,0 +1,10 @@ +from processor_command cimport FileCommand + +cdef class ApiClient: + cdef str email, password, token, folder, token_file, api_url + + cdef get_encryption_key(self, str hardware_hash) + cdef login(self, str email, str password, bint persist_token=*) + cdef bytes load_file(self, str filename, bint persist_token=*) + cdef bytes load_ai_model(self) + diff --git a/Azaion.AI/api_client.pyx b/Azaion.AI/api_client.pyx index 1a59061..5412cac 100644 --- a/Azaion.AI/api_client.pyx +++ b/Azaion.AI/api_client.pyx @@ -1,47 +1,41 @@ -# cython: language_level=3 import io import os from http import HTTPStatus import requests -import constants -from hardware_service import HardwareService -from processor_command import FileCommand, CommandType +cimport constants +from hardware_service cimport HardwareService, HardwareInfo from security import Security cdef class ApiClient: """Handles API authentication and downloading of the AI model.""" - cdef str email - cdef str password - cdef str token - cdef str folder - def __init__(self, str email, str password, str folder): self.email = email self.password = password self.folder = folder - if os.path.exists(constants.TOKEN_FILE): - with open(constants.TOKEN_FILE, "r") as file: + if os.path.exists(constants.TOKEN_FILE): + with open(constants.TOKEN_FILE, "r") as file: self.token = file.read().strip() else: self.token = None - cdef get_encryption_key(self, command: FileCommand, str hardware_hash): + cdef get_encryption_key(self, str hardware_hash): return f'{self.email}-{self.password}-{hardware_hash}-#%@AzaionKey@%#---' - cdef login(self, str email, str password, persist_token:bool=False): + cdef login(self, str email, str password, bint persist_token=False): response = requests.post(f"{constants.API_URL}/login", json={"email": email, "password": password}) response.raise_for_status() self.token = response.json()["token"] + print(f'') if persist_token: - with open(constants.TOKEN_FILE, 'w') as file: + with open(constants.TOKEN_FILE, 'w') as file: file.write(self.token) - cdef bytes load_file(self, command: FileCommand, persist_token:bool=False): + cdef bytes load_file(self, str filename, bint persist_token=False): hardware_service = HardwareService() - hardware = hardware_service.get_hardware_info() + cdef HardwareInfo hardware = hardware_service.get_hardware_info() if self.token is None: self.login(self.email, self.password, persist_token) @@ -51,7 +45,7 @@ cdef class ApiClient: payload = { "password": self.password, "hardware": hardware, - "fileName": command.filename + "fileName": filename } response = requests.post(url, json=payload, headers=headers, stream=True) @@ -59,13 +53,12 @@ cdef class ApiClient: self.login(self.email, self.password, persist_token) response = requests.post(url, json=payload, headers=headers, stream=True) - key = self.get_encryption_key(command, hardware.hash) + key = self.get_encryption_key(hardware.hash) encrypted_stream = io.BytesIO(response.content) decrypted_stream = io.BytesIO() Security.decrypt_to(encrypted_stream, decrypted_stream, key) return decrypted_stream cdef bytes load_ai_model(self): - file_command = FileCommand(CommandType.LOAD, constants.AI_MODEL_FILE) - return self.load_file(file_command, True) + return self.load_file(constants.AI_MODEL_FILE, True) diff --git a/Azaion.AI/constants.pxd b/Azaion.AI/constants.pxd new file mode 100644 index 0000000..0d1f018 --- /dev/null +++ b/Azaion.AI/constants.pxd @@ -0,0 +1,12 @@ +cdef str SOCKET_HOST # Host for the socket server +cdef int SOCKET_PORT # Port for the socket server +cdef int SOCKET_BUFFER_SIZE # Buffer size for socket communication + +cdef int QUEUE_MAXSIZE # Maximum size of the command queue +cdef str COMMANDS_QUEUE # Name of the commands queue in rabbit +cdef str ANNOTATIONS_QUEUE # Name of the annotations queue in rabbit + +cdef str API_URL # Base URL for the external API +cdef str TOKEN_FILE # Name of the token file where temporary token would be stored +cdef str QUEUE_CONFIG_FILENAME # queue config filename to load from api +cdef str AI_MODEL_FILE # AI Model file diff --git a/Azaion.AI/constants.pyx b/Azaion.AI/constants.pyx index d020ef8..9b06a38 100644 --- a/Azaion.AI/constants.pyx +++ b/Azaion.AI/constants.pyx @@ -1,5 +1,3 @@ -# cython: language_level=3 - cdef str SOCKET_HOST = "127.0.0.1" # Host for the socket server cdef int SOCKET_PORT = 9127 # Port for the socket server cdef int SOCKET_BUFFER_SIZE = 4096 # Buffer size for socket communication @@ -8,10 +6,7 @@ cdef int QUEUE_MAXSIZE = 1000 # Maximum size of the command queue cdef str COMMANDS_QUEUE = "azaion-commands" cdef str ANNOTATIONS_QUEUE = "azaion-annotations" - cdef str API_URL = "https://api.azaion.com" # Base URL for the external API cdef str TOKEN_FILE = "token" - - cdef str QUEUE_CONFIG_FILENAME = "secured-config.json" -cdef str AI_MODEL_FILE = "azaion.pt" \ No newline at end of file +cdef str AI_MODEL_FILE = "azaion.pt" diff --git a/Azaion.AI/debug.py b/Azaion.AI/debug.py new file mode 100644 index 0000000..e4e7f6b --- /dev/null +++ b/Azaion.AI/debug.py @@ -0,0 +1,13 @@ +import main +from main import ParsedArguments + +def start_server(): + args = ParsedArguments('admin@azaion.com', 'Az@1on1000Odm$n', 'stage', True) + processor = main.CommandProcessor(args) + try: + processor.start() + except Exception as e: + processor.stop() + +if __name__ == "__main__": + start_server() \ No newline at end of file diff --git a/Azaion.AI/hardware_service.pxd b/Azaion.AI/hardware_service.pxd new file mode 100644 index 0000000..eee5d08 --- /dev/null +++ b/Azaion.AI/hardware_service.pxd @@ -0,0 +1,9 @@ +cdef class HardwareInfo: + cdef str cpu, gpu, memory, mac_address, hash + + +cdef class HardwareService: + cdef bint is_windows + + cdef HardwareInfo get_hardware_info(self) + cdef str calc_hash(self, str s) \ No newline at end of file diff --git a/Azaion.AI/hardware_service.pyx b/Azaion.AI/hardware_service.pyx index c069875..783b68b 100644 --- a/Azaion.AI/hardware_service.pyx +++ b/Azaion.AI/hardware_service.pyx @@ -1,15 +1,8 @@ -# cython: language_level=3 import base64 import subprocess from hashlib import sha384 cdef class HardwareInfo: - cdef str cpu - cdef str gpu - cdef str memory - cdef str mac_address - cdef str hash - def __init__(self, str cpu, str gpu, str memory, str mac_address, str hw_hash): self.cpu = cpu self.gpu = gpu @@ -25,11 +18,13 @@ cdef class HardwareService: def __init__(self): try: - if subprocess.check_output("ver", shell=True).decode('utf-8').startswith("Microsoft"): + res = subprocess.check_output("ver", shell=True).decode('utf-8') + if "Microsoft Windows" in res: self.is_windows = True else: self.is_windows = False except Exception: + print('Error during os type checking') self.is_windows = False cdef HardwareInfo get_hardware_info(self): diff --git a/Azaion.AI/inference.pyx b/Azaion.AI/inference.pyx index 3352520..48ac5e3 100644 --- a/Azaion.AI/inference.pyx +++ b/Azaion.AI/inference.pyx @@ -1,9 +1,9 @@ -# cython: language_level=3 from ultralytics import YOLO import mimetypes import cv2 from ultralytics.engine.results import Boxes from processor_command import FileCommand +from annotation cimport Detection, Annotation cdef class Inference: """Handles YOLO inference using the AI model.""" @@ -66,25 +66,3 @@ cdef class Inference: _, encoded_image = cv2.imencode('.jpg', frame[0]) image_bytes = encoded_image.tobytes() return Annotation(image_bytes, time, detections) - - -cdef class Detection: - cdef double x - cdef double y - cdef double w - cdef double h - cdef int cls - - def __init__(self, double x, double y, double w, double h, int cls): - self.x = x - self.y = y - self.w = w - self.h = h - self.cls = cls - -cdef class Annotation: - - def __init__(self, image_bytes: bytes, float time, detections: [Detection]): - self.image = image_bytes - self.time = time - self.detections = detections diff --git a/Azaion.AI/main.pyx b/Azaion.AI/main.pyx index fd17ded..4698190 100644 --- a/Azaion.AI/main.pyx +++ b/Azaion.AI/main.pyx @@ -1,44 +1,43 @@ -# cython: language_level=3 import queue import threading -import constants -from api_client import ApiClient -from inference import Inference, Annotation -from processor_command import FileCommand, CommandType, ProcessorType -from remote_handlers import SocketHandler, RabbitHandler +cimport constants +from api_client cimport ApiClient +from annotation cimport Annotation +from inference import Inference +from processor_command cimport FileCommand, CommandType, ProcessorType +from remote_handlers cimport SocketHandler, RabbitHandler import argparse -cdef enum ListenOption: - SOCKET = 1 - QUEUE = 2 - cdef class ParsedArguments: - cdef ListenOption listen - cdef str email - cdef str password - cdef str folder + cdef str email, password, folder; cdef bint persist_token - def __init__(self, ListenOption listen, str email, str password, str folder, bint persist_token): - self.listen = listen + def __init__(self, str email, str password, str folder, bint persist_token): self.email = email self.password = password self.folder = folder self.persist_token = persist_token cdef class CommandProcessor: + cdef ApiClient api_client + cdef SocketHandler socket_handler + cdef RabbitHandler rabbit_handler + cdef object command_queue + cdef bint running def __init__(self, args: ParsedArguments): self.api_client = ApiClient(args.email, args.password, args.folder) self.socket_handler = SocketHandler(self.on_message) - self.rabbit_handler = RabbitHandler(self.on_message) + self.socket_handler.start() + self.rabbit_handler = RabbitHandler(self.api_client, self.on_message) + self.rabbit_handler.start() self.command_queue = queue.Queue(maxsize=constants.QUEUE_MAXSIZE) self.running = True def start(self): threading.Thread(target=self.process_queue, daemon=True).start() - cdef on_message(self, cmd: FileCommand): + cdef on_message(self, FileCommand cmd): try: if cmd.command_type == CommandType.INFERENCE: self.command_queue.put(cmd) @@ -61,7 +60,7 @@ cdef class CommandProcessor: except Exception as e: print(f"Error processing queue: {e}") - cdef process_load(self, command: FileCommand): + cdef process_load(self, FileCommand command): response = self.api_client.load_file(command) handler = self.socket_handler if command.processor_type == ProcessorType.SOCKET else self.rabbit_handler handler.send(response) @@ -72,20 +71,18 @@ cdef class CommandProcessor: def parse_arguments(): parser = argparse.ArgumentParser(description="Command Processor") - parser.add_argument("--listen", type=ListenOption, choices=[ListenOption.SOCKET, ListenOption.QUEUE], default=ListenOption.SOCKET, help="socket: Local communication, queue: remote. Default is socket") - parser.add_argument("--email", type=str, default="", help="Email") - parser.add_argument("--pw", type=str, default="", help="Password") - parser.add_argument("--folder", type=str, default="", help="Folder to API inner folder to download file from") - parser.add_argument("--persist_token", type=bool, default=True, help="True for persisting token from API") + parser.add_argument("-e", "--email", type=str, default="", help="Email") + parser.add_argument("-p", "--pw", type=str, default="", help="Password") + parser.add_argument("-f", "--folder", type=str, default="", help="Folder to API inner folder to download file from") + parser.add_argument("-t", "--persist_token", type=bool, default=True, help="True for persisting token from API") cdef args = parser.parse_args() - cdef ListenOption listen = ListenOption(args.listen) cdef str email = args.email cdef str password = args.pw cdef str folder = args.folder cdef bint persist_token = args.persist_token - return ParsedArguments(listen, email, password, folder, persist_token) + return ParsedArguments(email, password, folder, persist_token) if __name__ == '__main__': args = parse_arguments() diff --git a/Azaion.AI/processor_command.pxd b/Azaion.AI/processor_command.pxd new file mode 100644 index 0000000..68c17b1 --- /dev/null +++ b/Azaion.AI/processor_command.pxd @@ -0,0 +1,15 @@ +cdef enum ProcessorType: + SOCKET = 1, + RABBIT = 2 + +cdef enum CommandType: + INFERENCE = 1 + LOAD = 2 + +cdef class FileCommand: + cdef CommandType command_type + cdef ProcessorType processor_type + cdef str filename + + @staticmethod + cdef from_msgpack(bytes data, ProcessorType processor_type) \ No newline at end of file diff --git a/Azaion.AI/processor_command.pyx b/Azaion.AI/processor_command.pyx index e96d7e6..812096e 100644 --- a/Azaion.AI/processor_command.pyx +++ b/Azaion.AI/processor_command.pyx @@ -1,23 +1,13 @@ import msgpack -cdef enum CommandType: - INFERENCE = 1 - LOAD = 2 - -cdef enum ProcessorType: - SOCKET = 1, - RABBIT = 2 - cdef class FileCommand: - cdef str filename - - def __init__(self, command_type: CommandType, processor_type: ProcessorType, str filename): + def __init__(self, command_type: CommandType, ProcessorType processor_type, str filename): self.command_type = command_type self.processor_type = processor_type self.filename = filename @staticmethod - cdef from_msgpack(bytes data, processor_type: ProcessorType): + cdef from_msgpack(bytes data, ProcessorType processor_type): unpacked = msgpack.unpackb(data, strict_map_key=False) return FileCommand(unpacked.get("CommandType"), processor_type, unpacked.get("Filename") ) diff --git a/Azaion.AI/remote_handlers.pxd b/Azaion.AI/remote_handlers.pxd new file mode 100644 index 0000000..c11716a --- /dev/null +++ b/Azaion.AI/remote_handlers.pxd @@ -0,0 +1,20 @@ +from annotation cimport Annotation + +cdef class SocketHandler: + cdef object on_message + cdef object _socket + cdef object _connection + + cdef start(self) + cdef start_inner(self) + cdef send(self, list[Annotation] message) + cdef close(self) + +cdef class RabbitHandler: + cdef object on_message + cdef object annotation_producer + cdef object command_consumer + + cdef start(self) + cdef send(self, object message) + cdef close(self) \ No newline at end of file diff --git a/Azaion.AI/remote_handlers.pyx b/Azaion.AI/remote_handlers.pyx index ac1244b..57167fa 100644 --- a/Azaion.AI/remote_handlers.pyx +++ b/Azaion.AI/remote_handlers.pyx @@ -1,4 +1,3 @@ -# cython: language_level=3 import json import socket import struct @@ -8,9 +7,10 @@ import msgpack from msgpack import packb from rstream import Producer, Consumer, AMQPMessage, ConsumerOffsetSpecification, OffsetType, MessageContext -import constants -from api_client import ApiClient -from processor_command import FileCommand, ProcessorType +cimport constants +from api_client cimport ApiClient +from processor_command cimport FileCommand, ProcessorType +from annotation cimport Annotation cdef class QueueConfig: cdef str host @@ -35,15 +35,15 @@ cdef class QueueConfig: cdef class SocketHandler: """Handles socket communication with size-prefixed messages.""" - def __init__(self, on_message): + def __init__(self, object on_message): self.on_message = on_message self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._socket.connect((constants.SOCKET_HOST, constants.SOCKET_PORT)) + self._socket.bind((constants.SOCKET_HOST, constants.SOCKET_PORT)) self._socket.listen(1) cdef start(self): - threading.Thread(target=self.start, daemon=True).start() + threading.Thread(target=self.start_inner, daemon=True).start() cdef start_inner(self): while True: @@ -62,53 +62,50 @@ cdef class SocketHandler: cmd = FileCommand.from_msgpack(data, ProcessorType.SOCKET) self.on_message(cmd) - async def send(self, object message): + cdef send(self, list[Annotation] message): data = msgpack.packb(message) size_prefix = len(data).to_bytes(4, 'big') self._connection.sendall(size_prefix + data) - def close(self): + cdef close(self): if self._socket: self._socket.close() self._socket = None cdef class RabbitHandler: - cdef str hardware_hash - - def __init__(self, config_filename, on_message): + def __init__(self, ApiClient api_client, object on_message): self.on_message = on_message - cdef str config_str = ApiClient().load_file(constants.QUEUE_CONFIG_FILENAME).decode(encoding='utf-8') - self.queue_config = QueueConfig.from_json(config_str) + cdef str config_str = api_client.load_file(constants.QUEUE_CONFIG_FILENAME).decode(encoding='utf-8') + queue_config = QueueConfig.from_json(config_str) self.annotation_producer = Producer( - host=self.queue_config.host, - port=self.queue_config.port, - username=self.queue_config.producer_user, - password=self.queue_config.producer_pw + host=queue_config.host, + port=queue_config.port, + username=queue_config.producer_user, + password=queue_config.producer_pw ) self.command_consumer = Consumer( - host=self.queue_config.host, - port=self.queue_config.port, - username=self.queue_config.consumer_user, - password=self.queue_config.consumer_pw + host=queue_config.host, + port=queue_config.port, + username=queue_config.consumer_user, + password=queue_config.consumer_pw ) cdef start(self): self.command_consumer.start() - self.command_consumer.subscribe(stream=constants.COMMANDS_QUEUE, callback=self.on_message_inner, + self.command_consumer.subscribe(stream=constants.COMMANDS_QUEUE, callback=self.on_message_inner, offset_specification=ConsumerOffsetSpecification(OffsetType.FIRST, None)) # put real offset - - cdef on_message_inner(self, message: AMQPMessage, message_context: MessageContext): + def on_message_inner(self, message: AMQPMessage, message_context: MessageContext): cdef bytes body = message.body cmd = FileCommand.from_msgpack(body, ProcessorType.RABBIT) self.on_message(cmd) - cpdef send(self, object message): + cdef send(self, object message): packed_message = AMQPMessage(body=packb(message)) - self.annotation_producer.send(constants.ANNOTATIONS_QUEUE, packed_message) + self.annotation_producer.send(constants.ANNOTATIONS_QUEUE, packed_message) - async def close(self): + cdef close(self): if self.annotation_producer: - await self.annotation_producer.close() + self.annotation_producer.close() if self.command_consumer: - await self.command_consumer.close() \ No newline at end of file + self.command_consumer.close() \ No newline at end of file diff --git a/Azaion.AI/setup.py b/Azaion.AI/setup.py index 4230875..c578d2f 100644 --- a/Azaion.AI/setup.py +++ b/Azaion.AI/setup.py @@ -2,14 +2,15 @@ from setuptools import setup, Extension from Cython.Build import cythonize extensions = [ - Extension('main', ['main.pyx']), - Extension('api_client', ['api_client.pyx']), Extension('constants', ['constants.pyx']), - Extension('hardware_service', ['hardware_service.pyx']), - Extension('inference', ['inference.pyx']), + Extension('annotation', ['annotation.pyx']), + Extension('hardware_service', ['hardware_service.pyx'], extra_compile_args=["-g"], extra_link_args=["-g"]), Extension('processor_command', ['processor_command.pyx']), + Extension('api_client', ['api_client.pyx']), + Extension('inference', ['inference.pyx']), Extension('remote_handlers', ['remote_handlers.pyx']), - Extension('security', ['security.pyx']) + Extension('security', ['security.pyx']), + Extension('main', ['main.pyx']), ] setup( @@ -20,8 +21,7 @@ setup( "language_level": 3, "emit_code_comments" : False, "binding": True - }, - gdb_debug=True + } ), zip_safe=False ) \ No newline at end of file