add README and RESULTS.md
This commit is contained in:
parent
dcc36f8f26
commit
b343b164dc
4 changed files with 234 additions and 345 deletions
93
README.md
Normal file
93
README.md
Normal 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
141
RESULTS.md
Normal 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 | - |
|
||||||
|
|
@ -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())
|
|
||||||
142
ege-checker.py
142
ege-checker.py
|
|
@ -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())
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue