Project structure:

DeepFit_TEST2/
├── backend/
│   ├── DeepFit/
│   │   ├── DeepFitClassifier.py
│   │   └── deepfit_classifier_v3.tflite
│   ├── static/
│   │   ├── script.js
│   │   └── styles.css
│   ├── templates/
│   │   └── index.html
│   ├── main.py
│   ├── pushup_counter.py
│   ├── renderer.py
│   ├── requirements.txt
│   └── utils.py
└── V2_GenerateProjectShort.py


Project files:

backend/main.py
```py
# backend/main.py

import eventlet

eventlet.monkey_patch()

import os
import base64
import cv2
import numpy as np
from flask import Flask, render_template
from flask_socketio import SocketIO, emit
from pushup_counter import PushUpCounter
import json
from pyngrok import ngrok

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!' #  ВАЖНО: добавить секретный ключ
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet')

# Инициализация PushUpCounter
counter = PushUpCounter()


@app.route('/')
def index():
    return render_template('index.html')


@socketio.on('connect')
def handle_connect():
    print('Client connected')
    emit('status', {'status': 'Connected to server'})


@socketio.on('image')
def handle_image(data):
    try:
        # Декодирование base64 изображения
        header, encoded = data.split(',', 1)
        decoded = base64.b64decode(encoded)
        np_data = np.frombuffer(decoded, np.uint8)
        frame = cv2.imdecode(np_data, cv2.IMREAD_COLOR)
        if frame is None:
            emit('result', {'status': 'error', 'message': 'Invalid image data'})
            return

        # Обработка кадра
        result = counter.process_frame(frame)

        # Формирование данных для отправки
        response = {
            'status': 'processing',
            'progress': result.get('count', 0),
            'landmarks': result.get('landmarks', []),
            'angles': result.get('angles', {}),
            'feedback': result.get('feedback', '')
        }

        # Отправка данных обратно клиенту
        emit('result', json.dumps(response))
    except Exception as e:
        print(f"Error processing frame: {e}")
        emit('result', {'status': 'error', 'message': str(e)})


@socketio.on('disconnect')
def handle_disconnect():
    print('Client disconnected')


if __name__ == '__main__':
    ngrok.set_auth_token("<NGROK_AUTHTOKEN>")
    # Открыть туннель ngrok
    public_url = ngrok.connect(5000)
    print(f"Public URL: {public_url}")

    # Запуск Flask-SocketIO сервера
    socketio.run(app, host='0.0.0.0', port=5000)

```


backend/pushup_counter.py
```py
# backend/pushup_counter.py

import cv2
import mediapipe as mp
import numpy as np
from collections import deque
from utils import set_pose_parameters, calculate_angle, LM_DICT
import os


# Если вы используете DeepFitClassifier, импортируйте его здесь.
# Убедитесь, что структура папок соответствует
# from DeepFit.DeepFitClassifier import DeepFitClassifier

class PushUpCounter:
    def __init__(self, deepfit_model_path='DeepFit/deepfit_classifier_v3.tflite'):
        # Инициализация параметров позы
        params = set_pose_parameters()
        (mode, complexity, smooth_landmarks, enable_segmentation,
         smooth_segmentation, detectionCon, trackCon, mpPose) = params
        self.pose = mpPose.Pose(
            static_image_mode=mode,
            model_complexity=complexity,
            smooth_landmarks=smooth_landmarks,
            enable_segmentation=enable_segmentation,
            smooth_segmentation=smooth_segmentation,
            min_detection_confidence=detectionCon,
            min_tracking_confidence=trackCon
        )
        self.mpDraw = mp.solutions.drawing_utils

        # Инициализация классификатора DeepFit (опционально)
        if os.path.exists(deepfit_model_path):
            # self.classifier = DeepFitClassifier(deepfit_model_path)
            self.classifier = None  # Замените на инициализацию вашего классификатора
            print(f"Классификатор DeepFit загружен по пути: {deepfit_model_path}")
        else:
            self.classifier = None
            print(f"Классификатор DeepFit не найден по пути: {deepfit_model_path}")

        # Инициализация счетчиков
        self.reset_counters()

    def reset_counters(self):
        self.count = 0
        self.direction = 0
        self.form = 0
        self.feedback = "Bad Form. Correct Posture."
        self.frame_queue = deque(maxlen=250)

    def process_frame(self, frame):
        """
        Обрабатывает один кадр и обновляет счетчики.
        Возвращает информацию для фронтенда.

        :param frame: Полученный кадр в формате BGR
        :return: Словарь с данными
        """
        image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        results = self.pose.process(image)
        landmark_list = self.get_position(results, frame.shape[0], frame.shape[1])

        data = {
            'landmarks': landmark_list,
            'angles': {},
            'feedback': self.feedback,
            'count': self.count
        }

        if landmark_list:
            # Пример расчета углов
            shoulder = self.get_landmark(landmark_list, 11)
            elbow = self.get_landmark(landmark_list, 13)
            wrist = self.get_landmark(landmark_list, 15)
            hip = self.get_landmark(landmark_list, 23)

            if shoulder and elbow and wrist and hip:
                elbow_angle = calculate_angle(
                    (shoulder[1], shoulder[2]),
                    (elbow[1], elbow[2]),
                    (wrist[1], wrist[2])
                )
                shoulder_angle = calculate_angle(
                    (elbow[1], elbow[2]),
                    (shoulder[1], shoulder[2]),
                    (hip[1], hip[2])
                )
                # Для hip_angle можно использовать другую ключевую точку для улучшения
                hip_angle = calculate_angle(
                    (shoulder[1], shoulder[2]),
                    (hip[1], hip[2]),
                    (hip[1], hip[2])  # Можно заменить на другую точку, например, 25 или 24
                )

                data['angles'] = {
                    'elbow': elbow_angle,
                    'shoulder': shoulder_angle,
                    'hip': hip_angle
                }

                # Подсчет отжиманий
                self.update_counters(elbow_angle, shoulder_angle, hip_angle)
                data['feedback'] = self.feedback
                data['count'] = self.count

        return data

    def get_position(self, results, height, width):
        """
        Извлекает координаты ключевых точек.

        :param results: Результаты обработки Mediapipe Pose
        :param height: Высота кадра
        :param width: Ширина кадра
        :return: Список ключевых точек
        """
        landmark_list = []
        if results.pose_landmarks:
            for id, lm in enumerate(results.pose_landmarks.landmark):
                px, py = int(lm.x * width), int(lm.y * height)
                landmark_list.append([id, px, py])
        return landmark_list

    def get_landmark(self, landmark_list, landmark_id):
        """
        Получает координаты конкретной ключевой точки.

        :param landmark_list: Список ключевых точек
        :param landmark_id: ID ключевой точки
        :return: Список [id, x, y] или None
        """
        for lm in landmark_list:
            if lm[0] == landmark_id:
                return lm
        return None

    def update_counters(self, elbow_angle, shoulder_angle, hip_angle):
        """
        Обновляет счетчики отжиманий на основе углов.

        :param elbow_angle: Угол в локте
        :param shoulder_angle: Угол в плече
        :param hip_angle: Угол в бедре
        """
        # Интерполяция угла локтя для прогресс-бара и процента успешности
        pushup_success_percentage = np.interp(elbow_angle, (90, 160), (0, 100))
        # Прогресс-бар будет рассчитан на фронтенде, поэтому можно его не отправлять

        # Проверка начальной формы
        if elbow_angle > 160 and shoulder_angle > 40 and hip_angle > 160:
            self.form = 1
        else:
            self.form = 0

        # Полный цикл выполнения отжиманий
        if self.form == 1:
            if pushup_success_percentage == 0:
                if elbow_angle <= 90 and hip_angle > 160:
                    if self.direction == 0:
                        self.count += 0.5
                        self.direction = 1
                    self.feedback = "Go Up"
                else:
                    self.feedback = "Bad Form. Correct Posture."
            elif pushup_success_percentage == 100:
                if elbow_angle > 160 and shoulder_angle > 40 and hip_angle > 160:
                    if self.direction == 1:
                        self.count += 0.5
                        self.direction = 0
                    self.feedback = "Go Down"
                else:
                    self.feedback = "Bad Form. Correct Posture."
        else:
            self.feedback = "Bad Form. Correct Posture."
```


backend/renderer.py
```py
# renderer.py

import cv2


class Renderer:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def draw_annotations(self, frame, data):
        """
        Рисует аннотации на кадре на основе переданных данных.

        :param frame: Исходный кадр изображения.
        :param data: Словарь с данными для отрисовки.
        :return: Аннотированный кадр.
        """
        # Отрисовка ключевых точек
        if data['landmarks']:
            for lm in data['landmarks']:
                cv2.circle(frame, (lm[1], lm[2]), 5, (255, 0, 0), cv2.FILLED)

        # Отрисовка прогресса отжиманий
        if 'angles' in data and data['angles']:
            elbow_angle = data['angles'].get('elbow', 0)
            pushup_success_percentage = int(np.interp(elbow_angle, (90, 160), (0, 100)))
            pushup_progress_bar = int(np.interp(elbow_angle, (90, 160), (380, 50)))

            cv2.rectangle(frame, (580, 50), (600, 380), (0, 255, 0), 3)
            cv2.rectangle(frame, (580, pushup_progress_bar), (600, 380), (0, 255, 0), cv2.FILLED)
            cv2.putText(frame, f'{pushup_success_percentage}%', (565, 430),
                        cv2.FONT_HERSHEY_PLAIN, 2, (255, 0, 0), 2)

        # Отображение счёта отжиманий
        cv2.rectangle(frame, (0, 380), (100, 480), (0, 255, 0), cv2.FILLED)
        cv2.putText(frame, str(int(data['count'])), (25, 455),
                    cv2.FONT_HERSHEY_PLAIN, 5, (255, 0, 0), 5)

        # Отображение обратной связи
        cv2.rectangle(frame, (500, 0), (640, 40), (255, 255, 255), cv2.FILLED)
        cv2.putText(frame, data['feedback'], (500, 40),
                    cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2)

        return frame
```


backend/requirements.txt
```txt
python-multipart

tensorflow[and-cuda]
matplotlib


flask
flask-socketio
eventlet
opencv-python
mediapipe
numpy
```


backend/utils.py
```py
# backend/utils.py

import mediapipe as mp
import math

# Словарь для сопоставления ключевых точек от Mediapipe с моделью классификатора
LM_DICT = {
    0: 0, 1: 10, 2: 12, 3: 14, 4: 16, 5: 11, 6: 13, 7: 15,
    8: 24, 9: 26, 10: 28, 11: 23, 12: 25, 13: 27,
    14: 5, 15: 2, 16: 8, 17: 7,
}


def set_pose_parameters():
    mode = False
    complexity = 1
    smooth_landmarks = True
    enable_segmentation = False
    smooth_segmentation = True
    detectionCon = 0.5
    trackCon = 0.5
    mpPose = mp.solutions.pose
    return mode, complexity, smooth_landmarks, enable_segmentation, smooth_segmentation, detectionCon, trackCon, mpPose


def calculate_angle(point1, point2, point3):
    """
    Вычисляет угол между тремя точками.

    :param point1: Координаты первой точки (x1, y1)
    :param point2: Координаты второй точки (x2, y2)
    :param point3: Координаты третьей точки (x3, y3)
    :return: Угол в градусах
    """
    x1, y1 = point1
    x2, y2 = point2
    x3, y3 = point3

    angle = math.degrees(
        math.atan2(y3 - y2, x3 - x2) -
        math.atan2(y1 - y2, x1 - x2)
    )

    # Корректировка угла
    if angle < 0:
        angle += 360
    if angle > 180:
        angle = 360 - angle
    return angle
```


