Project structure:

DarkDataset/
├── projects/
│   ├── 1/
│   │   ├── test2/
│   │   │   ├── VADIM_ART3_HD.jpg
│   │   │   ├── VADIM_ART3_HD.txt
│   │   │   ├── anubis_map.jpg
│   │   │   ├── anubis_map.txt
│   │   │   ├── javascript_spizjenno.png
│   │   │   └── javascript_spizjenno.txt
├── static/
│   ├── css/
│   │   ├── account-tab.css
│   │   ├── base.css
│   │   ├── edit_project.css
│   │   ├── home-tab.css
│   │   ├── hotbar.css
│   │   ├── join_project.css
│   │   ├── loading.css
│   │   ├── login-tab.css
│   │   ├── new_project.css
│   │   ├── tab.css
│   │   ├── windows.css
│   │   └── workspace.css
│   ├── icons/
│   │   ├── h_icon.webp
│   │   ├── profile_icon.webp
│   │   └── s_icon.webp
│   ├── js/
│   │   ├── account_manager.js
│   │   ├── account_page.js
│   │   ├── edit_project.js
│   │   ├── hotbar-manager.js
│   │   ├── join.js
│   │   ├── login.js
│   │   ├── main.js
│   │   ├── new_project.js
│   │   └── workspace.js
├── templates/
│   ├── components/
│   │   ├── account_tab.html
│   │   ├── home_tab.html
│   │   ├── hotbar.html
│   │   ├── login_tab.html
│   │   └── windows.html
│   ├── account.html
│   ├── edit_project.html
│   ├── index.html
│   ├── join.html
│   ├── login.html
│   ├── new_project.html
│   └── workspace.html
├── DatabaseManager.py
├── V2_GenerateProjectShort.py
├── database.db
├── main.py
├── requirements.txt
└── utils.py


Project files:

