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