diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f73967 --- /dev/null +++ b/README.md @@ -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-модель + +--- \ No newline at end of file diff --git a/RESULTS.md b/RESULTS.md new file mode 100644 index 0000000..bcdea67 --- /dev/null +++ b/RESULTS.md @@ -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 | - | \ No newline at end of file diff --git a/check-with-source.py b/check-with-source.py deleted file mode 100644 index 75e943a..0000000 --- a/check-with-source.py +++ /dev/null @@ -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()) diff --git a/ege-checker.py b/ege-checker.py deleted file mode 100644 index 13231ad..0000000 --- a/ege-checker.py +++ /dev/null @@ -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())