DatabaseManager.py
```py
import json
import os
import sqlite3

from werkzeug.security import generate_password_hash, check_password_hash

DATABASE = 'database.db'

class DatabaseManager:
    def __init__(self, db_path=DATABASE):
        # Проверяем, существует ли база данных, если нет - создаем
        if not os.path.exists(DATABASE):
            open(DATABASE, 'w').close()

        self.db_path = db_path
        self.create_tables()

    def create_connection(self):
        conn = sqlite3.connect(self.db_path)
        # Для возврата строк как словарей
        conn.row_factory = sqlite3.Row
        return conn

    def create_tables(self):
        conn = self.create_connection()
        cursor = conn.cursor()

        # Создаем таблицу accounts
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS accounts (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                username TEXT UNIQUE NOT NULL,
                password_hash TEXT NOT NULL
            )
        ''')

        # Создаем таблицу projects
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS projects (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                author_id INTEGER NOT NULL,
                user_ids TEXT,
                data TEXT,
                FOREIGN KEY (author_id) REFERENCES accounts(id)
            )
        ''')

        # Создаем таблицу project_images
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS project_images (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                project_id INTEGER NOT NULL,
                image_path TEXT NOT NULL,
                status TEXT NOT NULL DEFAULT 'unannotated',
                assigned_to INTEGER,
                FOREIGN KEY (project_id) REFERENCES projects(id),
                FOREIGN KEY (assigned_to) REFERENCES accounts(id)
            )
        ''')

        conn.commit()
        conn.close()

    # Методы для работы с аккаунтами
    def create_account(self, username, password):
        password_hash = generate_password_hash(password)
        conn = self.create_connection()
        cursor = conn.cursor()
        try:
            cursor.execute('''
                INSERT INTO accounts (username, password_hash) VALUES (?, ?)
            ''', (username, password_hash))
            conn.commit()
            return True
        except sqlite3.IntegrityError:
            # Пользователь с таким именем уже существует
            return False
        finally:
            conn.close()

    def get_account(self, username):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT * FROM accounts WHERE username = ?
        ''', (username,))
        account = cursor.fetchone()
        conn.close()
        return account

    def get_user_by_id(self, user_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT * FROM accounts WHERE id = ?
        ''', (user_id,))
        account = cursor.fetchone()
        conn.close()
        return account

    def get_user_by_username(self, username):
        """
        Возвращает пользователя как словарь по имени пользователя.
        """
        account = self.get_account(username)
        if account:
            return {
                'id': account['id'],
                'username': account['username']
                # Добавьте другие поля по необходимости
            }
        return None

    def verify_account(self, username, password):
        account = self.get_account(username)
        if account:
            return check_password_hash(account['password_hash'], password)
        return False

    # Методы для работы с проектами
    def create_project(self, author_id, user_ids, data, images):
        user_ids_json = json.dumps(user_ids)
        data_json = json.dumps(data)
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            INSERT INTO projects (author_id, user_ids, data) VALUES (?, ?, ?)
        ''', (author_id, user_ids_json, data_json))
        project_id = cursor.lastrowid

        # Добавляем изображения в таблицу project_images
        project_images = [(project_id, image_path) for image_path in images]
        cursor.executemany('''
            INSERT INTO project_images (project_id, image_path) VALUES (?, ?)
        ''', project_images)

        conn.commit()
        conn.close()
        return project_id

    def get_project(self, project_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT * FROM projects WHERE id = ?
        ''', (project_id,))
        project = cursor.fetchone()
        conn.close()
        if project:
            project_dict = {
                'id': project['id'],
                'author_id': project['author_id'],
                'user_ids': json.loads(project['user_ids']) if project['user_ids'] else [],
                'data': json.loads(project['data']) if project['data'] else {}
            }
            return project_dict
        return None

    def update_project(self, project_id, user_ids=None, data=None):
        conn = self.create_connection()
        cursor = conn.cursor()
        if user_ids is not None:
            user_ids_json = json.dumps(user_ids)
            cursor.execute('''
                UPDATE projects SET user_ids = ? WHERE id = ?
            ''', (user_ids_json, project_id))
        if data is not None:
            data_json = json.dumps(data)
            cursor.execute('''
                UPDATE projects SET data = ? WHERE id = ?
            ''', (data_json, project_id))
        conn.commit()
        conn.close()


    # MAIN MAIN MAIN MAIN

    def get_projects_by_user(self, user_id):
        conn = self.create_connection()
        cursor = conn.cursor()

        # Get projects where the user is the author
        cursor.execute('''
            SELECT * FROM projects WHERE author_id = ?
        ''', (user_id,))
        authored_projects = cursor.fetchall()

        # Get projects where the user is a participant
        cursor.execute('''
            SELECT * FROM projects WHERE user_ids LIKE ?
        ''', (f'%{user_id}%',))
        participating_projects = cursor.fetchall()

        # Combine and remove duplicates
        projects = {project['id']: {
            'id': project['id'],
            'author_id': project['author_id'],
            'user_ids': json.loads(project['user_ids']) if project['user_ids'] else [],
            'data': json.loads(project['data']) if project['data'] else {}
        } for project in authored_projects + participating_projects}

        conn.close()
        return list(projects.values())

    def get_next_image_for_annotation(self, project_id, user_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        try:
            # Start a transaction
            cursor.execute('BEGIN TRANSACTION')

            # Try to fetch an image already assigned to the user but not annotated
            cursor.execute('''
                SELECT * FROM project_images 
                WHERE project_id = ? AND status = 'unannotated' AND assigned_to = ?
                LIMIT 1
            ''', (project_id, user_id))
            image = cursor.fetchone()

            if not image:
                # Assign a new image to the user atomically
                cursor.execute('''
                    SELECT * FROM project_images 
                    WHERE project_id = ? AND status = 'unannotated' AND assigned_to IS NULL
                    LIMIT 1
                ''', (project_id,))
                image = cursor.fetchone()
                if image:
                    cursor.execute('''
                        UPDATE project_images SET assigned_to = ? WHERE id = ?
                    ''', (user_id, image['id']))
                    conn.commit()
                else:
                    # No unassigned images left
                    conn.commit()
                    return None
            else:
                conn.commit()
            return image
        except Exception as e:
            conn.rollback()
            print(f"Error in get_next_image_for_annotation: {e}")
            return None
        finally:
            conn.close()

    def get_project_progress(self, project_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT COUNT(*) FROM project_images WHERE project_id = ?
        ''', (project_id,))
        total_images = cursor.fetchone()[0]

        cursor.execute('''
            SELECT COUNT(*) FROM project_images WHERE project_id = ? AND status = 'annotated'
        ''', (project_id,))
        annotated_images = cursor.fetchone()[0]
        conn.close()
        return total_images, annotated_images

    def get_project_image(self, image_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT * FROM project_images WHERE id = ?
        ''', (image_id,))
        image = cursor.fetchone()
        conn.close()
        return image

    def update_image_status(self, image_id, status):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            UPDATE project_images SET status = ? WHERE id = ?
        ''', (status, image_id))
        conn.commit()
        conn.close()

    def get_project_images_paginated(self, project_id, page, per_page):
        conn = self.create_connection()
        cursor = conn.cursor()
        offset = (page - 1) * per_page
        cursor.execute('''
            SELECT pi.*, a.username as annotated_by_username
            FROM project_images pi
            LEFT JOIN accounts a ON pi.assigned_to = a.id
            WHERE pi.project_id = ?
            LIMIT ? OFFSET ?
        ''', (project_id, per_page, offset))
        images = cursor.fetchall()
        conn.close()
        images_list = []
        for image in images:
            annotated_by_username = image['annotated_by_username'] if image['status'] == 'annotated' else None
            images_list.append({
                'id': image['id'],
                'image_path': image['image_path'],
                'status': image['status'],
                'assigned_to': image['assigned_to'],
                'annotated_by_username': annotated_by_username
            })
        return images_list

    def get_project_images_count(self, project_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT COUNT(*) FROM project_images WHERE project_id = ?
        ''', (project_id,))
        count = cursor.fetchone()[0]
        conn.close()
        return count

    def delete_project_image(self, image_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            DELETE FROM project_images WHERE id = ?
        ''', (image_id,))
        conn.commit()
        conn.close()

    def delete_project(self, project_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        # Get all images associated with the project
        cursor.execute('''
            SELECT * FROM project_images WHERE project_id = ?
        ''', (project_id,))
        images = cursor.fetchall()

        # Delete images from filesystem
        for image in images:
            try:
                os.remove(image['image_path'])
                annotation_path = os.path.splitext(image['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 images from database
        cursor.execute('''
            DELETE FROM project_images WHERE project_id = ?
        ''', (project_id,))

        # Delete project
        cursor.execute('''
            DELETE FROM projects WHERE id = ?
        ''', (project_id,))

        conn.commit()
        conn.close()

    def get_project_progress_percent(self, project_id):
        total_images, annotated_images = self.get_project_progress(project_id)
        if total_images > 0:
            return (annotated_images / total_images) * 100
        else:
            return 0

    def unassign_user_from_project_images(self, project_id, user_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            UPDATE project_images
            SET assigned_to = NULL
            WHERE project_id = ? AND assigned_to = ? AND status = 'unannotated'
        ''', (project_id, user_id))
        conn.commit()
        conn.close()

    def get_project_users_stats(self, project_id):
        conn = self.create_connection()
        cursor = conn.cursor()

        # Get the project
        cursor.execute('SELECT * FROM projects WHERE id = ?', (project_id,))
        project = cursor.fetchone()
        if not project:
            conn.close()
            return None

        # Get author_id and user_ids
        author_id = project['author_id']
        user_ids = json.loads(project['user_ids']) if project['user_ids'] else []

        # Include the author in the user list
        all_user_ids = [author_id] + user_ids

        # Prepare the stats
        user_stats = []
        for user_id in all_user_ids:
            # Get user info
            user = self.get_user_by_id(user_id)
            if user:
                username = user['username']
                # Get number of images annotated by the user in this project
                cursor.execute('''
                    SELECT COUNT(*) FROM project_images
                    WHERE project_id = ? AND assigned_to = ? AND status = 'annotated'
                ''', (project_id, user_id))
                annotated_count = cursor.fetchone()[0]
                user_stats.append({
                    'user_id': user_id,
                    'username': username,
                    'annotated_count': annotated_count
                })

        conn.close()
        return user_stats

```


