```python
import os
import pytesseract
from PIL import ImageGrab, Image
from pynput import keyboard
from pynput.keyboard import Key, Controller
import threading
import cv2
import numpy as np
import json
import time
import sys
import tensorflow as tf
from tensorflow.keras.models import load_model, Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense
from tensorflow.keras.optimizers import Adam

# Константы
CONFIG_PATH = 'config.json'
DATA_PATH = 'training_data.npz'
MODEL_PATH = 'digit_recognizer.h5'
BATCH_SIZE = 32
CONFIDENCE_THRESHOLD = 0.5  # Порог уверенности для предсказания нейросети
REQUIRED_DIGITS = 5  # Требуемое количество цифр при ручном вводе

# Инициализация контроллера клавиатуры
keyboard_controller = Controller()

def load_config(config_path=CONFIG_PATH):
    """Загружает настройки из config.json"""
    try:
        with open(config_path, 'r') as f:
            return json.load(f)
    except FileNotFoundError:
        return {}
    except json.JSONDecodeError:
        print("Ошибка: Невозможно декодировать JSON из config.json.")
        return {}

def save_config(config, config_path=CONFIG_PATH):
    """Сохраняет настройки в config.json"""
    try:
        with open(config_path, 'w') as f:
            json.dump(config, f)
    except Exception as e:
        print(f"Ошибка при сохранении конфигурации: {e}")

def build_model():
    """Создаёт и возвращает новую одновыходную CNN модель"""
    model = Sequential([
        Conv2D(32, (3,3), activation='relu', input_shape=(28,28,1)),
        MaxPooling2D((2,2)),
        Conv2D(64, (3,3), activation='relu'),
        MaxPooling2D((2,2)),
        Flatten(),
        Dense(128, activation='relu'),
        Dense(10, activation='softmax')  # Одновыходной слой для цифр 0-9
    ])
    model.compile(optimizer=Adam(),
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
    return model

def load_or_create_model():
    """Загружает модель, если она существует, иначе создаёт новую"""
    if os.path.exists(MODEL_PATH):
        model = load_model(MODEL_PATH)
        print(f"Модель загружена из {MODEL_PATH}")
    else:
        model = build_model()
        print("Создана новая модель.")
    return model

def load_training_data(path=DATA_PATH):
    """Загружает тренировочные данные, если они существуют, иначе инициализирует пустые массивы"""
    if os.path.exists(path):
        data = np.load(path, allow_pickle=True)
        X = data['X']
        y = data['y']
        print(f"Загружены тренировочные данные из {path}. Всего образцов: {len(y)}")
    else:
        X = []
        y = []
        print("Тренировочные данные не найдены. Начинаем с нуля.")
    return X, y

def save_training_data(X, y, path=DATA_PATH):
    """Сохраняет тренировочные данные в файл"""
    X = np.array(X)
    y = np.array(y, dtype=object)  # Используем dtype=object для массивов списков
    np.savez_compressed(path, X=X, y=y)
    print(f"Тренировочные данные сохранены в {path}.")

def preprocess_image_for_model(image):
    """
    Предобрабатывает изображение для нейросети:
    - Если изображение имеет 3 канала, конвертирует в оттенки серого
    - Изменяет размер до 28x28
    - Нормализует
    """
    if len(image.shape) == 3 and image.shape[2] == 3:
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    else:
        gray = image.copy()
    # Изменение размера до 28x28
    resized = cv2.resize(gray, (28,28), interpolation=cv2.INTER_AREA)
    # Нормализация
    normalized = resized.astype('float32') / 255.0
    # Добавление измерения канала
    normalized = np.expand_dims(normalized, axis=-1)
    # Добавление измерения батча
    normalized = np.expand_dims(normalized, axis=0)
    return normalized

def split_into_digits(image, expected_digits=5):
    """
    Разбивает изображение с несколькими цифрами на отдельные изображения каждой цифры.

    :param image: Входное изображение с несколькими цифрами (BGR)
    :param expected_digits: Ожидаемое количество цифр в изображении
    :return: Список изображений отдельных цифр
    """
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    # Применение морфологических операций для улучшения сегментации
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2,2))
    morph = cv2.morphologyEx(gray, cv2.MORPH_OPEN, kernel, iterations=1)
    
    # Пороговая обработка
    thresh = cv2.threshold(morph, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
    
    # Поиск контуров цифр
    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    digit_images = []

    for cnt in contours:
        x, y, w, h = cv2.boundingRect(cnt)
        # Фильтрация по размеру цифры
        if h > 10 and w > 10:
            digit = gray[y:y+h, x:x+w]
            digit_images.append((x, digit))

    # Сортировка цифр по положению слева направо
    digit_images = sorted(digit_images, key=lambda item: item[0])

    # Проверка количества найденных цифр
    if len(digit_images) != expected_digits:
        print(f"Найдено {len(digit_images)} цифр, ожидается {expected_digits}. Попытка разделить изображение равными частями.")
        digit_width = image.shape[1] // expected_digits
        digit_images = []
        for i in range(expected_digits):
            x_start = i * digit_width
            x_end = (i + 1) * digit_width if i < expected_digits -1 else image.shape[1]
            digit = gray[:, x_start:x_end]
            digit_images.append(digit)
        return digit_images

    # Извлечение только изображений цифр
    digit_images = [img for (pos, img) in digit_images]

    return digit_images

def capture_and_process(box_left, box_top, box_width, box_height, model, X, y, num_digits=5):
    """
    Захватывает указанную область экрана, пытается распознать цифры.
    Если распознавания нет или количество цифр не совпадает, запрашивает ввод пользователя для обучения нейросети.
    """
    try:
        if box_width <= 0 or box_height <= 0:
            print("Неверные размеры области захвата. Проверьте конфигурацию.")
            return

        # Захват скриншота
        screenshot = ImageGrab.grab()

        box = (box_left, box_top, box_left + box_width, box_top + box_height)
        cropped_image_pil = screenshot.crop(box)

        # Конвертация в формат OpenCV
        cropped_image = cv2.cvtColor(np.array(cropped_image_pil), cv2.COLOR_RGB2BGR)

        # Визуализация захвата (опционально)
        visualize_capture(cropped_image, box)

        # Предобработка для OCR
        custom_config = r'--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789'
        gray = cv2.cvtColor(cropped_image, cv2.COLOR_BGR2GRAY)
        _, thresh_img = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
        processed_pil = Image.fromarray(thresh_img)

        # Распознавание с помощью Tesseract
        text = pytesseract.image_to_string(processed_pil, config=custom_config)
        digits = ''.join(filter(str.isdigit, text))
        print(f"Распознанные цифры с помощью Tesseract: '{digits}'")

        if digits and len(digits) == num_digits:
            # Разбиваем изображение на отдельные цифры
            digit_images = split_into_digits(cropped_image, expected_digits=num_digits)
            if not digit_images or len(digit_images) != num_digits:
                # Если не удалось корректно сегментировать цифры
                print("Не удалось корректно сегментировать цифры. Требуется ручной ввод.")
                manual_input_and_train(cropped_image, model, X, y, num_digits)
                return

            # Эмуляция ввода цифр
            for num in digits:
                keyboard_controller.press(num)
                keyboard_controller.release(num)
                time.sleep(0.05)  # Уменьшение задержки для большей скорости

            # Эмуляция нажатия Enter
            keyboard_controller.press(Key.enter)
            keyboard_controller.release(Key.enter)
            print("Цифры введены автоматически.")

            # Добавление данных в тренировочный набор
            for digit_img, digit_char in zip(digit_images, digits):
                processed = preprocess_image_for_model(digit_img)
                X.append(processed[0])
                y.append(int(digit_char))

            # Сохранение тренировочных данных
            save_training_data(X, y)

            # Обучение модели с новыми данными
            if len(y) >= 100:  # Минимальное количество образцов для обучения (регулируйте по необходимости)
                print("Обучение модели с новыми данными...")
                X_np = np.array(X)
                y_np = np.array(y)

                # Перемешивание данных
                indices = np.arange(len(y_np))
                np.random.shuffle(indices)
                X_np = X_np[indices]
                y_np = y_np[indices]

                # Обучение модели
                model.fit(X_np, y_np, epochs=5, batch_size=BATCH_SIZE, verbose=1)

                # Сохранение обученной модели
                model.save(MODEL_PATH)
                print("Модель обучена и сохранена.")
            else:
                print(f"Недостаточно данных для обучения. Всего обучающих образцов: {len(y)}")
        else:
            print(f"Распознаваемые цифры недопустимого количества: {len(digits)} вместо {num_digits}. Требуется ввод вручную.")
            manual_input_and_train(cropped_image, model, X, y, num_digits)
    except Exception as e:
        print(f"Ошибка при обработке скриншота: {e}")

def manual_input_and_train(cropped_image, model, X, y, num_digits=5):
    """
    Запрашивает у пользователя ввод ровно 5 цифр и обучает модель на этих данных.
    """
    while True:
        user_input = input(f"Введите распознанные цифры (ровно {num_digits} символов, например 06690): ").strip()
        if user_input.isdigit() and len(user_input) == num_digits:
            # Эмуляция ввода цифр
            for num in user_input:
                keyboard_controller.press(num)
                keyboard_controller.release(num)
                time.sleep(0.05)

            # Эмуляция нажатия Enter
            keyboard_controller.press(Key.enter)
            keyboard_controller.release(Key.enter)
            print("Цифры введены вручную.")

            # Разбиваем изображение на отдельные цифры
            digit_images = split_into_digits(cropped_image, expected_digits=num_digits)
            if not digit_images or len(digit_images) != num_digits:
                print("Не удалось корректно сегментировать цифры. Пожалуйста, попробуйте снова.")
                continue

            # Предобработка изображения и добавление в тренировочные данные
            for digit_img, digit_char in zip(digit_images, user_input):
                processed = preprocess_image_for_model(digit_img)
                X.append(processed[0])
                y.append(int(digit_char))

            # Сохранение тренировочных данных
            save_training_data(X, y)

            # Обучение модели с новыми данными
            if len(y) >= 100:  # Минимальное количество образцов для обучения (регулируйте по необходимости)
                print("Обучение модели с новыми данными...")
                X_np = np.array(X)
                y_np = np.array(y)

                # Перемешивание данных
                indices = np.arange(len(y_np))
                np.random.shuffle(indices)
                X_np = X_np[indices]
                y_np = y_np[indices]

                # Обучение модели
                model.fit(X_np, y_np, epochs=5, batch_size=BATCH_SIZE, verbose=1)

                # Сохранение обученной модели
                model.save(MODEL_PATH)
                print("Модель обучена и сохранена.")
            else:
                print(f"Недостаточно данных для обучения. Всего обучающих образцов: {len(y)}")
            break  # Выход из цикла после успешного ввода
        else:
            print(f"Неверный ввод. Требуется ввести ровно {num_digits} цифры. Попробуйте снова.")

def visualize_capture(cropped_image, box):
    """
    Отображает захваченное изображение с выделенной областью.
    """
    try:
        # Рисование прямоугольника вокруг области захвата
        cv2.rectangle(cropped_image, (0, 0), (box[2]-box[0], box[3]-box[1]), (0, 255, 0), 2)

        # Название окна
        window_name = "Захват экрана с выделенной областью"

        # Отображение изображения
        cv2.imshow(window_name, cropped_image)

        # Ожидание заданного времени или нажатия любой клавиши
        cv2.waitKey(2000)  # 2000 миллисекунд = 2 секунды

        # Закрытие окна
        cv2.destroyWindow(window_name)

    except Exception as e:
        print(f"Ошибка при визуализации: {e}")

def on_press(key, model, X, y, box_left, box_top, box_width, box_height):
    """
    Обработчик нажатий клавиш.
    'n' - захват и обработка области экрана.
    """
    try:
        if key.char.lower() == 'n':
            print("Нажата клавиша 'n'. Начало захвата и обработки области экрана.")
            # Запуск в отдельном потоке
            threading.Thread(target=capture_and_process, args=(box_left, box_top, box_width, box_height, model, X, y, REQUIRED_DIGITS), daemon=True).start()
    except AttributeError:
        if key == Key.esc:
            # Остановка слушателя при нажатии Esc
            print("Нажата клавиша 'Esc'. Завершение работы.")
            return False  # Останавливает слушатель

def select_area(image):
    """
    Позволяет пользователю интерактивно выбрать область на изображении с помощью OpenCV.
    Нажмите 'c' для подтверждения выбора или 'r' для сброса.
    Возвращает координаты выбранного прямоугольника или None.
    """
    ref_point = []
    cropping = False

    def mouse_callback(event, x, y, flags, param):
        nonlocal ref_point, cropping, image

        if event == cv2.EVENT_LBUTTONDOWN:
            ref_point = [(x, y)]
            cropping = True

        elif event == cv2.EVENT_MOUSEMOVE:
            if cropping:
                temp_image = image.copy()
                cv2.rectangle(temp_image, ref_point[0], (x, y), (0, 255, 0), 2)
                cv2.imshow("Выбор области", temp_image)

        elif event == cv2.EVENT_LBUTTONUP:
            ref_point.append((x, y))
            cropping = False
            cv2.rectangle(image, ref_point[0], ref_point[1], (0, 255, 0), 2)
            cv2.imshow("Выбор области", image)

    clone = image.copy()
    cv2.namedWindow("Выбор области")
    cv2.setMouseCallback("Выбор области", mouse_callback)

    while True:
        cv2.imshow("Выбор области", image)
        key = cv2.waitKey(1) & 0xFF

        if key == ord("r"):
            image = clone.copy()
            ref_point = []
            cv2.imshow("Выбор области", image)

        elif key == ord("c"):
            break

    cv2.destroyWindow("Выбор области")

    if len(ref_point) == 2:
        return ref_point
    else:
        return None

def get_user_selection():
    """
    Захватывает скриншот, отображает его и позволяет пользователю выбрать область для OCR.
    Возвращает координаты выбранного прямоугольника или None.
    """
    try:
        screenshot = ImageGrab.grab()
        screenshot_cv = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)
        points = select_area(screenshot_cv)
        return points
    except Exception as e:
        print(f"Ошибка при выборе области: {e}")
        return None

def main():
    """
    Основная функция:
    - Загружает или запрашивает выбор области захвата.
    - Загружает модель и тренировочные данные.
    - Запускает слушатель клавиатуры для обработки нажатий.
    """
    config = load_config()

    # Проверка существования настроек области захвата
    if all(k in config for k in ('box_left', 'box_top', 'box_width', 'box_height')):
        BOX_LEFT = config['box_left']
        BOX_TOP = config['box_top']
        BOX_WIDTH = config['box_width']
        BOX_HEIGHT = config['box_height']
        print(f"Загружены настройки области захвата: {config}")
    else:
        print("Настройки области захвата не найдены. Пожалуйста, выберите область захвата.")
        box = get_user_selection()
        if box:
            x1, y1 = box[0]
            x2, y2 = box[1]

            # Определение верхней левой и нижней правой точек
            BOX_LEFT = min(x1, x2)
            BOX_TOP = min(y1, y2)
            BOX_RIGHT = max(x1, x2)
            BOX_BOTTOM = max(y1, y2)

            BOX_WIDTH = BOX_RIGHT - BOX_LEFT
            BOX_HEIGHT = BOX_BOTTOM - BOX_TOP

            config = {
                "box_width": BOX_WIDTH,
                "box_height": BOX_HEIGHT,
                "box_left": BOX_LEFT,
                "box_top": BOX_TOP
            }
            save_config(config)
            print(f"Сохранены настройки области захвата: {config}")
        else:
            print("Область захвата не выбрана. Завершение работы.")
            sys.exit()

    # Загрузка модели
    model = load_or_create_model()

    # Загрузка или инициализация тренировочных данных
    X, y = load_training_data()

    # Запуск слушателя клавиатуры
    with keyboard.Listener(on_press=lambda key: on_press(key, model, X, y, BOX_LEFT, BOX_TOP, BOX_WIDTH, BOX_HEIGHT)) as listener:
        print("Слушатель клавиатуры запущен.")
        print("Нажмите 'n' для захвата и обработки области экрана.")
        print("Нажмите 'Esc' для выхода.")
        listener.join()

if __name__ == "__main__":
    main()
```
Оптимизируй код, ускорь его выполнение, уменьши нагрузку, ускорь ввод цифр, сделай чтобы оно быстрее читало цифры