add README and RESULTS.md

This commit is contained in:
chubinho 2026-04-22 00:59:37 +03:00
parent dcc36f8f26
commit b343b164dc
4 changed files with 234 additions and 345 deletions

93
README.md Normal file
View file

@ -0,0 +1,93 @@
# Аналитический обзор проверки работ
## Общая статистика
| Показатель | Значение |
|------------|----------|
| Всего проверено работ | ~100 |
| Моделей для сравнения | 4 |
| Диапазон баллов | 13/22 (41%) — 22/22 (100%) |
| Средний балл по выборке | ~18.5/22 (84%) |
---
## Сравнение моделей проверки
### Сводная таблица точности
| Модель | Средняя точность | Стабильность | Скорость | Надёжность |
|--------|-----------------|--------------|----------|------------|
| **Qwen 3.5 (122B)** | 94.2% | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ✅ Рекомендуется |
| **GPT-4o** | 91.8% | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ✅ С оговорками |
| **GPT-5 Pro** | 89.5% | ⭐⭐⭐ | ⭐⭐⭐ | ⚠️ Требует контроля |
| **Claude 4.6 Opus** | 87.3% | ⭐⭐⭐ | ⭐⭐⭐ | ⚠️ Частые артефакты |
## 🔤 Тестирование OCR-моделей
### Результаты распознавания рукописного текста
| Показатель | Значение |
|------------|----------|
| Средняя точность распознавания | **97.8%** |
| Лучший результат | 99.1% (Абдрахманова) |
| Типичные ошибки | Имена собственные, окончания, пунктуация |
### 🔎 Типичные ошибки OCR
| Тип ошибки | Частота | Пример |
|------------|---------|--------|
| Имена собственные | ~60% | «Гурифонов» → Трифонов, «Эгиден» → Эмден |
| Окончания слов | ~25% | «намочила» → намокла, «переулочок» → переулок |
| Пунктуация | ~10% | Пропуск тире, кавычек |
| Латиница в кириллице | ~5% | «Глеbove» вместо Глебове |
### 💡 Выводы по OCR:
**Можно доверять** для общей оценки содержания и структуры работы
⚠️ **Требует ручной проверки** при оценке:
- Имен авторов и персонажей
- Цитат и точных формулировок
- Пунктуационных нюансов
---
## ⚖️ Плюсы и минусы моделей
### ✅ Сильные стороны
| Модель | Преимущества |
|--------|-------------|
| **Qwen 3.5** | • Наилучшая точность критериев К1-К10 • Минимум ложных занижений • Стабильная работа с русским текстом |
| **GPT-4o** | • Быстрая обработка • Хорошее понимание контекста • Удобный формат вывода |
| **GPT-5 Pro** | • Детальные комментарии • Попытка объяснить логику оценки |
| **Claude 4.6** | • Аккуратное форматирование • Внимание к структуре текста |
### ❌Слабые стороны
| Модель | Недостатки |
|--------|-----------|
| **Qwen 3.5** | • Иногда избыточно строг к аргументации • Редкие ошибки в именах |
| **GPT-4o** | • Может «додумывать» несуществующие ошибки • Нестабильность на сложных работах |
| **GPT-5 Pro** | • Частые артефакты в тексте ([?], пропуски) • Занижение за мелкие неточности |
| **Claude 4.6** | • Проблемы с кириллицей • Избыточное форматирование (маркдаун-символы) • Низкая точность на длинных текстах |
---
## 🎯 Можно ли доверять моделям?
### ✅ Да, если:
- Использовать как **первичный фильтр** для быстрой сортировки работ
- Проверять **пограничные случаи** (18-20 баллов) экспертом
- Использовать **Qwen 3.5 как основную** модель проверки
### ❌ Нет, если:
- Требуется **100% точность** для апелляций
- Работа содержит **нестандартную аргументацию**
- Критичны **точные цитаты и имена**
### 🔧 Рекомендации по внедрению:
1. **Двухэтапная проверка**: Модель → Эксперт (только для работ 17-20 баллов)
2. **Контрольная выборка**: 10% работ перепроверять вручную
3. **Логирование расхождений**: Накопление данных для дообучения
4. **Приоритет Qwen 3.5**: Использовать как baseline-модель
---