main.py
```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

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)
    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


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

### Страницы

# Главная страница
@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)

```


utils.py
```py
def convert_to_yolo_format(annotation):
    yolo_annotations = []
    for ann in annotation:
        class_id = ann['class_id']
        x_center = ann['x_center']
        y_center = ann['y_center']
        width = ann['width']
        height = ann['height']
        yolo_annotations.append(f"{class_id} {x_center} {y_center} {width} {height}")
    return '\n'.join(yolo_annotations)
```


templates/account.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">

    <link rel="stylesheet" href="../static/css/tab.css">
    <link rel="stylesheet" href="../static/css/account-tab.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/account_page.js"></script>

<div class="screen">
    {% include "components/hotbar.html" %}
    {% include "components/account_tab.html" %}
    <!--{% include "components/home_tab.html" %}-->
    <!--{% include "components/windows.html" %}-->
</div>
</body>
</html>
```


templates/edit_project.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Edit Project - DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">

    <link rel="stylesheet" href="../static/css/tab.css">
    <link rel="stylesheet" href="../static/css/edit_project.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/edit_project.js"></script>
<div class="screen">
    {% include "components/hotbar.html" %}
    <div class="tab edit-project-tab">
        <h2>Edit Project</h2>
        <div class="project-info">
            <div>
                <label>Project ID:</label>
                <input type="text" id="project-id" readonly>
                <button id="copy-project-id-btn">Copy</button>
            </div>
            <div>
                <label>Title:</label>
                <input type="text" id="project-title-input">
                <button id="save-title-btn">Save Title</button>
            </div>
        </div>
        <button id="delete-project-btn">Delete Project</button>
        <div class="images-container" id="images-container">
            <!-- Images will be displayed here -->
        </div>
        <div class="pagination-container">
            <button id="prev-page-btn">Previous</button>
            <span id="page-info"></span>
            <button id="next-page-btn">Next</button>
        </div>
        <div id="users-container" class="users-container">
            <!-- User list will be loaded here -->
        </div>
    </div>
</div>
</body>
</html>
```


templates/index.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">
    <link rel="stylesheet" href="../static/css/windows.css">

    <link rel="stylesheet" href="../static/css/tab.css">
    <link rel="stylesheet" href="../static/css/home-tab.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/main.js"></script>

<div class="screen">
    {% include "components/hotbar.html" %}
    {% include "components/home_tab.html" %}
    <!--{% include "components/windows.html" %}-->
</div>
</body>
</html>
```


templates/join.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Join Project - DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">

    <link rel="stylesheet" href="../static/css/tab.css">
    <link rel="stylesheet" href="../static/css/join_project.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/join.js"></script>
<div class="screen">
    {% include "components/hotbar.html" %}
    <div class="tab join-project-tab">
        <h2>Join a Project</h2>
        <div class="project-id-container">
            <label for="project-id-input">Project ID:</label>
            <input type="text" id="project-id-input" placeholder="Enter Project ID">
        </div>
        <div id="project-info" style="display: none;">
            <!-- Project info will be displayed here -->
        </div>
        <button id="join-btn" style="display: none;">Join</button>
    </div>
</div>
</body>
</html>
```


templates/login.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">

    <link rel="stylesheet" href="../static/css/tab.css">
    <link rel="stylesheet" href="../static/css/login-tab.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/login.js"></script>
<div class="screen">
    {% include "components/login_tab.html" %}
