Вот мой полный код js:
```js
// Helper function to convert hex to rgba
function hexToRgba(hex, alpha) {
    var r = parseInt(hex.slice(1, 3), 16),
        g = parseInt(hex.slice(3, 5), 16),
        b = parseInt(hex.slice(5, 7), 16);

    if (alpha) {
        return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")";
    } else {
        return "rgb(" + r + ", " + g + ", " + b + ")";
    }
}

let MOUSE_POS = { x:0, y:0 }
let annotationCounter = 0;

$(document).ready(function () {
    var urlParams = new URLSearchParams(window.location.search);
    var projectId = urlParams.get('project_id');

    if (!projectId) {
        alert("Project ID is required.");
        window.location.href = '/main';
        return;
    }

    // Обертка для масштабирования и панорамирования
    var $zoomWrapper = $('<div class="zoom-wrapper"></div>');
    $('#image-display').wrap($zoomWrapper);

    // Обновляем ссылку на $zoomWrapper
    $zoomWrapper = $('#image-display').parent('.zoom-wrapper');
    var zoomLevel = 1;
    var minZoom = 1;
    var maxZoom = 5;
    var zoomStep = 0.2;

    // Переменные для панорамирования
    var isPanning = false;
    var startPan = { x: 0, y: 0 };
    var panOffset = { x: 0, y: 0 };

    var currentImageId = null;
    var classes = [];
    var selectedClass = null;
    var isDrawing = false;
    var startX, startY;
    var annotationGrab = false;
    var annotationMode = false;

    var $imageContainer = $('.image-container');
    var imageContainerOffset;

    let crosshairVisible = false;
    let crosshairX = 0;
    let crosshairY = 0;




    // CONTEXT MENU START
    var $contextMenu = $(".context-menu")
    // Function to show the context menu
    function showContextMenu(x, y, $annotation) {
        $contextMenu.css({
            left: x,
            top: y
        }).removeClass('hidden').data('annotation', $annotation); // Store the annotation element
    }
    // Function to hide the context menu
    function hideContextMenu() {
        $contextMenu.addClass('hidden').removeData('annotation');
    }
    // Context menu click handler
    $contextMenu.on('click', '.delete-annotation', function() {
        var $annotation = $contextMenu.data('annotation');
        console.log("$annotation.attr('id') = ", $annotation.attr('id'))
        var $annotation_link = $("#class-link-"+$annotation.attr('id'))
        if ($annotation) {
            $annotation.remove();
        }
        if ($annotation_link) {
            console.log("$annotation_link remove ", $annotation_link)
            $annotation_link.remove();
        }
        else {
            alert("noo")
        }
        hideContextMenu();
    });
    // Annotation right-click handler
    $imageContainer.on('contextmenu', '.annotation', function(e) {
        e.preventDefault();

        if (annotationMode) {
          return;
        }

        var $annotation = $(this);
        showContextMenu(e.pageX, e.pageY, $annotation);


    });
    // Hide context menu on click outside
    $(document).on('click', function(e) {
        if (!$(e.target).closest('.context-menu').length) {
            hideContextMenu();
        }
    });
    // Prevent default image actions on image display
    $('#image-display').on('contextmenu', function(e) {
        e.preventDefault(); // Prevent context menu
    }).on('dragstart', function(e) {
        e.preventDefault(); // Prevent dragging
    });
    // CONTEXT MENU END



    // SERVICE START
    // Fetch account info
    AccountManager.getAccountInfo(function (error, data) {
        if (error) {
            window.location.href = '/login';
        } else {
            $('#username').text(data.username);
        }
    });
    // Fetch the image and project info
    $.ajax({
        url: '/api/get_image',
        type: 'GET',
        data: { project_id: projectId },
        success: function (response) {
            if (response.message) {
                alert(response.message);
                window.location.href = '/main';
                return;
            }

            $('#project-title').text(response.project_title || 'Project');
            $('#project-progress').val(response.progress);
            $('#progress-text').text('Image ' + response.current + ' of ' + response.total + ' (' + Math.round(response.progress) + '%)');

            // Set up classes
            classes = response.classes;
            var $classList = $('#class-list');
            $classList.empty();
            classes.forEach(function (cls, index) {
                var $classItem = $('<div class="class-item"></div>');
                $classItem.text(cls.name);
                $classItem.css('background-color', cls.color);
                $classItem.data('classIndex', index);
                $classList.append($classItem);
            });

            // Class selection handler
            $('.class-item').on('click', function () {
                selectedClass = classes[$(this).data('classIndex')];

                // Highlight selected class
                $('.class-item').removeClass('selected');
                $(this).addClass('selected');

                // Enter annotation mode
                annotationMode = true;
                $('#selected-class-name').text(selectedClass.name);
                $('#annotation-mode-indicator').removeClass('hidden');
                // Fade out existing annotations
                $('.annotation').css({
                    'opacity': 0.2,
                    'transition': 'opacity 0.3s ease'
                });
                $('.annotation').css('pointer-events', 'none');
            });

            // Load image
            currentImageId = response.image_id;
            var imageUrl = response.image_url;
            var $imageDisplay = $('#image-display');
            $imageDisplay.attr('src', imageUrl);

            // Set up image load handler
            $imageDisplay.on('load', function () {
                // Adjust image container dimensions
                $imageContainer.css({
                    'width': $imageDisplay.width(),
                    'height': $imageDisplay.height()
                });
                imageContainerOffset = $imageContainer.offset();
            });

            // Mouse events for drawing annotations
            $imageContainer.on('mousedown', function (e) {
                if (annotationMode && selectedClass) {
                    e.preventDefault();
                    isDrawing = true;
                    var offset = getMousePosition(e);
                    console.log("offset1:::", offset)
                    startX = offset.x;
                    startY = offset.y;

                    // Create a new annotation element
                    var $annotation = $('<div class="annotation"></div>');
                    $annotation.css({
                        'left': startX,
                        'top': startY,
                        'border-color': selectedClass.color,
                        'background-color': hexToRgba(selectedClass.color, 0.25),
                        'width': '0px',
                        'height': '0px'
                    });
                    $annotation.data('class', selectedClass);
                    $annotation.append('<div class="delete-handle">&times;</div>');
                    $annotation.on('mousedown', annotationMouseDown);

                    // Добавляем угловые точки ресайза
                    $annotation.append('<div class="resize-handle nw"></div>');
                    $annotation.append('<div class="resize-handle ne"></div>');
                    $annotation.append('<div class="resize-handle sw"></div>');
                    $annotation.append('<div class="resize-handle se"></div>');

                    // События для перетаскивания угловых точек
                    $annotation.find('.resize-handle').on('mousedown', resizeMouseDown);

                    // Add to zoom-wrapper
                    $zoomWrapper.append($annotation);

                    // Attach delete handler
                    $annotation.find('.delete-handle').on('click', function (e) {
                        if (annotationMode) {
                            return;
                        }
                        e.stopPropagation();
                        $(this).parent('.annotation').remove();
                    });

                    // Store current annotation element
                    $zoomWrapper.data('currentAnnotation', $annotation);

                    // Присваиваем уникальный ID аннотации
                    let annotationId = 'annotation-' + annotationCounter++;
                    $annotation.attr('id', annotationId);
                    console.log("afuhygsadfuyhsdfiuysdgfy = ",$annotation.attr('id'))

                    // Создаём .class-rect-link в .explorer
                    let $classRectLink = $('<div class="class-rect-link" id="class-link-' + annotationId + '"></div>');
                    let $classColor = $('<div class="class-color"></div>').css('background-color', selectedClass.color);
                    let $classTitle = $('<div class="class-title"></div>').text(selectedClass.name);
                    let $delButton = $('<button class="del">X</button>');
                    $classRectLink.append($classColor, $classTitle, $delButton);
                    $('.explorer').append($classRectLink);

                    // Связываем аннотацию и класс-rect-link
                    $annotation.data('link-id', 'class-link-' + annotationId);
                    $classRectLink.data('annotation-id', annotationId);

                    // Обработчик удаления
                    $delButton.on('click', function(e) {
                        e.stopPropagation();
                        // Удаляем аннотацию
                        $('#' + annotationId).remove();
                        // Удаляем .class-rect-link
                        $('#class-link-' + annotationId).remove();
                    });

                    // Обработчики наведения на .class-rect-link
                    $classRectLink.on('mouseenter', function () {
                        if (!annotationMode) {
                            let annotationId = $(this).data('annotation-id');
                            // Добавляем класс HOVER самому .class-rect-link
                            $(this).addClass('class-rect-link_HOVER');
                            $annotation.addClass('highlighted');
                            // Находим соответствующую аннотацию и добавляем класс highlighted
                            $annotation.css("opacity", 1);
                            // Все остальные аннотации делаем полупрозрачными
                            $('.annotation').not($annotation).css("opacity", 0.2);
                        }
                    });

                    $classRectLink.on('mouseleave', function () {
                        if (!annotationMode) {
                            let annotationId = $(this).data('annotation-id');
                            // Убираем класс HOVER
                            $(this).removeClass('class-rect-link_HOVER');
                            // Убираем класс highlighted с соответствующей аннотации
                            $annotation.removeClass('highlighted');
                            $annotation.css("opacity", 1);
                            // Убираем полупрозрачность с всех аннотаций
                            $('.annotation').css("opacity", 1);
                        }
                    });

                    // Обработчики наведения на `.annotation`
                    $annotation.on('mouseenter', function () {
                        if (!annotationMode) {
                            let annotationId = $(this).data('annotation-id');
                            // Добавляем класс highlighted к аннотации
                            // Все остальные аннотации делаем полупрозрачными
                            ////$('.annotation').not(this).addClass('dimmed');
                            // Добавляем класс HOVER к связанному .class-rect-link
                            console.log("GGG = ", annotationId)
                            $classRectLink.addClass('class-rect-link_HOVER');
                        }
                    });

                    $annotation.on('mouseleave', function () {
                        if (!annotationMode) {
                            let annotationId = $(this).data('annotation-id');
                            // Убираем класс highlighted с аннотации
                            // Убираем полупрозрачность с всех аннотаций
                            ////$('.annotation').removeClass('dimmed');
                            // Убираем класс HOVER с связанного .class-rect-link
                            $classRectLink.removeClass('class-rect-link_HOVER');
                        }
                    });
                }
            });

            $(document).on('mousemove', function (e) {
                if (isDrawing) {
                    var offset = getMousePosition(e);
                    var currentX = offset.x;
                    //console.log("offset2:::", offset)
                    var currentY = offset.y;
                    var width = currentX - startX;
                    var height = currentY - startY;

                    var absWidth = Math.abs(width);
                    var absHeight = Math.abs(height);

                    var left = width < 0 ? currentX : startX;
                    var top = height < 0 ? currentY : startY;

                    var $annotation = $zoomWrapper.data('currentAnnotation');
                    $annotation.css({
                        'width': absWidth + 'px',
                        'height': absHeight + 'px',
                        'left': left + 'px',
                        'top': top + 'px'
                    });
                }

                if (annotationMode && !isDrawing) {
                    crosshairX = e.clientX + 2;
                    crosshairY = e.clientY + 2;

                    $(".crosshairVertical").css("left", `${crosshairX}px`); // Update crosshair position
                    $(".crosshairHorizontal").css("top", `${crosshairY}px`); // Update crosshair position

                    if (!crosshairVisible) {
                        $(".crosshair").show();
                        crosshairVisible = true;
                    }
                } else {
                    $(".crosshair").hide();
                    crosshairVisible = false;
                }
            });

            $(document).on('mouseup', function (e) {
                if (isDrawing) {
                    isDrawing = false;
                    $imageContainer.removeData('currentAnnotation');

                    // Fade in existing annotations
                    $('.annotation').css({ // Exclude current
                        'opacity': 1,
                        'transition': 'opacity 0.3s ease'
                    });

                    // Re-enable pointer events after drawing is complete
                    $('.annotation').css('pointer-events', 'auto');

                    // После установки маркера автоматически выходим из режима установки
                    annotationMode = false;
                    $('#annotation-mode-indicator').addClass('hidden');
                    $('.class-item').removeClass('selected');
                    selectedClass = null;
                }
            });


            /////////////
            ////////

            // Функция для ресайза
            // Annotation resizing
            function resizeMouseDown(e) {
                e.stopPropagation();
                e.preventDefault();

                if (annotationMode) {
                    return;
                }

                var $handle = $(this);
                var $annotation = $handle.parent();
                var startX = e.pageX;
                var startY = e.pageY;

                // Получаем начальные значения с учетом масштаба
                var startWidth = $annotation.width();
                var startHeight = $annotation.height();
                var annotationOffset = $annotation.offset();
                var startLeft = (annotationOffset.left - $imageContainer.offset().left - panOffset.x) / zoomLevel;
                var startTop = (annotationOffset.top - $imageContainer.offset().top - panOffset.y) / zoomLevel;


                $(document).on('mousemove.resize', function (e) {
                    // Изменения в координатах мыши с учетом масштаба
                    var dx = (e.pageX - startX) / zoomLevel;
                    var dy = (e.pageY - startY) / zoomLevel;

                    var newLeft = startLeft;
                    var newTop = startTop;
                    var newWidth = startWidth;
                    var newHeight = startHeight;

                    if ($handle.hasClass('nw')) {
                        newLeft += dx;
                        newTop += dy;
                        newWidth -= dx;
                        newHeight -= dy;
                    } else if ($handle.hasClass('ne')) {
                        newTop += dy;
                        newWidth += dx;
                        newHeight -= dy;
                    } else if ($handle.hasClass('sw')) {
                        newLeft += dx;
                        newWidth -= dx;
                        newHeight += dy;
                    } else if ($handle.hasClass('se')) {
                        newWidth += dx;
                        newHeight += dy;
                    }

                    $annotation.css({
                        'left': newLeft + 'px',
                        'top': newTop + 'px',
                        'width': newWidth + 'px',
                        'height': newHeight + 'px'
                    });
                });

                $(document).on('mouseup.resize', function () {
                    $(document).off('.resize');
                });
            }
            //////
            ////////

            // Cancel annotation mode
            $('#cancel-annotation-btn').on('click', function () {
                // Fade in existing annotations
                $('.annotation').css({ // Exclude current
                    'opacity': 1,
                    'transition': 'opacity 0.3s ease'
                });
                $('.annotation').css('pointer-events', 'all');
                annotationMode = false;
                $('#annotation-mode-indicator').addClass('hidden');
                $('.class-item').removeClass('selected');
                selectedClass = null;
            });
        },
        error: function (xhr) {
            alert("An error occurred while fetching the image.");
            window.location.href = '/main';
        }
    });
    // SERVICE END



    // ZOOM START
    function setZoom(newZoom) {
        // Use Math.min and Math.max to clamp the zoom level *before* applying the step
        newZoom = Math.min(maxZoom, Math.max(minZoom, newZoom)); // Clamp first

        // Check if zoom change is effectively negligible due to floating point errors.
        if (Math.abs(zoomLevel - newZoom) < 0.001) { // Tolerance check
            return; // No significant zoom change.
        }

         if (annotationGrab || annotationMode) {
             return;
         }

        // Получаем текущую позицию мыши относительно контейнера изображения
        var mouseX = MOUSE_POS.x;
        var mouseY = MOUSE_POS.y;

        // Вычисляем позицию мыши в масштабированном изображении ДО изменения масштаба
        var offsetX = mouseX * zoomLevel;
        var offsetY = mouseY * zoomLevel;


        // Обновляем уровень масштабирования
        zoomLevel = newZoom;

        // Устанавливаем текст для zoom-multiplayer
        var roundedZoom = zoomLevel.toFixed(1);
        $(".zoom-multiplayer").text(roundedZoom + "x");

        // Вычисляем новую позицию мыши в масштабированном изображении ПОСЛЕ изменения масштаба
        var newOffsetX = mouseX * zoomLevel;
        var newOffsetY = mouseY * zoomLevel;

        // Вычисляем разницу в позициях для корректировки panOffset
        var deltaX = newOffsetX - offsetX;
        var deltaY = newOffsetY - offsetY;

        // Применяем масштабирование к обертке
        $zoomWrapper.css({
            'transform': 'scale(' + zoomLevel + ')',
            'transform-origin': 'top left' // Важно!
        });

        // Корректируем panOffset с учетом изменений масштаба и позиции мыши
        panOffset.x -= deltaX;
        panOffset.y -= deltaY;

        // Ограничиваем панорамирование после изменения масштаба и panOffset
        var containerWidth = $('.image-container').width();
        var containerHeight = $('.image-container').height();
        var contentWidth = $zoomWrapper.width() * zoomLevel;
        var contentHeight = $zoomWrapper.height() * zoomLevel;

        var maxPanX = 0;
        var maxPanY = 0;
        var minPanX = containerWidth - contentWidth;
        var minPanY = containerHeight - contentHeight;

        panOffset.x = Math.min(Math.max(panOffset.x, minPanX), maxPanX);
        panOffset.y = Math.min(Math.max(panOffset.y, minPanY), maxPanY);

        // Применяем панорамирование
        updatePan();
    }
    function updatePan() {
        if (!annotationMode) {
            $zoomWrapper.css({
                'transform': 'scale(' + zoomLevel + ') translate(' + panOffset.x / zoomLevel + 'px, ' + panOffset.y / zoomLevel + 'px)'
            });
        }
    }
    // Обработчик колеса мыши для масштабирования
    $imageContainer.on('wheel', function(e) {
        e.preventDefault();
        if (annotationMode) return; // Отключаем zoom при аннотировании

        var delta = e.originalEvent.deltaY < 0 ? 1 : -1; // 1 - zoom in, -1 - zoom out
        setZoom(zoomLevel + delta * zoomStep);
    });
    // Начало панорамирования
    $zoomWrapper.on('mousedown', function(e) {
        if (annotationMode) return; // Панорамирование только без аннотационного режима
        if (zoomLevel <= 1) return; // Панорамирование только при зуме >1

        isPanning = true;
        startPan.x = e.clientX - panOffset.x;
        startPan.y = e.clientY - panOffset.y;
        $('body').addClass('no-select'); // Предотвращаем выделение текста
    });
    // Во время панорамирования
    $(document).on('mousemove', function(e) {
        if (!isPanning) return;

        panOffset.x = e.clientX - startPan.x;
        panOffset.y = e.clientY - startPan.y;

        // Ограничиваем панорамирование
        var containerWidth = $('.image-container').width();
        var containerHeight = $('.image-container').height();
        var contentWidth = $zoomWrapper.width() * zoomLevel;
        var contentHeight = $zoomWrapper.height() * zoomLevel;

        var maxPanX = 0;
        var maxPanY = 0;
        var minPanX = containerWidth - contentWidth;
        var minPanY = containerHeight - contentHeight;

        panOffset.x = Math.min(Math.max(panOffset.x, minPanX), maxPanX);
        panOffset.y = Math.min(Math.max(panOffset.y, minPanY), maxPanY);

        updatePan();
    });
    // Завершение панорамирования
    $(document).on('mouseup', function(e) {
        if (isPanning) {
            isPanning = false;
            $('body').removeClass('no-select');
        }
    });
    // ZOOM END



    // UTILS START
    // Обновление функции getMousePosition
    function getMousePosition(e) {
        var parentOffset = $imageContainer.offset();
        var x = (e.pageX - parentOffset.left - panOffset.x) / zoomLevel;
        var y = (e.pageY - parentOffset.top - panOffset.y) / zoomLevel;
        MOUSE_POS = { x: x, y: y };
        return { x: x, y: y };
    }
    // Annotation movement
    function annotationMouseDown(e) {
        if ($(e.target).hasClass('delete-handle') || $(e.target).hasClass('resize-handle')) {
            return;
        }
        e.stopPropagation();

        var $annotation = $(this);
        var id = $annotation.attr('id').replace('annotation-', '');
        var startOffset = $annotation.offset(); // Use offset() instead of position()
        var startX = e.pageX;
        var startY = e.pageY;
        var parentOffset = $zoomWrapper.offset(); // Get zoom wrapper offset


        $(document).on('mousemove.annotation', function (e) {
            annotationGrab = true;
            var dx = (e.pageX - startX);
            var dy = (e.pageY - startY);

            var newLeft = (startOffset.left + dx - parentOffset.left) / zoomLevel;
            var newTop = (startOffset.top + dy - parentOffset.top) / zoomLevel;

            newLeft = Math.round(newLeft)
            newTop = Math.round(newTop)

            $annotation.css({
                'left': newLeft + 'px',
                'top': newTop + 'px'
            });
        });

        $(document).on('mouseup.annotation', function () {
            annotationGrab = false;
            $(document).off('.annotation');
        });
    }
    // Следим за движением мыши над изображением
    $imageContainer.on('mousemove', function (e) {
        var mousePos = getMousePosition(e);

        // Для каждой аннотации проверяем расстояние до ручек ресайза
        $('.annotation').each(function () {
            var $annotation = $(this);
            var annotationOffset = $annotation.offset();
            var annotationLeft = (annotationOffset.left - $imageContainer.offset().left - panOffset.x) / zoomLevel;
            var annotationTop = (annotationOffset.top - $imageContainer.offset().top - panOffset.y) / zoomLevel;

            var annotationWidth = $annotation.width(); // width без учета zoom, т.к. это происходит автоматически в css
            var annotationHeight = $annotation.height(); // height без учета zoom, т.к. это происходит автоматически в css

            // Массив ручек ресайза
            var handles = [
                { name: 'nw', x: annotationLeft, y: annotationTop },
                { name: 'ne', x: annotationLeft + annotationWidth, y: annotationTop },
                { name: 'sw', x: annotationLeft, y: annotationTop + annotationHeight },
                { name: 'se', x: annotationLeft + annotationWidth, y: annotationTop + annotationHeight }
            ];

            // Проверяем каждую ручку
            $annotation.find('.resize-handle').each(function (index) {
                var $handle = $(this);
                var handlePos = handles[index];
                var distance = Math.hypot(mousePos.x - handlePos.x, mousePos.y - handlePos.y);
                //console.log(distance)
                if (distance <= 20) {
                    $handle.addClass('visible');
                } else {
                    $handle.removeClass('visible');
                }
            });
        });
    });
    // Когда курсор покидает область изображения, скрываем все ручки ресайза
    $imageContainer.on('mouseleave', function () {
        $('.resize-handle').removeClass('visible');
    });
    // UTILS END



    // CONTROLS BUTTONS
    $('#bad-btn').on('click', function () {
        if (!currentImageId || !projectId) {
            alert("Не загружено изображение.");
            return;
        }

        //if (!confirm("Вы уверены, что хотите пометить это изображение как плохое? Оно будет удалено.")) {
        //    return;
        //}

        // Отключаем кнопку, чтобы предотвратить множественные клики
        $('#bad-btn').prop('disabled', true);

        $.ajax({
            url: '/api/projects/' + projectId + '/images/' + currentImageId + '/flag_bad',
            type: 'POST',
            success: function (response) {
                //alert(response.message);
                // Загрузка следующего изображения, как при нажатии на кнопку "Next"
                // Reload the page for the next image
                location.reload();
            },
            error: function (xhr) {
                var errorMsg = "Произошла ошибка.";
                if (xhr.responseJSON && xhr.responseJSON.message) {
                    errorMsg = xhr.responseJSON.message;
                }
                alert(errorMsg);
            },
            complete: function () {
                // Включаем кнопку обратно
                $('#bad-btn').prop('disabled', false);
            }
        });
    });
    // Next button click handler
    $('#next-btn').on('click', function () {
        // Collect annotation data
        var annotations = [];
        $('.annotation').each(function () {
            var $annotation = $(this);
            var cls = $annotation.data('class');
            var left = parseInt($annotation.css('left'));
            var top = parseInt($annotation.css('top'));
            var width = $annotation.width();
            var height = $annotation.height();

            // Normalize coordinates relative to image dimensions
            var imageWidth = $imageContainer.width();
            var imageHeight = $imageContainer.height();

            var x_center = (left + width / 2) / imageWidth;
            var y_center = (top + height / 2) / imageHeight;
            var w = width / imageWidth;
            var h = height / imageHeight;

            annotations.push({
                class_id: classes.indexOf(cls),
                x_center: x_center,
                y_center: y_center,
                width: w,
                height: h
            });
        });

        // Send annotations to server
        $.ajax({
            url: '/api/submit_annotation',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify({
                project_id: projectId,
                image_id: currentImageId,
                annotation: annotations
            }),
            success: function (response) {
                // Reload the page for the next image
                location.reload();
            },
            error: function (xhr) {
                alert("An error occurred while submitting the annotation.");
            }
        });
    });
});
```

Вот мой код сервера:
```py
import json
import os
import shutil
from functools import wraps

from flask import Flask, request, jsonify, render_template, make_response, send_file
import jwt
import datetime

from DatabaseManager import DatabaseManager
from utils import convert_to_yolo_format, write_classes_txt

app = Flask(__name__)

# Секретный ключ для JWT
SECRET_KEY = 'your_secret_key'  # Замените на ваш секретный ключ
app.config['SECRET_KEY'] = SECRET_KEY

db_manager = DatabaseManager()

# Декоратор для защиты маршрутов, требующих аутентификации
def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.cookies.get('auth_token')

        if not token:
            return jsonify({'message': 'Токен отсутствует! Пожалуйста, войдите в систему.'}), 401

        try:
            data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            current_user = db_manager.get_user_by_username(data['username'])
            if not current_user:
                raise Exception('Пользователь не найден')
        except jwt.ExpiredSignatureError:
            return jsonify({'message': 'Срок действия токена истек. Пожалуйста, войдите в систему снова.'}), 401
        except Exception as e:
            return jsonify({'message': 'Токен недействителен!'}), 401

        return f(current_user, *args, **kwargs)
    return decorated

#########################################################################################################
#########################################################################################################
#########################################################################################################

### Эндпоинты API

# Регистрация пользователя
@app.route('/api/register', methods=['POST'])
def register():
    data = request.json
    username = data.get('username')
    password = data.get('password')
    if username and password:
        success = db_manager.create_account(username, password)
        if success:
            return jsonify({'message': 'Аккаунт успешно создан'}), 201
        else:
            return jsonify({'message': 'Пользователь с таким именем уже существует'}), 400
    else:
        return jsonify({'message': 'Требуются логин и пароль'}), 400


# Аутентификация пользователя
@app.route('/api/login', methods=['POST'])
def login_api():
    data = request.json
    username = data.get('username')
    password = data.get('password')
    if username and password:
        valid = db_manager.verify_account(username, password)
        if valid:
            # Генерация JWT токена
            token = jwt.encode({
                'username': username,
                'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24)
            }, app.config['SECRET_KEY'], algorithm='HS256')

            # Создаем ответ и устанавливаем куки с токеном
            response = make_response(jsonify({'message': 'Вход выполнен успешно', 'token': token}))
            response.set_cookie('auth_token', token, httponly=True,
                                samesite='Strict')  # Можно добавить 'secure=True' при использовании HTTPS
            return response, 200
        else:
            return jsonify({'message': 'Неверные учетные данные'}), 401
    else:
        return jsonify({'message': 'Требуются логин и пароль'}), 400


# Выход из аккаунта
@app.route('/api/logout', methods=['POST'])
def logout_api():
    response = make_response(jsonify({'message': 'Выход выполнен успешно'}))
    response.set_cookie('auth_token', '', expires=0)
    return response, 200


# Эндпоинт для получения информации об аккаунте
@app.route('/api/account', methods=['GET'])
@token_required
def get_account_info(current_user):
    print("get_account_info")
    return jsonify({
        'id': current_user['id'],
        'username': current_user['username']
        # Добавьте другие необходимые поля
    }), 200


# Создание нового проекта
@app.route('/api/projects', methods=['POST'])
@token_required
def create_project(current_user):
    # Получаем данные из формы
    title = request.form.get('title')
    path = request.form.get('path')
    classes_json = request.form.get('classes')
    images = request.files.getlist('images')

    if not title:
        return jsonify({'message': 'Title is required'}), 400

    if not images and not path:
        return jsonify({'message': 'Please upload images or specify a path'}), 400

    # Парсируем классы
    try:
        classes = json.loads(classes_json)
    except:
        return jsonify({'message': 'Invalid classes data'}), 400

    # Обрабатываем изображения
    image_paths = []

    # Директория для сохранения изображений проекта
    project_dir = os.path.join('projects', str(current_user['id']), title)
    os.makedirs(project_dir, exist_ok=True)

    # Сохраняем загруженные изображения
    for image in images:
        filename = image.filename
        image_path = os.path.join(project_dir, filename)
        image.save(image_path)
        image_paths.append(image_path)

    # Если указан путь, используем изображения из него
    if path:
        if os.path.exists(path) and os.path.isdir(path):
            for root, dirs, files in os.walk(path):
                for file in files:
                    if file.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tiff')):
                        file_path = os.path.join(root, file)
                        dest_path = os.path.join(project_dir, file)
                        shutil.copy2(file_path, dest_path)
                        image_paths.append(dest_path)
        else:
            return jsonify({'message': 'Invalid path specified'}), 400

    # Подготавливаем данные проекта
    project_data = {
        'title': title,
        'classes': classes,
        # Другие необходимые данные
    }

    # Создаем проект в базе данных
    author_id = current_user['id']
    user_ids = []  # Пока что только автор
    project_id = db_manager.create_project(author_id, user_ids, project_data, image_paths)

    # Write classes.txt
    write_classes_txt(project_dir, classes)

    return jsonify({'message': 'Проект создан', 'project_id': project_id}), 201


# Получение изображения для аннотации
@app.route('/api/get_image', methods=['GET'])
@token_required
def get_image(current_user):
    project_id = request.args.get('project_id', type=int)
    if not project_id:
        return jsonify({'message': 'Project ID is required'}), 400

    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    # Проверяем доступ пользователя к проекту
    if project['author_id'] != current_user['id'] and current_user['id'] not in project['user_ids']:
        return jsonify({'message': 'Access denied'}), 403

    # Получаем следующее неразмеченное изображение
    image_info = db_manager.get_next_image_for_annotation(project_id, current_user['id'])

    if not image_info:
        return jsonify({'message': 'No more images to annotate'}), 200

    # Получаем прогресс проекта
    total_images, annotated_images = db_manager.get_project_progress(project_id)
    progress = (annotated_images / total_images) * 100 if total_images > 0 else 0

    image_url = '/api/project_image/{}'.format(image_info['id'])

    response_data = {
        'image_id': image_info['id'],
        'image_url': image_url,
        'classes': project['data']['classes'],
        'current': annotated_images + 1,  # Предполагая, что мы собираемся аннотировать это изображение
        'total': total_images,
        'progress': progress,
        'project_title': project['data']['title']
    }
    return jsonify(response_data), 200


# Отправка аннотации для изображения
@app.route('/api/submit_annotation', methods=['POST'])
@token_required
def submit_annotation(current_user):
    data = request.json
    project_id = data.get('project_id')
    image_id = data.get('image_id')
    annotation = data.get('annotation')

    if not project_id or not image_id or annotation is None:
        return jsonify({'message': 'project_id, image_id and annotation are required'}), 400

    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    # Проверяем доступ пользователя к проекту
    if project['author_id'] != current_user['id'] and current_user['id'] not in project['user_ids']:
        return jsonify({'message': 'Access denied'}), 403

    # Сохраняем аннотацию в формате YOLO
    image_info = db_manager.get_project_image(image_id)
    if not image_info:
        return jsonify({'message': 'Image not found'}), 404

    # Проверяем, что изображение назначено текущему пользователю
    if image_info['assigned_to'] != current_user['id']:
        return jsonify({'message': 'This image is not assigned to you'}), 403

    # Путь к изображению
    image_path = image_info['image_path']
    annotation_path = os.path.splitext(image_path)[0] + '.txt'

    # Преобразуем аннотацию в формат YOLO
    yolo_annotation = convert_to_yolo_format(annotation)

    with open(annotation_path, 'w') as f:
        f.write(yolo_annotation)

    # Обновляем статус изображения на "annotated"
    db_manager.update_image_status(image_id, 'annotated')

    return jsonify({'message': 'Annotation submitted'}), 200


# Получение списка проектов пользователя
@app.route('/api/projects', methods=['GET'])
@token_required
def get_user_projects(current_user):
    projects = db_manager.get_projects_by_user(current_user['id'])
    return jsonify({'projects': projects}), 200


# Получение информации о проекте
@app.route('/api/projects/<int:project_id>', methods=['GET'])
@token_required
def get_project(current_user, project_id):
    project = db_manager.get_project(project_id)
    if project:
        # Check if user has access
        if project['author_id'] != current_user['id'] and current_user['id'] not in project['user_ids']:
            return jsonify({'message': 'Access denied'}), 403

        # Get total_images
        total_images = db_manager.get_project_images_count(project_id)
        project['total_images'] = total_images

        return jsonify(project), 200
    else:
        return jsonify({'message': 'Project not found'}), 404


# Обновление проекта
@app.route('/api/projects/<int:project_id>', methods=['PUT'])
@token_required
def update_project(current_user, project_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Проект не найден'}), 404

    # Проверка, является ли текущий пользователь автором проекта
    if project['author_id'] != current_user['id']:
        return jsonify({'message': 'Только автор проекта может его обновлять'}), 403

    data = request.json
    user_ids = data.get('user_ids')
    project_data = data.get('data')
    db_manager.update_project(project_id, user_ids=user_ids, data=project_data)
    return jsonify({'message': 'Проект обновлен'}), 200


# Сервис для безопасной выдачи изображений
@app.route('/api/project_image/<int:image_id>')
@token_required
def project_image(current_user, image_id):
    image_info = db_manager.get_project_image(image_id)
    if not image_info:
        return "Image not found", 404

    project = db_manager.get_project(image_info['project_id'])
    if not project:
        return "Project not found", 404

    # Проверяем доступ пользователя к проекту
    if project['author_id'] != current_user['id'] and current_user['id'] not in project['user_ids']:
        return "Access denied", 403

    return send_file(image_info['image_path'])


# Update project title
@app.route('/api/projects/<int:project_id>/update_title', methods=['POST'])
@token_required
def update_project_title(current_user, project_id):
    data = request.json
    new_title = data.get('title')

    if not new_title:
        return jsonify({'message': 'Title is required'}), 400

    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    if project['author_id'] != current_user['id']:
        return jsonify({'message': 'Only the author can update the project title'}), 403

    # Update the project's data
    project_data = project['data']
    project_data['title'] = new_title
    db_manager.update_project(project_id, data=project_data)

    return jsonify({'message': 'Project title updated successfully'}), 200


# Get images of a project with pagination
@app.route('/api/projects/<int:project_id>/images', methods=['GET'])
@token_required
def get_project_images(current_user, project_id):
    page = request.args.get('page', default=1, type=int)
    per_page = request.args.get('per_page', default=20, type=int)

    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    if project['author_id'] != current_user['id']:
        return jsonify({'message': 'Access denied'}), 403

    images = db_manager.get_project_images_paginated(project_id, page, per_page)
    total_images = db_manager.get_project_images_count(project_id)

    return jsonify({'images': images, 'total_images': total_images}), 200


# Delete an image from a project
@app.route('/api/projects/<int:project_id>/images/<int:image_id>', methods=['DELETE'])
@token_required
def delete_project_image(current_user, project_id, image_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    if project['author_id'] != current_user['id']:
        return jsonify({'message': 'Access denied'}), 403

    image_info = db_manager.get_project_image(image_id)
    if not image_info or image_info['project_id'] != project_id:
        return jsonify({'message': 'Image not found in this project'}), 404

    # Delete image file
    try:
        os.remove(image_info['image_path'])
        annotation_path = os.path.splitext(image_info['image_path'])[0] + '.txt'
        if os.path.exists(annotation_path):
            os.remove(annotation_path)
    except Exception as e:
        print(f"Error deleting image file: {e}")

    # Delete image from database
    db_manager.delete_project_image(image_id)

    return jsonify({'message': 'Image deleted successfully'}), 200


# Delete a project
@app.route('/api/projects/<int:project_id>', methods=['DELETE'])
@token_required
def delete_project(current_user, project_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    if project['author_id'] != current_user['id']:
        return jsonify({'message': 'Only the author can delete the project'}), 403

    db_manager.delete_project(project_id)

    return jsonify({'message': 'Project deleted successfully'}), 200


# Get project info for joining
@app.route('/api/projects/<int:project_id>/info', methods=['GET'])
@token_required
def get_project_info(current_user, project_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    project_info = {
        'id': project['id'],
        'title': project['data'].get('title', 'Untitled'),
        'author': db_manager.get_user_by_id(project['author_id'])['username'],
        'total_images': db_manager.get_project_images_count(project_id),
        'classes': project['data'].get('classes', []),
        'progress': db_manager.get_project_progress_percent(project_id)
    }

    return jsonify({'project': project_info}), 200


# Join a project
@app.route('/api/projects/<int:project_id>/join', methods=['POST'])
@token_required
def join_project(current_user, project_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    if current_user['id'] == project['author_id']:
        return jsonify({'message': 'You are the author of this project'}), 400

    if current_user['id'] in project['user_ids']:
        return jsonify({'message': 'You are already a participant in this project'}), 400

    project['user_ids'].append(current_user['id'])
    db_manager.update_project(project_id, user_ids=project['user_ids'])

    return jsonify({'message': 'You have joined the project'}), 200


# Leave a project
@app.route('/api/projects/<int:project_id>/leave', methods=['POST'])
@token_required
def leave_project(current_user, project_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    if current_user['id'] == project['author_id']:
        return jsonify({
            'message': 'You are the author of this project and cannot leave it. Consider deleting the project if you no longer need it.'}), 400

    if current_user['id'] not in project['user_ids']:
        return jsonify({'message': 'You are not a participant in this project'}), 400

    project['user_ids'].remove(current_user['id'])
    db_manager.update_project(project_id, user_ids=project['user_ids'])

    # Optionally, unassign images assigned to the user in this project
    db_manager.unassign_user_from_project_images(project_id, current_user['id'])

    return jsonify({'message': 'You have left the project'}), 200


@app.route('/api/projects/<int:project_id>/users', methods=['GET'])
@token_required
def get_project_users(current_user, project_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    # Only author can get the list of users
    if project['author_id'] != current_user['id']:
        return jsonify({'message': 'Access denied'}), 403

    user_stats = db_manager.get_project_users_stats(project_id)
    return jsonify({'users': user_stats}), 200


@app.route('/api/projects/<int:project_id>/kick_user', methods=['POST'])
@token_required
def kick_user_from_project(current_user, project_id):
    data = request.json
    user_id_to_kick = data.get('user_id')

    if not user_id_to_kick:
        return jsonify({'message': 'User ID is required'}), 400

    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    # Only author can kick users
    if project['author_id'] != current_user['id']:
        return jsonify({'message': 'Access denied'}), 403

    user_id_to_kick = int(user_id_to_kick)

    if user_id_to_kick == project['author_id']:
        return jsonify({'message': 'Cannot kick the project owner'}), 400

    if user_id_to_kick not in project['user_ids']:
        return jsonify({'message': 'User not in project'}), 400

    # Remove user from project
    project['user_ids'].remove(user_id_to_kick)
    db_manager.update_project(project_id, user_ids=project['user_ids'])

    # Unassign images assigned to the user in this project
    db_manager.unassign_user_from_project_images(project_id, user_id_to_kick)

    return jsonify({'message': 'User has been kicked from the project'}), 200

# Добавьте этот код рядом с другими API-эндпоинтами

@app.route('/api/projects/<int:project_id>/images/<int:image_id>/flag_bad', methods=['POST'])
@token_required
def flag_bad_image(current_user, project_id, image_id):
    # Проверка существования проекта
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Проект не найден'}), 404

    # Проверка прав доступа пользователя к проекту
    if project['author_id'] != current_user['id'] and current_user['id'] not in project['user_ids']:
        return jsonify({'message': 'Доступ запрещен'}), 403

    # Проверка существования изображения и его принадлежности к проекту
    image_info = db_manager.get_project_image(image_id)
    if not image_info or image_info['project_id'] != project_id:
        return jsonify({'message': 'Изображение не найдено в этом проекте'}), 404

    # Проверка, что изображение еще не помечено как плохое (опционально)
    # Например, можно добавить поле 'is_bad' в базу данных

    # Удаление файла изображения и соответствующей аннотации
    try:
        os.remove(image_info['image_path'])
        annotation_path = os.path.splitext(image_info['image_path'])[0] + '.txt'
        if os.path.exists(annotation_path):
            os.remove(annotation_path)
    except Exception as e:
        print(f"Ошибка при удалении файла изображения: {e}")
        return jsonify({'message': 'Ошибка при удалении файла изображения'}), 500

    # Удаление записи об изображении из базы данных
    db_manager.delete_project_image(image_id)

    return jsonify({'message': 'Изображение помечено как плохое и удалено, загружается следующее изображение.'}), 200

#########################################################################################################
#########################################################################################################
#########################################################################################################

### Страницы

# Главная страница
@app.route('/main')
def index():
    return render_template('index.html')


# Страница логина и регистрации
@app.route('/login')
def login():
    return render_template('login.html')


# Страница аккаунта
@app.route('/account')
def account():
    return render_template('account.html')


@app.route('/new_project')
@token_required
def new_project(current_user):
    return render_template('new_project.html')


@app.route('/workspace')
@token_required
def workspace(current_user):
    project_id = request.args.get('project_id', type=int)
    if not project_id:
        return "Project ID is required", 400
    return render_template('workspace.html', project_id=project_id)


@app.route('/edit_project')
@token_required
def edit_project(current_user):
    project_id = request.args.get('project_id', type=int)
    if not project_id:
        return "Project ID is required", 400

    project = db_manager.get_project(project_id)
    if not project:
        return "Project not found", 404

    if project['author_id'] != current_user['id']:
        return "Access denied", 403

    return render_template('edit_project.html', project_id=project_id)

@app.route('/join')
@token_required
def join_project_page(current_user):
    return render_template('join.html')


if __name__ == '__main__':
    app.run(debug=True, host="0.0.0.0", port=8081)
```

Сделай так, чтобы после нажатия на кнопку "#ai-btn", на сервер отправится запрос на автоматическую маркировку изображения.
Сервер должен вернуть результат и мы должны отрисовать все rectangles и link к ним, причем связать между собой правильно.

Напиши такую часть кода на js(полный js приводить не надо, он очень большой, только то что необходимо).
Также, напиши API на самом Python сервере. Саму нейросеть не пиши, напиши просто затычку. Остальное я сделаю сам.