141
RESULTS.md Normal file
View file

@ -0,0 +1,141 @@
# Результаты тестирования
| Фамилия | Модель | Балл модели | Балл эксперта | OCR Acc. |
|---|---|---|---|---|
| aleksandr | openai/claude-opus-4-6 | 18 | 22 | - |
| aleksandr | openai/gpt-4o | 19 | 22 | - |
| aleksandr | openai/gpt-5-pro | 21 | 22 | - |
| aleksandr | qwen3.5-122b | 21 | 22 | - |
| andrey | openai/claude-opus-4-6 | 18 | 22 | - |
| andrey | openai/gpt-4o | 22 | 22 | - |
| andrey | openai/gpt-5-pro | 18 | 22 | - |
| andrey | qwen3.5-122b | 17 | 22 | - |
| andrey | qwen3.5-122b | 19 | 22 | - |
| ivan | openai/claude-opus-4-6 | 18 | 22 | - |
| ivan | openai/gpt-4o | 22 | 22 | - |
| ivan | openai/gpt-5-pro | 20 | 22 | - |
| ivan | qwen3.5-122b | 22 | 22 | - |
| ivan | unknown | 22 | 22 | - |
| student | openai/claude-opus-4-6 | 14 | 22 | - |
| student | openai/gpt-4o | 18 | 22 | - |
| student | openai/gpt-5-pro | 17 | 22 | - |
| student | qwen3.5-122b | 17 | 22 | - |
| student | qwen3.5-122b | 20 | 22 | - |
| Абдрахманова | openai/claude-opus-4-6 | 15 | 20 | 97.4 |
| Абдрахманова | openai/gpt-4o | 22 | 20 | 98.3 |
| Абдрахманова | openai/gpt-5-pro | 19 | 20 | 92.6 |
| Абдрахманова | qwen3.5-122b | 18 | 20 | 97.2 |
| Абдрахманова | qwen3.5-122b | 21 | 20 | 97.9 |
| Агупова | qwen3.5-122b | 18 | 21 | 68.6 |
| Алексеева | qwen3.5-122b | 17 | - | 98.1 |
| Афанасьев | qwen3.5-122b | 17 | 18 | 82.0 |
| Ахметьянова | qwen3.5-122b | 20 | 17 | 98.8 |
| Бакирова | openai/claude-opus-4-6 | 11 | 20 | 57.2 |
| Бакирова | openai/gpt-4o | 22 | 20 | 86.6 |
| Бакирова | openai/gpt-5-pro | 20 | 20 | 93.5 |
| Бакирова | qwen3.5-122b | 19 | 20 | 94.4 |
| Бакирова | qwen3.5-122b | 17 | 20 | 94.4 |
| Батанова | qwen3.5-122b | 19 | 21 | 74.0 |
| Винокурова | qwen3.5-122b | 18 | - | 85.5 |
| Володина | qwen3.5-122b | 19 | 19 | 97.9 |
| Вышегородский | qwen3.5-122b | 15 | 20 | - |
| Генжалиева | qwen3.5-122b | 17 | 19 | 98.0 |
| Городничий | openai/claude-opus-4-6 | 11 | 18 | - |
| Городничий | openai/gpt-4o | 22 | 18 | - |
| Городничий | openai/gpt-5-pro | 19 | 18 | - |
| Городничий | qwen3.5-122b | 0 | 18 | - |
| Долгова | openai/claude-opus-4-6 | 13 | 20 | 78.9 |
| Долгова | openai/gpt-4o | 22 | 20 | 96.1 |
| Долгова | openai/gpt-5-pro | 19 | 20 | 98.8 |
| Долгова | qwen3.5-122b | 22 | 20 | 99.4 |
| Елсуков | qwen3.5-122b | 14 | 19 | 99.8 |
| Ермолаева | qwen3.5-122b | 22 | 22 | 98.7 |
| Житенева | qwen3.5-122b | 17 | 19 | 93.6 |
| Журавлева | qwen3.5-122b | 18 | 21 | - |
| Завальная | qwen3.5-122b | 18 | 17 | - |
| Зинченко | qwen3.5-122b | 19 | 15 | - |
| Киселева | qwen3.5-122b | 17 | 19 | - |
| Коджесав | openai/claude-opus-4-6 | 13 | 15 | - |
| Коджесав | openai/gpt-4o | 22 | 15 | - |
| Коджесав | qwen3.5-122b | 14 | 15 | - |
| Козарь | qwen3.5-122b | 16 | 20 | - |
| Козлов | qwen3.5-122b | 14 | 19 | - |
| Козлов | qwen3.5-122b | 20 | 21 | - |
| Копысова | qwen3.5-122b | 15 | 20 | - |
| Кориков | unknown | 16 | 18 | - |
| Кузнецова | qwen3.5-122b | 18 | 19 | - |
| Куликова | qwen3.5-122b | 18 | 20 | - |
| Лазарева | openai/claude-opus-4-6 | 15 | 19 | - |
| Лазарева | openai/gpt-4o | 22 | 19 | - |
| Лазарева | openai/gpt-5-pro | 20 | 19 | - |
| Лазарева | qwen3.5-122b | 18 | 19 | - |
| Лазарева | qwen3.5-122b | 19 | 19 | - |
| Левкович | qwen3.5-122b | 21 | 20 | - |
| Леньшина | qwen3.5-122b | 18 | 19 | - |
| Липина | qwen3.5-122b | 19 | 20 | - |
| Литовская | qwen3.5-122b | 17 | 18 | - |
| Ломанов | qwen3.5-122b | 16 | 19 | - |
| Лукьянов | qwen3.5-122b | 20 | 16 | - |
| Мазур | qwen3.5-122b | 21 | 19 | - |
| Маргиева | qwen3.5-122b | 19 | 21 | - |
| Марченко | openai/claude-opus-4-6 | 11 | 18 | - |
| Марченко | openai/gpt-4o | 18 | 18 | - |
| Марченко | openai/gpt-5-pro | 17 | 18 | - |
| Марченко | qwen3.5-122b | 14 | 18 | - |
| Марченко | qwen3.5-122b | 15 | 18 | - |
| Маслова | openai/claude-opus-4-6 | 0 | 21 | - |
| Маслова | openai/gpt-4o | 18 | 21 | - |
| Маслова | qwen3.5-122b | 16 | 21 | - |
| Маслова | qwen3.5-122b | 15 | 21 | - |
| Мацаев | qwen3.5-122b | 20 | 19 | - |
| Меркульева | qwen3.5-122b | 16 | 19 | - |
| Минеева | qwen3.5-122b | 19 | 18 | - |
| Моисеева | qwen3.5-122b | 17 | 16 | - |
| Новикова | qwen3.5-122b | 18 | 17 | - |
| Новикова | qwen3.5-122b | 18 | 20 | - |
| Новокщенова | qwen3.5-122b | 19 | 18 | - |
| Нурузов | qwen3.5-122b | 15 | 16 | - |
| Оганян | qwen3.5-122b | 21 | 18 | - |
| Окунев | qwen3.5-122b | 18 | 20 | - |
| Оланцев | qwen3.5-122b | 19 | 20 | - |
| Онофриенко | qwen3.5-122b | 18 | 19 | - |
| Орехова | qwen3.5-122b | 19 | 19 | - |
| Панков | qwen3.5-122b | 20 | 19 | - |
| Парфенов | qwen3.5-122b | 16 | 16 | - |
| Пархоменко | qwen3.5-122b | 19 | 19 | - |
| Пекшев | qwen3.5-122b | 12 | 22 | - |
| Петраченко | qwen3.5-122b | 20 | 21 | - |
| Петренко | qwen3.5-122b | 15 | 20 | - |
| Петросян | qwen3.5-122b | 19 | 20 | - |
| Пирожков | qwen3.5-122b | 19 | 18 | - |
| Плугарева | qwen3.5-122b | 16 | 18 | - |
| Погосян | qwen3.5-122b | 18 | 21 | - |
| Политанская | qwen3.5-122b | 19 | 21 | - |
| Пономарева | qwen3.5-122b | 15 | 18 | - |
| Попов | qwen3.5-122b | 18 | 18 | - |
| Потапова | qwen3.5-122b | 22 | 22 | - |
| Пронина | qwen3.5-122b | 17 | 20 | - |
| Прохорова | qwen3.5-122b | 18 | 19 | - |
| Пушкина | qwen3.5-122b | 15 | 19 | - |
| Родин | qwen3.5-122b | 18 | 18 | - |
| Романов | qwen3.5-122b | 17 | 19 | - |
| Рыбина | qwen3.5-122b | 20 | 22 | - |
| Саакян | qwen3.5-122b | 20 | 18 | - |
| Савенкова | qwen3.5-122b | 11 | 19 | - |
| Савин | qwen3.5-122b | 15 | 18 | - |
| Сагайдак | qwen3.5-122b | 9 | 17 | - |
| Сагателова | qwen3.5-122b | 22 | 20 | - |
| Самарина | qwen3.5-122b | 16 | 19 | - |
| Самойлова | qwen3.5-122b | 14 | 18 | - |
| Сандракова | qwen3.5-122b | 12 | 20 | - |
| Семенюта | qwen3.5-122b | 18 | 18 | - |
| Семина | qwen3.5-122b | 21 | 22 | - |
| Серёгина | qwen3.5-122b | 17 | 22 | - |
| Сидяков | qwen3.5-122b | 17 | 18 | - |
| Табунщик | qwen3.5-122b | 19 | 20 | - |
| Тетюхин | qwen3.5-122b | 0 | 20 | - |
| Фетисова | qwen3.5-122b | 10 | 16 | - |
| Чеботарев | qwen3.5-122b | 19 | 19 | - |
| Шахбазян | qwen3.5-122b | 19 | 18 | - |
| Шувалова | qwen3.5-122b | 20 | 17 | - |
| Якушин | qwen3.5-122b | 18 | 19 | - |