backend/DeepFit/DeepFitClassifier.py
```py
import tensorflow as tf
import numpy as np


# def load_X(X_path):
#     file = open(X_path, "r")
#     X_ = np.array([elem for elem in [row.split(",") for row in file]], dtype=np.float32)
#     file.close()
#     return X_


def euclidean_dist(a, b):
    # This function calculates the euclidean distance between 2 point in 2-D coordinates
    # if one of two points is (0,0), dist = 0
    # a, b: input array with dimension: m, 2
    # m: number of samples
    # 2: x and y coordinate
    try:
        if a.shape[1] == 2 and a.shape == b.shape:
            # check if element of a and b is (0,0)
            bol_a = (a[:, 0] != 0).astype(int)
            bol_b = (b[:, 0] != 0).astype(int)
            dist = np.linalg.norm(a - b, axis=1)
            return (dist * bol_a * bol_b).reshape(a.shape[0], 1)
    except:
        print("[Error]: Check dimension of input vector")
        return 0


def norm_X(X):
    num_sample = X.shape[0]
    # Keypoints
    Nose = X[:, 0 * 2 : 0 * 2 + 2]
    Neck = X[:, 1 * 2 : 1 * 2 + 2]
    RShoulder = X[:, 2 * 2 : 2 * 2 + 2]
    RElbow = X[:, 3 * 2 : 3 * 2 + 2]
    RWrist = X[:, 4 * 2 : 4 * 2 + 2]
    LShoulder = X[:, 5 * 2 : 5 * 2 + 2]
    LElbow = X[:, 6 * 2 : 6 * 2 + 2]
    LWrist = X[:, 7 * 2 : 7 * 2 + 2]
    RHip = X[:, 8 * 2 : 8 * 2 + 2]
    RKnee = X[:, 9 * 2 : 9 * 2 + 2]
    RAnkle = X[:, 10 * 2 : 10 * 2 + 2]
    LHip = X[:, 11 * 2 : 11 * 2 + 2]
    LKnee = X[:, 12 * 2 : 12 * 2 + 2]
    LAnkle = X[:, 13 * 2 : 13 * 2 + 2]
    REye = X[:, 14 * 2 : 14 * 2 + 2]
    LEye = X[:, 15 * 2 : 15 * 2 + 2]
    REar = X[:, 16 * 2 : 16 * 2 + 2]
    LEar = X[:, 17 * 2 : 17 * 2 + 2]

    # Length of head
    length_Neck_LEar = euclidean_dist(Neck, LEar)
    length_Neck_REar = euclidean_dist(Neck, REar)
    length_Neck_LEye = euclidean_dist(Neck, LEye)
    length_Neck_REye = euclidean_dist(Neck, REye)
    length_Nose_LEar = euclidean_dist(Nose, LEar)
    length_Nose_REar = euclidean_dist(Nose, REar)
    length_Nose_LEye = euclidean_dist(Nose, LEye)
    length_Nose_REye = euclidean_dist(Nose, REye)
    length_head = np.maximum.reduce(
        [
            length_Neck_LEar,
            length_Neck_REar,
            length_Neck_LEye,
            length_Neck_REye,
            length_Nose_LEar,
            length_Nose_REar,
            length_Nose_LEye,
            length_Nose_REye,
        ]
    )
    # length_head      = np.sqrt(np.square((LEye[:,0:1]+REye[:,0:1])/2 - Neck[:,0:1]) + np.square((LEye[:,1:2]+REye[:,1:2])/2 - Neck[:,1:2]))

    # Length of torso
    length_Neck_LHip = euclidean_dist(Neck, LHip)
    length_Neck_RHip = euclidean_dist(Neck, RHip)
    length_torso = np.maximum(length_Neck_LHip, length_Neck_RHip)
    # length_torso     = np.sqrt(np.square(Neck[:,0:1]-(LHip[:,0:1]+RHip[:,0:1])/2) + np.square(Neck[:,1:2]-(LHip[:,1:2]+RHip[:,1:2])/2))

    # Length of right leg
    length_leg_right = euclidean_dist(RHip, RKnee) + euclidean_dist(RKnee, RAnkle)
    # length_leg_right = np.sqrt(np.square(RHip[:,0:1]-RKnee[:,0:1]) + np.square(RHip[:,1:2]-RKnee[:,1:2])) \
    # + np.sqrt(np.square(RKnee[:,0:1]-RAnkle[:,0:1]) + np.square(RKnee[:,1:2]-RAnkle[:,1:2]))

    # Length of left leg
    length_leg_left = euclidean_dist(LHip, LKnee) + euclidean_dist(LKnee, LAnkle)
    # length_leg_left = np.sqrt(np.square(LHip[:,0:1]-LKnee[:,0:1]) + np.square(LHip[:,1:2]-LKnee[:,1:2])) \
    # + np.sqrt(np.square(LKnee[:,0:1]-LAnkle[:,0:1]) + np.square(LKnee[:,1:2]-LAnkle[:,1:2]))

    # Length of leg
    length_leg = np.maximum(length_leg_right, length_leg_left)

    # Length of body
    length_body = length_head + length_torso + length_leg

    # Check all samples have length_body of 0
    length_chk = (length_body > 0).astype(int)

    # Check keypoints at origin
    keypoints_chk = (X > 0).astype(int)

    chk = length_chk * keypoints_chk

    # Set all length_body of 0 to 1 (to avoid division by 0)
    length_body[length_body == 0] = 1

    # The center of gravity
    # number of point OpenPose locates:
    num_pts = (X[:, 0::2] > 0).sum(1).reshape(num_sample, 1)
    centr_x = X[:, 0::2].sum(1).reshape(num_sample, 1) / num_pts
    centr_y = X[:, 1::2].sum(1).reshape(num_sample, 1) / num_pts

    # The  coordinates  are  normalized relative to the length of the body and the center of gravity
    X_norm_x = (X[:, 0::2] - centr_x) / length_body
    X_norm_y = (X[:, 1::2] - centr_y) / length_body

    # Stack 1st element x and y together
    X_norm = np.column_stack((X_norm_x[:, :1], X_norm_y[:, :1]))

    for i in range(1, X.shape[1] // 2):
        X_norm = np.column_stack(
            (X_norm, X_norm_x[:, i : i + 1], X_norm_y[:, i : i + 1])
        )

    # Set all samples have length_body of 0 to origin (0, 0)
    X_norm = X_norm * chk

    return X_norm


# encodings for labels in dataset
LABELS = [
    "squats",
    "lunges",
    "bicep_curls",
    "situps",
    "pushups",
    "tricep_extensions",
    "dumbbell_rows",
    "jumping_jacks",
    "dumbbell_shoulder_press",
    "lateral_shoulder_raises",
]
class_d = {}
for i, ex in enumerate(LABELS):
    class_d[ex] = i


class DeepFitClassifier:
    def __init__(self, model_path):
        """
        Load the TFLite model and allocate tensors \n
        Get input and output tensors.
        """
        self.interpreter = tf.lite.Interpreter(model_path=model_path)
        self.interpreter.allocate_tensors()
        self.input_details = self.interpreter.get_input_details()
        self.output_details = self.interpreter.get_output_details()

    def predict(self, X) -> str:
        """
        X -> (1, 36) shaped numpy array
        First 18 are the x coordinates of the 18 keypoints \n
        next 18 are the y coordinates of the 18 keypoints \n
        Returns: \n
        Winning Label
        """
        # Setup input
        X_sample = np.array(X).reshape(1, -1)
        X_sample_norm = norm_X(X_sample)
        input_data = np.array(X_sample_norm[0], np.float32).reshape(1, 36)
        # Invoke the model on the input data
        self.interpreter.set_tensor(self.input_details[0]["index"], input_data)
        self.interpreter.invoke()
        # Get the result
        output_data = self.interpreter.get_tensor(self.output_details[0]["index"])

        np_output_data = np.array(output_data)
        champ_idx = np.argmax(np_output_data)
        self.results = dict(zip(LABELS, output_data[0]))

        return LABELS[champ_idx]

    def get_results(self) -> dict:
        """
        Get a dictionary containing the probability of each label for the last predicted input
        """
        return self.results
        # return 0


```