</div>
</body>
</html>
```


templates/new_project.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>New Project - DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">

    <link rel="stylesheet" href="../static/css/new_project.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/new_project.js"></script>
<div class="screen">
    {% include "components/hotbar.html" %}
    <div class="tab new-project-tab">
        <h2>Create New Project</h2>
        <form id="new-project-form">
            <div class="form-group">
                <label for="project-title">Project Title:</label>
                <input type="text" id="project-title" name="title" required>
            </div>
            <div class="form-group">
                <label for="project-images">Upload Images:</label>
                <input type="file" id="project-images" name="images" multiple accept="image/*">
            </div>
            <div class="form-group">
                <label for="project-path">Or specify absolute path to images folder (optional):</label>
                <input type="text" id="project-path" name="path">
            </div>
            <div class="form-group" id="classes-container">
                <label>Classes:</label>
                <div class="class-item">
                    <input type="text" name="class-name" placeholder="Class Name">
                    <input type="color" name="class-color" value="#ff0000">
                    <button type="button" class="remove-class-btn">Remove</button>
                </div>
            </div>
            <button type="button" id="add-class-btn">Add Class</button>
            <button type="submit" id="create-project-btn">Create</button>
        </form>
        <div id="upload-progress" style="display:none;">
            <progress id="progress-bar" value="0" max="100"></progress>
            <span id="progress-text"></span>
        </div>
    </div>
</div>
</body>
</html>
```


templates/workspace.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Project Workspace - DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">

    <link rel="stylesheet" href="../static/css/workspace.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/workspace.js"></script>
<div class="screen">
    {% include "components/hotbar.html" %}
    <div class="tab workspace-tab">
        <h2 id="project-title">Project Title</h2>
        <div id="username">Username</div>
        <div class="progress-container">
            <progress id="project-progress" value="0" max="100"></progress>
        </div>
        <div class="workspace-container">
            <div class="classes-list" id="class-list">
                <!-- Classes will be dynamically added here -->
            </div>
            <div class="image-container">
                <img id="image-display" alt="Annotation Image">
                <!-- Annotations will be added here as overlays -->
            </div>
        </div>
        <div id="annotation-mode-indicator" class="hidden">
            <span>Annotation Mode: <span id="selected-class-name"></span></span>
            <button id="cancel-annotation-btn">Cancel</button>
        </div>
        <button id="next-btn">Next</button>
    </div>
</div>
</body>
</html>
```


templates/components/account_tab.html
```html
<div class="tab account-tab">
    <div class="account-tile">
        <div class="account-name loading" id="account-title">.....</div>
        <div class="account-details-container">
            <div class="account-projects-in">0</div>
            <div class="account-projects-host">0</div>
        </div>
    </div>
    <div class="account-actions">
        <button id="logout-btn">Logout</button>
    </div>
</div>
```


templates/components/home_tab.html
```html
<div class="tab home-tab">
    <div class="account-title loading" id="account-title">Hey, ...!</div>
    <div class="tab-title">
        Your projects:
        <button id="new-project-btn">New Project</button>
        <button id="join-project-btn">Join Project</button>
    </div>
    <div class="projects-container">
        <!-- Projects will be displayed here -->
    </div>
</div>
```


templates/components/hotbar.html
```html
<script src="../static/js/hotbar-manager.js"></script>
<div class="hotbar">
    <button class="hotbar-button" id="home-btn">
        <img src="../../static/icons/h_icon.webp">
    </button>
    <button class="hotbar-button" id="settings-btn">
        <img src="../../static/icons/s_icon.webp">
    </button>
    <button class="hotbar-button" id="profile-btn">
        <img src="../../static/icons/profile_icon.webp">
    </button>
</div>
```


templates/components/login_tab.html
```html
<div class="tab login-tab">
    <div class="tab-title">Login or register</div>
    <div class="login-boxes-container">
        <div class="login-box">
            <input class="login-field" placeholder="login">
            <input type="password" class="password-field" placeholder="password">
        </div>
        <div class="login-buttons">
            <button class="login-btn" id="login-btn">Login</button>
            or
            <button class="login-btn" id="register-btn">Register</button>
        </div>
    </div>
</div>
```


templates/components/windows.html
```html
<div class="windows-container">
    <div class="window">
        <div class="window-controls-container">
            <div class="window-title">Home</div>
            <div class="window-controls">
                <button class="close-btn"></button>
            </div>
        </div>
    </div>
</div>
```


Сделай так, чтобы в проекте генерировался файл classes.txt, такой как нужен для YOLO