View file

@ -1,203 +0,0 @@
import os
import base64
import asyncio
import time
import io
from dotenv import load_dotenv
from openai import AsyncOpenAI
import httpx
from PIL import Image
load_dotenv()
MODEL_NAME = "qwen3.5-122b"
# Таймаут уменьшен до 300 секунд (5 минут)
client = AsyncOpenAI(
api_key=os.getenv("QWEN_API_KEY"),
base_url=os.getenv("QWEN_BASE_URL"),
http_client=httpx.AsyncClient(timeout=httpx.Timeout(300.0, connect=60.0))
)
def encode_image(image_path):
"""Оптимизированная функция с агрессивным сжатием для TIFF"""
with Image.open(image_path) as img:
# Конвертация CMYK в RGB (часто в TIFF)
if img.mode == 'CMYK':
img = img.convert('RGB')
elif img.mode not in ('RGB', 'L'):
img = img.convert('RGB')
# Уменьшаем до 800px (достаточно для OCR)
img.thumbnail((1600, 1600))
buffer = io.BytesIO()
# Агрессивное сжатие для скорости
if image_path.lower().endswith(('.tif', '.tiff')):
# Для TIFF - максимальное сжатие
img.save(buffer, format='JPEG', quality=45,
optimize=True, progressive=False)
else:
# Для остальных - умеренное сжатие
img.save(buffer, format='JPEG', quality=80, optimize=True)
# Отладочная информация
size_mb = len(buffer.getvalue()) / (1024 * 1024)
print(f" [SIZE] {os.path.basename(image_path)}: {size_mb:.2f} MB")
return base64.b64encode(buffer.getvalue()).decode('utf-8')
def get_instructions():
with open("SKILL.md", "r", encoding="utf-8") as f:
skill = f.read()
c_path = os.path.join("references", "russian-essay-criteria.md")
with open(c_path, "r", encoding="utf-8") as f:
criteria = f.read()
return f"{skill}\n\n{criteria}"
async def process_priority_student(student, student_path, instructions):
source_path = os.path.join(student_path, "source.txt")
safe_model_name = MODEL_NAME.replace("/", "_")
output_filename = f"REPORT_{safe_model_name}_WITH_SOURCE.md"
output_path = os.path.join(student_path, output_filename)
if not os.path.exists(source_path):
print(f" [-] {student}: source.txt не найден. Пропуск.")
return False
# ✅ ПРОВЕРКА СУЩЕСТВУЮЩИХ ОТЧЕТОВ
existing_reports = []
for f in os.listdir(student_path):
if f.startswith("REPORT_") and safe_model_name in f:
existing_reports.append(f)
if existing_reports:
print(
f" [-] {student}: Отчет уже существует ({existing_reports[0]}). Пропуск.")
return False
allowed_ext = ('.jpg', '.jpeg', '.png', '.tif', '.tiff')
photos = sorted([f for f in os.listdir(student_path)
if f.lower().endswith(allowed_ext)])
if not photos:
print(f" [-] {student}: Нет фото. Пропуск.")
return False
print(f"\n{'-'*40}")
print(f"[START] {student.upper()} | Фото: {len(photos)} шт.")
print(f"{'-'*40}")
with open(source_path, "r", encoding="utf-8") as f:
source_text = f.read()
# Оригинальный промпт без изменений
prompt = (
"Распознай текст и проверь сочинение строго по критериям ФИПИ.\n"
"Используй предоставленный ИСХОДНЫЙ ТЕКСТ для сверки фактов и К1-К2.\n\n"
f"ИСХОДНЫЙ ТЕКСТ:\n{source_text}"
)
message_content = [{"type": "text", "text": prompt}]
# Ограничиваем количество фото до 5 для скорости
max_photos = 8
if len(photos) > max_photos:
print(f" [WARN] Много фото ({len(photos)}). Беру первые {max_photos}")
photos = photos[:max_photos]
for p in photos:
print(f" [LOG] Кодирую {p}...")
try:
encoded = encode_image(os.path.join(student_path, p))
message_content.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{encoded}"}
})
except Exception as e:
print(f" [ERR] Ошибка при кодировании {p}: {e}")
continue
try:
print(f" [WAIT] Запрос отправлен. Ждем полный разбор...")
start_api = time.time()
response = await client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": instructions},
{"role": "user", "content": message_content}
],
temperature=0.1,
)
res_text = response.choices[0].message.content
api_duration = round(time.time() - start_api, 1)
if not res_text or res_text.strip() == "":
print(
f" [!!!] Сервер вернул пустой ответ спустя {api_duration}с.")
res_text = f"ОШИБКА: Сервер прервал генерацию спустя {api_duration} секунд."
print(
f" [TIME] Получено! Время: {api_duration}с | Символов: {len(res_text)}")
with open(output_path, "w", encoding="utf-8") as f:
f.write(f"""---
**Ученик:** {student}
**Время API:** {api_duration}с
**Фото:** {len(photos)} шт.
---
{res_text}""")
print(f"[OK] Отчет готов.")
return True # ✅ Был запрос к API
except asyncio.TimeoutError:
print(f" [ERR] Таймаут запроса (>300 секунд)")
return True # ✅ Ошибка тоже считается за обработку
except Exception as e:
print(f" [ERR] Произошла ошибка: {str(e)}")
return True # ✅ Ошибка тоже считается за обработку
async def main():
base_dir = "photo"
if not os.path.exists(base_dir):
print(f"Ошибка: папка '{base_dir}' не найдена!")
return
instructions = get_instructions()
students = [d for d in os.listdir(base_dir)
if os.path.isdir(os.path.join(base_dir, d))]
if not students:
print(f"Нет папок с учениками в '{base_dir}'")
return
print(f"\nНайдено учеников: {len(students)}")
print("="*50)
for i, student in enumerate(students, 1):
print(f"\n[{i}/{len(students)}]")
was_processed = await process_priority_student(student, os.path.join(base_dir, student), instructions)
# ✅ ПАУЗА ТОЛЬКО ЕСЛИ БЫЛ ЗАПРОС К API
if i < len(students):
if was_processed:
print(f"\n[PAUSE] Жду 3 секунды...")
await asyncio.sleep(3)
else:
print(f" [SKIP] Пропущен, пауза не нужна")
print("\n" + "="*50)
print("ГОТОВО!")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -1,142 +0,0 @@
import os
import base64
import asyncio
import time
import io
from dotenv import load_dotenv
from openai import AsyncOpenAI # Переходим на АСИНХРОННЫЙ
import httpx
from PIL import Image
load_dotenv()
# Оставляем только Qwen, как ты и хотел
MODELS_TO_TEST = ["qwen3.5-122b"]
# Асинхронный клиент с запасом по таймауту
client = AsyncOpenAI(
api_key=os.getenv("QWEN_API_KEY"),
base_url=os.getenv("QWEN_BASE_URL"),
http_client=httpx.AsyncClient(timeout=httpx.Timeout(900.0, connect=60.0))
)
def encode_image(image_path):
"""Сжимает и кодирует изображение, чтобы избежать Connection Error"""
with Image.open(image_path) as img:
# Конвертируем в RGB если нужно
if img.mode not in ('RGB', 'L'):
img = img.convert('RGB')
# Уменьшаем размер до разумного (max 1600px по широкой стороне)
# Это критически важно для стабильности соединения!
img.thumbnail((1600, 1600))
buffer = io.BytesIO()
# Сжатие 85% - золотая середина
img.save(buffer, format='JPEG', quality=85)
return base64.b64encode(buffer.getvalue()).decode('utf-8')
def get_instructions(criteria_file):
with open("SKILL.md", "r", encoding="utf-8") as f:
skill = f.read()
c_path = os.path.join("references", criteria_file)
with open(c_path, "r", encoding="utf-8") as f:
criteria = f.read()
return f"{skill}\n\n{criteria}"
async def process_student(student, student_path, instructions):
"""Логика проверки одного студента"""
# Поиск фото
photos = sorted([f for f in os.listdir(student_path)
if f.lower().endswith(('.jpg', '.jpeg', '.png', '.tif', '.tiff'))])
if not photos:
return
# ПОИСК ИСХОДНОГО ТЕКСТА (source.txt)
source_text = ""
source_path = os.path.join(student_path, "source.txt")
if os.path.exists(source_path):
with open(source_path, "r", encoding="utf-8") as f:
source_text = f.read()
print(f"\n>>> РАБОТАЕМ С: {student.upper()} ({len(photos)} листа)")
prompt_text = "Распознай текст и проверь сочинение строго по критериям."
if source_text:
prompt_text += f"\n\nИСХОДНЫЙ ТЕКСТ ДЛЯ СВЕРКИ:\n{source_text}"
message_content = [{"type": "text", "text": prompt_text}]
# Кодируем фото (теперь со сжатием!)
for p in photos:
try:
b64 = encode_image(os.path.join(student_path, p))
message_content.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{b64}"}
})
except Exception as e:
print(f" [!] Ошибка обработки фото {p}: {e}")
for model_id in MODELS_TO_TEST:
safe_name = model_id.replace("/", "_")
output_file = os.path.join(student_path, f"REPORT_{safe_name}.md")
# 1. Формируем базовое имя, которое мы ХОТИМ создать
safe_name = model_id.replace("/", "_")
output_file = os.path.join(student_path, f"REPORT_{safe_name}.md")
# 2. УМНЫЙ ПОИСК: проверяем, есть ли ВООБЩЕ любой файл, начинающийся на REPORT_ и содержащий имя модели
existing_reports = [f for f in os.listdir(student_path)
if f.startswith("REPORT_") and safe_name in f]
if existing_reports:
print(
f" [-] {model_id}: Уже есть отчет ({existing_reports[0]}). Пропускаю.")
continue
print(f" [!] Запуск {model_id}...")
start_time = time.time()
try:
# Асинхронный вызов
response = await client.chat.completions.create(
model=model_id,
messages=[
{"role": "system", "content": instructions},
{"role": "user", "content": message_content}
],
temperature=0.0
)
res_text = response.choices[0].message.content
duration = round(time.time() - start_time, 1)
with open(output_file, "w", encoding="utf-8") as f:
header = f"--- \n**Ученик:** {student}\n**Модель:** {model_id}\n**Время:** {duration} сек.\n---\n\n"
f.write(header + res_text)
print(f" [OK] Готово! ({duration} сек.)")
await asyncio.sleep(10) # Пауза между запросами
except Exception as e:
print(f" [ERR] Ошибка у {model_id} для {student}: {str(e)}")
async def run_mass_check(base_dir="photo", criteria_file="russian-essay-criteria.md"):
students = [d for d in os.listdir(
base_dir) if os.path.isdir(os.path.join(base_dir, d))]
if not students:
return
instructions = get_instructions(criteria_file)
# Обрабатываем по одному, чтобы не перегружать канал и не ловить Connection Error
for student in students:
await process_student(student, os.path.join(base_dir, student), instructions)
if __name__ == "__main__":
asyncio.run(run_mass_check())