Compare commits

..

No commits in common. "master" and "feat-english" have entirely different histories.

18 changed files with 1701 additions and 1370 deletions

5
.gitignore vendored
View file

@ -1,4 +1 @@
.venv
.scans
ege-checker-local
ege-checker-local/*

194
REPORT.md
View file

@ -1,194 +0,0 @@
# EGE-Checker — Отчёт о проделанной работе
**Период:** март — май 2026
**Команда:** Лаборатория Лямбда 3.0, МАИ
---
## 1. Цель проекта
Скилл для автоматической проверки заданий ЕГЭ с выставлением баллов по официальным критериям ФИПИ 2026. Два модуля:
- **Модуль 1 — Сочинение (русский язык):** OCR рукописных бланков + оценка по критериям К1К10
- **Модуль 2 — Говорение (английский язык):** STT аудиозаписей через Whisper + оценка по 4 заданиям
Целевой сценарий использования: помощник для эксперта — модель проверяет работу параллельно с человеком, показывает своё мнение по каждому критерию с подробным разбором.
---
## 2. Архитектура и стек
### Стек
| Компонент | Инструмент |
|-----------|-----------|
| Агент | ZeroClaw (Rust) / OpenClaw |
| LLM | Qwen3.5-122B (основная), GPT-4o, Claude Opus (тестирование) |
| OCR рукописи | Vision-возможности LLM (Qwen Vision) |
| STT аудио | faster-whisper (модель medium, локально) |
| Интерфейс | Telegram, Matrix |
### Файловая структура скилла
```
ege-checker/
├── SKILL.md # Инструкции и логика агента
├── recognition.py # STT-модуль (faster-whisper)
└── references/
├── russian-essay-criteria.md # Критерии К1К10, ЕГЭ 2026
└── english-speaking-criteria.md # Критерии 4 заданий говорения, ЕГЭ 2026
```
### Схема работы (текущая)
```
Telegram / Matrix - сообщение
Claw-агент
Агент (Qwen3.5-122B) + SKILL.md
Ответ в Telegram
```
---
## 3. Реализованные компоненты
### 3.1. SKILL.md — логика агента
Описывает два режима работы для каждого модуля:
**Модуль 1 (сочинение):**
- Только сканы → распознать рукопись, сообщить количество слов, запросить исходный текст
- Сканы + исходный текст + тема → распознать + оценить К1К10 с объяснением снятий
**Модуль 2 (говорение):**
- Только аудио → транскрибировать через recognition.py, вывести текст
- Аудио + задание → транскрибировать + оценить по критериям
Поддержка нескольких аудиофайлов (каждое задание отдельно).
### 3.2. recognition.py — STT-модуль
Транскрибирует аудиофайлы с ответами ученика через faster-whisper. Ключевые возможности:
- Автовыбор устройства (CPU / CUDA)
- VAD-фильтрация тишины и пауз
- WHISPER_PROMPT с описанием формата ответов для точного распознавания
- Сегментация с таймкодами
- Возвращает `TranscriptResult`: текст, язык, длительность, сегменты
Поддерживаемые форматы: MP3, WAV, M4A, OGG, WEBM, FLAC.
### 3.3. Критерии ЕГЭ 2026
**Русский язык (К1К10), итого 22 балла:**
| К1 | К2 | К3 | К4 | К5 | К6 | К7 | К8 | К9 | К10 |
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:---:|
| 1 | 3 | 2 | 1 | 2 | 1 | 3 | 3 | 3 | 3 |
**Английский язык — Говорение (4 задания), итого 20 баллов:**
| Задание | Тип | Макс. баллов |
|---------|-----|:------------:|
| 1 | Чтение вслух | 1 |
| 2 | Диалог-расспрос (4 вопроса) | 4 |
| 3 | Диалог-интервью (5 ответов) | 5 |
| 4 | Монолог (3 критерия: содержание 4б + организация 3б + язык 3б) | 10 |
---
## 4. Тестирование
### 4.1. OCR + оценка сочинения (RESULTS.md)
Протестировано на реальных работах учеников с известными баллами экспертов.
**Сводка по моделям:**
| Модель | Характеристика |
|--------|---------------|
| **qwen3.5-122b** | Основная рабочая модель. Хорошее качество, часть работ — точное совпадение с экспертом (±01 балл) |
| **gpt-4o** | Минимальная предвзятость, на ряде работ точнее qwen. Лучший результат: 22/22 |
| **gpt-5-pro** | Стабильно высокое качество, несколько работ — 2122/22 |
| **claude-opus-4-6** | Системно занижает оценки, закончились токены до завершения тестирования |
**Примеры результатов (выборка):**
| Балл эксперта | Лучший балл модели | Лучшая модель |
|:-------------:|:-----------------:|---------------|
| 22 | 22 | qwen3.5-122b |
| 22 | 22 | qwen3.5-122b |
| 20 | 22 | qwen3.5-122b |
| 22 | 22 | gpt-4o |
| 22 | 22 | gpt-4o / qwen |
Типичное расхождение: ±13 балла. На сложных работах с плохим OCR (низкий OCR Accuracy) расхождение возрастает.
**OCR Accuracy** измерена для части работ. Диапазон: 5799%. Низкая точность (Бакирова 57%, Агупова 68%) коррелирует с большим расхождением в оценке.
### 4.2. Оценка говорения (transcriptions-report.md)
Протестировано на 14 учениках из 4 регионов (Адыгея, Башкортостан, Бурятия, Алтай).
**Средние показатели по заданиям:**
| Метрика | Задание 1 | Задание 2 | Задание 3 | Задание 4 |
|---------|:---------:|:---------:|:---------:|:---------:|
| Avg True (эксперт) | 0.786 | 2.429 | 1.857 | 5.857 |
| Avg Pred (модель) | 0.357 | 2.286 | 1.357 | 7.000 |
| MAE | **0.429** | **0.143** | **0.500** | **1.143** |
**Выводы по заданиям:**
- **Задание 2** — наиболее точное (MAE 0.143). Объективный критерий (форма вопроса) хорошо распознаётся
- **Задание 1** — систематическое занижение (Avg Pred 0.357 vs True 0.786). Модель строже экспертов в оценке фонетики
- **Задание 3** — заметное занижение (MAE 0.500). Проблема: ответы ученика идут сплошным текстом, сложно разбить на 5 отдельных ответов
- **Задание 4** — систематическое завышение (Avg Pred 7.000 vs True 5.857). Модель щедрее оценивает монолог
---
## 5. Текущее состояние и открытые вопросы
### Работает
- ✅ recognition.py запускается локально в прокси, транскрипт передаётся агенту готовым
- ✅ Критерии ЕГЭ 2026 актуализированы (русский + говорение английский)
- ✅ SKILL.md описывает корректную логику для обоих модулей
- ✅ OCR + оценка сочинения протестированы на реальных работах
### Открытые вопросы
**Оценка говорения:**
- Задание 1 (чтение): систематическое занижение — нужна калибровка критерия фонетики
- Задание 3 (ответы): нужен метод сегментации сплошного транскрипта на отдельные ответы
- Задание 4 (монолог): систематическое завышение — нужны few-shot примеры для калибровки
**OCR:**
- Нет золотого стандарта для измерения точности OCR — нужна разметка эталонного датасета
- Qwen Vision может исправлять рукописные ошибки "на лету", что искажает результат OCR
**Инфраструктура:**
- Контекст задания (исходный текст из ФИПИ) не добавляется автоматически — нужна интеграция или ручная подача
- Тестирование говорения проводилось на небольшой выборке (14 человек, 4 региона) — нужно расширить
---
## 6. Соответствие дорожной карте
| Фаза | Срок по плану | Статус |
|------|:-------------:|--------|
| Ф1: Базовый прототип | 23 марта — 5 апреля | ✅ Выполнено. Скилл работает на тестовом наборе |
| М1: Первые цифры расхождения | 5 апреля | ✅ RESULTS.md + transcriptions-report.md |
| Ф2: Улучшение распознавания | 619 апреля | ✅ Выполнено. OCR итерации проведены, STT настроен |
| М2: OCR >90%, STT >85% | 19 апреля | ✅ OCR: достигнут на чистых бланках. STT: выявлены систематические отклонения |
| Ф3: Стабильность оценки | 20 апреля — 10 мая | ✅ Выполнено. Корректировка критериев проверки |
| Ф4: Интеграция и финализация | 1124 мая | 🔄 В процессе. |
---
## 8. Дальнейшая работа
1. **Расширение тестовой выборки:** протестировать говорение на 30+ работах из разных регионов для статистически значимых выводов
2. **Интеграция с платформой:** после готовности agent-api от команды платформы — переехать с локального Claw на общего агента

BIN
ege-checker-roadmap.docx Normal file

Binary file not shown.

Binary file not shown.

View file

@ -5,154 +5,218 @@ description: >
Используй этот скилл когда пользователь:
- Загружает фото/скан рукописного сочинения ЕГЭ по русскому языку
- Просит проверить сочинение ЕГЭ, выставить баллы, оценить работу
- Загружает аудиозапись ответов по разделу "Говорение" ЕГЭ по английскому
- Упоминает "ЕГЭ", "сочинение", "критерии К1-К12", "говорение английский"
- Загружает аудиозапись или текст ответов по разделу "Аудирование" ЕГЭ по английскому
- Упоминает "ЕГЭ", "сочинение", "критерии К1-К12", "аудирование английский"
- Просит разобрать ошибки в сочинении по критериям ФИПИ
Скилл умеет: OCR рукописного текста через vision, STT аудиозаписей через faster-whisper,
оценку по критериям ФИПИ 2026. Работает с любой multimodal LLM (Qwen, GPT-4o и др.).
Скилл умеет: OCR рукописного текста через vision (multimodal), оценку по критериям, подробные комментарии. Работает с любой multimodal LLM (Qwen, GPT-4o и др.).
---
# ЕГЭ-Checker
## Обзор возможностей
| Модуль | Вход — только распознавание | Вход — распознавание + оценка |
|--------|----------------------------|-------------------------------|
| Сочинение (русский) | Сканы бланков | Сканы + исходный текст + тема |
| Говорение (английский) | Аудиозапись | Аудиозапись + задания (текст, ключевые фразы, вопросы, план, фотографии) |
| Модуль | Статус | Вход | Выход |
|--------|--------|------|-------|
| Сочинение (русский) | ✅ MVP | Фото рукописи или текст | Баллы К1К12 + комментарии |
| Аудирование (английский) | ✅ MVP | Аудиофайл + ключи | Баллы по заданиям |
---
## Модуль 1: Сочинение ЕГЭ по русскому языку
### Логика запуска
### Что получает агент от проверяющего
Проверяющий присылает в одном сообщении:
1. **Сканы бланков** — фото рукописного сочинения (один или несколько листов)
2. **Исходный текст задания** — публицистический/художественный текст, по которому написано сочинение
3. **Тему/формулировку задания** — если есть отдельно (иногда тема вытекает из текста)
**Если прислали только сканы бланков (без исходного текста и темы):**
→ Только распознать рукопись и вывести текст. Оценку не выставлять.
→ Сообщить: "Текст распознан. Чтобы выставить баллы по критериям, пришлите также исходный текст задания и тему сочинения."
Без исходного текста невозможно проверить К1, К2, К3 и К12. Если текст не прислан — попроси его перед началом проверки.
**Если прислали сканы + исходный текст задания + тему:**
→ Распознать рукопись, затем выставить баллы по К1К10 с объяснением снятий.
---
### Режим 1: Только распознавание
### Шаг 1 — Получить текст сочинения
**Если загружено фото/скан:**
1. Внимательно рассмотри все изображения — бланки могут быть на нескольких листах
2. Распознай рукописный текст максимально точно, сохраняя абзацное деление
3. Зачёркнутые слова отмечай как ~~зачёркнуто~~
4. Выведи распознанный текст и сообщи количество слов
5. Уточни: "Если есть неточности — поправьте. Чтобы выставить баллы, пришлите исходный текст задания."
3. Выведи распознанный текст и попроси подтвердить: *"Вот что распознано. Если есть неточности — поправьте перед проверкой."*
4. Дождись подтверждения или правок
---
**Если текст вставлен напрямую:**
Переходи к Шагу 2 без распознавания.
### Режим 2: Распознавание + оценка по критериям
### Шаг 2 — Загрузить критерии
**Шаг 1 — Распознать текст** (как в режиме 1, шаги 14)
Попроси подтвердить распознанный текст перед оценкой.
Прочитай файл `references/russian-essay-criteria.md` — там полные критерии К1К12 с баллами и примерами.
**Шаг 2 — Загрузить критерии**
Прочитай `references/russian-essay-criteria.md` — критерии К1К10 с баллами (ЕГЭ 2026).
### Шаг 3 — Оценить по каждому критерию
**Шаг 3 — Проверить предварительные условия**
- Подсчитай слова. Если менее 150 → все критерии = 0, сообщи об этом явно.
- Если К1 = 0 → К2 и К3 автоматически = 0.
**Шаг 4 — Оценить по каждому критерию К1К10 последовательно**
Для каждого критерия:
Пройдись по всем критериям К1К12 **последовательно**. Для каждого:
- Процитируй конкретный фрагмент из сочинения (если уместно)
- Объясни снятие баллов — что именно не выполнено и почему
- Объясни своё решение
- Выставь балл
**Шаг 5 — Итоговый вывод**
**Важные правила:**
- Если К1 = 0 (нет формулировки проблемы) → К2, К3, К4 автоматически = 0
- Если работа ≤ 70 слов → все баллы = 0, только К10 может быть ненулевым
- Считай слова перед проверкой (служебные части речи считаются)
### Шаг 4 — Итоговый вывод
Используй этот формат вывода:
```
## Результаты проверки сочинения ЕГЭ (2026)
## Результаты проверки сочинения ЕГЭ
### Распознанный текст
[текст если было фото, иначе пропусти]
### Количество слов: XX
---
### Оценка по критериям
| Критерий | Название | Балл | Макс |
|----------|----------|------|------|
| К1 | Позиция автора | X | 1 |
| К2 | Комментарий к позиции автора | X | 3 |
| К3 | Собственное отношение + аргумент | X | 2 |
| К4 | Фактическая точность | X | 1 |
| К5 | Логичность | X | 2 |
| К6 | Этические нормы | X | 1 |
| К1 | Формулировка проблем исходного текста | X | 1 |
| К2 | Комментарий к проблеме | X | 6 |
| К3 | Отражение позиции автора | X | 1 |
| К4 | Отношение к позиции автора | X | 1 |
| К5 | Смысловая цельность, связность | X | 2 |
| К6 | Точность и выразительность речи | X | 2 |
| К7 | Орфография | X | 3 |
| К8 | Пунктуация | X | 3 |
| К9 | Грамматика | X | 3 |
| К10 | Речевые нормы | X | 3 |
| **ИТОГО** | | **XX** | **22** |
| К9 | Языковые нормы | X | 2 |
| К10 | Речевые нормы | X | 2 |
| К11 | Этические нормы | X | 1 |
| К12 | Фактическая точность | X | 1 |
| **ИТОГО** | | **XX** | **25** |
---
### Подробные комментарии
**К1 — [балл/1]**
[что именно засчитано или почему снято]
[объяснение]
**К2 — [балл/3]**
[разбор с цитатами: есть ли 2 примера, пояснения, смысловая связь]
**К2 — [балл/6]**
[объяснение с цитатами из текста]
...и так далее по каждому критерию...
... и так далее по каждому критерию ...
### Снятия баллов (итого)
- К2: -1 — смысловая связь между примерами есть, но не пояснена
- К7: -1 — ошибка в слове "..."
...
---
### Главные рекомендации
1. [самое важное]
1. [самое важное для улучшения]
2. ...
```
---
## Модуль 2: Говорение ЕГЭ по английскому
## Модуль 2: Аудирование ЕГЭ по английскому
### Логика запуска
### Что получает агент от проверяющего
Проверяющий присылает в одном сообщении:
1. **Аудиозапись** — файл с устными ответами ученика (MP3, WAV, M4A и т.д.)
2. **Правильные ответы (ключи)** — текстом или фото бланка с ключами
**Если прислали только аудиозапись:**
→ Прослушай аудиозапись и выведи транскрипт
### Шаг 1 — Сохранить аудиофайл во временную директорию
**Если прислали аудиозапись и задания:**
→ Прослушай аудиозапись и проверь ответ ученика по критериям
Сохрани полученный аудиофайл во временный путь `/tmp/ege_audio_<timestamp>.<ext>`.
**Все задания присылаются отдельными аудиофайлами - их может быть несколько, может быть 1 - это нормально**
**Если прислали не все задания - проверь то, что прислали, и ничего больше**
```python
import os, time
---
**Шаг 1 — Запустить recognition.py**
```bash
python3 ~/.zeroclaw/workspace/skills/ege-checker/recognition.py <tmp_path>
ext = os.path.splitext(original_filename)[1] or ".mp3"
tmp_path = f"/tmp/ege_audio_{int(time.time())}{ext}"
with open(tmp_path, "wb") as f:
f.write(audio_bytes)
```
**Шаг 2 - Загрузить критерии**
### Шаг 2 — Запустить recognition.py
Прочитай `references/english-speaking-criteria.md` — критерии 1-4 задания с баллами (ЕГЭ 2026).
**Через subprocess (рекомендуется):**
```bash
python3 ~/zeroclaw-bot/recognition.py {tmp_path} --output json
```
**Шаг 3 — Оценить задания по критериям**
**Через прямой импорт:**
```python
import sys
sys.path.insert(0, os.path.expanduser("~/zeroclaw-bot"))
from recognition import process_audio
Для каждого критерия:
- Объясни снятие баллов — что именно не выполнено и почему
- Выставь балл
transcript, answers = process_audio(tmp_path, model_size="medium", verbose=False)
recognition_json = {
"transcript": transcript.text,
"answers": answers.to_dict(),
"unrecognized": answers.unrecognized,
}
```
**Шаг 4 — Итоговый вывод**
Результат — JSON:
- `transcript` — полный текст того, что сказал ученик
- `answers.task1` — {"A": "3", "B": "1", ...}
- `answers.task2_9` — {"2": "1", "3": "2", ...}
- `answers.task10_18` — {"10": "2", "11": "1", ...}
- `answers.unrecognized` — задания, которые не удалось извлечь (засчитать как 0)
Список: задание - распознанный ответ - выставленный балл - обьяснение оценки
Если есть нераспознанные — явно отметить.
### Шаг 3 — Получить и распознать ключи
Если ключи пришли текстом — использовать напрямую.
Если пришло фото бланка с ключами — распознать через vision.
### Шаг 4 — Передать в LLM для сверки и выставления баллов
Сформируй промпт для LLM на основе результатов recognition.py.
Подставь реальные значения вместо плейсхолдеров:
```
Ты эксперт-проверяющий ЕГЭ по английскому (аудирование).
Транскрипт ответов ученика (распознан через Whisper):
[TRANSCRIPT]
Извлечённые ответы ученика:
- Задание 1 (A-F): [TASK1]
- Задания 2-9: [TASK2_9]
- Задания 10-18: [TASK10_18]
- Не распознаны (засчитать как 0): [UNRECOGNIZED]
Правильные ответы (ключи):
[KEYS]
Сверь ответы с ключами. 1 балл за совпадение, 0 — за несовпадение или отсутствие.
Верни строго JSON без markdown:
{"results":{"task1":{"A":true},"task2_9":{"2":true},"task10_18":{"10":false}},
"scores":{"task1":0,"task2_9":0,"task10_18":0,"total":0},
"errors":["Задание 1B: ученик ответил 5, верный ответ 1"]}
```
Читай `references/english-listening-criteria.md` для максимальных баллов каждого блока.
### Шаг 5 — Удалить временный файл
Обязательно удали аудиофайл после получения ответа от LLM:
```python
import os
if os.path.exists(tmp_path):
os.remove(tmp_path)
```
### Шаг 6 — Вывести результат пользователю
Три блока (Задание 1 / Задания 2-9 / Задания 10-18):
колонки — задание, ответ ученика, ключ, результат (✓/✗).
Итог: баллы по каждому блоку + общий итог из 23.
Если были нераспознанные задания — явно указать:
"Задания X, Y не были распознаны в аудио и засчитаны как неверные."
---
## Общие принципы
## Общие принципы проверки
- **Актуальность**: Критерии соответствуют ФИПИ ЕГЭ 2026
- **Строгость**: Придерживайся формулировок критериев точно
- **Снятия**: Всегда объясняй конкретно что именно не выполнено и почему снят балл
- **Пограничные случаи**: При сомнении — объясни оба варианта, выбери более обоснованный
- **Тон**: Конструктивный, учебный инструмент — не карательный
- **Совместимость**: Скилл работает с любым multimodal агентом (Qwen3, GPT-4o и др.)
- **Актуальность**: Критерии соответствуют демоверсии ФИПИ 20242025
- **Строгость**: Придерживайся формулировок критериев, не занижай и не завышай
- **Пограничные случаи**: При сомнении между баллами — объясни оба варианта и выбери более обоснованный
- **Тон**: Конструктивный, поддерживающий. Это учебный инструмент, не карательный
- **Совместимость**: Скилл написан без привязки к конкретной модели. Работает с любым multimodal агентом, поддерживающим vision и аудио (Qwen3, GPT-4o, и др.)

View file

@ -1,206 +0,0 @@
"""
recognition.py модуль распознавания аудиофайла с ответами ученика ЕГЭ (говорение, английский язык).
Зависимости:
pip install faster-whisper
"""
from __future__ import annotations
import re
import logging
from dataclasses import dataclass, field
from pathlib import Path
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Константы
# ---------------------------------------------------------------------------
# Модели faster-whisper по убыванию скорости / возрастанию качества:
# tiny, base, small, medium, large-v2, large-v3
DEFAULT_MODEL = "medium"
# Подсказка для Whisper — описывает формат ответов ученика.
WHISPER_PROMPT = (
"Student answers to EGE English speaking exam. "
"Task one: read aloud. "
"Task two: ask four direct questions. "
"Task three: answer five questions. "
"Task four: monologue - compare two photos. "
)
# ---------------------------------------------------------------------------
# Структуры данных
# ---------------------------------------------------------------------------
@dataclass
class TranscriptResult:
"""Результат транскрипции аудиофайла."""
text: str # Полный текст транскрипта
language: str # Определённый язык ("en")
duration_seconds: float # Длительность аудио в секундах
segments: list[dict] = field(default_factory=list) # Детальные сегменты с таймкодами
model_used: str = DEFAULT_MODEL
# ---------------------------------------------------------------------------
# Транскрипция
# ---------------------------------------------------------------------------
def transcribe(
audio_path: str | Path,
model_size: str = DEFAULT_MODEL,
device: str = "auto",
compute_type: str = "auto",
language: str = "en",
beam_size: int = 5,
) -> TranscriptResult:
"""
Транскрибирует аудиофайл с ответами ученика.
Args:
audio_path: Путь к аудиофайлу (MP3, WAV, M4A, OGG, WEBM, FLAC).
model_size: Размер модели Whisper: tiny/base/small/medium/large-v2/large-v3.
medium хороший баланс скорость/качество для ЕГЭ.
large-v3 максимальное качество, медленнее.
device: "auto" | "cpu" | "cuda". "auto" выберет GPU если доступен.
compute_type: "auto" | "int8" | "float16" | "float32".
"auto" подберёт оптимальный тип для устройства.
language: Язык аудио. "en" для ответов на английском.
beam_size: Ширина луча beam search. 5 стандарт, выше = точнее но медленнее.
Returns:
TranscriptResult с текстом, языком, длительностью и сегментами.
Raises:
FileNotFoundError: Если аудиофайл не найден.
RuntimeError: Если faster-whisper не установлен.
"""
try:
from faster_whisper import WhisperModel
except ImportError:
raise RuntimeError(
"faster-whisper не установлен. Установите: pip install faster-whisper"
)
audio_path = Path(audio_path)
if not audio_path.exists():
raise FileNotFoundError(f"Аудиофайл не найден: {audio_path}")
# Автовыбор устройства и типа вычислений
resolved_device, resolved_compute = _resolve_device(device, compute_type)
logger.info(
"Загрузка модели %s на %s (%s)...",
model_size, resolved_device, resolved_compute
)
model = WhisperModel(
model_size,
device=resolved_device,
compute_type=resolved_compute,
)
logger.info("Транскрибирую: %s", audio_path.name)
segments_gen, info = model.transcribe(
str(audio_path),
language=language,
beam_size=beam_size,
initial_prompt=WHISPER_PROMPT,
word_timestamps=False,
vad_filter=True, # Фильтрация тишины — полезно для записей с паузами
vad_parameters={
"min_silence_duration_ms": 500, # Паузы >0.5с считаются тишиной
"speech_pad_ms": 200,
},
)
# Материализуем генератор сегментов
segments = []
full_text_parts = []
for seg in segments_gen:
segments.append({
"start": round(seg.start, 2),
"end": round(seg.end, 2),
"text": seg.text.strip(),
})
full_text_parts.append(seg.text.strip())
full_text = " ".join(full_text_parts)
logger.info(
"Транскрипция завершена. Длительность: %.1f сек, слов ~%d",
info.duration, len(full_text.split())
)
return TranscriptResult(
text=full_text,
language=info.language,
duration_seconds=round(info.duration, 1),
segments=segments,
model_used=model_size,
)
def _resolve_device(device: str, compute_type: str) -> tuple[str, str]:
"""Определяет оптимальное устройство и тип вычислений."""
if device != "auto" and compute_type != "auto":
return device, compute_type
# Проверяем наличие CUDA
try:
from torch import cuda
has_cuda = cuda.is_available()
except ImportError:
has_cuda = False
if device == "auto":
device = "cuda" if has_cuda else "cpu"
if compute_type == "auto":
if device == "cuda":
compute_type = "float16" # GPU: float16 быстрее и точнее чем int8
else:
compute_type = "int8" # CPU: int8 значительно быстрее float32
return device, compute_type
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import argparse
import sys
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
parser = argparse.ArgumentParser(
description="Распознавание аудиоответов ЕГЭ (говорение, английский язык)"
)
parser.add_argument("audio", help="Путь к аудиофайлу")
parser.add_argument(
"--model", default=DEFAULT_MODEL,
choices=["tiny", "base", "small", "medium", "large-v2", "large-v3"],
help=f"Размер модели Whisper (по умолчанию: {DEFAULT_MODEL})"
)
parser.add_argument(
"--device", default="auto",
choices=["auto", "cpu", "cuda"],
help="Устройство для инференса (по умолчанию: auto)"
)
args = parser.parse_args()
try:
result = transcribe(args.audio, model_size=args.model, device=args.device)
print(result.text)
except (FileNotFoundError, RuntimeError) as e:
print(f"Ошибка: {e}", file=sys.stderr)
sys.exit(1)

View file

@ -0,0 +1,30 @@
# Критерии оценивания раздела "Аудирование" ЕГЭ по английскому языку
## Статус: заглушка — будет заполнено в следующей итерации
### Структура раздела (ЕГЭ 20242025)
**Задание 1** (B1) — установление соответствия: 6 высказываний → 7 утверждений
- 1 балл за каждое верное соответствие
- Максимум: 6 баллов
**Задания 29** (B2) — верно/неверно/не сказано (True/False/Not Stated)
- 1 балл за каждый верный ответ
- Максимум: 8 баллов
**Задания 1018** — краткий ответ/выбор из нескольких вариантов
- 1 балл за каждый верный ответ
- Максимум: 9 баллов
**Итого по разделу "Аудирование": 20 первичных баллов**
---
## STT интеграция (планируется)
Для обработки аудиозаписей потребуется:
- OpenAI Whisper API (рекомендуется для русской и английской речи)
- Или Deepgram (альтернатива)
- Эндпоинт: `https://api.openai.com/v1/audio/transcriptions`
- Модель: `whisper-1`
Временное решение для MVP: попросить пользователя ввести ответы текстом.

View file

@ -1,125 +0,0 @@
# Критерии оценивания раздела "Говорение" ЕГЭ по английскому языку
## Источник: ФИПИ, спецификация ЕГЭ 2026
---
## Структура раздела "Говорение"
4 задания, максимум — 20 первичных баллов.
| Задание | Тип | Макс. баллов |
|---------|-----|-------------|
| 1 | Чтение текста | 1 |
| 2 | Диалог-расспрос (вопросы) | 4 |
| 3 | Диалог-интервью (ответы) | 5 |
| 4 | Монологическое высказывание | 10 |
| | **Итого** | **20** |
---
## Задание 1 — Чтение текста (01 балл)
| Балл | Критерий |
|------|----------|
| 1 | Допускается не более 5 фонетических ошибок, в том числе 12 ошибки, искажающие смысл |
| 0 | Сделано более 5 фонетических ошибок, ИЛИ сделано 3 и более фонетические ошибки, искажающие смысл |
---
## Задание 2 — Диалог-расспрос: вопросы (4 вопроса × 01 балл = 0-4 баллов)
Ученик задаёт 4 вопроса по заданной теме.
| Балл | Критерий |
|------|----------|
| 1 | Вопрос по содержанию отвечает поставленной задаче; имеет правильную грамматическую форму прямого вопроса; возможные фонетические и лексические погрешности не затрудняют восприятия |
| 0 | Вопрос не задан, ИЛИ заданный вопрос по содержанию не отвечает поставленной задаче, И/ИЛИ не имеет правильной грамматической формы прямого вопроса, И/ИЛИ фонетические и лексические ошибки препятствуют коммуникации |
---
## Задание 3 — Диалог-интервью: ответы на вопросы интервьювера в виде текста (5 ответов × 01 балл = 0-5 баллов)
Ученик отвечает на 5 вопросов интервьюера в виде одного текста.
Для каждого вопроса сначала найди предложения в тексте, потом оценивай.
| Балл | Критерий |
|------|----------|
| 1 | Дан полный и точный ответ на запрос информации: 23 коммуникативно обусловленные фразы, в которых отсутствуют элементарные лексико-грамматические и/или фонетические ошибки |
| 0 | Ответ на вопрос не дан, ИЛИ содержание ответа не соответствует запросу информации, ИЛИ ответ содержит менее 2 фраз, ИЛИ в ответе имеются элементарные лексико-грамматические и/или фонетические ошибки (в том числе когда ответ носит характер набора слов) |
---
## Задание 4 — Монологическое высказывание (010 баллов по 3 критериям)
Тематическое монологическое высказывание высокого уровня с элементами описания и рассуждения.
### Критерий 1: Решение коммуникативной задачи / Содержание (04 балла)
| Балл | Критерий |
|------|----------|
| 4 | Коммуникативная задача выполнена полностью — содержание полно, точно и развёрнуто отражает все аспекты, указанные в задании (1215 фраз) |
| 3 | Коммуникативная задача выполнена в основном: 1 аспект не раскрыт (остальные раскрыты полно) ИЛИ 12 аспекта раскрыты неполно/неточно (1215 фраз) |
| 2 | Коммуникативная задача выполнена не полностью: 1 аспект не раскрыт и 1 раскрыт неполно/неточно ИЛИ 3 аспекта раскрыты неполно/неточно (1011 фраз) |
| 1 | Коммуникативная задача выполнена частично: 1 аспект не раскрыт и 2 раскрыты неполно/неточно, ИЛИ 2 аспекта не раскрыты (остальные раскрыты полно), ИЛИ все аспекты раскрыты неполно/неточно (89 фраз) |
| 0 | Коммуникативная задача выполнена менее чем на 50%: 3 или более аспекта не раскрыты, ИЛИ 2 аспекта не раскрыты и 1 и более раскрыты неполно/неточно, ИЛИ 1 аспект не раскрыт и остальные раскрыты неполно/неточно, ИЛИ объём высказывания — 7 и менее фраз |
### Критерий 2: Организация высказывания (03 балла)
| Балл | Критерий |
|------|----------|
| 3 | Высказывание логично; имеет завершённый характер (есть вступительная фраза с обращением к другу И заключительная фраза); средства логической связи используются правильно. Допускается 1 ошибка в логичности/средствах логической связи |
| 2 | Высказывание в основном логично и имеет достаточно завершённый характер (есть вступительная фраза с обращением к другу И заключительная фраза); имеются 23 ошибки в логичности/средствах логической связи |
| 1 | Высказывание не имеет завершённого характера: отсутствует вступительная ИЛИ заключительная фраза, И/ИЛИ имеются 45 ошибок в логичности/средствах логической связи |
| 0 | Высказывание не имеет завершённого характера: отсутствуют вступительная И заключительная фразы, И/ИЛИ имеются 6 и более ошибок в логичности/средствах логической связи |
### Критерий 3: Языковое оформление (03 балла)
| Балл | Критерий |
|------|----------|
| 3 | Словарный запас, грамматические структуры, фонетическое оформление соответствуют задаче. Допускается не более 3 негрубых лексико-грамматических ошибок И/ИЛИ не более 3 негрубых фонетических ошибок |
| 2 | Словарный запас, грамматика, фонетика в основном соответствуют задаче. Допускается не более 45 лексико-грамматических (из них не более 2 грубых) И/ИЛИ не более 45 фонетических ошибок (из них не более 2 грубых) |
| 1 | Языковое оформление частично соответствует задаче. Допускается не более 67 лексико-грамматических (из них не более 3 грубых) И/ИЛИ не более 67 фонетических ошибок (из них не более 3 грубых) |
| 0 | Понимание высказывания затруднено из-за многочисленных ошибок: 8 и более лексико-грамматических ошибок ИЛИ 4 и более грубых лексико-грамматических ошибок, И/ИЛИ 8 и более фонетических ошибок ИЛИ 4 и более грубых фонетических ошибок, ИЛИ ответ носит характер набора слов |
### Итоговая таблица задания 4
| Критерий | Макс. баллов |
|----------|-------------|
| 1. Решение коммуникативной задачи (содержание) | 4 |
| 2. Организация высказывания | 3 |
| 3. Языковое оформление | 3 |
| **Итого за задание 4** | **10** |
---
## Алгоритм проверки агентом
### Что нужно для оценки
1. Аудиозапись ответа ученика
2. Задание (текст для 1 задания, 4 ключевых фразы для 2 задания, 5 вопросов интервьювера для 3 задания, 2 фотографии и план монолога для 4 задания)
### Логика оценки по заданиям
**Задание 1 (чтение вслух):**
- Проверить правильность транскрипта и отсутствие фонетических, грамматических ошибок
- Вынести 0 или 1 балл с обоснованием
**Задание 2 (вопросы):**
- Оценить каждый из 4 вопросов отдельно (0 или 1)
- Проверить: соответствие теме + грамматическая форма прямого вопроса + понятность
**Задание 3 (ответы):**
- Оценить ответы ученика на вопросы - 5 ответов (0 или 1)
- Проверить: полнота (23 фразы) + соответствие вопросу + отсутствие грубых ошибок
**Задание 4 (монолог):**
- К1: посчитать аспекты из задания, проверить раскрытие каждого + подсчитать фразы
- К2: проверить наличие вступления с обращением, заключения, логических связок
- К3: посчитать лексико-грамматические и фонетические ошибки, разделить на грубые/негрубые
### Формат вывода
Для каждого задания: балл + краткое обоснование со ссылкой на критерий.
Итоговая таблица: задание | балл | макс.
Общий итог из 20.
При наличии ошибок — конкретные примеры из транскрипта.

View file

@ -1,19 +1,5 @@
# Критерии оценивания сочинения ЕГЭ по русскому языку
## Источник: ФИПИ, демоверсия ЕГЭ 2026 (задание 27)
---
## Формулировка задания 27
Напишите сочинение-рассуждение по проблеме, поставленной в исходном тексте.
Сформулируйте позицию автора (рассказчика) по указанной проблеме.
Прокомментируйте, как в тексте раскрывается эта позиция. Включите в комментарий два
примера-иллюстрации из текста, важные для понимания позиции автора, и поясните их.
Укажите и поясните смысловую связь между примерами-иллюстрациями.
Сформулируйте и обоснуйте своё отношение к позиции автора. Включите в обоснование
пример-аргумент (читательский, историко-культурный или жизненный опыт).
Объём — не менее 150 слов.
## Источник: ФИПИ, демоверсия 20242025
---
@ -21,151 +7,200 @@
### Подсчёт слов
- Считаются все слова, включая служебные (предлоги, союзы, частицы)
- Инициалы с фамилией = одно слово («М.Ю. Лермонтов» — одно слово)
- Цифры при подсчёте не учитываются («5 лет» — одно слово, «пять лет» — два)
- «всё-таки» — одно слово, «всё же» — два слова
- Если слов менее 150 → работа не засчитывается, все критерии = 0
- Если сочинение написано не по тексту → 0 баллов
- Если полный пересказ/переписывание исходного текста → 0 по К1К10
- Если слов **меньше 70** → все критерии = 0 (кроме К10)
- Если слов **70149** → рекомендуемый объём не выдержан, но работа проверяется
- Рекомендуемый объём: **150300 слов**
### Правило нулевого К1
Если К1 = 0 → К2 = 0, К3 = 0 автоматически
Если К1 = 0 (проблема не сформулирована или сформулирована неверно):
К2 = 0, К3 = 0, К4 = 0 **автоматически**
---
## I. Содержание сочинения
## К1 — Формулировка проблем исходного текста (01 балл)
### К1 — Отражение позиции автора (рассказчика) (01 балл)
**1 балл**: Экзаменуемый (в той или иной форме) верно сформулировал одну из проблем исходного текста. Фактических ошибок, связанных с пониманием и формулировкой проблемы, нет.
1 балл: Позиция автора по указанной проблеме сформулирована верно.
0 баллов: Позиция автора не сформулирована или сформулирована неверно.
**0 баллов**: Экзаменуемый не смог верно сформулировать ни одну из проблем исходного текста.
Важно (изменение 2026): проблема задана в формулировке задания. Ученик раскрывает
позицию АВТОРА по этой проблеме — не формулирует проблему самостоятельно.
**Подсказки для проверки:**
- Проблема должна быть сформулирована как вопрос или тезис
- Она должна соответствовать содержанию исходного текста
- Допустимы разные формулировки одной и той же проблемы
---
### К2 — Комментарий к позиции автора (03 балла)
## К2 — Комментарий к сформулированной проблеме исходного текста (06 баллов)
Структура: 2 примера-иллюстрации с пояснениями + смысловая связь с пояснением.
Структура: **2 примера-иллюстрации** из текста + **смысловая связь** между ними.
| Баллов | Условия |
|--------|---------|
| 3 | 2 примера с пояснениями + смысловая связь с пояснением |
| 2 | 2 примера с пояснениями + связь без пояснения ИЛИ связь не указана/неверна |
| 1 | 1 пример с пояснением |
| 0 | Пример без пояснения ИЛИ нет примеров ИЛИ пересказ ИЛИ цитирование большого фрагмента ИЛИ комментарий без опоры на текст |
| Баллы | Условия |
|-------|---------|
| 6 | Оба примера с пояснениями + связь с анализом |
| 5 | Оба примера с пояснениями + связь без анализа |
| 4 | Оба примера с пояснениями, связь не указана |
| 3 | Один пример с пояснением + связь |
| 2 | Оба примера без пояснений + связь |
| 1 | Один пример без пояснения |
| 0 | Примеры отсутствуют или текст пересказан |
Правила:
- Пример без пояснения не засчитывается
- Фактическая ошибка в комментарии учитывается по К4
- Не принимаются: комикс, аниме, манга, фанфик, графический роман, компьютерная игра
**Важно:**
- Пример-иллюстрация = конкретный фрагмент/факт из текста с объяснением его роли
- Смысловая связь: сравнение, противопоставление, причина-следствие, вывод и т.д.
- Пересказ без анализа = 0 баллов
- Фактические ошибки в комментарии снижают балл (1 за каждую, но не более 2)
---
### К3 — Собственное отношение к позиции автора (02 балла)
## К3 — Отражение позиции автора исходного текста (01 балл)
2 балла: Отношение сформулировано и обосновано + приведён пример-аргумент.
1 балл: Отношение обосновано, но аргумент не приведён.
ИЛИ Аргумент есть, но отношение формальное («Я согласен с автором»).
0 баллов: Только формальное согласие/несогласие без обоснования.
ИЛИ Отношение не сформулировано.
ИЛИ Не соответствует указанной проблеме.
**1 балл**: Позиция автора верно сформулирована. Фактических ошибок нет.
Источники аргумента: читательский, историко-культурный или жизненный опыт.
Не принимаются: комикс, аниме, манга, фанфик, графический роман, компьютерная игра.
**0 баллов**: Позиция автора не сформулирована или сформулирована неверно.
**Подсказки:**
- Позиция = ответ автора на поставленную проблему
- Должна быть явно выражена, не просто угадана
- Можно цитировать или пересказывать
---
## II. Речевое оформление
## К4 — Отношение к позиции автора по проблеме исходного текста (01 балл)
### К4 — Фактическая точность речи (01 балл)
**1 балл**: Экзаменуемый выразил своё отношение к позиции автора и обосновал его.
1 балл: Фактические ошибки отсутствуют.
0 баллов: Допущена 1 фактическая ошибка или более.
**0 баллов**: Экзаменуемый не выразил своё отношение или оно не обосновано.
Фактические ошибки: неверные имена/даты/названия, искажение содержания упоминаемых
текстов и событий, ошибки в именах реальных людей.
**Подсказки:**
- Формальное согласие/несогласие без обоснования = 0
- Обоснование: личный опыт, примеры из жизни/литературы, логические доводы
- Фраза "я согласен с автором" без аргумента = 0
---
### К5 — Логичность речи (02 балла)
## К5 — Смысловая цельность, речевая связность и последовательность изложения (02 балла)
2 балла: Логические ошибки отсутствуют.
1 балл: 12 логические ошибки.
0 баллов: 3 и более логические ошибки.
**2 балла**: Работа характеризуется смысловой цельностью, речевой связностью и последовательностью изложения. Логических ошибок нет. Абзацное членение присутствует.
Типичные: нарушение последовательности, противоречия, нарушение причинно-следственных
связей, отсутствие связи между абзацами.
**1 балл**: 12 логические ошибки ИЛИ нарушено абзацное членение.
**0 баллов**: 3+ логические ошибки ИЛИ отсутствует абзацное членение.
**Типичные логические ошибки:**
- Нарушение последовательности мысли
- Отсутствие связи между абзацами
- Повтор одной мысли разными словами
- Противоречия в тексте
---
### К6 — Соблюдение этических норм (01 балл)
## К6 — Точность и выразительность речи (02 балла)
1 балл: Этические ошибки отсутствуют.
0 баллов: Пропаганда экстремизма/фашизма/нетрадиционных ценностей, нецензурная брань,
материалы запрещённые среди несовершеннолетних, иностранные слова при наличии
общеупотребительных русских аналогов не из нормативных словарей.
**2 балла**: Работа характеризуется точностью выражения мысли, разнообразием грамматического строя речи.
**1 балл**: Работа характеризуется точностью выражения мысли, но однообразием грамматического строя речи ИЛИ выразительностью, но использованием неточных слов.
**0 баллов**: Точность и выразительность речи существенно снижены.
**Примечание:** К6 не может быть выше К10. Если К10 = 0, то К6 = 0.
---
## III. Грамотность
## К7 — Соблюдение орфографических норм (03 балла)
> Нормы К7К10 разработаны для сочинений от 150 слов.
> При объёме менее 150 слов — работа не засчитывается, все баллы = 0.
| Баллы | Количество ошибок |
|-------|-------------------|
| 3 | 0 ошибок |
| 2 | 1 ошибка |
| 1 | 23 ошибки |
| 0 | 4+ ошибки |
### К7 — Орфография (03 балла)
**Что считается ошибкой:**
- Неверное написание слова
- Слитное/раздельное/дефисное написание
- Прописные/строчные буквы
| Баллов | Ошибок |
|--------|--------|
| 3 | 0 |
| 2 | 12 |
| 1 | 34 |
| 0 | 5+ |
### К8 — Пунктуация (03 балла)
| Баллов | Ошибок |
|--------|--------|
| 3 | 0 |
| 2 | 12 |
| 1 | 34 |
| 0 | 5+ |
### К9 — Грамматика (03 балла)
| Баллов | Ошибок |
|--------|--------|
| 3 | 0 |
| 2 | 12 |
| 1 | 34 |
| 0 | 5+ |
Типичные: ошибки в управлении и согласовании, неправильное образование форм слова,
нарушение синтаксических норм.
### К10 — Речевые нормы (03 балла)
| Баллов | Ошибок |
|--------|--------|
| 3 | 0 |
| 2 | 12 |
| 1 | 34 |
| 0 | 5+ |
Типичные: тавтология, плеоназм, неуместное/несвойственное употребление слова.
**Что НЕ считается ошибкой:**
- Описки (если исправлены)
- Перенос слова
---
## Итоговая таблица (ЕГЭ 2026)
## К8 — Соблюдение пунктуационных норм (03 балла)
| К1 | К2 | К3 | К4 | К5 | К6 | К7 | К8 | К9 | К10 | Итого |
|----|----|----|----|----|----|----|----|----|-----|-------|
| 1 | 3 | 2 | 1 | 2 | 1 | 3 | 3 | 3 | 3 | 22 |
| Баллы | Количество ошибок |
|-------|-------------------|
| 3 | 0 ошибок |
| 2 | 12 ошибки |
| 1 | 34 ошибки |
| 0 | 5+ ошибок |
Изменения 2026 vs 2025:
- К2: максимум снижен с 6 до 3 баллов
- К3: максимум увеличен с 1 до 2 баллов
- К9, К10: максимум увеличен с 2 до 3 баллов каждый
- К11 (этика) и К12 (фактика) упразднены, включены в К5 и К6
- Итоговый максимум: 22 балла (было 25)
**Типичные ошибки:**
- Пропуск запятой при однородных членах
- Ошибки в сложных предложениях
- Неверное оформление прямой речи
---
## К9 — Соблюдение языковых норм (грамматика) (02 балла)
| Баллы | Количество ошибок |
|-------|-------------------|
| 2 | 0 ошибок |
| 1 | 12 ошибки |
| 0 | 3+ ошибки |
**Типичные грамматические ошибки:**
- Ошибки в согласовании (управлении)
- Неправильное образование форм слова
- Нарушение синтаксических норм
---
## К10 — Соблюдение речевых норм (02 балла)
| Баллы | Количество ошибок |
|-------|-------------------|
| 2 | 01 ошибка |
| 1 | 23 ошибки |
| 0 | 4+ ошибки |
**Типичные речевые ошибки:**
- Тавтология (повтор одного слова/однокоренных слов)
- Плеоназм (масло масляное)
- Неуместное употребление слова
- Употребление слова в несвойственном значении
---
## К11 — Соблюдение этических норм (01 балл)
**1 балл**: Этические ошибки отсутствуют.
**0 баллов**: Допущена 1+ этическая ошибка.
**Этические ошибки:**
- Грубость, оскорбления в адрес кого-либо
- Национальная/религиозная нетерпимость
- Речевая агрессия
---
## К12 — Соблюдение фактологической точности (01 балл)
**1 балл**: Фактические ошибки отсутствуют.
**0 баллов**: Допущена 1+ фактическая ошибка.
**Фактические ошибки:**
- Неверные имена, даты, названия произведений
- Искажение содержания упомянутых текстов
- Неверные факты о реальных событиях/людях
---
## Таблица максимальных баллов
| К1 | К2 | К3 | К4 | К5 | К6 | К7 | К8 | К9 | К10 | К11 | К12 | Итого |
|----|----|----|----|----|----|----|----|----|-----|-----|-----|-------|
| 1 | 6 | 1 | 1 | 2 | 2 | 3 | 3 | 2 | 2 | 1 | 1 | **25** |

View file

@ -0,0 +1,563 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ЕГЭ Английский — Аудирование</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f5f4f0;
--surface: #ffffff;
--border: #e0ddd6;
--border2: #c8c5bc;
--text: #1a1a18;
--muted: #6b6a65;
--accent: #2d5be3;
--ok: #1a7a45;
--ok-bg: #e8f5ee;
--err: #b92c2c;
--err-bg: #fdeaea;
--warn: #8a5c00;
--warn-bg: #fff8e6;
--radius: 10px;
--radius-sm: 6px;
}
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.6; }
.container { max-width: 860px; margin: 0 auto; padding: 2rem 1rem 4rem; }
h1 { font-size: 22px; font-weight: 600; margin-bottom: 4px; }
.subtitle { color: var(--muted); font-size: 13px; margin-bottom: 2rem; }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.25rem 1.5rem; margin-bottom: 1rem; }
.card-title { font-size: 13px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 1rem; }
label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 4px; }
input[type=text], input[type=password], textarea, select {
width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm);
font-size: 13px; font-family: inherit; background: var(--bg); color: var(--text);
outline: none; transition: border-color 0.15s;
}
input:focus, textarea:focus { border-color: var(--accent); }
textarea { resize: vertical; min-height: 80px; }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.row3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
.field { margin-bottom: 12px; }
button {
padding: 9px 18px; border-radius: var(--radius-sm); border: 1px solid var(--border2);
background: var(--surface); color: var(--text); font-size: 13px; font-family: inherit;
cursor: pointer; transition: all 0.15s;
}
button:hover { background: var(--bg); border-color: var(--accent); color: var(--accent); }
.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn-primary:hover { background: #1e4ac9; color: #fff; border-color: #1e4ac9; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.drop-zone {
border: 2px dashed var(--border2); border-radius: var(--radius); padding: 2rem;
text-align: center; cursor: pointer; color: var(--muted); font-size: 13px;
position: relative; transition: all 0.15s;
}
.drop-zone:hover, .drop-zone.over { border-color: var(--accent); color: var(--accent); background: #f0f4ff; }
.drop-zone input { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
.drop-zone .icon { font-size: 28px; margin-bottom: 8px; }
.file-info { margin-top: 10px; padding: 8px 12px; background: var(--ok-bg); border-radius: var(--radius-sm); font-size: 13px; color: var(--ok); }
.step { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 1rem; }
.step-num { width: 26px; height: 26px; min-width: 26px; border-radius: 50%; background: var(--accent); color: #fff; font-size: 12px; font-weight: 600; display: flex; align-items: center; justify-content: center; margin-top: 1px; }
.step-num.done { background: var(--ok); }
.step-num.pending { background: var(--border2); color: var(--muted); }
.step-body { flex: 1; }
.step-title { font-size: 14px; font-weight: 500; margin-bottom: 6px; }
.answers-grid { display: grid; gap: 6px; }
.answer-row { display: grid; grid-template-columns: 80px 1fr 1fr; gap: 8px; align-items: center; font-size: 13px; }
.answer-row .task-label { color: var(--muted); font-weight: 500; }
.answer-row input { padding: 5px 8px; }
.section-header { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; padding: 6px 0 4px; border-bottom: 1px solid var(--border); margin-bottom: 8px; display: grid; grid-template-columns: 80px 1fr 1fr; gap: 8px; }
.log { background: #1a1a18; color: #e8e6e0; border-radius: var(--radius); padding: 1rem; font-family: 'Menlo', 'Monaco', monospace; font-size: 12px; line-height: 1.8; max-height: 200px; overflow-y: auto; white-space: pre-wrap; }
.log .ok { color: #6ee7a0; }
.log .err { color: #f87171; }
.log .info { color: #93c5fd; }
.result-card { border-radius: var(--radius); overflow: hidden; margin-bottom: 1rem; }
.result-block { margin-bottom: 1.5rem; }
.result-block h3 { font-size: 14px; font-weight: 600; margin-bottom: 8px; }
table.check-table { width: 100%; border-collapse: collapse; font-size: 13px; }
table.check-table th { font-size: 11px; font-weight: 600; color: var(--muted); text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--border); }
table.check-table td { padding: 5px 8px; border-bottom: 1px solid var(--border); }
table.check-table tr:last-child td { border-bottom: none; }
.mark-ok { color: var(--ok); font-weight: 600; }
.mark-err { color: var(--err); font-weight: 600; }
.score-pill { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; }
.score-pill.ok { background: var(--ok-bg); color: var(--ok); }
.score-pill.mid { background: var(--warn-bg); color: var(--warn); }
.score-pill.low { background: var(--err-bg); color: var(--err); }
.total-box { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.25rem; display: flex; align-items: center; justify-content: space-between; }
.total-score { font-size: 36px; font-weight: 700; }
.total-label { font-size: 13px; color: var(--muted); }
.errors-list { font-size: 13px; }
.error-item { padding: 6px 0; border-bottom: 1px solid var(--border); color: var(--err); }
.error-item:last-child { border-bottom: none; }
.transcript-box { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 1rem; font-size: 13px; line-height: 1.7; color: var(--text); max-height: 200px; overflow-y: auto; white-space: pre-wrap; }
.badge { display: inline-block; font-size: 11px; padding: 1px 7px; border-radius: 20px; }
.badge-ok { background: var(--ok-bg); color: var(--ok); }
.badge-err { background: var(--err-bg); color: var(--err); }
.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid rgba(255,255,255,0.3); border-top-color: #fff; border-radius: 50%; animation: spin 0.7s linear infinite; vertical-align: middle; margin-right: 6px; }
@keyframes spin { to { transform: rotate(360deg); } }
#result-section { display: none; }
.divider { border: none; border-top: 1px solid var(--border); margin: 1.5rem 0; }
</style>
</head>
<body>
<div class="container">
<h1>ЕГЭ — Аудирование (Английский язык)</h1>
<p class="subtitle">Загрузи аудиозапись ответов ученика → получи транскрипт → сверь с ключами → баллы</p>
<!-- API Settings -->
<div class="card">
<div class="card-title">Настройки API</div>
<div class="row">
<div class="field">
<label>Whisper эндпоинт (STT)</label>
<input type="text" id="whisper-url" value="https://api.openai.com/v1/audio/transcriptions" placeholder="https://...">
</div>
<div class="field">
<label>LLM эндпоинт (проверка)</label>
<input type="text" id="llm-url" value="https://llm.lambda.coredump.ru/v1" placeholder="https://...">
</div>
</div>
<div class="row">
<div class="field">
<label>API Key (Whisper)</label>
<input type="password" id="whisper-key" placeholder="sk-...">
</div>
<div class="field">
<label>API Key (LLM)</label>
<input type="password" id="llm-key" placeholder="ключ если нужен">
</div>
</div>
<div class="field">
<label>Модель LLM</label>
<input type="text" id="llm-model" value="openai/Qwen3.5-122B-A10B" placeholder="имя модели">
</div>
</div>
<!-- Step 1: Audio -->
<div class="card">
<div class="step">
<div class="step-num" id="step1-num">1</div>
<div class="step-body">
<div class="step-title">Загрузи аудиозапись ответов ученика</div>
<div class="drop-zone" id="audio-drop">
<div class="icon">🎤</div>
<div>Нажми или перетащи аудиофайл</div>
<div style="font-size:11px;margin-top:4px;color:var(--muted)">MP3, WAV, M4A, OGG, WEBM — до 25 МБ</div>
<input type="file" id="audio-file" accept="audio/*" onchange="handleAudio(this.files[0])">
</div>
<div id="audio-info" style="display:none" class="file-info"></div>
</div>
</div>
</div>
<!-- Step 2: Keys -->
<div class="card">
<div class="step">
<div class="step-num pending" id="step2-num">2</div>
<div class="step-body">
<div class="step-title">Введи правильные ответы (ключи)</div>
<p style="font-size:12px;color:var(--muted);margin-bottom:12px">Заполни ключи для заданий, которые есть в варианте. Пустые задания игнорируются.</p>
<div class="section-header">
<span>Задание</span><span>Ключ (верный)</span><span>— пусто —</span>
</div>
<div style="font-size:12px;font-weight:600;color:var(--muted);margin:8px 0 4px">Задание 1 — Соответствие (AF → цифра 17)</div>
<div class="answers-grid" id="keys-b1">
<!-- A-F rows generated by JS -->
</div>
<div style="font-size:12px;font-weight:600;color:var(--muted);margin:14px 0 4px">Задания 29 — True(1) / False(2) / Not Stated(3)</div>
<div class="answers-grid" id="keys-b2">
<!-- 2-9 rows -->
</div>
<div style="font-size:12px;font-weight:600;color:var(--muted);margin:14px 0 4px">Задания 1018 — Выбор из 3 вариантов (1/2/3)</div>
<div class="answers-grid" id="keys-c1">
<!-- 10-18 rows -->
</div>
</div>
</div>
</div>
<!-- Run button -->
<div style="display:flex;gap:10px;margin-bottom:1.5rem">
<button class="btn-primary" id="run-btn" onclick="runCheck()" disabled>
Распознать и проверить →
</button>
<button onclick="resetAll()">Сбросить</button>
</div>
<!-- Log -->
<div id="log-section" style="display:none;margin-bottom:1rem">
<div class="log" id="log"></div>
</div>
<!-- Results -->
<div id="result-section">
<div class="card">
<div class="card-title">Транскрипт ответов ученика</div>
<div class="transcript-box" id="transcript-box"></div>
<div style="margin-top:8px">
<div class="card-title" style="margin-bottom:4px">Распознанные ответы</div>
<div class="transcript-box" id="parsed-answers-box" style="font-family:monospace"></div>
</div>
</div>
<div class="card" id="result-detail">
<div class="card-title">Детальная проверка</div>
<div class="result-block">
<h3>Задание 1 — Установление соответствия <span id="score-b1-pill" class="score-pill"></span></h3>
<table class="check-table">
<thead><tr><th>Высказывание</th><th>Ответ ученика</th><th>Ключ</th><th>Результат</th></tr></thead>
<tbody id="table-b1"></tbody>
</table>
</div>
<hr class="divider">
<div class="result-block">
<h3>Задания 29 — True / False / Not Stated <span id="score-b2-pill" class="score-pill"></span></h3>
<table class="check-table">
<thead><tr><th>Задание</th><th>Ответ ученика</th><th>Ключ</th><th>Результат</th></tr></thead>
<tbody id="table-b2"></tbody>
</table>
</div>
<hr class="divider">
<div class="result-block">
<h3>Задания 1018 — Выбор ответа <span id="score-c1-pill" class="score-pill"></span></h3>
<table class="check-table">
<thead><tr><th>Задание</th><th>Ответ ученика</th><th>Ключ</th><th>Результат</th></tr></thead>
<tbody id="table-c1"></tbody>
</table>
</div>
</div>
<div class="total-box card">
<div>
<div class="total-label">Итоговый балл</div>
<div class="total-score" id="total-score"></div>
<div class="total-label" id="total-label">из 23 первичных баллов</div>
</div>
<div style="text-align:right">
<div class="total-label" style="margin-bottom:4px">Ошибки</div>
<div id="errors-summary" class="errors-list"></div>
</div>
</div>
</div>
</div>
<script>
let audioFile = null;
let audioFileName = '';
// Build key input rows
function buildKeyRows() {
const b1 = document.getElementById('keys-b1');
['A','B','C','D','E','F'].forEach(letter => {
b1.innerHTML += `<div class="answer-row">
<span class="task-label">Задание 1${letter}</span>
<input type="text" id="key-b1-${letter}" maxlength="1" placeholder="17" style="width:60px">
<span></span>
</div>`;
});
const b2 = document.getElementById('keys-b2');
for (let i = 2; i <= 9; i++) {
b2.innerHTML += `<div class="answer-row">
<span class="task-label">Задание ${i}</span>
<input type="text" id="key-b2-${i}" maxlength="1" placeholder="1/2/3" style="width:60px">
<span></span>
</div>`;
}
const c1 = document.getElementById('keys-c1');
for (let i = 10; i <= 18; i++) {
c1.innerHTML += `<div class="answer-row">
<span class="task-label">Задание ${i}</span>
<input type="text" id="key-c1-${i}" maxlength="1" placeholder="1/2/3" style="width:60px">
<span></span>
</div>`;
}
}
function handleAudio(file) {
if (!file) return;
audioFile = file;
audioFileName = file.name;
const info = document.getElementById('audio-info');
info.style.display = 'block';
info.textContent = `✓ ${file.name} (${(file.size/1024/1024).toFixed(1)} МБ)`;
document.getElementById('step1-num').classList.add('done');
document.getElementById('step2-num').classList.remove('pending');
document.getElementById('run-btn').disabled = false;
}
// Drag & drop
const dropZone = document.getElementById('audio-drop');
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('over'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('over'));
dropZone.addEventListener('drop', e => {
e.preventDefault(); dropZone.classList.remove('over');
const f = e.dataTransfer.files[0];
if (f) { document.getElementById('audio-file').files; handleAudio(f); }
});
function log(msg, type='') {
const el = document.getElementById('log');
document.getElementById('log-section').style.display = '';
const span = document.createElement('span');
span.className = type;
span.textContent = msg + '\n';
el.appendChild(span);
el.scrollTop = el.scrollHeight;
}
function getKeys() {
const b1 = {};
['A','B','C','D','E','F'].forEach(l => {
const v = document.getElementById(`key-b1-${l}`).value.trim();
if (v) b1[l] = v;
});
const b2 = {};
for (let i = 2; i <= 9; i++) {
const v = document.getElementById(`key-b2-${i}`).value.trim();
if (v) b2[i] = v;
}
const c1 = {};
for (let i = 10; i <= 18; i++) {
const v = document.getElementById(`key-c1-${i}`).value.trim();
if (v) c1[i] = v;
}
return { b1, b2, c1 };
}
async function transcribeAudio(file) {
const url = document.getElementById('whisper-url').value.trim();
const key = document.getElementById('whisper-key').value.trim();
const formData = new FormData();
formData.append('file', file, file.name);
formData.append('model', 'whisper-1');
formData.append('language', 'en');
formData.append('prompt', 'Student answers to English listening exam: task 1 matching A B C D E F with numbers 1-7, tasks 2-9 True False Not Stated as 1 2 or 3, tasks 10-18 multiple choice 1 2 or 3.');
const headers = {};
if (key) headers['Authorization'] = 'Bearer ' + key;
const resp = await fetch(url, { method: 'POST', headers, body: formData });
if (!resp.ok) throw new Error('Whisper error: ' + resp.status + ' ' + await resp.text());
const data = await resp.json();
return data.text || '';
}
async function parseAnswersWithLLM(transcript, keys) {
const url = document.getElementById('llm-url').value.trim().replace(/\/$/, '');
const model = document.getElementById('llm-model').value.trim();
const key = document.getElementById('llm-key').value.trim();
const keysText = JSON.stringify(keys, null, 2);
const prompt = `You are grading a Russian EGE English listening exam.
The student recorded their answers verbally. Here is the transcript:
"""
${transcript}
"""
Extract the student's answers for each task. The exam structure:
- Task 1 (matching): letters A,B,C,D,E,F each matched to a number 1-7
- Tasks 2-9 (True/False/Not Stated): answers are 1 (True), 2 (False), or 3 (Not Stated)
- Tasks 10-18 (multiple choice): answers are 1, 2, or 3
The teacher's answer key is:
${keysText}
Now compare student answers to the key and return ONLY valid JSON, no markdown:
{
"transcript_clean": "cleaned up version of student answers",
"student_answers": {
"b1": {"A":"3","B":"1","C":"5","D":"7","E":"2","F":"4"},
"b2": {"2":"1","3":"2","4":"1","5":"3","6":"2","7":"1","8":"3","9":"2"},
"c1": {"10":"2","11":"1","12":"3","13":"1","14":"2","15":"3","16":"1","17":"2","18":"1"}
},
"results": {
"b1": {"A":true,"B":false,"C":true,"D":true,"E":true,"F":true},
"b2": {"2":true,"3":false,"4":true,"5":true,"6":false,"7":true,"8":true,"9":true},
"c1": {"10":true,"11":true,"12":false,"13":true,"14":true,"15":true,"16":true,"17":false,"18":true}
},
"scores": {
"b1": 5,
"b2": 6,
"c1": 7,
"total": 18
},
"errors": [
"Задание 1B: ученик ответил 5, верный ответ 1",
"Задание 6: ученик ответил 1, верный ответ 2"
]
}
Only include tasks that have keys provided. If student answer is unclear, mark it as "?" and count as wrong.`;
const headers = { 'Content-Type': 'application/json' };
if (key) headers['Authorization'] = 'Bearer ' + key;
const resp = await fetch(url + '/chat/completions', {
method: 'POST',
headers,
body: JSON.stringify({
model,
max_tokens: 2000,
messages: [{ role: 'user', content: prompt }]
})
});
if (!resp.ok) throw new Error('LLM error: ' + resp.status + ' ' + await resp.text());
const data = await resp.json();
const raw = data.choices?.[0]?.message?.content || '';
const clean = raw.replace(/```json|```/g, '').trim();
return JSON.parse(clean);
}
function scorePill(score, max) {
const pct = score / max;
const cls = pct >= 0.8 ? 'ok' : pct >= 0.5 ? 'mid' : 'low';
return `<span class="score-pill ${cls}">${score} / ${max}</span>`;
}
function renderResults(data, keys) {
document.getElementById('result-section').style.display = '';
document.getElementById('transcript-box').textContent = data.transcript_clean || '(не удалось извлечь)';
// Show parsed answers
const sa = data.student_answers || {};
let parsedText = '';
if (sa.b1) parsedText += 'Задание 1: ' + Object.entries(sa.b1).map(([k,v]) => `${k}=${v}`).join(' ') + '\n';
if (sa.b2) parsedText += 'Задания 2-9: ' + Object.entries(sa.b2).map(([k,v]) => `${k}=${v}`).join(' ') + '\n';
if (sa.c1) parsedText += 'Задания 10-18: ' + Object.entries(sa.c1).map(([k,v]) => `${k}=${v}`).join(' ');
document.getElementById('parsed-answers-box').textContent = parsedText;
// B1 table
const b1res = data.results?.b1 || {};
const b1body = document.getElementById('table-b1');
b1body.innerHTML = '';
['A','B','C','D','E','F'].filter(l => keys.b1[l]).forEach(l => {
const ok = b1res[l];
const studentAns = sa.b1?.[l] ?? '?';
b1body.innerHTML += `<tr>
<td>${l}</td>
<td>${studentAns}</td>
<td>${keys.b1[l]}</td>
<td class="${ok ? 'mark-ok' : 'mark-err'}">${ok ? '✓' : '✗'}</td>
</tr>`;
});
const b1max = Object.keys(keys.b1).length;
document.getElementById('score-b1-pill').outerHTML = scorePill(data.scores?.b1 ?? 0, b1max);
// B2 table
const b2res = data.results?.b2 || {};
const b2body = document.getElementById('table-b2');
b2body.innerHTML = '';
for (let i = 2; i <= 9; i++) {
if (!keys.b2[i]) continue;
const ok = b2res[i];
const studentAns = sa.b2?.[i] ?? '?';
const labels = { '1': 'True', '2': 'False', '3': 'Not Stated' };
b2body.innerHTML += `<tr>
<td>${i}</td>
<td>${studentAns} ${labels[studentAns] ? `<span style="color:var(--muted);font-size:11px">(${labels[studentAns]})</span>` : ''}</td>
<td>${keys.b2[i]} <span style="color:var(--muted);font-size:11px">(${labels[keys.b2[i]] || ''})</span></td>
<td class="${ok ? 'mark-ok' : 'mark-err'}">${ok ? '✓' : '✗'}</td>
</tr>`;
}
const b2max = Object.keys(keys.b2).length;
document.getElementById('score-b2-pill').outerHTML = scorePill(data.scores?.b2 ?? 0, b2max);
// C1 table
const c1res = data.results?.c1 || {};
const c1body = document.getElementById('table-c1');
c1body.innerHTML = '';
for (let i = 10; i <= 18; i++) {
if (!keys.c1[i]) continue;
const ok = c1res[i];
const studentAns = sa.c1?.[i] ?? '?';
c1body.innerHTML += `<tr>
<td>${i}</td>
<td>${studentAns}</td>
<td>${keys.c1[i]}</td>
<td class="${ok ? 'mark-ok' : 'mark-err'}">${ok ? '✓' : '✗'}</td>
</tr>`;
}
const c1max = Object.keys(keys.c1).length;
document.getElementById('score-c1-pill').outerHTML = scorePill(data.scores?.c1 ?? 0, c1max);
// Total
const total = data.scores?.total ?? 0;
const maxTotal = b1max + b2max + c1max;
const totalEl = document.getElementById('total-score');
totalEl.textContent = total;
totalEl.style.color = total / maxTotal >= 0.8 ? 'var(--ok)' : total / maxTotal >= 0.5 ? 'var(--warn)' : 'var(--err)';
document.getElementById('total-label').textContent = `из ${maxTotal} первичных баллов`;
// Errors
const errList = document.getElementById('errors-summary');
if (data.errors?.length) {
errList.innerHTML = data.errors.map(e => `<div class="error-item">✗ ${e}</div>`).join('');
} else {
errList.innerHTML = '<span style="color:var(--ok)">Ошибок нет!</span>';
}
}
async function runCheck() {
if (!audioFile) return;
const keys = getKeys();
const btn = document.getElementById('run-btn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span>Обработка...';
document.getElementById('log-section').style.display = '';
document.getElementById('log').innerHTML = '';
document.getElementById('result-section').style.display = 'none';
try {
log('→ Отправляю аудио в Whisper...', 'info');
const transcript = await transcribeAudio(audioFile);
log('✓ Транскрипция получена: ' + transcript.substring(0, 80) + '...', 'ok');
log('→ Отправляю в LLM для разбора ответов и проверки...', 'info');
const result = await parseAnswersWithLLM(transcript, keys);
log('✓ Проверка завершена. Итог: ' + result.scores?.total + ' баллов', 'ok');
renderResults(result, keys);
} catch(e) {
log('✗ Ошибка: ' + e.message, 'err');
}
btn.disabled = false;
btn.innerHTML = 'Распознать и проверить →';
}
function resetAll() {
audioFile = null;
document.getElementById('audio-info').style.display = 'none';
document.getElementById('audio-file').value = '';
document.getElementById('run-btn').disabled = true;
document.getElementById('result-section').style.display = 'none';
document.getElementById('log-section').style.display = 'none';
document.getElementById('log').innerHTML = '';
document.getElementById('step1-num').classList.remove('done');
document.getElementById('step2-num').classList.add('pending');
}
buildKeyRows();
</script>
</body>
</html>

503
recognition.py Normal file
View file

@ -0,0 +1,503 @@
"""
recognition.py модуль распознавания аудиофайла с ответами ученика ЕГЭ (аудирование, английский язык).
Зависимости:
pip install faster-whisper
Использование:
from recognition import transcribe, extract_answers
# Полный pipeline: аудио -> транскрипт -> структурированные ответы
result = transcribe("student_answers.mp3")
answers = extract_answers(result.text)
print(answers)
"""
from __future__ import annotations
import re
import logging
from dataclasses import dataclass, field
from pathlib import Path
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Константы
# ---------------------------------------------------------------------------
# Модели faster-whisper по убыванию скорости / возрастанию качества:
# tiny, base, small, medium, large-v2, large-v3
DEFAULT_MODEL = "medium"
# Подсказка для Whisper — описывает формат ответов ученика.
# Критически важна для правильного распознавания "one/two/three" как цифр
# и "A equals 3" как ответа на задание 1.
WHISPER_PROMPT = (
"Student answers to EGE English listening exam. "
"Task one matching: speaker A answer three, speaker B answer one, "
"speaker C answer five, speaker D answer seven, speaker E answer two, speaker F answer four. "
"Tasks two through nine True False Not Stated: "
"task two true, task three false, task four not stated. "
"Tasks ten through eighteen multiple choice one two or three: "
"task ten two, task eleven one, task twelve three."
)
# ---------------------------------------------------------------------------
# Структуры данных
# ---------------------------------------------------------------------------
@dataclass
class TranscriptResult:
"""Результат транскрипции аудиофайла."""
text: str # Полный текст транскрипта
language: str # Определённый язык ("en")
duration_seconds: float # Длительность аудио в секундах
segments: list[dict] = field(default_factory=list) # Детальные сегменты с таймкодами
model_used: str = DEFAULT_MODEL
@dataclass
class StudentAnswers:
"""Структурированные ответы ученика, извлечённые из транскрипта."""
# Задание 1: соответствие AF → цифра 17
task1: dict[str, str] = field(default_factory=dict) # {"A": "3", "B": "1", ...}
# Задания 29: True(1) / False(2) / Not Stated(3)
task2_9: dict[int, str] = field(default_factory=dict) # {2: "1", 3: "2", ...}
# Задания 1018: выбор из вариантов 1/2/3
task10_18: dict[int, str] = field(default_factory=dict) # {10: "2", 11: "1", ...}
# Задания, которые не удалось распознать
unrecognized: list[str] = field(default_factory=list)
def to_dict(self) -> dict:
return {
"task1": self.task1,
"task2_9": {str(k): v for k, v in self.task2_9.items()},
"task10_18": {str(k): v for k, v in self.task10_18.items()},
"unrecognized": self.unrecognized,
}
def summary(self) -> str:
"""Читаемое представление для вывода агенту / в лог."""
lines = ["=== Распознанные ответы ученика ==="]
if self.task1:
lines.append("\nЗадание 1 (соответствие):")
for letter in "ABCDEF":
ans = self.task1.get(letter, "?")
lines.append(f" {letter}{ans}")
if self.task2_9:
lines.append("\nЗадания 29 (True/False/Not Stated):")
labels = {"1": "True", "2": "False", "3": "Not Stated"}
for task_num in range(2, 10):
ans = self.task2_9.get(task_num, "?")
label = labels.get(ans, ans)
lines.append(f" Задание {task_num}: {ans} ({label})")
if self.task10_18:
lines.append("\nЗадания 1018 (выбор):")
for task_num in range(10, 19):
ans = self.task10_18.get(task_num, "?")
lines.append(f" Задание {task_num}: {ans}")
if self.unrecognized:
lines.append(f"\nНе распознано: {', '.join(self.unrecognized)}")
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Транскрипция
# ---------------------------------------------------------------------------
def transcribe(
audio_path: str | Path,
model_size: str = DEFAULT_MODEL,
device: str = "auto",
compute_type: str = "auto",
language: str = "en",
beam_size: int = 5,
) -> TranscriptResult:
"""
Транскрибирует аудиофайл с ответами ученика.
Args:
audio_path: Путь к аудиофайлу (MP3, WAV, M4A, OGG, WEBM, FLAC).
model_size: Размер модели Whisper: tiny/base/small/medium/large-v2/large-v3.
medium хороший баланс скорость/качество для ЕГЭ.
large-v3 максимальное качество, медленнее.
device: "auto" | "cpu" | "cuda". "auto" выберет GPU если доступен.
compute_type: "auto" | "int8" | "float16" | "float32".
"auto" подберёт оптимальный тип для устройства.
language: Язык аудио. "en" для ответов на английском.
beam_size: Ширина луча beam search. 5 стандарт, выше = точнее но медленнее.
Returns:
TranscriptResult с текстом, языком, длительностью и сегментами.
Raises:
FileNotFoundError: Если аудиофайл не найден.
RuntimeError: Если faster-whisper не установлен.
"""
try:
from faster_whisper import WhisperModel
except ImportError:
raise RuntimeError(
"faster-whisper не установлен. Установите: pip install faster-whisper"
)
audio_path = Path(audio_path)
if not audio_path.exists():
raise FileNotFoundError(f"Аудиофайл не найден: {audio_path}")
# Автовыбор устройства и типа вычислений
resolved_device, resolved_compute = _resolve_device(device, compute_type)
logger.info(
"Загрузка модели %s на %s (%s)...",
model_size, resolved_device, resolved_compute
)
model = WhisperModel(
model_size,
device=resolved_device,
compute_type=resolved_compute,
)
logger.info("Транскрибирую: %s", audio_path.name)
segments_gen, info = model.transcribe(
str(audio_path),
language=language,
beam_size=beam_size,
initial_prompt=WHISPER_PROMPT,
word_timestamps=False,
vad_filter=True, # Фильтрация тишины — полезно для записей с паузами
vad_parameters={
"min_silence_duration_ms": 500, # Паузы >0.5с считаются тишиной
"speech_pad_ms": 200,
},
)
# Материализуем генератор сегментов
segments = []
full_text_parts = []
for seg in segments_gen:
segments.append({
"start": round(seg.start, 2),
"end": round(seg.end, 2),
"text": seg.text.strip(),
})
full_text_parts.append(seg.text.strip())
full_text = " ".join(full_text_parts)
logger.info(
"Транскрипция завершена. Длительность: %.1f сек, слов ~%d",
info.duration, len(full_text.split())
)
return TranscriptResult(
text=full_text,
language=info.language,
duration_seconds=round(info.duration, 1),
segments=segments,
model_used=model_size,
)
def _resolve_device(device: str, compute_type: str) -> tuple[str, str]:
"""Определяет оптимальное устройство и тип вычислений."""
if device != "auto" and compute_type != "auto":
return device, compute_type
# Проверяем наличие CUDA
try:
from torch import cuda
has_cuda = cuda.is_available()
except ImportError:
has_cuda = False
if device == "auto":
device = "cuda" if has_cuda else "cpu"
if compute_type == "auto":
if device == "cuda":
compute_type = "float16" # GPU: float16 быстрее и точнее чем int8
else:
compute_type = "int8" # CPU: int8 значительно быстрее float32
return device, compute_type
# ---------------------------------------------------------------------------
# Извлечение ответов из транскрипта
# ---------------------------------------------------------------------------
def extract_answers(transcript_text: str) -> StudentAnswers:
"""
Извлекает структурированные ответы из текста транскрипта.
Обрабатывает разные форматы речи ученика:
- "A three", "speaker A answer three", "A equals 3", "A — три"
- "task two true", "number two true", "two — true", "2 true"
- "task ten two", "ten — 2", "question ten answer two"
Args:
transcript_text: Текст транскрипта от Whisper.
Returns:
StudentAnswers со структурированными ответами.
"""
answers = StudentAnswers()
text = transcript_text.lower().strip()
_extract_task1(text, answers)
_extract_task2_9(text, answers)
_extract_task10_18(text, answers)
logger.debug("Извлечено ответов: task1=%d, task2_9=%d, task10_18=%d, нераспознано=%d",
len(answers.task1), len(answers.task2_9),
len(answers.task10_18), len(answers.unrecognized))
return answers
def _extract_task1(text: str, answers: StudentAnswers) -> None:
"""
Задание 1: соответствие AF цифра 17.
Примеры: "A three", "speaker A answer 3", "A equals three", "A — 3"
"""
# Числа словами → цифры
word_to_digit = {
"one": "1", "two": "2", "three": "3", "four": "4",
"five": "5", "six": "6", "seven": "7",
# На случай русских ответов
"один": "1", "два": "2", "три": "3", "четыре": "4",
"пять": "5", "шесть": "6", "семь": "7",
}
for letter in "abcdef":
# Паттерн: буква, затем необязательный разделитель, затем цифра/слово
pattern = (
rf"(?:speaker\s+)?{letter}"
rf"(?:\s+(?:answer|equals|is|—|-|:))?"
rf"\s+"
rf"({_digit_or_word_pattern(1, 7)})"
)
match = re.search(pattern, text)
if match:
raw = match.group(1).strip()
digit = word_to_digit.get(raw, raw) if not raw.isdigit() else raw
if digit in [str(i) for i in range(1, 8)]:
answers.task1[letter.upper()] = digit
else:
answers.unrecognized.append(f"1{letter.upper()}")
else:
answers.unrecognized.append(f"1{letter.upper()}")
def _extract_task2_9(text: str, answers: StudentAnswers) -> None:
"""
Задания 29: True(1) / False(2) / Not Stated(3).
Примеры: "task two true", "number 3 false", "four not stated", "5 — 2"
"""
tfs_map = {
"true": "1", "1": "1",
"false": "2", "2": "2",
"not stated": "3", "not_stated": "3", "3": "3",
}
num_words = {
"two": 2, "three": 3, "four": 4, "five": 5,
"six": 6, "seven": 7, "eight": 8, "nine": 9,
"2": 2, "3": 3, "4": 4, "5": 5,
"6": 6, "7": 7, "8": 8, "9": 9,
}
for word, num in num_words.items():
pattern = (
rf"(?:task|number|question|задание)?\s*{re.escape(word)}"
rf"(?:\s+(?:is|answer|—|-|:))?"
rf"\s+"
rf"(true|false|not\s+stated|not_stated|[123])"
)
match = re.search(pattern, text)
if match:
raw = match.group(1).strip().replace(" ", "_")
digit = tfs_map.get(raw) or tfs_map.get(raw.replace("_", " "))
if digit:
answers.task2_9[num] = digit
else:
answers.unrecognized.append(str(num))
else:
answers.unrecognized.append(str(num))
def _extract_task10_18(text: str, answers: StudentAnswers) -> None:
"""
Задания 1018: выбор из вариантов 1/2/3.
Примеры: "task ten two", "eleven — 1", "question 12 answer three"
"""
word_to_digit_choice = {
"one": "1", "two": "2", "three": "3",
"1": "1", "2": "2", "3": "3",
}
num_words = {
"ten": 10, "eleven": 11, "twelve": 12, "thirteen": 13,
"fourteen": 14, "fifteen": 15, "sixteen": 16,
"seventeen": 17, "eighteen": 18,
"10": 10, "11": 11, "12": 12, "13": 13,
"14": 14, "15": 15, "16": 16, "17": 17, "18": 18,
}
for word, num in num_words.items():
pattern = (
rf"(?:task|number|question|задание)?\s*{re.escape(word)}"
rf"(?:\s+(?:is|answer|—|-|:))?"
rf"\s+"
rf"({_digit_or_word_pattern(1, 3)})"
)
match = re.search(pattern, text)
if match:
raw = match.group(1).strip()
digit = word_to_digit_choice.get(raw)
if digit:
answers.task10_18[num] = digit
else:
answers.unrecognized.append(str(num))
else:
answers.unrecognized.append(str(num))
def _digit_or_word_pattern(min_val: int, max_val: int) -> str:
"""Строит regex-паттерн для диапазона цифр и их словесных форм."""
digits = [str(i) for i in range(min_val, max_val + 1)]
words = {
1: "one", 2: "two", 3: "three", 4: "four", 5: "five",
6: "six", 7: "seven", 8: "eight", 9: "nine", 10: "ten",
11: "eleven", 12: "twelve", 13: "thirteen", 14: "fourteen",
15: "fifteen", 16: "sixteen", 17: "seventeen", 18: "eighteen",
}
options = digits + [words[i] for i in range(min_val, max_val + 1) if i in words]
return "(" + "|".join(sorted(options, key=len, reverse=True)) + ")"
# ---------------------------------------------------------------------------
# Удобный pipeline
# ---------------------------------------------------------------------------
def process_audio(
audio_path: str | Path,
model_size: str = DEFAULT_MODEL,
device: str = "auto",
verbose: bool = False,
) -> tuple[TranscriptResult, StudentAnswers]:
"""
Полный pipeline: аудиофайл транскрипт структурированные ответы.
Args:
audio_path: Путь к аудиофайлу.
model_size: Размер модели Whisper (medium по умолчанию).
device: "auto" | "cpu" | "cuda".
verbose: Если True выводить промежуточные результаты в stdout.
Returns:
Кортеж (TranscriptResult, StudentAnswers).
Example:
result, answers = process_audio("student.mp3")
print(answers.summary())
# Передать answers.to_dict() агенту для сверки с ключами
"""
if verbose:
print(f"[1/2] Транскрибирую {Path(audio_path).name} (модель: {model_size})...")
transcript = transcribe(audio_path, model_size=model_size, device=device)
if verbose:
print(f" Длительность: {transcript.duration_seconds} сек")
print(f" Транскрипт: {transcript.text[:120]}...")
print("[2/2] Извлекаю ответы...")
answers = extract_answers(transcript.text)
if verbose:
print(answers.summary())
return transcript, answers
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import argparse
import json
import sys
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
parser = argparse.ArgumentParser(
description="Распознавание аудиоответов ЕГЭ (аудирование, английский язык)"
)
parser.add_argument("audio", help="Путь к аудиофайлу")
parser.add_argument(
"--model", default=DEFAULT_MODEL,
choices=["tiny", "base", "small", "medium", "large-v2", "large-v3"],
help=f"Размер модели Whisper (по умолчанию: {DEFAULT_MODEL})"
)
parser.add_argument(
"--device", default="auto",
choices=["auto", "cpu", "cuda"],
help="Устройство для инференса (по умолчанию: auto)"
)
parser.add_argument(
"--output", choices=["summary", "json", "transcript"],
default="summary",
help="Формат вывода: summary (читаемый), json (машинный), transcript (сырой текст)"
)
parser.add_argument(
"--transcript-only", action="store_true",
help="Только транскрипция без извлечения ответов"
)
args = parser.parse_args()
try:
if args.transcript_only or args.output == "transcript":
result = transcribe(args.audio, model_size=args.model, device=args.device)
print(result.text)
else:
result, answers = process_audio(
args.audio,
model_size=args.model,
device=args.device,
verbose=(args.output == "summary"),
)
if args.output == "json":
output = {
"transcript": result.text,
"language": result.language,
"duration_seconds": result.duration_seconds,
"model_used": result.model_used,
"answers": answers.to_dict(),
}
print(json.dumps(output, ensure_ascii=False, indent=2))
elif args.output == "summary":
# verbose=True уже вывел всё в process_audio
pass
except (FileNotFoundError, RuntimeError) as e:
print(f"Ошибка: {e}", file=sys.stderr)
sys.exit(1)

View file

@ -1,2 +1,2 @@
faster-whisper==1.2.1
torch==2.11.0
faster-whisper==1.2.1
torch==2.11.0

View file

@ -0,0 +1,300 @@
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-sans); }
.wrap { padding: 1rem 0; }
.section { background: var(--color-background-primary); border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-lg); padding: 1rem 1.25rem; margin-bottom: 12px; }
.label { font-size: 12px; color: var(--color-text-secondary); margin-bottom: 6px; }
.row { display: flex; gap: 8px; align-items: center; }
input[type=text], input[type=password] { flex: 1; font-size: 13px; }
.drop-zone { border: 1px dashed var(--color-border-secondary); border-radius: var(--border-radius-md); padding: 2rem; text-align: center; cursor: pointer; color: var(--color-text-secondary); font-size: 13px; transition: background 0.15s; position: relative; }
.drop-zone:hover { background: var(--color-background-secondary); }
.drop-zone input { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
.preview-grid { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.preview-img { width: 80px; height: 80px; object-fit: cover; border-radius: var(--border-radius-md); border: 0.5px solid var(--color-border-tertiary); }
.tab-row { display: flex; gap: 4px; margin-bottom: 12px; }
.tab { padding: 6px 14px; font-size: 13px; border-radius: var(--border-radius-md); cursor: pointer; border: 0.5px solid var(--color-border-tertiary); background: transparent; color: var(--color-text-secondary); }
.tab.active { background: var(--color-background-secondary); color: var(--color-text-primary); border-color: var(--color-border-secondary); }
.result-box { background: var(--color-background-secondary); border-radius: var(--border-radius-md); padding: 1rem; font-size: 13px; line-height: 1.7; color: var(--color-text-primary); white-space: pre-wrap; min-height: 80px; max-height: 420px; overflow-y: auto; }
.badge { display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: var(--border-radius-md); }
.badge-ok { background: var(--color-background-success); color: var(--color-text-success); }
.badge-err { background: var(--color-background-danger); color: var(--color-text-danger); }
.badge-wait { background: var(--color-background-warning); color: var(--color-text-warning); }
.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--color-border-secondary); border-top-color: var(--color-text-secondary); border-radius: 50%; animation: spin 0.7s linear infinite; vertical-align: middle; margin-right: 6px; }
@keyframes spin { to { transform: rotate(360deg); } }
.score-table { width: 100%; font-size: 13px; border-collapse: collapse; }
.score-table td, .score-table th { padding: 4px 8px; border-bottom: 0.5px solid var(--color-border-tertiary); text-align: left; }
.score-table th { font-size: 11px; color: var(--color-text-secondary); font-weight: 400; }
.total-row td { font-weight: 500; border-top: 1px solid var(--color-border-secondary); }
</style>
<div class="wrap">
<div class="section">
<div class="label">Эндпоинт и модель</div>
<div class="row" style="margin-bottom:8px">
<input type="text" id="endpoint" value="https://llm.lambda.coredump.ru/v1" placeholder="https://...">
</div>
<div class="row" style="margin-bottom:8px">
<input type="text" id="model" value="openai/Qwen3.5-122B-A10B" placeholder="имя модели">
</div>
<div class="row">
<input type="password" id="apikey" placeholder="API key">
</div>
</div>
<div class="section">
<div class="label">Режим</div>
<div class="tab-row">
<button class="tab active" onclick="setMode('ocr',this)">OCR рукописи</button>
<button class="tab" onclick="setMode('grade',this)">Проверка сочинения</button>
<button class="tab" onclick="setMode('full',this)">Полный цикл</button>
</div>
<div id="pane-ocr">
<div class="label">Загрузи фото бланка (можно несколько)</div>
<div class="drop-zone" id="drop">
Нажми или перетащи изображения сюда
<input type="file" id="file-input" accept="image/*" multiple onchange="handleFiles(this.files)">
</div>
<div class="preview-grid" id="previews"></div>
</div>
<div id="pane-grade" style="display:none">
<div class="label">Текст сочинения</div>
<textarea id="essay-text" style="width:100%;height:160px;font-size:13px;padding:8px;border-radius:var(--border-radius-md);border:0.5px solid var(--color-border-tertiary);background:var(--color-background-primary);color:var(--color-text-primary);resize:vertical" placeholder="Вставьте текст сочинения..."></textarea>
</div>
<div id="pane-full" style="display:none">
<div class="label">Загрузи фото + автоматически распознаёт и проверит по критериям</div>
<div class="drop-zone">
Нажми или перетащи изображения сюда
<input type="file" id="file-input-full" accept="image/*" multiple onchange="handleFilesFull(this.files)">
</div>
<div class="preview-grid" id="previews-full"></div>
</div>
</div>
<div style="margin-bottom:12px">
<button onclick="run()" style="width:100%;padding:10px;font-size:14px">Запустить ↗</button>
</div>
<div class="section" id="status-section" style="display:none">
<div class="row" style="justify-content:space-between;margin-bottom:8px">
<span style="font-size:13px;font-weight:500">Результат</span>
<span id="status-badge" class="badge badge-wait">ожидание</span>
</div>
<div id="result-content"></div>
</div>
</div>
<script>
let mode = 'ocr';
let images = [];
let imagesFull = [];
function setMode(m, el) {
mode = m;
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
el.classList.add('active');
document.getElementById('pane-ocr').style.display = m === 'ocr' ? '' : 'none';
document.getElementById('pane-grade').style.display = m === 'grade' ? '' : 'none';
document.getElementById('pane-full').style.display = m === 'full' ? '' : 'none';
}
function handleFiles(files) {
images = [];
const grid = document.getElementById('previews');
grid.innerHTML = '';
Array.from(files).forEach(f => {
const reader = new FileReader();
reader.onload = e => {
images.push({data: e.target.result.split(',')[1], type: f.type});
const img = document.createElement('img');
img.src = e.target.result;
img.className = 'preview-img';
grid.appendChild(img);
};
reader.readAsDataURL(f);
});
}
function handleFilesFull(files) {
imagesFull = [];
const grid = document.getElementById('previews-full');
grid.innerHTML = '';
Array.from(files).forEach(f => {
const reader = new FileReader();
reader.onload = e => {
imagesFull.push({data: e.target.result.split(',')[1], type: f.type});
const img = document.createElement('img');
img.src = e.target.result;
img.className = 'preview-img';
grid.appendChild(img);
};
reader.readAsDataURL(f);
});
}
function showStatus(text, type) {
const s = document.getElementById('status-section');
s.style.display = '';
const badge = document.getElementById('status-badge');
badge.className = 'badge badge-' + type;
badge.textContent = type === 'wait' ? 'загрузка...' : type === 'ok' ? 'готово' : 'ошибка';
document.getElementById('result-content').innerHTML = text;
}
async function callAPI(messages, maxTokens) {
const endpoint = document.getElementById('endpoint').value.replace(/\/$/, '');
const model = document.getElementById('model').value;
const apikey = document.getElementById('apikey').value;
const headers = {'Content-Type': 'application/json'};
if (apikey) headers['Authorization'] = 'Bearer ' + apikey;
const resp = await fetch(endpoint + '/chat/completions', {
method: 'POST',
headers,
body: JSON.stringify({model, max_tokens: maxTokens || 2000, messages})
});
if (!resp.ok) throw new Error('HTTP ' + resp.status + ': ' + await resp.text());
const data = await resp.json();
return data.choices?.[0]?.message?.content || '';
}
function buildImageContent(imgList, textPrompt) {
const content = [];
imgList.forEach(img => {
content.push({type: 'image_url', image_url: {url: `data:${img.type};base64,${img.data}`}});
});
content.push({type: 'text', text: textPrompt});
return content;
}
const CRITERIA = `К1 (0-1): Формулировка проблемы
К2 (0-6): Комментарий (2 примера + связь)
К3 (0-1): Позиция автора
К4 (0-1): Отношение к позиции автора
К5 (0-2): Смысловая цельность и связность
К6 (0-2): Точность и выразительность речи
К7 (0-3): Орфография (3=0ош, 2=1ош, 1=2-3ош, 0=4+)
К8 (0-3): Пунктуация (3=0ош, 2=1-2ош, 1=3-4ош, 0=5+)
К9 (0-2): Языковые нормы (грамматика)
К10 (0-2): Речевые нормы
К11 (0-1): Этические нормы
К12 (0-1): Фактическая точность
Итого: 25 баллов
Правила: если К1=0 → К2,К3,К4 автоматически=0. К6 не может быть выше К10.`;
async function run() {
showStatus('<span class="spinner"></span>Отправляю запрос...', 'wait');
try {
if (mode === 'ocr') {
if (images.length === 0) throw new Error('Загрузи хотя бы одно изображение');
const content = buildImageContent(images,
'Ты эксперт по распознаванию рукописного текста. Внимательно прочитай рукописное сочинение ЕГЭ на изображении и выведи его текст максимально точно. Сохраняй абзацное деление. Зачёркнутые слова отмечай в скобках [зачёркнуто]. Не добавляй никаких комментариев — только текст.');
const text = await callAPI([{role:'user', content}], 2000);
showStatus(`<div class="result-box">${escHtml(text)}</div>`, 'ok');
} else if (mode === 'grade') {
const essay = document.getElementById('essay-text').value.trim();
if (!essay) throw new Error('Вставьте текст сочинения');
const prompt = `Ты эксперт-проверяющий ЕГЭ по русскому языку. Проверь сочинение по критериям ФИПИ и выставь баллы.
КРИТЕРИИ:
${CRITERIA}
СОЧИНЕНИЕ:
${essay}
Ответь строго в формате JSON без markdown-обёрток:
{"k1":0,"k2":0,"k3":0,"k4":0,"k5":0,"k6":0,"k7":0,"k8":0,"k9":0,"k10":0,"k11":0,"k12":0,"total":0,"comments":{"k1":"...","k2":"...","k3":"...","k4":"...","k5":"...","k6":"...","k7":"...","k8":"...","k9":"...","k10":"...","k11":"...","k12":"..."},"recommendations":["...","..."]}`;
const raw = await callAPI([{role:'user',content:prompt}], 2000);
renderGrading(raw);
} else if (mode === 'full') {
if (imagesFull.length === 0) throw new Error('Загрузи хотя бы одно изображение');
showStatus('<span class="spinner"></span>Шаг 1/2: распознаю рукопись...', 'wait');
const content = buildImageContent(imagesFull,
'Ты эксперт по распознаванию рукописного текста. Внимательно прочитай рукописное сочинение ЕГЭ на изображении и выведи его текст максимально точно. Сохраняй абзацное деление. Не добавляй никаких комментариев — только текст сочинения.');
const essayText = await callAPI([{role:'user', content}], 2000);
showStatus('<span class="spinner"></span>Шаг 2/2: проверяю по критериям...', 'wait');
const prompt = `Ты эксперт-проверяющий ЕГЭ по русскому языку. Проверь сочинение по критериям ФИПИ и выставь баллы.
КРИТЕРИИ:
${CRITERIA}
СОЧИНЕНИЕ:
${essayText}
Ответь строго в формате JSON без markdown-обёрток:
{"recognized_text":"...","k1":0,"k2":0,"k3":0,"k4":0,"k5":0,"k6":0,"k7":0,"k8":0,"k9":0,"k10":0,"k11":0,"k12":0,"total":0,"comments":{"k1":"...","k2":"...","k3":"...","k4":"...","k5":"...","k6":"...","k7":"...","k8":"...","k9":"...","k10":"...","k11":"...","k12":"..."},"recommendations":["...","..."]}`;
const raw = await callAPI([{role:'user',content:prompt}], 3000);
renderGrading(raw, essayText);
}
} catch(e) {
showStatus(`<div style="font-size:13px;color:var(--color-text-danger)">${escHtml(e.message)}</div>`, 'err');
}
}
function renderGrading(raw, ocrText) {
let data;
try {
const clean = raw.replace(/```json|```/g, '').trim();
data = JSON.parse(clean);
} catch(e) {
showStatus(`<div class="result-box">${escHtml(raw)}</div>`, 'ok');
return;
}
const keys = ['k1','k2','k3','k4','k5','k6','k7','k8','k9','k10','k11','k12'];
const names = {k1:'Формулировка проблемы',k2:'Комментарий',k3:'Позиция автора',k4:'Отношение к позиции',k5:'Связность',k6:'Точность речи',k7:'Орфография',k8:'Пунктуация',k9:'Языковые нормы',k10:'Речевые нормы',k11:'Этика',k12:'Фактика'};
const maxes = {k1:1,k2:6,k3:1,k4:1,k5:2,k6:2,k7:3,k8:3,k9:2,k10:2,k11:1,k12:1};
let rows = keys.map(k => {
const score = data[k] ?? '?';
const max = maxes[k];
const pct = typeof score === 'number' ? score/max : 0;
const color = pct === 1 ? 'var(--color-text-success)' : pct >= 0.5 ? 'var(--color-text-warning)' : 'var(--color-text-danger)';
return `<tr><td style="color:var(--color-text-secondary)">${k.toUpperCase()}</td><td>${names[k]}</td><td style="text-align:right;font-weight:500;color:${color}">${score}/${max}</td></tr>`;
}).join('');
const total = data.total ?? keys.reduce((s,k) => s + (data[k]||0), 0);
const totalColor = total >= 20 ? 'var(--color-text-success)' : total >= 13 ? 'var(--color-text-warning)' : 'var(--color-text-danger)';
let comments = '';
if (data.comments) {
comments = keys.map(k => data.comments[k] ? `<div style="margin-bottom:10px"><span style="font-size:12px;font-weight:500;color:var(--color-text-secondary)">${k.toUpperCase()} — </span><span style="font-size:13px">${escHtml(data.comments[k])}</span></div>` : '').join('');
}
let recs = '';
if (data.recommendations?.length) {
recs = data.recommendations.map((r,i) => `<div style="font-size:13px;margin-bottom:6px">${i+1}. ${escHtml(r)}</div>`).join('');
}
let ocrBlock = '';
if (ocrText || data.recognized_text) {
const t = data.recognized_text || ocrText;
ocrBlock = `<div style="margin-bottom:12px"><div class="label" style="margin-bottom:4px">Распознанный текст</div><div class="result-box" style="max-height:160px">${escHtml(t)}</div></div>`;
}
showStatus(`
${ocrBlock}
<table class="score-table">
<thead><tr><th>Кр.</th><th>Название</th><th style="text-align:right">Балл</th></tr></thead>
<tbody>${rows}</tbody>
<tfoot><tr class="total-row"><td colspan="2">Итого</td><td style="text-align:right;color:${totalColor}">${total}/25</td></tr></tfoot>
</table>
${comments ? `<div style="margin-top:14px"><div class="label" style="margin-bottom:8px">Комментарии</div>${comments}</div>` : ''}
${recs ? `<div style="margin-top:12px"><div class="label" style="margin-bottom:8px">Рекомендации</div>${recs}</div>` : ''}
`, 'ok');
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>');
}
</script>

View file

@ -1,134 +0,0 @@
#!/usr/bin/env python3
import subprocess
import json
import os
import sys
from pathlib import Path
def transcribe_file(
file_path: Path, script_path: Path, timeout_seconds: int = 60
) -> str:
"""
Вызывает recognition.py для одного файла, возвращает транскрипцию.
"""
cmd = ["python3", str(script_path), str(file_path)]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
encoding="utf-8",
timeout=timeout_seconds,
)
# Предполагаем, что скрипт выводит транскрипцию в stdout
transcript = result.stdout.strip()
if not transcript:
print(f"Предупреждение: пустая транскрипция для {file_path}")
return transcript
except subprocess.TimeoutExpired:
error_msg = f"TIMEOUT: превышен лимит в {timeout_seconds} секунд"
print(f"Ошибка: {error_msg} для {file_path}")
return f"ERROR: {error_msg}"
except subprocess.CalledProcessError as e:
print(f"Ошибка при обработке {file_path}: {e}")
print(f"stderr: {e.stderr}")
return f"ERROR: {e.stderr}"
def main():
# Пути
base_dir = Path(
r"/mnt/c/proga/projects/ege-skill-dev/.scans/Сложные работы_Английский язык УЧ часть 1/"
)
regions = ["2-Республика Башкортостан", "3-Республика Бурятия"]
recognizer_script = Path.home() / ".zeroclaw" / "workspace" / "skills" / "ege-checker" / "recognition.py"
# Таймаут на транскрипцию одного файла (в секундах)
TIMEOUT_PER_FILE = 120
# Проверки
if not base_dir.exists():
print(f"Ошибка: директория {base_dir} не найдена")
sys.exit(1)
if not recognizer_script.exists():
print(f"Ошибка: скрипт {recognizer_script} не найден")
sys.exit(1)
# Собираем все .ogg файлы, кроме тех, где озвузичвается код участника
ogg_files = []
for region in regions:
ogg_files.extend(
[
file
for i, file in enumerate(list(Path(f"{base_dir}/{region}/").rglob("*.ogg")))
if i % 5 != 0
]
)
print(f"Найдено {len(ogg_files)} .ogg файлов")
results = []
for idx, ogg_path in enumerate(ogg_files, 1):
print(f"\n[{idx}/{len(ogg_files)}] Обработка: {ogg_path}")
# Структура: .../{region}/{fio}/{file}
region = ogg_path.parent.parent.name
fio = ogg_path.parent.name
print(f" Регион: {region}")
print(f" ФИО: {fio}")
print(
f" Транскрибирование (таймаут: {TIMEOUT_PER_FILE} сек)...",
end=" ",
flush=True,
)
transcript = transcribe_file(ogg_path, recognizer_script, TIMEOUT_PER_FILE)
print("готово")
# Показываем первые 100 символов транскрипции, если она есть
preview = transcript[:100].replace("\n", " ")
if preview and not transcript.startswith("ERROR:"):
print(f" Транскрипция (начало): {preview}...")
elif transcript.startswith("ERROR:"):
print(f" {transcript}")
results.append(
{
"region": region,
"fio": fio,
"transcript": transcript
}
)
# Сохраняем JSON
output_file = Path.cwd() / "tests" / "transcriptions.json"
if Path.exists(output_file):
with open(output_file, "r", encoding="utf-8") as f:
results_to_file: list = json.load(f)
results_to_file.extend(results)
else:
results_to_file = results
with open(output_file, "w", encoding="utf-8") as f:
json.dump(results_to_file, f, ensure_ascii=False, indent=2)
# Статистика
total_files = len(results)
error_files = sum(1 for r in results if r["transcript"].startswith("ERROR:"))
timeout_files = sum(1 for r in results if "TIMEOUT" in r["transcript"])
success_files = total_files - error_files
print(f"\n✅ Готово! Результаты сохранены в {output_file}")
print(f"Всего обработано файлов: {total_files}")
print(f" - Успешно: {success_files}")
print(f" - Ошибок: {error_files} (из них timeout: {timeout_files})")
if __name__ == "__main__":
main()

File diff suppressed because one or more lines are too long

View file

@ -1,28 +0,0 @@
## Отчет по результатам оценки заданий (на основе transcriptions.json)
| Регион | Фамилия | Задание 1 (True/Pred) | Задание 2 (True/Pred) | Задание 3 (True/Pred) | Задание 4 (True/Pred) | Общий True | Общий Pred |
| :--- | :--- | :---: | :---: | :---: | :---: | :---: | :---: |
| 1-Республика Адыгея | Кузьминова | 1 / 0 | 2 / 4 | 3 / 0 | 5 / 7 | **11** | **11** |
| 1-Республика Адыгея | Штельмах | 1 / 1 | 1 / 0 | 2 / 2 | 2 / 9 | **6** | **12** |
| 4-Республика Алтай | Борбуева | 1 / 0 | 3 / 3 | 1 / 3 | 6 / 8 | **11** | **14** |
| 4-Республика Алтай | Гаршина | 0 / 0 | 2 / 2 | 0 / 0 | 5 / 9 | **7** | **11** |
| 2-Республика Башкортостан | Авсахова | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 2 | **0** | **2** |
| 2-Республика Башкортостан | Иркабаев | 1 / 1 | 4 / 4 | 4 / 4 | 5 / 9 | **14** | **18** |
| 2-Республика Башкортостан | Панова | 1 / 0 | 2 / 1 | 1 / 0 | 7 / 6 | **11** | **7** |
| 2-Республика Башкортостан | Пешкова | 1 / 1 | 4 / 4 | 2 / 5 | 10 / 9 | **17** | **19** |
| 2-Республика Башкортостан | Шафикова | 1 / 1 | 3 / 3 | 0 / 2 | 7 / 9 | **11** | **15** |
| 2-Республика Башкортостан | Яппаров | 1 / 0 | 2 / 2 | 2 / 0 | 6 / 6 | **11** | **8** |
| 3-Республика Бурятия | Алексеев | 0 / 0 | 3 / 0 | 0 / 0 | 6 / 3 | **9** | **3** |
| 3-Республика Бурятия | Очиров | 1 / 0 | 1 / 2 | 3 / 0 | 7 / 6 | **12** | **8** |
| 3-Республика Бурятия | Ошорова | 1 / 1 | 3 / 3 | 5 / 0 | 9 / 9 | **18** | **13** |
| 3-Республика Бурятия | Чимбеева | 1 / 0 | 4 / 4 | 3 / 3 | 7 / 6 | **15** | **13** |
---
### Общая статистика
| Метрика | Task 1 | Task 2 | Task 3 | Task 4 |
| :--- | :---: | :---: | :---: | :---: |
| **Avg True** | 0.786 | 2.429 | 1.857 | 5.857 |
| **Avg Pred** | 0.357 | 2.286 | 1.357 | 7.000 |
| **MAE** | 0.429 | 0.143 | 0.500 | 1.143 |

View file

@ -1,38 +0,0 @@
import json
from collections import defaultdict
TRANSCRIPTIONS = "transcriptions.json"
def get_stats():
stats = defaultdict(dict)
with open(TRANSCRIPTIONS, "r") as f:
data = json.load(f)
for i, elem in enumerate(data):
task_num = i % 4 + 1
obj = stats[f"Task {task_num}"]
if 'Sum True' in obj:
obj['Sum True'] += elem['score_true']
obj['Sum Pred'] += elem['score_pred']
obj['Count'] += 1
else:
obj['Sum True'] = elem['score_true']
obj['Sum Pred'] = elem['score_pred']
obj['Count'] = 1
for key, obj in stats.items():
stats[key]['Average True'] = stats[key]['Sum True'] / stats[key]['Count']
stats[key]['Average Pred'] = stats[key]['Sum Pred'] / stats[key]['Count']
stats[key]['MAE'] = abs(stats[key]['Average True'] - stats[key]['Average Pred'])
return stats
def main():
stats = get_stats()
print(f"Статистика:")
for name, obj in stats.items():
print(name)
print(f'Avg True / Avg Pred / MAE = {obj['Average True']:.3f} / {obj['Average Pred']:.3f} / {obj['MAE']:.3f}')
if __name__ == '__main__':
main()

View file

@ -1,394 +0,0 @@
[
{
"region": "1-Республика Адыгея",
"fio": "Кузьминова_7921829566",
"transcript": "It usually snows more in countries that are in the North. For instance, Japan, Russia or some parts of the USA may have hard snowfalls. Mountains are also the places which have a lot of snow. This is especially true for such countries as China. At the same time, there are tropical countries which hardly see any snow. For some countries snow is one of the main reasons why tourists come here. It's not every place in the world has snowfalls. People from hard countries come to snow-covered places in order to live the experience of seeing, touching and feeling this natural phenomenon. You may think it's wonderful to have a lot of snow, but it's not always the case. A heavy snowfall can paralyze the life of a city or a country. If snows is rare in a certain location, people there are not used to it and find it hard to commute or even go outside.",
"score_true": 1,
"score_pred": 0
},
{
"region": "1-Республика Адыгея",
"fio": "Кузьминова_7921829566",
"transcript": "How much variety of jobs do you have? What is the minimum age for a part-time job? What is required number of hours per week? What is the pay per hour?",
"score_true": 2,
"score_pred": 4
},
{
"region": "1-Республика Адыгея",
"fio": "Кузьминова_7921829566",
"transcript": "Usually I spend my weekend with my family and my friends. We are going to the parks or rivers and lakes or forests to walk with them. If it is among my friends, it's walking in the parks and going to the stadium. I think it's really interesting spending free time for my coming weekend. Me, my brother and our friends will go to the river and go to swim then. When the weekend is over, I feel sad because my weekend is really funny and I feel happy when I have a weekend. I would like to spend more time outside with my friends.",
"score_true": 3,
"score_pred": 0
},
{
"region": "1-Республика Адыгея",
"fio": "Кузьминова_7921829566",
"transcript": "Hi there! I have found two photos for our school project on doing homework. I'd like to tell you about them. In the first picture one can see a girl. She is doing homework. There are a girl with her mom that is doing homework together in the second picture. Talking about the differences, the key difference is in the first picture girl doing homework alone, while in the second picture girl doing homework with her parents. I think these photos are perfect for our project because they illustrate different kinds of doing homework. I believe that the two types of doing homework have their advantages and disadvantages. As for the advantages, doing homework alone can help you for better concentration. What about doing homework with somebody you can't do it. We'll discuss about the disadvantages. As for doing homework alone, I can say that these disadvantages are the risk of getting worth your attention and doing incorrect answers. One downside of doing homework together is that you can have a fear of taking incorrect answers. Overall, any kind of doing homework is beneficial for our education. I'd prefer doing homework alone because that way I have a lot of concentration of my job. That's all I wanted to tell you. Bye!",
"score_true": 5,
"score_pred": 7
},
{
"region": "1-Республика Адыгея",
"fio": "Штельмах_7924913136",
"transcript": "It usually snows more in countries that are in the north. For instance Japan, Russia or some parts of the USA may have large snowfalls. Mountains are also the place which have a lot of snow. It is especially true for such countries as China. At the same time there are tropical countries which hardly see any snow. For some countries snow is one of the main reasons why tourists come there. It is not every place in the world has snowfalls. People from hot countries come to snow-covered places in order to live the experience of seeing, touching and feeling this natural phenomenon. You may think it is wonderful to have a lot of snow, but it is not always the case. Heavy snow can paralyze the life of a city or a country. If snow is rare in a certain location, people are not used to it and find it hard to commute or even go outside.",
"score_true": 1,
"score_pred": 1
},
{
"region": "1-Республика Адыгея",
"fio": "Штельмах_7924913136",
"transcript": "What variety of jobs you have to start there, working, pay as much as you pay per hour.",
"score_true": 1,
"score_pred": 0
},
{
"region": "1-Республика Адыгея",
"fio": "Штельмах_7924913136",
"transcript": "I usually spend my weekend with my friends. We usually go to the cinema or play computer games. It's sport. I like to do some exercise in the gym. My plans at the weekend is go to trip to the mountain. We have planned this with my friends. I feel so bad because when it's over, I feel so bad. But sometimes I feel good because I can go to the school. I like to change my daily routine. I want to do more homework. It's important for me.",
"score_true": 2,
"score_pred": 2
},
{
"region": "1-Республика Адыгея",
"fio": "Штельмах_7924913136",
"transcript": "Hi Peter, I have found two photos for our school project doing homework and I would like to tell you about them. Let me describe them for you. One picture depicts a girl doing homework, in the other picture girl doing her homework with mom. However, these photos have some differences. The key difference is that the first picture shows girl doing her homework alone, while in the other picture girl doing her homework with mom. The picture is perfectly suited our project because both of them illustrate doing homework theme of our project. I believe that both photos have advantages and disadvantages. Talking about advantages of the first photo is she can improve her cognitive skills when she is doing her homework alone. Also the best thing in the second picture is mom can help her with her homework. As for disadvantages of the first picture, it would be hard for her. There is a major drawback of the second picture, she cannot upgrade her cognitive skills. Personally, I would prefer the first version of doing homework alone because it's better for my cognitive skills. That's all for now, that's all for now. I'll be waiting for your opinion on the photos.",
"score_true": 2,
"score_pred": 9
},
{
"region": "4-Республика Алтай",
"fio": орбуева_8420415899",
"transcript": "Rain is an important part of the water cycle which never stops on our planet. Water is delivered to the ground by rains. It is retained as clouds in the sky and falls over and through the land. Actually this is one of the reasons why the earth is cold in winter and warm in summer. Water escapes into the atmosphere and turns into clouds. Rain is useful for us in many different ways. Firstly it waters the earth and refills streams, rivers, lakes and oceans. The water in the oceans is home to millions of sea creatures and the water in the streams, rivers and lakes is home to freshwater fish and other water animals. Secondly, rain provides the water trees and other plants need. Rain also gives wild animals the water they need to drink. Finally, people love rainy day, rainy weather. It makes the air fresh and clean.",
"score_true": 1,
"score_pred": 0
},
{
"region": "4-Республика Алтай",
"fio": орбуева_8420415899",
"transcript": "Where is ordering flowers located? What is the cost of the flowers delivery? Do you have any special occasion decorations in your flower shop? Do you have other potted flowers in your flower shop?",
"score_true": 3,
"score_pred": 3
},
{
"region": "4-Республика Алтай",
"fio": орбуева_8420415899",
"transcript": "In my daily life I use a mobile phone and my tablet. I use them because these gadgets are very comfortable for using in the daily life. Digital technologies make our lives easier in many ways. For example, people can call their family members whatever they want and whenever they are located at the moment and also we can order something online using our gadgets. The best invention of mankind is a mobile phone because this is a very useful gadget. The other gadgets. I think this is the most useful gadget. There is a lot of Russian inventors in my country, but one of them is Zaitsev. He invented the radio. I do not think that I can live without a mobile phone for a month because the mobile phone is a very important part of my life because I can call my family and my friends. I do learn with a mobile phone, so I do not think that I can live without a mobile phone for a month.",
"score_true": 1,
"score_pred": 3
},
{
"region": "4-Республика Алтай",
"fio": орбуева_8420415899",
"transcript": "Hi friend, how are you? I have some photos for our project, the games people like, and I would like to say a few words about them. I think these two types of photos are perfect for our project, as they illustrate two different types of the games people like. The first picture shows a girl playing video games, and the other picture shows two people playing table games. Talking about differences, the key difference is that in the first picture there is a girl and she is alone, and in the second picture there is a mother and daughter, and another difference is that in the first picture there is a girl and she is sitting on the chair, and in the other picture there is a mother and daughter sitting on the sofa. I believe that these two types of the games people like have their advantages and disadvantages. Talking about disadvantages, the disadvantage of video games is that the risk of eye injury, and the disadvantage of the table games is that there is not a lot of functions in the games. Talking about advantages, the advantage of the video games is that usually video games are very colorful and interesting, and the advantage of the table games is that it develops your brain. I think playing any games is beneficial for our mental health. I would prefer table games because I like to play with people in the real life, but that's all for me now. Please get in touch with me and tell me what do you think about these photos.",
"score_true": 6,
"score_pred": 8
},
{
"region": "4-Республика Алтай",
"fio": аршина_8421427187",
"transcript": "Rain is an important part of the water cycle which never stops on our planet. Water is delivered to the ground by rains. It is retained as clouds in the sky and falls over and throws the land. Actually, this is one of the reasons why the earth is cold in winter and warm in summer. Water escapes into the atmosphere and turns into clouds. Rain is useful for us in many different ways. Firstly, it waters the earth and refills streams, rivers, lakes and oceans. The water in the oceans is home to millions of sea creatures and the water in the streams, rivers and lakes is home to freshwater fish and other water animals. Secondly, rain provides water trees and other plants need. Rain also gives wild animals the water they need to drink. Finally, people love rain dairy whatever. Well, it makes the air fresh and clean.",
"score_true": 0,
"score_pred": 0
},
{
"region": "4-Республика Алтай",
"fio": аршина_8421427187",
"transcript": "Where your flowers shop is located? What is cost of delivery? What is cost of delivery? Do you have any special occasion decorations? Are there any potted flowers?",
"score_true": 2,
"score_pred": 2
},
{
"region": "4-Республика Алтай",
"fio": аршина_8421427187",
"transcript": "Well, I mostly use such gadgets as my phone and a computer, because I very like video games and watching videos. So I think that I really enjoy... Well, I think that new technologies make our life more easy, because, for example, you can use your phone. For that, you can call your relatives or friends. Also, you can make new videos and photos. Well, I think that the best invention is logic, which you can find in different shops and use for your life. Well, I think that the most used invention are phones and computers, because people use these engines very mostly. Well, I think that I can find mobile phone very used, because, as I said, people might use phone such as camera and go making videos and photos.",
"score_true": 0,
"score_pred": 0
},
{
"region": "4-Республика Алтай",
"fio": аршина_8421427187",
"transcript": "Hi Eileen, I'm calling you about some photos which I have found for our project, the games people like. So let me tell you about them. The first picture is suitable for our project because it shows video game. The second photo also works because it shows chess. So we can see two different ways that people can use for games. In the first picture I see a girl who is playing a video game on her computer. In the second photo I see a woman, a girl, who is playing chess. The key difference is that in the first picture a girl and the second picture are two players. Well, I can see some advantages and disadvantages of these two types of games. The advantage of video games is that you can play with people from all over the world. The advantage of playing chess is that you can play with your relatives and friends. However, the drawback of playing computer is that it can be harmful for your eyes. The drawback of playing chess is that it can be boring. As for me, I prefer video games because I really like it and I like when I can find different worlds and characters in games. That's all I wanted to say about these pictures. Call me back and tell your opinion. Bye.",
"score_true": 5,
"score_pred": 9
},
{
"region": "2-Республика Башкортостан",
"fio": "Авсахова_8020280717",
"transcript": "Rain is an important part of the water cycle which never stops on our planet. Water is delivered to the ground by rains. It retains as clouds in the sky and falls over through the land. Actually, this is one of the reasons why the earth is cold in winter and warm in summer. Water escapes into the atmosphere and turns into clouds. Rain is useful for us in many different ways. Firstly, it waters the earth and refills streams, rivers, lakes and oceans. The water in the oceans is home to millions of sea creatures and the water in the sea streams, rivers and lakes is home to freshwater fish and other water animals. Secondly, rain provides the water trees and other plants need. Rain also gives wild animals their water they need to drink. Finally, people love rainy water. It makes the air fresh and clean.",
"score_true": 0,
"score_pred": 0
},
{
"region": "2-Республика Башкортостан",
"fio": "Авсахова_8020280717",
"transcript": "Where is location? What cost of delivery? Where special location decorations? Where is the potted flowers?",
"score_true": 0,
"score_pred": 0
},
{
"region": "2-Республика Башкортостан",
"fio": "Авсахова_8020280717",
"transcript": "In my daily life I use my mobile phone and television because I chat with my friends and call my mom. The digital technology that made my life easier is my telephone because I chat with my friends, no more information. The best pictures on MedKindness is television. In my opinion, the famous Russian list is Mikhail Vasilyevich Lomonosov, the Russian scientist who knows a scientist like math and he is so curious. I think I don't see my life without my mobile phone because it keeps more information for me and I chat with my friends and call my mom. It improves my life and I don't see the life without it.",
"score_true": 0,
"score_pred": 0
},
{
"region": "2-Республика Башкортостан",
"fio": "Авсахова_8020280717",
"transcript": "Hi Kate, I have our project for you in two pictures we have seen. In the first pictures we see girls who play the computer games and she liked it because the computer game have a brain side and improve your brain explanation. In the second photo we have seen girls and her mom who play the chess in disadvantages. However, I can say advantages or disadvantages. In advantages first picture we can see that girl playing and improve your skills in computer game. In two pictures we have seen the brain hard game but disadvantages our pictures in the first pictures girls save more time for that and she will be angry about that your game is disappointed and two disadvantages what the most time kills because she save them. I prefer picture number one because the girl I love computer game and what about them. I wait your opinion.",
"score_true": 0,
"score_pred": 2
},
{
"region": "2-Республика Башкортостан",
"fio": ркабаев_8021293748",
"transcript": "Rain is an important part of the water cycle which never stops on our planet. Water is delivered to the ground by rains. It is retained as clouds in the sky and falls over and through the land. Actually, this is one of the reasons why the earth is cold in winter and warm in summer. Water escapes into the atmosphere and turns into clouds. Rain is useful for us in many different ways. Firstly, it waters the earth and refills streams, rivers, lakes and oceans. The water in the oceans is home to millions of sea creatures and the water in the streams, rivers and lakes is home to freshwater fish and other water animals. Secondly, rain provides the water trees and other plants need. Rain also gives wild animals the water they need to drink. Finally, people love rainy weather. It makes the air fresh and clean.",
"score_true": 1,
"score_pred": 1
},
{
"region": "2-Республика Башкортостан",
"fio": ркабаев_8021293748",
"transcript": "Where is your flower shop located? What is the cost of delivery? Are there any special occasion decorations? Are potted flowers available?",
"score_true": 4,
"score_pred": 4
},
{
"region": "2-Республика Башкортостан",
"fio": ркабаев_8021293748",
"transcript": "In my daily life I usually use such gadgets as smartphone, TV and personal computer. Modern technologies makes our life easier in that way that you don't need to go to the library to read some necessary information. It's more convenient now to go shopping or surfing the net than before. Well, I think the best invention of mankind is Internet because it allows us to do basically everything. You basically can't imagine our everyday life without Internet, without the Internet. Well, there is famous Russian inventor of a radio, his name is Popov. He invented radio before anyone else, as far as I know. Well, I think I could live without smartphone for a month because it's not that hard to avoid all these benefits that smartphone brings. But I have to admit it would be quite difficult.",
"score_true": 4,
"score_pred": 4
},
{
"region": "2-Республика Башкортостан",
"fio": ркабаев_8021293748",
"transcript": "Hi, John, how are you? I've just found two pictures that are suitable for our school project, the games people like. Let me quickly describe them. One of the pictures shows a girl playing computer games. The other picture shows a mother with her daughter playing chess mate. The main difference between these two photos is that it's two different types of games. As for the advantages of playing computer games, I would like to say that it develops quick reaction. Also, it develops your social skills because you have to communicate with your teammates in the game. As for the advantages of checkmates, I would like to say that it develops your logical thinking. Moreover, it allows you to analyze, it teaches you to analyze situations and make right decisions very fast. As for disadvantages, however, there are also disadvantages. Computer gaming actually makes your eyesight, makes your mental health worse. Also, it's bad for your eyesight. Checkmates also has some withdrawals such as badly influence your physical condition. Your posture can get worse and you feel worse about your physical condition. Personally, I would prefer computer gaming because I'm keen on games and other stuff because I like to play games with my friends. That's all for now. Keep in touch. Bye-bye.",
"score_true": 5,
"score_pred": 9
},
{
"region": "2-Республика Башкортостан",
"fio": анова_8021392635",
"transcript": "Rain is an important part of the water cycle which never stops in our planet. Water is delivered to the ground by rains. It is retained as clouds in the sky and falls over and through the land. Actually, this is one of the reasons why the earth is cold in winter and warm in summer. Water escapes into the atmosphere and turns into clouds. Rain is useful for us in many different ways. Firstly, it waters the earth and refills streams, rivers, lakes and oceans. The water in the oceans is home to millions of sea creatures and the water in the streams. Rivers and lakes is home to freshwater fish and other water animals. Secondly, rain provides the water trees and other plants need. Rain also gives wild animals the water they need to drink. Finally, people love rainy weather. It makes the air fresh and clean.",
"score_true": 1,
"score_pred": 0
},
{
"region": "2-Республика Башкортостан",
"fio": анова_8021392635",
"transcript": "What is the location of the flower store? Stop delivering. We have special occasion for decorations. Have any potted flowers?",
"score_true": 2,
"score_pred": 1
},
{
"region": "2-Республика Башкортостан",
"fio": анова_8021392635",
"transcript": "In my daily life, I use phone. Also, I usually use TV. People teaching online. Also, they can know stuff. Mindy Leyev creates a to-live without mobile phone for a month. That's all because Surf the Net habit. Surf the Net. Surf the Net.",
"score_true": 1,
"score_pred": 0
},
{
"region": "2-Республика Башкортостан",
"fio": анова_8021392635",
"transcript": "Hi guys, I found two photos for our project, the games people like, and I'd like to tell you about them. Let me describe them for you. On picture shows a girl who is playing on the computer. In the other picture there are mom and there is a woman with father who are playing in the chess. However, these photos have some differences. The key difference is that the first photo depicts the video games, while in the second photo there is intellect board games. These pictures perfectly suit our project because both of them illustrate types of games. I believe that both types of games present in the pictures their advantages and disadvantages. Talking about the advantages, the best thing about video games is that it's more funny. At the same time board games are great because they develop your thinking. As for the disadvantages of video games, one of them is that it's more expensive. Also there is a major drawback with games that can be boring. I would prefer video games because that's all for now. I'll be waiting for your opinion on the photos.",
"score_true": 7,
"score_pred": 6
},
{
"region": "2-Республика Башкортостан",
"fio": ешкова_8021424674",
"transcript": "Rain is an important part of the water cycle, which never stops on our planet. Water is delivered to the ground by rains. It is retained as clouds in the sky and falls over and through the land. Actually, this is one of the reasons why the Earth is cold in winter and warm in summer. Water escapes into the atmosphere and turns into clouds. Rain is useful for us in many different ways. Firstly, it waters the Earth and refills streams, rivers, lakes and oceans. The water in the oceans is home to millions of sea creatures and the water in the streams, rivers and lakes is home to freshwater fish and other water animals. Secondly, rain provides the water trees and other plants need. Rain also gives wild animals the water they need to drink. Finally, people love rainy weather. It makes the air fresh and clean.",
"score_true": 1,
"score_pred": 1
},
{
"region": "2-Республика Башкортостан",
"fio": ешкова_8021424674",
"transcript": "What is the location of your flowers shop? What is the cost of delivery? Do you provide any special occasion decorations for flowers? What potted flowers do you have?",
"score_true": 4,
"score_pred": 4
},
{
"region": "2-Республика Башкортостан",
"fio": ешкова_8021424674",
"transcript": "Personally, in my daily life, I use phone, notebook and computer. Also, I use headphones and I find it very useful. I think digital technologies make our life easier in so many ways. For example, we can find whatever we want in any time just using the Internet. Personally, I think that the best invention of the kind thing is our headphones. They help to listen to music at any time and not bother anyone with your own music. I know famous Russian inventor Lomonosov. He made a huge impact on the science of the whole world. He invented telescope. Unfortunately, I would say that I would not be able to live without mobile phone for a month. I think that because I need to listen to music every day and mobile phone gives me an ability to do it.",
"score_true": 2,
"score_pred": 5
},
{
"region": "2-Республика Башкортостан",
"fio": ешкова_8021424674",
"transcript": "Hey Kate, I've just found two pictures relatable for our project The Games People Like, but for some technical issues I can't send them to you right now. So let me describe them. Both pictures are representing types of games. One picture shows a girl playing her computer playing in computer with her headphones on. The other picture shows two women playing chess. The main difference between these pictures is that one picture shows digital type of games and the other picture shows old-fashioned type of games. Both types of games have their advantages. For example, digital games are popular among teenagers and there you can find new friends. As for the old-fashioned games, they make our minds stronger. Meanwhile, both types of games have disadvantages too. For example, digital games can be uncontrolled, especially among children, and you can have an addiction from them. As for the old-fashioned games, for some people they can be boring, especially for children. As for me, I prefer old-fashioned games because they remind me of my childhood. So what do you think about these pictures? Should we take them? Tell me later. Bye-bye!",
"score_true": 10,
"score_pred": 9
},
{
"region": "2-Республика Башкортостан",
"fio": афикова_8020277747",
"transcript": "Rain is an important part of the water cycle which never stops on our planet. Water is delivered to the ground by rains. It is retained as clouds in the sky and falls over through the land. Actually, this is one of the reasons why the Earth is cold in winter and warm in summer. Water escapes into the atmosphere and turns into clouds. Rain is useful for us in many different ways. Firstly, it waters the Earth and refills streams, rivers, lakes and oceans. The water in the oceans is home to millions of sea creatures and the water in the streams, rivers and lakes is home to freshwater fish and other water animals. Secondly, rain provides the water trees and other plants need. Rain also gives wild animals the water they need to drink. Finally, people love rainy weather. It makes the air fresh and clean.",
"score_true": 1,
"score_pred": 1
},
{
"region": "2-Республика Башкортостан",
"fio": афикова_8020277747",
"transcript": "Where is your flower shop located? How much does the delivery cost? Do you offer any special occasion decorations? What is the flower pot?",
"score_true": 3,
"score_pred": 3
},
{
"region": "2-Республика Башкортостан",
"fio": афикова_8020277747",
"transcript": "I use smartphone and laptop every day. I think that these gadgets are very important for people because these digital gadgets make life easier. In my opinion, Internet is the largest source of information. That's why people can find a lot of information very fast and easy. Besides, I think that with smartphone communicate with relatives and friends very easy. I think that the best inventions of mankind is car because car makes our life easier than in the past. I think that because cars can help people to go to other places very fast and in convenient conditions. I think that Pavel Durov is the famous Russian inventor. Pavel Durov is the founder of Telegram and other messaging platforms. I think that Pavel Durov really makes our life easier because he is a great man. I think I will not live without smartphone for a month. I think that because smartphone is very useful thing for me. I use my smartphone a lot of days, a lot of hours. I use them to chatting with my friends and relatives and to find information. That's why I think that smartphone is very important for me.",
"score_true": 0,
"score_pred": 2
},
{
"region": "2-Республика Башкортостан",
"fio": афикова_8020277747",
"transcript": "Hi Vasya, I've just found two photos for our project, the games people like. I have found two photos for... Hi Vasya, I've just found two photos for our project, the games people like. I have found two photos for our project and I'd like to tell you about them and share my ideas. The first picture depicts a young lady who is playing computer games. The other one depicts two young girls who are playing chess. In my opinion, these photos are perfectly suitable for our project because they show two types of games, namely playing computer games and playing intellectual games. I think that both types of games have their advantages, for example, playing computer games can be more interesting and funnier than other types of games. As for playing intellectual games, I think that these games can improve their cleverness and strategic skills. Speaking of disadvantages, I reckon that playing computer games can be harmful for kids' health. As for playing intellectual games, I think that playing intellectual games can be boring for kids. As for me, I prefer playing computer games because it is more interesting for me and I'd like to play computer games with my friends. That's all for now. I hope you like the photos and my ideas.",
"score_true": 7,
"score_pred": 9
},
{
"region": "2-Республика Башкортостан",
"fio": "Яппаров_8020266903",
"transcript": "Rain is an important part of the water cycle, which never stops on our planet. Water is delivered to the ground by rains. It is retained as clouds in the sky and falls over and through the land. Actually, this is one of the reasons why the Earth is cold in wind and warm in summer. Water escapes into the atmosphere and turns into clouds. Rain is useful for us in many different ways. Firstly, it waters the Earth and refills streams, rivers, lakes and oceans. The water in the oceans is home to millions of sea creatures. And the water in the streams, rivers and lakes is home to fresh water fish and other water animals. Secondly, rain provides the water trees and other planets need. Rain also gives wild animals the water they need to drink. Finally, people love rainy weather. It makes the air fresh and clean.",
"score_true": 1,
"score_pred": 0
},
{
"region": "2-Республика Башкортостан",
"fio": "Яппаров_8020266903",
"transcript": "Where the flower shop is located? How much does cost delivery? Do you provide any special occasion decorations? Do you have potted flowers?",
"score_true": 2,
"score_pred": 2
},
{
"region": "2-Республика Башкортостан",
"fio": "Яппаров_8020266903",
"transcript": "I usually use mobile phone for any activity. Also I oftentimes use computer for making projects or making my homework. I think digital gadgets... For us it makes it easier to search for exact information. Also it helps us to calculate many things. The best invention is computer. I think computer is the best invention of human. It's provided for humanity... Yes, it's provided for humanity to... Because it helps... I know Mindeliev. He invented a periodic table of chemical elements. I wouldn't be able to live a month without a mobile phone. It helps me to communicate with my friends. It helps me to communicate with my friends.",
"score_true": 2,
"score_pred": 0
},
{
"region": "2-Республика Башкортостан",
"fio": "Яппаров_8020266903",
"transcript": "Hello John, I have just found two photos from our school project. The games people like. And I'd like to describe them and share my ideas. In the first picture you can see a girl playing a computer game. She's wearing headphones and she looks very confident. And on the second picture you can see a daughter playing chess with her mother. They look happy and they enjoy their game. Both pictures depict two different types of games. In the first picture it is computer games. On the second picture it is chess. These types of games have their own advantages. Computer games are usually very interesting to play. And also it helps children to grow their mind. Chess helps people to train their brain. It is easy to play in any place. And you can play it in any place you want. Also they have their own advantages. For example, computer games can hurt your eyes if you use a computer for a long period of time. And also you can't take a computer in the park or in the nature. Chess has a drawback too. The rules are hard.",
"score_true": 6,
"score_pred": 6
},
{
"region": "3-Республика Бурятия",
"fio": "Алексеев_8121943809",
"transcript": "It's hard to imagine our life without rain. Rainfall is important for animals and plants to survive on earth. Rain keeps up the level of fresh water. When there is no rainfall, the flow of life is affected literally. If there is litter or no rain at all, lands will dry up. The dried up plants is no longer suitable for plants to grow. Farmers will have a hard time in cultivation. If there is no agriculture, humans will suffer from food shortage. The water available in the underground will be absorbed by plants. The water in wells will decrease. Since the water in the ocean is salty, the availability of fresh water will decrease considerably. It is also important to have a normal range of rainfall. If the rainfall is continuous with no intervals, it also causes big problems.",
"score_true": 0,
"score_pred": 0
},
{
"region": "3-Республика Бурятия",
"fio": "Алексеев_8121943809",
"transcript": "Where is your clothing store located? What are the opening hours of your clothing store? What kinds of shorts do you sell? Do you provide any discounts in your clothing store?",
"score_true": 3,
"score_pred": 0
},
{
"region": "3-Республика Бурятия",
"fio": "Алексеев_8121943809",
"transcript": "My best friend is Ilya. I met him when I was a child. In war, without friends, teenagers can't get such an idea. I usually spend a lot of hours with my friends every day. I think three or four hours. In school, I usually play games in the computer club with our friends. It's enjoyable if we spend a lot of time with our friends.",
"score_true": 0,
"score_pred": 0
},
{
"region": "3-Республика Бурятия",
"fio": "Алексеев_8121943809",
"transcript": "Hello, friend. I'm calling about our project Indoor Games. Let me tell you about them. I think one photo works for a project showing a couple of people playing tennis. The other is also suitable because it shows two men's playing chess. These two photos illustrate two different types of indoor games. I mean physical activity games and mind games because this can be very useful for our project. Well, we see some advantages and disadvantages of these two types of indoor games. The advantage of tennis is that there is a lot of physical activity. However, The disadvantages of this is that it spends more energy. The advantages of playing chess is that it's pure mind. The disadvantages is that it's difficult if you don't know a rule of the game. To be honest, I prefer the mind games because it's really good for me. That's all I wanted to say. Call me back. Bye.",
"score_true": 6,
"score_pred": 3
},
{
"region": "3-Республика Бурятия",
"fio": "Очиров_8121954518",
"transcript": "It's hard to imagine a life without rain. Rainfall is most is important for animals and plants to survive on earth. Rain keeps up the levels of level of the fresh water. When there is no rain rainfall the flow of life is affected largely. If there is no if there is little or no rain at all lands will dry up. The dried up land is no longer suitable for plants to grow. Farmers will have a hard time in cultivation. If there is no agriculture humans will suffer from food shortages. The water the water available in the underground will be absorbed by plants. The water in the wells will decrease. Since the water in the ocean is salty the availability of fresh water will decrease considerably. It is also important to have a normal range of rainfall. If the rainfall is continuous with no intervals that also causes big problems.",
"score_true": 1,
"score_pred": 0
},
{
"region": "3-Республика Бурятия",
"fio": "Очиров_8121954518",
"transcript": "What's the location of the clothing store? What are working hours for the store? What kind of shirts are sold in the clothing store? Is there any discounts available for now?",
"score_true": 1,
"score_pred": 2
},
{
"region": "3-Республика Бурятия",
"fio": "Очиров_8121954518",
"transcript": "My best friend is Toll. I met him at daycare when I was a child and we still are best friends to this day. Personally, I look for loyalty and charismatic abilities so I could talk to him for hours because it's always important to socialize within your age group. It's always important to have someone to rely your back. Friends will follow you throughout your whole life. Usually I spend about four to five hours with friends by hanging out or playing games together. Usually we just walk around our town or we just hang out at someone else's house. We sometimes buy groceries together so we could cook something good for ourselves. I was glad to answer your questions.",
"score_true": 3,
"score_pred": 0
},
{
"region": "3-Республика Бурятия",
"fio": "Очиров_8121954518",
"transcript": "Hey friend, I found two big two photos that could fit our project about indoor games perfectly. In one of the photos, there are woman and man playing ping pong together. They seem very engaged in it. They're probably somewhere at the sports center. In another photo, there's a two men sitting at the table. They're playing chess. They're really engaged into it. These two photos are relevant for our project because they represent two kinds of in-game indoor games. The active one and the passive one. The advantages of active one probably would be that you can test your qualities and make friends with your opponents whereas it can be very tiring and you can get trauma from it. For passive one, it probably would be that it can test your strategy and your skills. But disadvantage would be that it's mentally draining. Personally, I would pick the passive one because it's very fun to play someone who has same level of skills as you.",
"score_true": 7,
"score_pred": 6
},
{
"region": "3-Республика Бурятия",
"fio": "Ошорова_8121925358",
"transcript": "It is hard to imagine our life without rain. Rainfall is important for animals and plants to survive on earth. Rain keeps up the level of fresh water. When there is no rainfall, the flow of life is affected largely. If there is little or no rain at all, lands will dry up. The dried up land is no longer suitable for plants to grow. Farmers will have a hard time in cultivation. If there is no agriculture, humans will suffer from food shortages. The water available in the underground will be absorbed by plants. The water in wells will decrease. Since the water in the ocean is salty, the availability of fresh water will decrease considerably. It is also important to have a normal range of rainfall. If the rainfall is continuous with no intervals, that also causes big problems.",
"score_true": 1,
"score_pred": 1
},
{
"region": "3-Республика Бурятия",
"fio": "Ошорова_8121925358",
"transcript": "Where is the clothing store located? When the clothing store open and close? What kinds of shorts do you sell? Do you have any discounts in your clothing store?",
"score_true": 3,
"score_pred": 3
},
{
"region": "3-Республика Бурятия",
"fio": "Ошорова_8121925358",
"transcript": "Well, my best friend is Masha. I met her at school when we were 16. She is a very kind and friendly person. Well, it's important for me to have a friend who will be friendly and supportive. I think these qualities are the most important. I think it's important for a teenager to have good friends, since friends are the people who can help teenagers and support them. So it's important to have people who can help you anywhere. I usually spend two hours a day with my friends. We usually do homeworks together or go to cafes. Well, after school, my friends and me usually go to cafes together. Sometimes we can do homeworks also. Besides, we can go to the cinema.",
"score_true": 5,
"score_pred": 0
},
{
"region": "3-Республика Бурятия",
"fio": "Ошорова_8121925358",
"transcript": "Hi Vera, do you remember that we are doing a project Indoor Games? I have found two photos that will perfectly illustrate it. Let me tell you about them. In the one photo I can see a woman and a man playing together table tennis. In the other photo I can see two men playing together a chess. These photos are quite different. In the one photo, the one photo shows active type of indoor games, while the other one shows intellectual type of indoor games. Both photos have their advantages and disadvantages. The main advantage of active type of indoor games is that this type of indoor games helps people to keep fit and stay in shape, since during this game people usually stay active. However, the main disadvantage of this type of indoor games is that not every person has the possibility to have such active indoor games, and it can be quite expensive. As for intellectual type of indoor games, the main advantage of it is that this type of indoor games develop intellectual skills of person. However, the main disadvantage of this type of indoor games is that not every person can play intellectual type of indoor games, since these type of games are quite difficult to play. As for me, I would prefer playing intellectual type of games, since I find them really useful and pleasant for people's brain. That's all I wanted to tell you. Let me know what you think about these photos. Bye!",
"score_true": 9,
"score_pred": 9
},
{
"region": "3-Республика Бурятия",
"fio": "Чимбеева_8121939062",
"transcript": "It's hard to imagine our life without rain. Rainfall is important for animals and plants to survive on earth. Rain keeps up the level of fresh water. When there is no rainfall, the flow of life is affected largely. If there is little or no rain at all, the dry up land is no longer suitable for plants to grow. Farmers will have a hard time in cultivation. If there is no agriculture, humans will suffer from food shortages. The water available in the underground will be absorbed by plants. The water in wells will decrease. Since the water in the ocean is salty, the availability of fresh water will decrease considerably. It is also important to have a normal range of rainfall. If the rainfall is continuous with no intervals, that also causes big problems.",
"score_true": 1,
"score_pred": 0
},
{
"region": "3-Республика Бурятия",
"fio": "Чимбеева_8121939062",
"transcript": "Where is the clothing store located? What are the opening hours of the clothing store? What kinds of shorts are sold? Do you have any discounts?",
"score_true": 4,
"score_pred": 4
},
{
"region": "3-Республика Бурятия",
"fio": "Чимбеева_8121939062",
"transcript": "My best friend is my classmate. Her name is Zhenya. We met at the school. Usually I look for different kinds of qualities for friendship. For example, I think my friend should be easygoing, kind and attentive. It is important for teenagers to have friends. I think so because teenagers need to be with penias because they want to be listened and they want to feel themselves. I usually spend five hours a day with my friends. Usually it is real meeting or online text. I really love to spend my time with my friends. Usually after school we do different actions. The most liked action we like to go to cafe and play volleyball after school with my friends.",
"score_true": 3,
"score_pred": 3
},
{
"region": "3-Республика Бурятия",
"fio": "Чимбеева_8121939062",
"transcript": "Hi there. I have found a few photos for our project Indoor Games. I hope, sorry due to some technical problems, I can't submit them to you. I hope the illustrations are suitable for the project. Let me explain my choice. In one photo there are, there is a woman and a man. They are playing table tennis. They are wearing casual clothes. They are focused and they are happy. In the other photo there are two men. They are playing chess. They are focused and nervous. They are also wearing office clothes. Obviously, photos are different. One noticeable difference is that in one photo they are playing table tennis, whereas in the other photo they are playing chess. Besides, places are different. One photo represents the living room, whereas the other photo represents the living room. There are some disadvantages and advantages. Obvious advantages of playing table tennis is that you can keep fit and you can build your muscles. You can spend your time with your friends. Speaking about playing chess, you can improve your cognitive skills. Nevertheless, there are some disadvantages. As for the drawbacks of playing table tennis, you always need a companion. You can't play table tennis alone. Speaking about drawbacks of playing chess, it can be boring and also you can lose different items and you can't play chess without them. I find the subject of the project very important as people can see the positive and negative sides of two types of indoor games. As for me, I would prefer playing tennis because I really love sport and I really love sport. Please let me know whether you find the photos suitable for the project. If you don't, what are your ideas? Bye.",
"score_true": 7,
"score_pred": 6
}
]