backend/static/script.js
```js
// frontend/script.js

$(document).ready(function() {
    const video = document.getElementById('video');
    const overlay = document.getElementById('overlay');
    const ctx = overlay.getContext('2d');
    const feedback = $('#feedback');
    const countDisplay = $('#count');
    const progressBar = $('#progress');

    // Подключение к WebSocket серверу
    const socket = io.connect(`http://${window.location.host}`);

    socket.on('connect', function() {
        console.log('Подключено к серверу');
    });

    socket.on('status', function(data) {
        console.log(data.status);
    });

    socket.on('result', function(data) {
        try {
            const parsedData = JSON.parse(data);
            // Обновление статуса и счётчика
            feedback.text(parsedData.feedback);
            countDisplay.text(parsedData.progress);

            // Обновление прогресс-бара
            const progressPercentage = Math.min(Math.max(parsedData.progress, 0), 100);
            progressBar.css('width', `${progressPercentage}%`);

            // Отрисовка аннотаций
            drawAnnotations(parsedData.landmarks);
        } catch (e) {
            console.error('Ошибка при обработке данных:', e);
        }
    });

    socket.on('disconnect', function() {
        console.log('Отключено от сервера');
    });

    // Функция для отрисовки аннотаций
    function drawAnnotations(landmarks) {
        // Очистка канваса
        ctx.clearRect(0, 0, overlay.width, overlay.height);

        if (landmarks && landmarks.length > 0) {
            // Преобразование списка ключевых точек в объект для удобства доступа
            const lm = {};
            landmarks.forEach(point => {
                lm[point[0]] = {x: point[1], y: point[2]};
            });

            // Нарисовать круги на ключевых точках
            for (const id in lm) {
                const x = lm[id].x;
                const y = lm[id].y;
                ctx.beginPath();
                ctx.arc(x, y, 5, 0, 2 * Math.PI);
                ctx.fillStyle = 'red';
                ctx.fill();
            }

            // Нарисовать линии между ключевыми точками (по связям Mediapipe)
            const connections = [
                [11, 13], [13, 15],  // Левая рука
                [12, 14], [14, 16],  // Правая рука
                [11, 23], [12, 24],  // Туловище
                [23, 25], [24, 26],  // Ноги
                // Добавьте другие связи по необходимости
                [11, 12], [23, 24],
                [25, 26],
                [13, 11], [14, 12],
                // Вспомогательные связи для более полной визуализации
            ];

            connections.forEach(pair => {
                const [start, end] = pair;
                if (lm[start] && lm[end]) {
                    ctx.beginPath();
                    ctx.moveTo(lm[start].x, lm[start].y);
                    ctx.lineTo(lm[end].x, lm[end].y);
                    ctx.strokeStyle = 'blue';
                    ctx.lineWidth = 2;
                    ctx.stroke();
                }
            });
        }
    }

    // Захват видео с веб-камеры
    navigator.mediaDevices.getUserMedia({ video: true, audio: false })
        .then(function(stream) {
            video.srcObject = stream;
            video.play();
            // Настройка размера канваса после загрузки видео
            video.onloadedmetadata = function() {
                overlay.width = video.videoWidth;
                overlay.height = video.videoHeight;
                // Запуск отправки кадров
                sendFrames();
            };
        })
        .catch(function(err) {
            console.log("Ошибка доступа к камере: " + err);
        });

    // Функция для отправки кадров на сервер
    function sendFrames() {
        const FPS = 10; // Частота отправки кадров

        setInterval(function() {
            // Создание временного канваса для захвата кадра
            const tempCanvas = document.createElement('canvas');
            tempCanvas.width = video.videoWidth;
            tempCanvas.height = video.videoHeight;
            const tempCtx = tempCanvas.getContext('2d');
            tempCtx.drawImage(video, 0, 0, tempCanvas.width, tempCanvas.height);
            const dataURL = tempCanvas.toDataURL('image/jpeg');

            // Отправка изображения через WebSocket
            if (socket.connected) {
                socket.emit('image', dataURL);
            }
        }, 1000 / FPS);
    }
});
```


backend/static/styles.css
```css
/* frontend/styles.css */

body {
    font-family: Arial, sans-serif;
    background-color: #f0f2f5;
    text-align: center;
    margin: 0;
    padding: 0;
}

.container {
    padding: 20px;
}

h1 {
    color: #333;
}

.video-container {
    position: relative;
    display: inline-block;
}

#video {
    width: 640px;
    height: 480px;
    background-color: #000;
}

#overlay {
    position: absolute;
    top: 0;
    left: 0;
}

.status {
    margin-top: 20px;
}

#feedback {
    font-size: 1.2em;
    color: #555;
}

.progress-bar {
    width: 300px;
    height: 25px;
    background-color: #ddd;
    border-radius: 5px;
    margin: 10px auto;
    overflow: hidden;
}

#progress {
    height: 100%;
    width: 0;
    background-color: #4caf50;
    transition: width 0.5s;
}

#count {
    font-size: 2em;
    color: #333;
}
```


backend/templates/index.html
```html
<!-- frontend/index.html -->

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Подсчет Отжиманий в Реальном Времени</title>
    <link rel="stylesheet" href="../static/styles.css">
</head>
<body>
    <div class="container">
        <h1>Подсчет Отжиманий в Реальном Времени</h1>
        <div class="video-container">
            <video id="video" autoplay muted></video>
            <canvas id="overlay"></canvas>
        </div>
        <div class="status">
            <p id="feedback">Жду начала...</p>
            <p>Повторы: <span id="count">0</span></p>
            <div class="progress-bar">
                <div id="progress"></div>
            </div>
        </div>
    </div>

    <!-- Подключение jQuery -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <!-- Подключение Socket.IO -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.4/socket.io.min.js"
            integrity="sha512-JgC5fCimjqBvBBbkI8RoSZ7K3PoK/sqiymsdliFWhFE+tMP0rXlyFTqzBYTI+9yad0tLXIQUxFEFEzzSzLCmLg=="
            crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <!-- Подключение скрипта -->
    <script src="../static/script.js"></script>
</body>
</html>
```




Вот мой код.
Сделай так, чтобы на сайте была кнопка "Active camera" и она предлагала разрешить ее использовать.